mirror of
https://github.com/corda/corda.git
synced 2024-12-27 00:21:12 +00:00
Merge remote-tracking branch 'open/master' into colljos-merge-160118
This commit is contained in:
commit
eb3798da5e
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -158,7 +158,7 @@ class NotaryFlow {
|
||||
*/
|
||||
data class TransactionParts(val id: SecureHash, val inputs: List<StateRef>, 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
|
||||
}
|
||||
}
|
||||
|
@ -242,10 +242,14 @@ private fun IntProgression.toSpliterator(): Spliterator.OfInt {
|
||||
}
|
||||
|
||||
fun IntProgression.stream(parallel: Boolean = false): IntStream = StreamSupport.intStream(toSpliterator(), parallel)
|
||||
|
||||
inline fun <reified T> Stream<out T>.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 <reified T> Stream<out T>.toTypedArray(): Array<T> = uncheckedCast(toArray { size -> arrayOfNulls<T>(size) })
|
||||
fun <T> Stream<out T>.toTypedArray(componentType: Class<T>): Array<T> = toArray { size ->
|
||||
uncheckedCast<Any, Array<T?>>(java.lang.reflect.Array.newInstance(componentType, size))
|
||||
}
|
||||
|
||||
fun <T> Stream<out T?>.filterNotNull(): Stream<T> = uncheckedCast(filter(Objects::nonNull))
|
||||
fun <K, V> Stream<out Pair<K, V>>.toMap(): Map<K, V> = collect<LinkedHashMap<K, V>>(::LinkedHashMap, { m, (k, v) -> m.put(k, v) }, { m, t -> m.putAll(t) })
|
||||
fun <T> Class<T>.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]. */
|
||||
|
@ -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"))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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<String?> = Stream.of("one", "two", null).toTypedArray()
|
||||
assertEquals(Array<String?>::class.java, b.javaClass)
|
||||
assertArrayEquals(arrayOf("one", "two", null), b)
|
||||
val c: Array<CharSequence> = Stream.of("x", "y").toTypedArray(CharSequence::class.java)
|
||||
assertEquals(Array<CharSequence>::class.java, c.javaClass)
|
||||
assertArrayEquals(arrayOf("x", "y"), c)
|
||||
val d: Array<CharSequence?> = Stream.of("x", "y", null).toTypedArray(uncheckedCast(CharSequence::class.java))
|
||||
assertEquals(Array<CharSequence?>::class.java, d.javaClass)
|
||||
assertArrayEquals(arrayOf("x", "y", null), d)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Stream of Pairs toMap works`() {
|
||||
val m: Map<Comparable<*>, Serializable> = Stream.of<Pair<Comparable<*>, Serializable>>("x" to "y", 0 to 1, "x" to '2').toMap()
|
||||
assertEquals<Map<*, *>>(mapOf("x" to '2', 0 to 1), m)
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ CorDapps
|
||||
cordapp-overview
|
||||
writing-a-cordapp
|
||||
upgrade-notes
|
||||
upgrading-cordapps
|
||||
cordapp-build-systems
|
||||
building-against-master
|
||||
corda-api
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 -> {
|
@ -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) {
|
BIN
docs/source/resources/flow-interface.png
Normal file
BIN
docs/source/resources/flow-interface.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
@ -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
|
||||
|
@ -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 <changelog_m12>`), to :ref:`V1.0 <changelog_v1>`.
|
||||
|
417
docs/source/upgrading-cordapps.rst
Normal file
417
docs/source/upgrading-cordapps.rst
Normal file
@ -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<Int>(otherParty).unwrap { it.toString() }
|
||||
} else {
|
||||
receive<String>(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<Unit>() {
|
||||
@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<Unit>() {
|
||||
@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<in OldState : ContractState, out NewState : ContractState> : 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<OldState : ContractState, out NewState : ContractState>(
|
||||
originalState: StateAndRef<OldState>,
|
||||
newContractClass: Class<out UpgradedContract<OldState, NewState>>
|
||||
) : AbstractStateReplacementFlow.Instigator<OldState, NewState, Class<out UpgradedContract<OldState, NewState>>>(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<MappedSchema> = 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.
|
@ -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) {
|
||||
|
@ -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<Path>, notaryName: CordaX500Name, threshold: Int = 1): Party {
|
||||
fun generateDistributedNotaryCompositeIdentity(dirs: List<Path>, 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<Path>, 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")
|
||||
}
|
||||
}
|
||||
|
@ -8,8 +8,8 @@ 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.node.User
|
||||
import net.corda.testing.common.internal.ProjectStructure.projectRootDir
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.internal.IntegrationTest
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -13,14 +13,16 @@ 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.DUMMY_NOTARY_NAME
|
||||
import net.corda.testing.node.User
|
||||
import net.corda.testing.driver.NodeHandle
|
||||
import net.corda.testing.driver.PortAllocation
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.internal.IntegrationTest
|
||||
import net.corda.testing.internal.IntegrationTestSchemas
|
||||
import net.corda.testing.internal.performance.div
|
||||
import net.corda.testing.node.NotarySpec
|
||||
import net.corda.testing.node.internal.InternalDriverDSL
|
||||
import net.corda.testing.internal.toDatabaseSchemaName
|
||||
import net.corda.testing.internal.toDatabaseSchemaNames
|
||||
import net.corda.testing.node.NotarySpec
|
||||
|
@ -12,14 +12,18 @@ 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.node.services.Permissions.Companion.startFlow
|
||||
import net.corda.testing.ALICE_NAME
|
||||
import net.corda.testing.node.User
|
||||
import net.corda.testing.ALICE_NAME
|
||||
import net.corda.testing.driver.driver
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import net.corda.testing.internal.IntegrationTest
|
||||
import net.corda.testing.internal.IntegrationTestSchemas
|
||||
import net.corda.testing.internal.toDatabaseSchemaName
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.bouncycastle.util.io.Streams
|
||||
import org.junit.Ignore
|
||||
import org.junit.ClassRule
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
@ -34,7 +38,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 +59,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 +78,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 +101,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<FlowICanRun>()))
|
||||
@ -132,7 +131,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<FlowICanRun>()))
|
||||
|
@ -20,10 +20,14 @@ import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.internal.cordapp.CordappLoader
|
||||
import net.corda.node.internal.cordapp.CordappProviderImpl
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.DUMMY_BANK_A_NAME
|
||||
import net.corda.testing.DUMMY_NOTARY_NAME
|
||||
import net.corda.testing.SerializationEnvironmentRule
|
||||
import net.corda.testing.TestIdentity
|
||||
import net.corda.testing.driver.DriverDSL
|
||||
import net.corda.testing.driver.NodeHandle
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import net.corda.testing.internal.*
|
||||
import net.corda.testing.services.MockAttachmentStorage
|
||||
import org.junit.Assert.assertEquals
|
||||
@ -104,7 +108,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 +120,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 {
|
||||
|
@ -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"))
|
||||
|
||||
|
@ -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<CashIssueFlow>(),
|
||||
startFlow<CashPaymentFlow>(),
|
||||
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<Party, Int>()
|
||||
notaryStateMachines.expectEvents(isStrict = false) {
|
||||
replicate<Pair<Party, StateMachineUpdate>>(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<Party, Int>()
|
||||
notaryStateMachines.expectEvents(isStrict = false) {
|
||||
replicate<Pair<Party, StateMachineUpdate>>(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<Currency>) {
|
||||
aliceProxy.startFlow(::CashIssueFlow, amount, OpaqueBytes.of(0), raftNotaryIdentity).returnValue.getOrThrow()
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
/**
|
||||
|
@ -24,6 +24,10 @@ 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.testing.ALICE_NAME
|
||||
import net.corda.testing.BOB_NAME
|
||||
import net.corda.testing.node.User
|
||||
import net.corda.testing.chooseIdentity
|
||||
import net.corda.nodeapi.internal.config.User
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.internal.IntegrationTestSchemas
|
||||
|
@ -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
|
||||
|
109
node/src/main/kotlin/net/corda/lazyhub/LazyHub.kt
Normal file
109
node/src/main/kotlin/net/corda/lazyhub/LazyHub.kt
Normal file
@ -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 <T : Any> get(clazz: KClass<T>) = get(clazz.java)
|
||||
operator fun <T> get(clazz: Class<T>) = getOrNull(clazz) ?: throw NoSuchProviderException(clazz.toString())
|
||||
fun <T : Any> getAll(clazz: KClass<T>) = getAll(clazz.java)
|
||||
fun <T> getAll(clazz: Class<T>): List<T>
|
||||
fun <T : Any> getOrNull(clazz: KClass<T>) = getOrNull(clazz.java)
|
||||
fun <T> getOrNull(clazz: Class<T>): 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 <T : Any> obj(service: KClass<T>, 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 <S : Any, T : S> impl(service: KClass<S>, impl: KClass<T>)
|
||||
|
||||
/** Like the [KClass] variant if you don't have a static reference fo the class. */
|
||||
fun <S : Any, T : S> impl(service: KClass<S>, impl: Class<T>)
|
||||
|
||||
/**
|
||||
* 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 <S : Any, T : S> factory(service: KClass<S>, factory: KFunction<T>)
|
||||
}
|
200
node/src/main/kotlin/net/corda/lazyhub/LazyHubImpl.kt
Normal file
200
node/src/main/kotlin/net/corda/lazyhub/LazyHubImpl.kt
Normal file
@ -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<T : Any>(override val obj: T) : Provider<T> {
|
||||
override val type get() = obj.javaClass
|
||||
}
|
||||
|
||||
private class LazyProvider<T>(private val busyProviders: BusyProviders, private val underlying: Any?, override val type: Class<T>, val chooseInvocation: () -> Callable<T>) : Provider<T> {
|
||||
override val obj by lazy { busyProviders.runFactory(this) }
|
||||
override fun toString() = underlying.toString()
|
||||
}
|
||||
|
||||
private class Invocation<P, T>(val constructor: PublicConstructor<P, T>, val argSuppliers: List<Pair<P, ArgSupplier>>) : Callable<T> {
|
||||
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<LazyProvider<*>, Callable<*>>()
|
||||
fun <T> runFactory(provider: LazyProvider<T>): 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<*>, Class<*>> = mutableMapOf<Class<*>, 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<Class<*>, MutableList<Provider<*>>>()
|
||||
private fun add(provider: Provider<*>, type: Class<*> = provider.type, registered: MutableSet<Class<*>> = 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 <T> findProviders(clazz: Class<T>): List<Provider<T>>? = uncheckedCast(providers[clazz]) ?: parent?.findProviders(clazz)
|
||||
|
||||
private fun dropAll(serviceClass: Class<*>) {
|
||||
val removed = mutableSetOf<Provider<*>>()
|
||||
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 <T> getOrNull(clazz: Class<T>) = findProviders(clazz)?.run { (singleOrNull() ?: throw TooManyProvidersException(clazz.toString())).obj }
|
||||
override fun <T> getAll(clazz: Class<T>) = findProviders(clazz)?.map { it.obj } ?: emptyList()
|
||||
override fun child(): MutableLazyHub = LazyHubImpl(this)
|
||||
override fun obj(obj: Any) = add(SimpleProvider(obj))
|
||||
override fun <T : Any> obj(service: KClass<T>, obj: T) {
|
||||
dropAll(service.java)
|
||||
obj(obj)
|
||||
}
|
||||
|
||||
override fun <S : Any, T : S> factory(service: KClass<S>, factory: KFunction<T>) = factory.validate().let {
|
||||
dropAll(service.java)
|
||||
addFactory(it)
|
||||
}
|
||||
|
||||
override fun <S : Any, T : S> impl(service: KClass<S>, impl: KClass<T>) = impl.validate().let {
|
||||
dropAll(service.java)
|
||||
addConcrete(it)
|
||||
}
|
||||
|
||||
override fun <S : Any, T : S> impl(service: KClass<S>, impl: Class<T>) = impl.validate().let {
|
||||
dropAll(service.java)
|
||||
addConcrete(it)
|
||||
}
|
||||
|
||||
override fun factory(factory: KFunction<*>) = addFactory(factory.validate())
|
||||
private fun <T> addFactory(factory: KConstructor<T>) {
|
||||
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 <T : Any> addFactory(lh: LazyHub, type: KClass<T>) {
|
||||
add(LazyProvider(busyProviders, lh, type.java) { Callable { lh[type] } })
|
||||
}
|
||||
|
||||
override fun impl(impl: KClass<*>) = implGeneric(impl)
|
||||
private fun <T : Any> implGeneric(type: KClass<T>) = addConcrete(type.validate())
|
||||
override fun impl(impl: Class<*>) = implGeneric(impl)
|
||||
private fun <T> implGeneric(type: Class<T>) = addConcrete(type.validate())
|
||||
private fun <P : Param, T, C : PublicConstructor<P, T>> addConcrete(concrete: Concrete<T, C>) {
|
||||
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 <T> arrayProvider(arrayType: Class<*>, componentType: Class<T>): LazyProvider<Array<T>>? {
|
||||
val providers = findProviders(componentType) ?: return null
|
||||
return LazyProvider(busyProviders, null, uncheckedCast(arrayType)) {
|
||||
Callable { providers.stream().map { it.obj }.toTypedArray(componentType) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun <P : Param, T> PublicConstructor<P, T>.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())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
130
node/src/main/kotlin/net/corda/lazyhub/LazyHubModel.kt
Normal file
130
node/src/main/kotlin/net/corda/lazyhub/LazyHubModel.kt
Normal file
@ -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<T> {
|
||||
/** Most specific known type i.e. directly registered implementation class, or declared return type of factory method. */
|
||||
val type: Class<T>
|
||||
/** 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<P, out T> {
|
||||
val params: List<P>
|
||||
operator fun invoke(argSuppliers: List<Pair<P, ArgSupplier>>): T
|
||||
}
|
||||
|
||||
internal class KConstructor<out T>(val kFunction: KFunction<T>) : PublicConstructor<KParam, T> {
|
||||
companion object {
|
||||
fun <T> KFunction<T>.validate() = run {
|
||||
if (returnType.isMarkedNullable) throw NullableReturnTypeException(toString())
|
||||
isAccessible = true
|
||||
KConstructor(this)
|
||||
}
|
||||
}
|
||||
|
||||
override val params = kFunction.parameters.map(::KParam)
|
||||
override fun invoke(argSuppliers: List<Pair<KParam, ArgSupplier>>): T {
|
||||
return kFunction.callBy(argSuppliers.stream().map { (param, supplier) -> param.kParam to supplier() }.toMap())
|
||||
}
|
||||
|
||||
override fun toString() = kFunction.toString()
|
||||
}
|
||||
|
||||
internal class JConstructor<out T>(private val constructor: Constructor<T>) : PublicConstructor<JParam, T> {
|
||||
// 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<Pair<JParam, ArgSupplier>>): T {
|
||||
return constructor.newInstance(*argSuppliers.stream().map { (_, supplier) -> supplier() }.toTypedArray())
|
||||
}
|
||||
|
||||
override fun toString() = constructor.toString()
|
||||
}
|
||||
|
||||
internal interface Concrete<T, out C : PublicConstructor<*, T>> {
|
||||
val clazz: Class<T>
|
||||
val publicConstructors: List<C>
|
||||
}
|
||||
|
||||
internal class KConcrete<T : Any> private constructor(private val kClass: KClass<T>) : Concrete<T, KConstructor<T>> {
|
||||
companion object {
|
||||
fun <T : Any> KClass<T>.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<T> private constructor(override val clazz: Class<T>) : Concrete<T, JConstructor<T>> {
|
||||
companion object {
|
||||
fun <T> Class<T>.validate() = run {
|
||||
if (Modifier.isAbstract(modifiers)) throw AbstractTypeException(toString())
|
||||
JConcrete(this).apply {
|
||||
if (publicConstructors.isEmpty()) throw NoPublicConstructorsException(toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val publicConstructors = uncheckedCast<Array<out Constructor<*>>, Array<Constructor<T>>>(clazz.constructors).map(::JConstructor)
|
||||
override fun toString() = clazz.toString()
|
||||
}
|
@ -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<Unit>()
|
||||
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<Unit> get() = _nodeReadyFuture
|
||||
@ -175,11 +173,11 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
||||
}
|
||||
|
||||
private inline fun signNodeInfo(nodeInfo: NodeInfo, sign: (PublicKey, SerializedBytes<NodeInfo>) -> 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<Set<KeyPair>, 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<KeyPair>, schemaService: SchemaService, transactionStorage: WritableTransactionStorage, database: CordaPersistence, info: NodeInfo, identityService: IdentityServiceInternal, networkMapCache: NetworkMapCacheInternal): MutableList<Any> {
|
||||
private fun makeServices(lh: LazyHub, keyPairs: Set<KeyPair>, schemaService: SchemaService, transactionStorage: WritableTransactionStorage, database: CordaPersistence, info: NodeInfo, identityService: IdentityServiceInternal, networkMapCache: NetworkMapCacheInternal): MutableList<Any> {
|
||||
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<PartyAndCertificate, KeyPair> {
|
||||
val keyStore = KeyStoreWrapper(configuration.nodeKeystore, configuration.keyStorePassword)
|
||||
|
||||
|
@ -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://<host>:<port>/node
|
||||
|
@ -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<Closeable>()?.forEach { it.close() }
|
||||
manager.destroy()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@ -240,7 +241,7 @@ private class InMemoryRealm(users: List<User>,
|
||||
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<K, V> = org.apache.shiro.cache.Cache<K, V>
|
||||
|
@ -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"
|
||||
|
@ -1,17 +1,12 @@
|
||||
package net.corda.node.services.persistence
|
||||
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.internal.ThreadBox
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
import net.corda.core.internal.bufferUntilSubscribed
|
||||
import net.corda.core.internal.concurrent.doneFuture
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.internal.ThreadBox
|
||||
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.toFuture
|
||||
import net.corda.core.serialization.*
|
||||
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
|
||||
@ -20,9 +15,15 @@ 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<SerializedBytes<CoreTransaction>, List<TransactionSignature>>
|
||||
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,34 +38,52 @@ class DBTransactionStorage : WritableTransactionStorage, SingletonSerializeAsTok
|
||||
)
|
||||
|
||||
private companion object {
|
||||
fun createTransactionsMap(): AppendOnlyPersistentMap<SecureHash, SignedTransaction, DBTransaction, String> {
|
||||
return AppendOnlyPersistentMap(
|
||||
fun createTransactionsMap(maxSizeInBytes: Long)
|
||||
: AppendOnlyPersistentMapBase<SecureHash, TxCacheValue, DBTransaction, String> {
|
||||
return WeightBasedAppendOnlyPersistentMap<SecureHash, TxCacheValue, DBTransaction, String>(
|
||||
toPersistentEntityKey = { it.toString() },
|
||||
fromPersistentEntity = {
|
||||
Pair(SecureHash.parse(it.txId),
|
||||
it.transaction.deserialize<SignedTransaction>(context = SerializationDefaults.STORAGE_CONTEXT))
|
||||
it.transaction.deserialize<SignedTransaction>(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<TxCacheValue>): 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<SignedTransaction>().toSerialized()
|
||||
override val updates: Observable<SignedTransaction> = updatesPublisher.wrapWithDatabaseTransaction()
|
||||
@ -87,5 +106,5 @@ class DBTransactionStorage : WritableTransactionStorage, SingletonSerializeAsTok
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
val transactions: Iterable<SignedTransaction> get() = txStorage.content.allPersisted().map { it.second }.toList()
|
||||
val transactions: Iterable<SignedTransaction> get() = txStorage.content.allPersisted().map { it.second.toSignedTx() }.toList()
|
||||
}
|
||||
|
@ -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<StateAndRef<ContractState>>) {
|
||||
produced.forEach { persistState(it) }
|
||||
}
|
||||
|
||||
private fun persistState(stateAndRef: StateAndRef<ContractState>) {
|
||||
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<MappedSchema, MutableList<ContractStateAndRef>> = 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<ContractStateAndRef>, 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<K, V, E, out EK>(
|
||||
abstract class AppendOnlyPersistentMapBase<K, V, E, out EK>(
|
||||
val toPersistentEntityKey: (K) -> EK,
|
||||
val fromPersistentEntity: (E) -> Pair<K, V>,
|
||||
val toPersistentEntity: (key: K, value: V) -> E,
|
||||
val persistentEntityClass: Class<E>,
|
||||
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<E>
|
||||
) {
|
||||
|
||||
private companion object {
|
||||
private val log = contextLogger()
|
||||
}
|
||||
|
||||
private val cache = NonInvalidatingCache<K, Optional<V>>(
|
||||
bound = cacheBound,
|
||||
concurrencyLevel = 8,
|
||||
loadFunction = { key -> Optional.ofNullable(loadValue(key)) }
|
||||
)
|
||||
abstract protected val cache: LoadingCache<K, Optional<V>>
|
||||
|
||||
/**
|
||||
* Returns the value associated with the key, first loading that value from the storage if necessary.
|
||||
@ -116,7 +113,7 @@ class AppendOnlyPersistentMap<K, V, E, out EK>(
|
||||
}
|
||||
}
|
||||
|
||||
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<K, V, E, out EK>(
|
||||
cache.invalidateAll()
|
||||
}
|
||||
}
|
||||
|
||||
class AppendOnlyPersistentMap<K, V, E, out EK>(
|
||||
toPersistentEntityKey: (K) -> EK,
|
||||
fromPersistentEntity: (E) -> Pair<K, V>,
|
||||
toPersistentEntity: (key: K, value: V) -> E,
|
||||
persistentEntityClass: Class<E>,
|
||||
cacheBound: Long = 1024
|
||||
) : AppendOnlyPersistentMapBase<K, V, E, EK>(
|
||||
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<K, Optional<V>>(
|
||||
bound = cacheBound,
|
||||
concurrencyLevel = 8,
|
||||
loadFunction = { key -> Optional.ofNullable(loadValue(key)) })
|
||||
}
|
||||
|
||||
class WeightBasedAppendOnlyPersistentMap<K, V, E, out EK>(
|
||||
toPersistentEntityKey: (K) -> EK,
|
||||
fromPersistentEntity: (E) -> Pair<K, V>,
|
||||
toPersistentEntity: (key: K, value: V) -> E,
|
||||
persistentEntityClass: Class<E>,
|
||||
maxWeight: Long,
|
||||
weighingFunc: (K, Optional<V>) -> Int
|
||||
) : AppendOnlyPersistentMapBase<K, V, E, EK>(
|
||||
toPersistentEntityKey,
|
||||
fromPersistentEntity,
|
||||
toPersistentEntity,
|
||||
persistentEntityClass) {
|
||||
override val cache = NonInvalidatingWeightBasedCache<K, Optional<V>>(
|
||||
maxWeight = maxWeight,
|
||||
concurrencyLevel = 8,
|
||||
weigher = object : Weigher<K, Optional<V>> {
|
||||
override fun weigh(key: K, value: Optional<V>): Int {
|
||||
return weighingFunc(key, value)
|
||||
}
|
||||
},
|
||||
loadFunction = { key -> Optional.ofNullable(loadValue(key)) }
|
||||
)
|
||||
}
|
@ -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<K, V> private constructor(
|
||||
}
|
||||
|
||||
// TODO look into overriding loadAll() if we ever use it
|
||||
private class NonInvalidatingCacheLoader<K, V>(val loadFunction: (K) -> V) : CacheLoader<K, V>() {
|
||||
class NonInvalidatingCacheLoader<K, V>(val loadFunction: (K) -> V) : CacheLoader<K, V>() {
|
||||
override fun reload(key: K, oldValue: V): ListenableFuture<V> {
|
||||
throw IllegalStateException("Non invalidating cache refreshed")
|
||||
}
|
||||
|
||||
override fun load(key: K) = loadFunction(key)
|
||||
}
|
||||
}
|
||||
|
||||
class NonInvalidatingWeightBasedCache<K, V> private constructor(
|
||||
val cache: LoadingCache<K, V>
|
||||
) : LoadingCache<K, V> by cache {
|
||||
constructor (maxWeight: Long, concurrencyLevel: Int, weigher: Weigher<K, V>, loadFunction: (K) -> V) :
|
||||
this(buildCache(maxWeight, concurrencyLevel, weigher, loadFunction))
|
||||
|
||||
|
||||
private companion object {
|
||||
private fun <K, V> buildCache(maxWeight: Long, concurrencyLevel: Int, weigher: Weigher<K, V>, loadFunction: (K) -> V): LoadingCache<K, V> {
|
||||
val builder = CacheBuilder.newBuilder().maximumWeight(maxWeight).weigher(weigher).concurrencyLevel(concurrencyLevel)
|
||||
return builder.build(NonInvalidatingCache.NonInvalidatingCacheLoader(loadFunction))
|
||||
}
|
||||
}
|
||||
}
|
640
node/src/test/kotlin/net/corda/lazyhub/LazyHubTests.kt
Normal file
640
node/src/test/kotlin/net/corda/lazyhub/LazyHubTests.kt
Normal file
@ -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<out T : Serializable>(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 <X : Closeable, Y : X> 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<Closeable, String> = this::ntv
|
||||
lh.factory(uncheckedCast<Any, KFunction<String>>(ntv))
|
||||
lh.obj(arg)
|
||||
assertEquals(arg.toString(), lh[String::class])
|
||||
}
|
||||
|
||||
class PTWMB<out Y>(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 <Y> ptwmb(arg: Y) where Y : Closeable, Y : Serializable = arg.toString()
|
||||
@Test
|
||||
fun `factory parameter type with multiple bounds`() {
|
||||
val ptwmb: Function1<CloseableAndSerializable, String> = this::ptwmb
|
||||
val kFunction = uncheckedCast<Any, KFunction<String>>(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 <Y> upt(a: Y) = a.toString()
|
||||
@Test
|
||||
fun `unbounded parameter type`() {
|
||||
val upt: Function1<Any, String> = this::upt
|
||||
val kFunction: KFunction<String> = 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<String>)
|
||||
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<String>::class] }.run {
|
||||
assertSame(NoSuchProviderException::class.java, javaClass)
|
||||
assertEquals(Array<String>::class.java.toString(), message)
|
||||
}
|
||||
assertEquals(emptyList(), lh.getAll(String::class))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun arrayParam1() {
|
||||
class Impl(val v: Array<String>)
|
||||
lh.impl(Impl::class)
|
||||
lh.obj("a")
|
||||
assertArrayEquals(arrayOf("a"), lh[Impl::class].v)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun arrayParam2() {
|
||||
class Impl(val v: Array<String>)
|
||||
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<String>?)
|
||||
lh.impl(Impl::class)
|
||||
assertEquals(null, lh[Impl::class].v)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun arraysAreNotCached() {
|
||||
class B(val v: Array<String>)
|
||||
class A(val v: Array<String>, val b: B)
|
||||
class C(val v: Array<String>)
|
||||
class D(val v: Array<String>)
|
||||
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<Config>)
|
||||
|
||||
@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<Int>()
|
||||
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<String>, @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 })
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<ContractStateAndRef> = 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<ContractStateAndRef> = 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()
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -4,8 +4,9 @@ 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.node.User
|
||||
import net.corda.testing.driver.PortAllocation
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.internal.IntegrationTest
|
||||
|
@ -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
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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) ->
|
||||
|
@ -58,7 +58,7 @@ class RaftNotaryCordform : CordformDefinition() {
|
||||
}
|
||||
|
||||
override fun setup(context: CordformContext) {
|
||||
DevIdentityGenerator.generateDistributedNotaryIdentity(
|
||||
DevIdentityGenerator.generateDistributedNotarySingularIdentity(
|
||||
notaryNames.map { context.baseDirectory(it.toString()) },
|
||||
clusterName
|
||||
)
|
||||
|
@ -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<String>) = SingleNotaryCordform().deployNodes()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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<String>,
|
||||
}
|
||||
}
|
||||
|
||||
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<String>,
|
||||
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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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<String>)
|
@ -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<NotaryConfig>()
|
||||
// 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<List<NotaryInfo>> {
|
||||
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<CordaFuture<List<NodeHandle>>> {
|
||||
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()) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -12,10 +12,12 @@ 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.toConfig
|
||||
import net.corda.nodeapi.internal.config.User
|
||||
import net.corda.testing.internal.IntegrationTest
|
||||
import net.corda.testing.SerializationEnvironmentRule
|
||||
import net.corda.nodeapi.internal.network.NetworkParametersCopier
|
||||
import net.corda.testing.node.User
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.getFreeLocalPorts
|
||||
import net.corda.testing.internal.testThreadFactory
|
||||
@ -95,7 +97,7 @@ abstract class NodeBasedTest(private val cordappPackages: List<String> = 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
|
||||
)
|
||||
|
||||
|
@ -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 <reified I : RPCOps> 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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user