mirror of
https://github.com/corda/corda.git
synced 2025-03-16 00:55:24 +00:00
Merge pull request #1314 from corda/parkri-os-merge-20180802-1
OS -> ENT merge
This commit is contained in:
commit
fa0523f761
@ -205,7 +205,7 @@ class CollectSignatureFlow(val partiallySignedTx: SignedTransaction, val session
|
||||
*
|
||||
* @param otherSideSession The session which is providing you a transaction to sign.
|
||||
*/
|
||||
abstract class SignTransactionFlow(val otherSideSession: FlowSession,
|
||||
abstract class SignTransactionFlow @JvmOverloads constructor(val otherSideSession: FlowSession,
|
||||
override val progressTracker: ProgressTracker = SignTransactionFlow.tracker()) : FlowLogic<SignedTransaction>() {
|
||||
|
||||
companion object {
|
||||
|
@ -12,8 +12,10 @@ package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.internal.FetchDataFlow
|
||||
import net.corda.core.internal.readFully
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.unwrap
|
||||
|
||||
@ -52,6 +54,25 @@ open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any)
|
||||
override fun call(): Void? {
|
||||
// The first payload will be the transaction data, subsequent payload will be the transaction/attachment data.
|
||||
var payload = payload
|
||||
|
||||
// Depending on who called this flow, the type of the initial payload is different.
|
||||
// The authorisation logic is to maintain a dynamic list of transactions that the caller is authorised to make based on the transactions that were made already.
|
||||
// Each time an authorised transaction is requested, the input transactions are added to the list.
|
||||
// Once a transaction has been requested, it will be removed from the authorised list. This means that it is a protocol violation to request a transaction twice.
|
||||
val authorisedTransactions = when (payload) {
|
||||
is NotarisationPayload -> TransactionAuthorisationFilter().addAuthorised(getInputTransactions(payload.signedTransaction))
|
||||
is SignedTransaction -> TransactionAuthorisationFilter().addAuthorised(getInputTransactions(payload))
|
||||
is RetrieveAnyTransactionPayload -> TransactionAuthorisationFilter(acceptAll = true)
|
||||
is List<*> -> TransactionAuthorisationFilter().addAuthorised(payload.flatMap { stateAndRef ->
|
||||
if (stateAndRef is StateAndRef<*>) {
|
||||
getInputTransactions(serviceHub.validatedTransactions.getTransaction(stateAndRef.ref.txhash)!!) + stateAndRef.ref.txhash
|
||||
} else {
|
||||
throw Exception("Unknown payload type: ${stateAndRef!!::class.java} ?")
|
||||
}
|
||||
}.toSet())
|
||||
else -> throw Exception("Unknown payload type: ${payload::class.java} ?")
|
||||
}
|
||||
|
||||
// This loop will receive [FetchDataFlow.Request] continuously until the `otherSideSession` has all the data they need
|
||||
// to resolve the transaction, a [FetchDataFlow.EndRequest] will be sent from the `otherSideSession` to indicate end of
|
||||
// data request.
|
||||
@ -66,14 +87,47 @@ open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any)
|
||||
FetchDataFlow.Request.End -> return null
|
||||
}
|
||||
}
|
||||
|
||||
payload = when (dataRequest.dataType) {
|
||||
FetchDataFlow.DataType.TRANSACTION -> dataRequest.hashes.map {
|
||||
serviceHub.validatedTransactions.getTransaction(it) ?: throw FetchDataFlow.HashNotFound(it)
|
||||
FetchDataFlow.DataType.TRANSACTION -> dataRequest.hashes.map { txId ->
|
||||
if (!authorisedTransactions.isAuthorised(txId)) {
|
||||
throw FetchDataFlow.IllegalTransactionRequest(txId)
|
||||
}
|
||||
val tx = serviceHub.validatedTransactions.getTransaction(txId)
|
||||
?: throw FetchDataFlow.HashNotFound(txId)
|
||||
authorisedTransactions.removeAuthorised(tx.id)
|
||||
authorisedTransactions.addAuthorised(getInputTransactions(tx))
|
||||
tx
|
||||
}
|
||||
FetchDataFlow.DataType.ATTACHMENT -> dataRequest.hashes.map {
|
||||
serviceHub.attachments.openAttachment(it)?.open()?.readFully() ?: throw FetchDataFlow.HashNotFound(it)
|
||||
serviceHub.attachments.openAttachment(it)?.open()?.readFully()
|
||||
?: throw FetchDataFlow.HashNotFound(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun getInputTransactions(tx: SignedTransaction): Set<SecureHash> = tx.inputs.map { it.txhash }.toSet()
|
||||
|
||||
private class TransactionAuthorisationFilter(private val authorisedTransactions: MutableSet<SecureHash> = mutableSetOf(), val acceptAll: Boolean = false) {
|
||||
fun isAuthorised(txId: SecureHash) = acceptAll || authorisedTransactions.contains(txId)
|
||||
|
||||
fun addAuthorised(txs: Set<SecureHash>): TransactionAuthorisationFilter {
|
||||
authorisedTransactions.addAll(txs)
|
||||
return this
|
||||
}
|
||||
|
||||
fun removeAuthorised(txId: SecureHash) {
|
||||
authorisedTransactions.remove(txId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a wildcard payload to be used by the invoker of the [DataVendingFlow] to allow unlimited access to its vault.
|
||||
*
|
||||
* Todo Fails with a serialization exception if it is not a list. Why?
|
||||
*/
|
||||
@CordaSerializable
|
||||
object RetrieveAnyTransactionPayload : ArrayList<Any>()
|
@ -29,6 +29,7 @@ import net.corda.core.utilities.NonEmptySet
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.core.utilities.unwrap
|
||||
import java.nio.file.FileAlreadyExistsException
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
@ -60,6 +61,8 @@ sealed class FetchDataFlow<T : NamedByHash, in W : Any>(
|
||||
|
||||
class HashNotFound(val requested: SecureHash) : FlowException()
|
||||
|
||||
class IllegalTransactionRequest(val requested: SecureHash) : FlowException("Illegal attempt to request a transaction (${requested}) that is not in the transitive dependency graph of the sent transaction.")
|
||||
|
||||
@CordaSerializable
|
||||
data class Result<out T : NamedByHash>(val fromDisk: List<T>, val downloaded: List<T>)
|
||||
|
||||
@ -160,9 +163,15 @@ class FetchAttachmentsFlow(requests: Set<SecureHash>,
|
||||
for (attachment in downloaded) {
|
||||
with(serviceHub.attachments) {
|
||||
if (!hasAttachment(attachment.id)) {
|
||||
importAttachment(attachment.open(), "$P2P_UPLOADER:${otherSideSession.counterparty.name}", null)
|
||||
try {
|
||||
importAttachment(attachment.open(), "$P2P_UPLOADER:${otherSideSession.counterparty.name}", null)
|
||||
} catch (e: FileAlreadyExistsException) {
|
||||
// This can happen when another transaction will insert the same attachment during this transaction.
|
||||
// The outcome is the same (the attachment is imported), so we can ignore this exception.
|
||||
logger.debug("Attachment ${attachment.id} already inserted.")
|
||||
}
|
||||
} else {
|
||||
logger.info("Attachment ${attachment.id} already exists, skipping.")
|
||||
logger.debug("Attachment ${attachment.id} already exists, skipping.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -183,9 +192,11 @@ class FetchAttachmentsFlow(requests: Set<SecureHash>,
|
||||
* Given a set of tx hashes (IDs), either loads them from local disk or asks the remote peer to provide them.
|
||||
*
|
||||
* A malicious response in which the data provided by the remote peer does not hash to the requested hash results in
|
||||
* [FetchDataFlow.DownloadedVsRequestedDataMismatch] being thrown. If the remote peer doesn't have an entry, it
|
||||
* results in a [FetchDataFlow.HashNotFound] exception. Note that returned transactions are not inserted into
|
||||
* the database, because it's up to the caller to actually verify the transactions are valid.
|
||||
* [FetchDataFlow.DownloadedVsRequestedDataMismatch] being thrown.
|
||||
* If the remote peer doesn't have an entry, it results in a [FetchDataFlow.HashNotFound] exception.
|
||||
* If the remote peer is not authorized to request this transaction, it results in a [FetchDataFlow.IllegalTransactionRequest] exception.
|
||||
* Authorisation is accorded only on valid ancestors of the root transation.
|
||||
* Note that returned transactions are not inserted into the database, because it's up to the caller to actually verify the transactions are valid.
|
||||
*/
|
||||
class FetchTransactionsFlow(requests: Set<SecureHash>, otherSide: FlowSession) :
|
||||
FetchDataFlow<SignedTransaction, SignedTransaction>(requests, otherSide, DataType.TRANSACTION) {
|
||||
|
@ -82,7 +82,7 @@ class ResolveTransactionsFlow(txHashesArg: Set<SecureHash>,
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@Throws(FetchDataFlow.HashNotFound::class)
|
||||
@Throws(FetchDataFlow.HashNotFound::class, FetchDataFlow.IllegalTransactionRequest::class)
|
||||
override fun call() {
|
||||
val newTxns = ArrayList<SignedTransaction>(txHashes.size)
|
||||
// Start fetching data.
|
||||
|
@ -126,7 +126,7 @@ class AttachmentTests : WithMockNet {
|
||||
@InitiatedBy(InitiatingFetchAttachmentsFlow::class)
|
||||
private class FetchAttachmentsResponse(val otherSideSession: FlowSession) : FlowLogic<Void?>() {
|
||||
@Suspendable
|
||||
override fun call() = subFlow(TestDataVendingFlow(otherSideSession))
|
||||
override fun call() = subFlow(TestNoSecurityDataVendingFlow(otherSideSession))
|
||||
}
|
||||
|
||||
//region Generators
|
||||
|
@ -15,7 +15,7 @@ import net.corda.core.internal.FetchDataFlow
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
|
||||
// Flow to start data vending without sending transaction. For testing only.
|
||||
class TestDataVendingFlow(otherSideSession: FlowSession) : SendStateAndRefFlow(otherSideSession, emptyList()) {
|
||||
class TestNoSecurityDataVendingFlow(otherSideSession: FlowSession) : DataVendingFlow(otherSideSession, RetrieveAnyTransactionPayload) {
|
||||
@Suspendable
|
||||
override fun sendPayloadAndReceiveDataRequest(otherSideSession: FlowSession, payload: Any): UntrustworthyData<FetchDataFlow.Request> {
|
||||
return if (payload is List<*> && payload.isEmpty()) {
|
@ -16,8 +16,10 @@ import net.corda.core.flows.*
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.NonEmptySet
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.sequence
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.node.MockNetwork
|
||||
@ -44,6 +46,8 @@ class ResolveTransactionsFlowTest {
|
||||
private lateinit var miniCorp: Party
|
||||
private lateinit var notary: Party
|
||||
|
||||
private lateinit var rootTx: SignedTransaction
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts", "net.corda.core.internal"))
|
||||
@ -170,6 +174,34 @@ class ResolveTransactionsFlowTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Requesting a transaction while having the right to see it succeeds`() {
|
||||
val (_, stx2) = makeTransactions()
|
||||
val p = TestNoRightsVendingFlow(miniCorp, toVend = stx2, toRequest = stx2)
|
||||
val future = megaCorpNode.startFlow(p)
|
||||
mockNet.runNetwork()
|
||||
future.getOrThrow()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Requesting a transaction without having the right to see it results in exception`() {
|
||||
val (_, stx2) = makeTransactions()
|
||||
val (_, stx3) = makeTransactions()
|
||||
val p = TestNoRightsVendingFlow(miniCorp, toVend = stx2, toRequest = stx3)
|
||||
val future = megaCorpNode.startFlow(p)
|
||||
mockNet.runNetwork()
|
||||
assertFailsWith<FetchDataFlow.IllegalTransactionRequest> { future.getOrThrow() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Requesting a transaction twice results in exception`() {
|
||||
val (_, stx2) = makeTransactions()
|
||||
val p = TestResolveTwiceVendingFlow(miniCorp, stx2)
|
||||
val future = megaCorpNode.startFlow(p)
|
||||
mockNet.runNetwork()
|
||||
assertFailsWith<FetchDataFlow.IllegalTransactionRequest> { future.getOrThrow() }
|
||||
}
|
||||
|
||||
// DOCSTART 2
|
||||
private fun makeTransactions(signFirstTX: Boolean = true, withAttachment: SecureHash? = null): Pair<SignedTransaction, SignedTransaction> {
|
||||
// Make a chain of custody of dummy states and insert into node A.
|
||||
@ -197,8 +229,9 @@ class ResolveTransactionsFlowTest {
|
||||
}
|
||||
// DOCEND 2
|
||||
|
||||
|
||||
@InitiatingFlow
|
||||
private class TestFlow(val otherSide: Party, private val resolveTransactionsFlowFactory: (FlowSession) -> ResolveTransactionsFlow, private val txCountLimit: Int? = null) : FlowLogic<Unit>() {
|
||||
private open class TestFlow(val otherSide: Party, private val resolveTransactionsFlowFactory: (FlowSession) -> ResolveTransactionsFlow, private val txCountLimit: Int? = null) : FlowLogic<Unit>() {
|
||||
constructor(txHashes: Set<SecureHash>, otherSide: Party, txCountLimit: Int? = null) : this(otherSide, { ResolveTransactionsFlow(txHashes, it) }, txCountLimit = txCountLimit)
|
||||
constructor(stx: SignedTransaction, otherSide: Party) : this(otherSide, { ResolveTransactionsFlow(stx, it) })
|
||||
|
||||
@ -210,11 +243,54 @@ class ResolveTransactionsFlowTest {
|
||||
subFlow(resolveTransactionsFlow)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@InitiatedBy(TestFlow::class)
|
||||
private class TestResponseFlow(val otherSideSession: FlowSession) : FlowLogic<Void?>() {
|
||||
@Suspendable
|
||||
override fun call() = subFlow(TestDataVendingFlow(otherSideSession))
|
||||
override fun call() = subFlow(TestNoSecurityDataVendingFlow(otherSideSession))
|
||||
}
|
||||
|
||||
// Used by the no-rights test
|
||||
@InitiatingFlow
|
||||
private class TestNoRightsVendingFlow(val otherSide: Party, val toVend: SignedTransaction, val toRequest: SignedTransaction) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val session = initiateFlow(otherSide)
|
||||
session.send(toRequest)
|
||||
subFlow(DataVendingFlow(session, toVend))
|
||||
}
|
||||
}
|
||||
@Suppress("unused")
|
||||
@InitiatedBy(TestNoRightsVendingFlow::class)
|
||||
private open class TestResponseResolveNoRightsFlow(val otherSideSession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val noRightsTx = otherSideSession.receive<SignedTransaction>().unwrap { it }
|
||||
otherSideSession.receive<Any>().unwrap { it }
|
||||
otherSideSession.sendAndReceive<Any>(FetchDataFlow.Request.Data(NonEmptySet.of(noRightsTx.inputs.first().txhash), FetchDataFlow.DataType.TRANSACTION)).unwrap { it }
|
||||
otherSideSession.send(FetchDataFlow.Request.End)
|
||||
}
|
||||
}
|
||||
|
||||
//Used by the resolve twice test
|
||||
@InitiatingFlow
|
||||
private class TestResolveTwiceVendingFlow(val otherSide: Party, val tx: SignedTransaction) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val session = initiateFlow(otherSide)
|
||||
subFlow(DataVendingFlow(session, tx))
|
||||
}
|
||||
}
|
||||
@Suppress("unused")
|
||||
@InitiatedBy(TestResolveTwiceVendingFlow::class)
|
||||
private open class TestResponseResolveTwiceFlow(val otherSideSession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val tx = otherSideSession.receive<SignedTransaction>().unwrap { it }
|
||||
val parent1 = tx.inputs.first().txhash
|
||||
otherSideSession.sendAndReceive<Any>(FetchDataFlow.Request.Data(NonEmptySet.of(parent1), FetchDataFlow.DataType.TRANSACTION)).unwrap { it }
|
||||
otherSideSession.sendAndReceive<Any>(FetchDataFlow.Request.Data(NonEmptySet.of(parent1), FetchDataFlow.DataType.TRANSACTION)).unwrap { it }
|
||||
otherSideSession.send(FetchDataFlow.Request.End)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.TestDataVendingFlow
|
||||
import net.corda.core.flows.TestNoSecurityDataVendingFlow
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.FetchAttachmentsFlow
|
||||
import net.corda.core.internal.FetchDataFlow
|
||||
@ -99,7 +99,7 @@ class AttachmentSerializationTest {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
if (sendData) {
|
||||
subFlow(TestDataVendingFlow(clientSession))
|
||||
subFlow(TestNoSecurityDataVendingFlow(clientSession))
|
||||
}
|
||||
clientSession.receive<String>().unwrap { assertEquals("ping one", it) }
|
||||
clientSession.sendAndReceive<String>("pong").unwrap { assertEquals("ping two", it) }
|
||||
|
@ -19,8 +19,18 @@ JAR will contain:
|
||||
|
||||
Build tools
|
||||
-----------
|
||||
In the instructions that follow, we assume you are using ``gradle`` and the ``cordformation`` plugin to build your
|
||||
CorDapp. You can find examples of building a CorDapp using these tools in the ``build.gradle`` file of the `Kotlin CorDapp Template <https://github.com/corda/cordapp-template-kotlin>`_ and the `Java CorDapp Template <https://github.com/corda/cordapp-template-kotlin>`_.
|
||||
In the instructions that follow, we assume you are using Gradle and the ``cordformation`` plugin to build your
|
||||
CorDapp. You can find examples of building a CorDapp using these tools in the
|
||||
`Kotlin CorDapp Template <https://github.com/corda/cordapp-template-kotlin>`_ and the
|
||||
`Java CorDapp Template <https://github.com/corda/cordapp-template-kotlin>`_.
|
||||
|
||||
To ensure you are using the correct version of Gradle, you should use the provided Gradle Wrapper by copying across
|
||||
the following folder and files from the `Kotlin CorDapp Template <https://github.com/corda/cordapp-template-kotlin>`_ or the
|
||||
`Java CorDapp Template <https://github.com/corda/cordapp-template-kotlin>`_ to the root of your project:
|
||||
|
||||
* ``gradle/``
|
||||
* ``gradlew``
|
||||
* ``gradlew.bat``
|
||||
|
||||
Setting your dependencies
|
||||
-------------------------
|
||||
@ -101,7 +111,10 @@ For further information about managing dependencies, see
|
||||
|
||||
Example
|
||||
^^^^^^^
|
||||
Below is a sample of what a CorDapp's Gradle dependencies block might look like. When building your own CorDapp, you should base yourself on the ``build.gradle`` file of the `Kotlin CorDapp Template <https://github.com/corda/cordapp-template-kotlin>`_ and the `Java CorDapp Template <https://github.com/corda/cordapp-template-kotlin>`_.
|
||||
Below is a sample of what a CorDapp's Gradle dependencies block might look like. When building your own CorDapp, you should
|
||||
base yourself on the ``build.gradle`` file of the
|
||||
`Kotlin CorDapp Template <https://github.com/corda/cordapp-template-kotlin>`_ or the
|
||||
`Java CorDapp Template <https://github.com/corda/cordapp-template-kotlin>`_.
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
@ -135,13 +148,14 @@ Below is a sample of what a CorDapp's Gradle dependencies block might look like.
|
||||
|
||||
Creating the CorDapp JAR
|
||||
------------------------
|
||||
Once your dependencies are set correctly, you can build your CorDapp JAR using the gradle ``jar`` task:
|
||||
Once your dependencies are set correctly, you can build your CorDapp JAR(s) using the Gradle ``jar`` task
|
||||
|
||||
* Unix/Mac OSX: ``./gradlew jar``
|
||||
|
||||
* Windows: ``gradlew.bat jar``
|
||||
|
||||
The CorDapp JAR will be output to the ``build/libs`` folder.
|
||||
Each of the project's modules will be compiled into its own CorDapp JAR. You can find these CorDapp JARs in the ``build/libs``
|
||||
folders of each of the project's modules.
|
||||
|
||||
.. warning:: The hash of the generated CorDapp JAR is not deterministic, as it depends on variables such as the
|
||||
timestamp at creation. Nodes running the same CorDapp must therefore ensure they are using the exact same CorDapp
|
||||
@ -158,9 +172,9 @@ Installing the CorDapp JAR
|
||||
.. note:: Before installing a CorDapp, you must create one or more nodes to install it on. For instructions, please see
|
||||
:doc:`generating-a-node`.
|
||||
|
||||
At start-up, nodes will load any CorDapps present in their ``cordapps`` folder. Therefore, in order to install a CorDapp on
|
||||
a node, the CorDapp JAR must be added to the ``<node_dir>/cordapps/`` folder (where ``node_dir`` is the folder in which
|
||||
the node's JAR and configuration files are stored) and the node restarted.
|
||||
At start-up, nodes will load any CorDapps present in their ``cordapps`` folder. In order to install a CorDapp on a node, the
|
||||
CorDapp JAR must be added to the ``<node_dir>/cordapps/`` folder (where ``node_dir`` is the folder in which the node's JAR
|
||||
and configuration files are stored) and the node restarted.
|
||||
|
||||
CorDapp configuration files
|
||||
---------------------------
|
||||
@ -175,6 +189,3 @@ CorDapp configuration can be accessed from ``CordappContext::config`` whenever a
|
||||
|
||||
There is an example project that demonstrates in ``samples` called ``cordapp-configuration`` and API documentation in
|
||||
<api/kotlin/corda/net.corda.core.cordapp/index.html>`_.
|
||||
|
||||
|
||||
|
||||
|
@ -165,33 +165,45 @@ Building the example CorDapp
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
* Open a terminal window in the ``cordapp-example`` directory
|
||||
|
||||
* Build the test nodes with our CorDapp using the following command:
|
||||
* Run the ``deployNodes`` Gradle task to build four nodes with our CorDapp already installed on them:
|
||||
|
||||
* Unix/Mac OSX: ``./gradlew deployNodes``
|
||||
|
||||
* Windows: ``gradlew.bat deployNodes``
|
||||
|
||||
This will automatically build four nodes with our CorDapp already installed
|
||||
|
||||
.. note:: CorDapps can be written in any language targeting the JVM. In our case, we've provided the example source in
|
||||
both Kotlin (``/kotlin-source/src``) and Java (``/java-source/src``). Since both sets of source files are
|
||||
functionally identical, we will refer to the Kotlin version throughout the documentation.
|
||||
|
||||
* After the build finishes, you will see the generated nodes in the ``kotlin-source/build/nodes`` folder
|
||||
* After the build finishes, you will see the following output in the ``kotlin-source/build/nodes`` folder:
|
||||
|
||||
* There will be a folder for each generated node, plus a ``runnodes`` shell script (or batch file on Windows) to run
|
||||
all the nodes simultaneously
|
||||
* A folder for each generated node
|
||||
* A ``runnodes`` shell script for running all the nodes simultaneously on osX
|
||||
* A ``runnodes.bat`` batch file for running all the nodes simultaneously on Windows
|
||||
|
||||
* Each node in the ``nodes`` folder has the following structure:
|
||||
* Each node in the ``nodes`` folder will have the following structure:
|
||||
|
||||
.. sourcecode:: none
|
||||
|
||||
. nodeName
|
||||
├── corda.jar // The Corda node runtime.
|
||||
├── corda-webserver.jar // The node development webserver.
|
||||
├── node.conf // The node configuration file.
|
||||
└── cordapps // The node's CorDapps.
|
||||
.. sourcecode:: none
|
||||
|
||||
. nodeName
|
||||
├── additional-node-infos //
|
||||
├── certificates
|
||||
├── corda.jar // The Corda node runtime
|
||||
├── corda-webserver.jar // The development node webserver runtime
|
||||
├── cordapps // The node's CorDapps
|
||||
│ ├── corda-finance-3.2-corda.jar
|
||||
│ └── cordapp-example-0.1.jar
|
||||
├── drivers
|
||||
├── logs
|
||||
├── network-parameters
|
||||
├── node.conf // The node's configuration file
|
||||
├── nodeInfo-<HASH> // The hash will be different each time you generate a node
|
||||
└── persistence.mv.db // The node's database
|
||||
|
||||
.. note:: ``deployNodes`` is a utility task to create an entirely new set of nodes for testing your CorDapp. In production,
|
||||
you would instead create a single node as described in :doc:`generating-a-node` and build your CorDapp JARs as described
|
||||
in :doc:`cordapp-build-systems`.
|
||||
|
||||
Running the example CorDapp
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Start the nodes by running the following command from the root of the ``cordapp-example`` folder:
|
||||
@ -440,8 +452,8 @@ The nodes can be configured to communicate as a network even when distributed ac
|
||||
are distributed across machines. Otherwise, the nodes will not be able to communicate.
|
||||
|
||||
.. note:: If you are using H2 and wish to use the same ``h2port`` value for two or more nodes, you must only assign them that
|
||||
value after the nodes have been moved to their individual machines. The initial bootstrapping process requires access to the
|
||||
nodes' databases and if two nodes share the same H2 port, the process will fail.
|
||||
value after the nodes have been moved to their individual machines. The initial bootstrapping process requires access to
|
||||
the nodes' databases and if two nodes share the same H2 port, the process will fail.
|
||||
|
||||
Testing your CorDapp
|
||||
--------------------
|
||||
|
@ -11,6 +11,7 @@
|
||||
package net.corda.nodeapi.internal.persistence
|
||||
|
||||
import co.paralleluniverse.strands.Strand
|
||||
import org.hibernate.BaseSessionEventListener
|
||||
import org.hibernate.Session
|
||||
import org.hibernate.Transaction
|
||||
import rx.subjects.PublishSubject
|
||||
@ -33,6 +34,9 @@ class DatabaseTransaction(
|
||||
|
||||
private var _connectionCreated = false
|
||||
val connectionCreated get() = _connectionCreated
|
||||
val flushing: Boolean get() = _flushingCount > 0
|
||||
private var _flushingCount = 0
|
||||
|
||||
val connection: Connection by lazy(LazyThreadSafetyMode.NONE) {
|
||||
database.dataSource.connection
|
||||
.apply {
|
||||
@ -46,6 +50,27 @@ class DatabaseTransaction(
|
||||
|
||||
private val sessionDelegate = lazy {
|
||||
val session = database.entityManagerFactory.withOptions().connection(connection).openSession()
|
||||
session.addEventListeners(object : BaseSessionEventListener() {
|
||||
override fun flushStart() {
|
||||
_flushingCount++
|
||||
super.flushStart()
|
||||
}
|
||||
|
||||
override fun flushEnd(numberOfEntities: Int, numberOfCollections: Int) {
|
||||
super.flushEnd(numberOfEntities, numberOfCollections)
|
||||
_flushingCount--
|
||||
}
|
||||
|
||||
override fun partialFlushStart() {
|
||||
_flushingCount++
|
||||
super.partialFlushStart()
|
||||
}
|
||||
|
||||
override fun partialFlushEnd(numberOfEntities: Int, numberOfCollections: Int) {
|
||||
super.partialFlushEnd(numberOfEntities, numberOfCollections)
|
||||
_flushingCount--
|
||||
}
|
||||
})
|
||||
hibernateTransaction = session.beginTransaction()
|
||||
session
|
||||
}
|
||||
|
@ -42,6 +42,7 @@ class E2ETestKeyManagementService(val identityService: IdentityService) : Single
|
||||
private val mutex = ThreadBox(InnerState())
|
||||
// Accessing this map clones it.
|
||||
override val keys: Set<PublicKey> get() = mutex.locked { keys.keys }
|
||||
val keyPairs: Set<KeyPair> get() = mutex.locked { keys.map { KeyPair(it.key, it.value) }.toSet() }
|
||||
|
||||
override fun start(initialKeyPairs: Set<KeyPair>) {
|
||||
mutex.locked {
|
||||
|
@ -134,11 +134,14 @@ abstract class AppendOnlyPersistentMapBase<K, V, E, out EK>(
|
||||
|
||||
protected fun loadValue(key: K): V? {
|
||||
val session = currentDBSession()
|
||||
// IMPORTANT: The flush is needed because detach() makes the queue of unflushed entries invalid w.r.t. Hibernate internal state if the found entity is unflushed.
|
||||
// We want the detach() so that we rely on our cache memory management and don't retain strong references in the Hibernate session.
|
||||
session.flush()
|
||||
val flushing = contextTransaction.flushing
|
||||
if (!flushing) {
|
||||
// IMPORTANT: The flush is needed because detach() makes the queue of unflushed entries invalid w.r.t. Hibernate internal state if the found entity is unflushed.
|
||||
// We want the detach() so that we rely on our cache memory management and don't retain strong references in the Hibernate session.
|
||||
session.flush()
|
||||
}
|
||||
val result = session.find(persistentEntityClass, toPersistentEntityKey(key))
|
||||
return result?.apply { session.detach(result) }?.let(fromPersistentEntity)?.second
|
||||
return result?.apply { if (!flushing) session.detach(result) }?.let(fromPersistentEntity)?.second
|
||||
}
|
||||
|
||||
operator fun contains(key: K) = get(key) != null
|
||||
|
@ -0,0 +1,67 @@
|
||||
package net.corda.node.services.persistence
|
||||
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.`issued by`
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.finance.flows.CashIssueFlow
|
||||
import net.corda.node.services.identity.PersistentIdentityService
|
||||
import net.corda.node.services.keys.E2ETestKeyManagementService
|
||||
import net.corda.testing.core.BOC_NAME
|
||||
import net.corda.testing.node.InMemoryMessagingNetwork
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.StartedMockNode
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class HibernateColumnConverterTests {
|
||||
private lateinit var mockNet: MockNetwork
|
||||
private lateinit var bankOfCordaNode: StartedMockNode
|
||||
private lateinit var bankOfCorda: Party
|
||||
private lateinit var notary: Party
|
||||
|
||||
@Before
|
||||
fun start() {
|
||||
mockNet = MockNetwork(
|
||||
servicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin(),
|
||||
cordappPackages = listOf("net.corda.finance.contracts.asset", "net.corda.finance.schemas"))
|
||||
bankOfCordaNode = mockNet.createPartyNode(BOC_NAME)
|
||||
bankOfCorda = bankOfCordaNode.info.identityFromX500Name(BOC_NAME)
|
||||
notary = mockNet.defaultNotaryIdentity
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
mockNet.stopNodes()
|
||||
}
|
||||
|
||||
// AbstractPartyToX500NameAsStringConverter could cause circular flush of Hibernate session because it is invoked during flush, and a
|
||||
// cache miss was doing a flush. This also checks that loading during flush does actually work.
|
||||
@Test
|
||||
fun `issue some cash on a notary that exists only in the database to check cache loading works in our identity column converters during flush of vault update`() {
|
||||
val expected = 500.DOLLARS
|
||||
val ref = OpaqueBytes.of(0x01)
|
||||
|
||||
// Create parallel set of key and identity services so that the values are not cached, forcing the node caches to do a lookup.
|
||||
val identityService = PersistentIdentityService()
|
||||
val originalIdentityService: PersistentIdentityService = bankOfCordaNode.services.identityService as PersistentIdentityService
|
||||
identityService.database = originalIdentityService.database
|
||||
identityService.start(originalIdentityService.trustRoot)
|
||||
val keyService = E2ETestKeyManagementService(identityService)
|
||||
keyService.start((bankOfCordaNode.services.keyManagementService as E2ETestKeyManagementService).keyPairs)
|
||||
|
||||
// New identity for a notary (doesn't matter that it's for Bank Of Corda... since not going to use it as an actual notary etc).
|
||||
val newKeyAndCert = keyService.freshKeyAndCert(bankOfCordaNode.info.legalIdentitiesAndCerts[0], false)
|
||||
val randomNotary = Party(BOC_NAME, newKeyAndCert.owningKey)
|
||||
|
||||
val future = bankOfCordaNode.startFlow(CashIssueFlow(expected, ref, randomNotary))
|
||||
mockNet.runNetwork()
|
||||
val issueTx = future.getOrThrow().stx
|
||||
val output = issueTx.tx.outputsOfType<Cash.State>().single()
|
||||
assertEquals(expected.`issued by`(bankOfCorda.ref(ref)), output.amount)
|
||||
}
|
||||
}
|
@ -1,12 +1,13 @@
|
||||
package net.corda.bootstrapper
|
||||
|
||||
import com.jcabi.manifests.Manifests
|
||||
import net.corda.core.internal.rootMessage
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.nodeapi.internal.network.NetworkBootstrapper
|
||||
import picocli.CommandLine
|
||||
import picocli.CommandLine.*
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardCopyOption.REPLACE_EXISTING
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
@ -29,13 +30,13 @@ fun main(args: Array<String>) {
|
||||
versionProvider = CordaVersionProvider::class,
|
||||
mixinStandardHelpOptions = true,
|
||||
showDefaultValues = true,
|
||||
description = [ "Bootstrap a local test Corda network using a set of node conf files and CorDapp JARs" ]
|
||||
description = ["Bootstrap a local test Corda network using a set of node configuration files and CorDapp JARs"]
|
||||
)
|
||||
class Main : Runnable {
|
||||
@Option(
|
||||
names = ["--dir"],
|
||||
description = [
|
||||
"Root directory containing the node conf files and CorDapp JARs that will form the test network.",
|
||||
"Root directory containing the node configuration files and CorDapp JARs that will form the test network.",
|
||||
"It may also contain existing node directories."
|
||||
]
|
||||
)
|
||||
@ -47,7 +48,123 @@ class Main : Runnable {
|
||||
@Option(names = ["--verbose"], description = ["Enable verbose output."])
|
||||
var verbose: Boolean = false
|
||||
|
||||
@Option(names = ["--install-shell-extensions"], description = ["Install bootstrapper alias and autocompletion for bash and zsh"])
|
||||
var installShellExtensions: Boolean = false
|
||||
|
||||
private class SettingsFile(val filePath: Path) {
|
||||
private val lines: MutableList<String> by lazy { getFileLines() }
|
||||
var fileModified: Boolean = false
|
||||
|
||||
// Return the lines in the file if it exists, else return an empty mutable list
|
||||
private fun getFileLines(): MutableList<String> {
|
||||
return if (filePath.exists()) {
|
||||
filePath.toFile().readLines().toMutableList()
|
||||
} else {
|
||||
emptyList<String>().toMutableList()
|
||||
}
|
||||
}
|
||||
|
||||
fun addOrReplaceIfStartsWith(startsWith: String, replaceWith: String) {
|
||||
val index = lines.indexOfFirst { it.startsWith(startsWith) }
|
||||
if (index >= 0) {
|
||||
if (lines[index] != replaceWith) {
|
||||
lines[index] = replaceWith
|
||||
fileModified = true
|
||||
}
|
||||
} else {
|
||||
lines.add(replaceWith)
|
||||
fileModified = true
|
||||
}
|
||||
}
|
||||
|
||||
fun addIfNotExists(line: String) {
|
||||
if (!lines.contains(line)) {
|
||||
lines.add(line)
|
||||
fileModified = true
|
||||
}
|
||||
}
|
||||
|
||||
fun updateAndBackupIfNecessary() {
|
||||
if (fileModified) {
|
||||
val backupFilePath = filePath.parent / "${filePath.fileName}.backup"
|
||||
println("Updating settings in ${filePath.fileName} - existing settings file has been backed up to $backupFilePath")
|
||||
if (filePath.exists()) filePath.copyTo(backupFilePath, REPLACE_EXISTING)
|
||||
filePath.writeLines(lines)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val userHome: Path by lazy { Paths.get(System.getProperty("user.home")) }
|
||||
private val jarLocation: Path by lazy { this.javaClass.location.toPath() }
|
||||
|
||||
// If on Windows, Path.toString() returns a path with \ instead of /, but for bash Windows users we want to convert those back to /'s
|
||||
private fun Path.toStringWithDeWindowsfication(): String = this.toAbsolutePath().toString().replace("\\", "/")
|
||||
private fun jarVersion(alias: String) = "# $alias - Version: ${CordaVersionProvider.releaseVersion}, Revision: ${CordaVersionProvider.revision}"
|
||||
private fun getAutoCompleteFileLocation(alias: String) = userHome / ".completion" / alias
|
||||
|
||||
private fun generateAutoCompleteFile(alias: String) {
|
||||
println("Generating $alias auto completion file")
|
||||
val autoCompleteFile = getAutoCompleteFileLocation(alias)
|
||||
autoCompleteFile.parent.createDirectories()
|
||||
picocli.AutoComplete.main("-f", "-n", alias, this.javaClass.name, "-o", autoCompleteFile.toStringWithDeWindowsfication())
|
||||
|
||||
// Append hash of file to autocomplete file
|
||||
autoCompleteFile.toFile().appendText(jarVersion(alias))
|
||||
}
|
||||
|
||||
private fun installShellExtensions(alias: String) {
|
||||
// Get jar location and generate alias command
|
||||
val command = "alias $alias='java -jar \"${jarLocation.toStringWithDeWindowsfication()}\"'"
|
||||
generateAutoCompleteFile(alias)
|
||||
|
||||
// Get bash settings file
|
||||
val bashSettingsFile = SettingsFile(userHome / ".bashrc")
|
||||
// Replace any existing bootstrapper alias. There can be only one.
|
||||
bashSettingsFile.addOrReplaceIfStartsWith("alias $alias", command)
|
||||
val completionFileCommand = "for bcfile in ~/.completion/* ; do . \$bcfile; done"
|
||||
bashSettingsFile.addIfNotExists(completionFileCommand)
|
||||
bashSettingsFile.updateAndBackupIfNecessary()
|
||||
|
||||
// Get zsh settings file
|
||||
val zshSettingsFile = SettingsFile(userHome / ".zshrc")
|
||||
zshSettingsFile.addIfNotExists("autoload -U +X compinit && compinit")
|
||||
zshSettingsFile.addIfNotExists("autoload -U +X bashcompinit && bashcompinit")
|
||||
zshSettingsFile.addOrReplaceIfStartsWith("alias $alias", command)
|
||||
zshSettingsFile.addIfNotExists(completionFileCommand)
|
||||
zshSettingsFile.updateAndBackupIfNecessary()
|
||||
|
||||
println("Installation complete, $alias is available in bash with autocompletion. ")
|
||||
println("Type `$alias <options>` from the commandline.")
|
||||
println("Restart bash for this to take effect, or run `. ~/.bashrc` in bash or `. ~/.zshrc` in zsh to re-initialise your shell now")
|
||||
}
|
||||
|
||||
private fun checkForAutoCompleteUpdate(alias: String) {
|
||||
val autoCompleteFile = getAutoCompleteFileLocation(alias)
|
||||
|
||||
// If no autocomplete file, it hasn't been installed, so don't do anything
|
||||
if (!autoCompleteFile.exists()) return
|
||||
|
||||
var lastLine = ""
|
||||
autoCompleteFile.toFile().forEachLine { lastLine = it }
|
||||
|
||||
if (lastLine != jarVersion(alias)) {
|
||||
println("Old auto completion file detected... regenerating")
|
||||
generateAutoCompleteFile(alias)
|
||||
println("Restart bash for this to take effect, or run `. ~/.bashrc` to re-initialise bash now")
|
||||
}
|
||||
}
|
||||
|
||||
private fun installOrUpdateShellExtensions(alias: String) {
|
||||
if (installShellExtensions) {
|
||||
installShellExtensions(alias)
|
||||
exitProcess(0)
|
||||
} else {
|
||||
checkForAutoCompleteUpdate(alias)
|
||||
}
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
installOrUpdateShellExtensions("bootstrapper")
|
||||
if (verbose) {
|
||||
System.setProperty("logLevel", "trace")
|
||||
}
|
||||
@ -56,10 +173,15 @@ class Main : Runnable {
|
||||
}
|
||||
|
||||
private class CordaVersionProvider : IVersionProvider {
|
||||
companion object {
|
||||
val releaseVersion: String by lazy { Manifests.read("Corda-Release-Version") }
|
||||
val revision: String by lazy { Manifests.read("Corda-Revision") }
|
||||
}
|
||||
|
||||
override fun getVersion(): Array<String> {
|
||||
return arrayOf(
|
||||
"Version: ${Manifests.read("Corda-Release-Version")}",
|
||||
"Revision: ${Manifests.read("Corda-Revision")}"
|
||||
"Version: $releaseVersion",
|
||||
"Revision: $revision"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user