Merge remote-tracking branch 'open/master' into colljos-merge-160118

This commit is contained in:
josecoll 2018-01-16 12:13:49 +00:00
commit eb3798da5e
74 changed files with 2068 additions and 303 deletions

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
}
}

View File

@ -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]. */

View File

@ -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"))
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -7,6 +7,7 @@ CorDapps
cordapp-overview
writing-a-cordapp
upgrade-notes
upgrading-cordapps
cordapp-build-systems
building-against-master
corda-api

View File

@ -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

View File

@ -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

View File

@ -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 -> {

View File

@ -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) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -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

View File

@ -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>`.

View 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 isnt 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.

View File

@ -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) {

View File

@ -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")
}
}

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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>()))

View File

@ -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 {

View File

@ -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"))

View File

@ -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()
}

View File

@ -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(

View File

@ -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

View File

@ -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
/**

View File

@ -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

View File

@ -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

View 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>)
}

View 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())
}
}
})
}

View 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()
}

View File

@ -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)

View File

@ -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

View File

@ -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>

View File

@ -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"

View File

@ -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()
}

View File

@ -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()
}
}
}

View File

@ -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)) }
)
}

View File

@ -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))
}
}
}

View 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 })
}
}

View File

@ -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))
}
}
})

View File

@ -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)
}
}

View File

@ -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()
}

View File

@ -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

View File

@ -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

View File

@ -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
/**

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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) ->

View File

@ -58,7 +58,7 @@ class RaftNotaryCordform : CordformDefinition() {
}
override fun setup(context: CordformContext) {
DevIdentityGenerator.generateDistributedNotaryIdentity(
DevIdentityGenerator.generateDistributedNotarySingularIdentity(
notaryNames.map { context.baseDirectory(it.toString()) },
clusterName
)

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)
}

View File

@ -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>)

View File

@ -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()) {

View File

@ -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)
}
}

View File

@ -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
)

View File

@ -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,

View File

@ -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)
}

View File

@ -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) {

View File

@ -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