mirror of
https://github.com/corda/corda.git
synced 2025-03-15 00:36:49 +00:00
commit
134f6a016c
1258
.ci/api-current.txt
1258
.ci/api-current.txt
File diff suppressed because it is too large
Load Diff
@ -28,6 +28,9 @@ import java.util.*
|
||||
*/
|
||||
@StartableByRPC
|
||||
@InitiatingFlow
|
||||
// TODO Make this non-initiating as otherwise any CorDapp using confidential identities will cause its node to have an
|
||||
// open door where any counterparty will be able to swap identities at will. Instead SwapIdentitiesFlow and its counterpart,
|
||||
// SwapIdentitiesHandler, should be in-lined and called by CorDapp specfic-flows.
|
||||
class SwapIdentitiesFlow(private val otherParty: Party,
|
||||
private val revocationEnabled: Boolean,
|
||||
override val progressTracker: ProgressTracker) : FlowLogic<LinkedHashMap<Party, AnonymousParty>>() {
|
||||
|
@ -18,8 +18,8 @@ import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.CHARLIE_NAME
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.node.internal.FINANCE_CORDAPP
|
||||
import net.corda.testing.node.internal.InternalMockNetwork
|
||||
import net.corda.testing.node.internal.cordappsForPackages
|
||||
import net.corda.testing.node.internal.startFlow
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
@ -35,7 +35,7 @@ class IdentitySyncFlowTests {
|
||||
fun before() {
|
||||
// We run this in parallel threads to help catch any race conditions that may exist.
|
||||
mockNet = InternalMockNetwork(
|
||||
cordappsForAllNodes = cordappsForPackages("net.corda.finance.contracts.asset", "net.corda.finance.schemas"),
|
||||
cordappsForAllNodes = listOf(FINANCE_CORDAPP),
|
||||
networkSendManuallyPumped = false,
|
||||
threadPerNode = true
|
||||
)
|
||||
|
@ -187,9 +187,9 @@ class CollectSignatureFlow(val partiallySignedTx: SignedTransaction, val session
|
||||
* }
|
||||
*
|
||||
* // Invoke the subFlow, in response to the counterparty calling [CollectSignaturesFlow].
|
||||
* val stx = subFlow(flow)
|
||||
* val expectedTxId = subFlow(flow).id
|
||||
*
|
||||
* return waitForLedgerCommit(stx.id)
|
||||
* return subFlow(ReceiveFinalityFlow(otherPartySession, expectedTxId))
|
||||
* }
|
||||
* }
|
||||
*
|
||||
|
@ -1,10 +1,14 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.groupAbstractPartyByWellKnownParty
|
||||
import net.corda.core.internal.cordapp.CordappInfoResolver
|
||||
import net.corda.core.internal.pushToLoggingContext
|
||||
import net.corda.core.node.StatesToRecord
|
||||
import net.corda.core.node.StatesToRecord.ONLY_RELEVANT
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
@ -17,23 +21,53 @@ import net.corda.core.utilities.ProgressTracker
|
||||
* The transaction is expected to have already been resolved: if its dependencies are not available in local
|
||||
* storage, verification will fail. It must have signatures from all necessary parties other than the notary.
|
||||
*
|
||||
* If specified, the extra recipients are sent the given transaction. The base set of parties to inform are calculated
|
||||
* from the contract-given set of participants.
|
||||
* A list of [FlowSession]s is required for each non-local participant of the transaction. These participants will receive
|
||||
* the final notarised transaction by calling [ReceiveFinalityFlow] in their counterpart flows. Sessions with non-participants
|
||||
* can also be included, but they must specifiy [StatesToRecord.ALL_VISIBLE] for statesToRecortd if they wish to record the
|
||||
* contract states into their vaults.
|
||||
*
|
||||
* The flow returns the same transaction but with the additional signatures from the notary.
|
||||
*
|
||||
* NOTE: This is an inlined flow but for backwards compatibility is annotated with [InitiatingFlow].
|
||||
*
|
||||
* @param transaction What to commit.
|
||||
* @param extraRecipients A list of additional participants to inform of the transaction.
|
||||
* @param sessions A collection of [FlowSession]s who will be given the notarised transaction. This list **must** include
|
||||
* all participants in the transaction (excluding the local identity).
|
||||
*/
|
||||
// To maintain backwards compatibility with the old API, FinalityFlow can act both as an intiating flow and as an inlined flow.
|
||||
// This is only possible because a flow is only truly initiating when the first call to initiateFlow is made (where the
|
||||
// presence of @InitiatingFlow is checked). So the new API is inlined simply because that code path doesn't call initiateFlow.
|
||||
@InitiatingFlow
|
||||
class FinalityFlow(val transaction: SignedTransaction,
|
||||
private val extraRecipients: Set<Party>,
|
||||
override val progressTracker: ProgressTracker) : FlowLogic<SignedTransaction>() {
|
||||
constructor(transaction: SignedTransaction, extraParticipants: Set<Party>) : this(transaction, extraParticipants, tracker())
|
||||
constructor(transaction: SignedTransaction) : this(transaction, emptySet(), tracker())
|
||||
constructor(transaction: SignedTransaction, progressTracker: ProgressTracker) : this(transaction, emptySet(), progressTracker)
|
||||
class FinalityFlow private constructor(val transaction: SignedTransaction,
|
||||
private val extraRecipients: Set<Party>,
|
||||
override val progressTracker: ProgressTracker,
|
||||
private val sessions: Collection<FlowSession>?) : FlowLogic<SignedTransaction>() {
|
||||
@Deprecated(DEPRECATION_MSG)
|
||||
constructor(transaction: SignedTransaction, extraRecipients: Set<Party>, progressTracker: ProgressTracker) : this(
|
||||
transaction, extraRecipients, progressTracker, null
|
||||
)
|
||||
@Deprecated(DEPRECATION_MSG)
|
||||
constructor(transaction: SignedTransaction, extraRecipients: Set<Party>) : this(transaction, extraRecipients, tracker(), null)
|
||||
@Deprecated(DEPRECATION_MSG)
|
||||
constructor(transaction: SignedTransaction) : this(transaction, emptySet(), tracker(), null)
|
||||
@Deprecated(DEPRECATION_MSG)
|
||||
constructor(transaction: SignedTransaction, progressTracker: ProgressTracker) : this(transaction, emptySet(), progressTracker, null)
|
||||
|
||||
constructor(transaction: SignedTransaction, sessions: Collection<FlowSession>, progressTracker: ProgressTracker) : this(
|
||||
transaction, emptySet(), progressTracker, sessions
|
||||
)
|
||||
constructor(transaction: SignedTransaction, sessions: Collection<FlowSession>) : this(
|
||||
transaction, emptySet(), tracker(), sessions
|
||||
)
|
||||
constructor(transaction: SignedTransaction, firstSession: FlowSession, vararg restSessions: FlowSession) : this(
|
||||
transaction, emptySet(), tracker(), listOf(firstSession) + restSessions.asList()
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val DEPRECATION_MSG = "It is unsafe to use this constructor as it requires nodes to automatically " +
|
||||
"accept notarised transactions without first checking their relevancy. Instead, use one of the constructors " +
|
||||
"that takes in existing FlowSessions."
|
||||
|
||||
object NOTARISING : ProgressTracker.Step("Requesting signature by notary service") {
|
||||
override fun childProgressTracker() = NotaryFlow.Client.tracker()
|
||||
}
|
||||
@ -47,6 +81,19 @@ class FinalityFlow(val transaction: SignedTransaction,
|
||||
@Suspendable
|
||||
@Throws(NotaryException::class)
|
||||
override fun call(): SignedTransaction {
|
||||
if (sessions == null) {
|
||||
require(CordappInfoResolver.currentTargetVersion < 4) {
|
||||
"A flow session for each external participant to the transaction must be provided. If you wish to continue " +
|
||||
"using this insecure API then specify a target platform version of less than 4 for your CorDapp."
|
||||
}
|
||||
logger.warn("The current usage of FinalityFlow is unsafe. Please consider upgrading your CorDapp to use " +
|
||||
"FinalityFlow with FlowSessions.")
|
||||
} else {
|
||||
require(sessions.none { serviceHub.myInfo.isLegalIdentity(it.counterparty) }) {
|
||||
"Do not provide flow sessions for the local node. FinalityFlow will record the notarised transaction locally."
|
||||
}
|
||||
}
|
||||
|
||||
// Note: this method is carefully broken up to minimize the amount of data reachable from the stack at
|
||||
// the point where subFlow is invoked, as that minimizes the checkpointing work to be done.
|
||||
//
|
||||
@ -54,26 +101,49 @@ class FinalityFlow(val transaction: SignedTransaction,
|
||||
// Then send to the notary if needed, record locally and distribute.
|
||||
|
||||
transaction.pushToLoggingContext()
|
||||
val commandDataTypes = transaction.tx.commands.map { it.value }.mapNotNull { it::class.qualifiedName }.distinct()
|
||||
logger.info("Started finalization, commands are ${commandDataTypes.joinToString(", ", "[", "]")}.")
|
||||
val parties = getPartiesToSend(verifyTx())
|
||||
logCommandData()
|
||||
val externalParticipants = extractExternalParticipants(verifyTx())
|
||||
|
||||
if (sessions != null) {
|
||||
val missingRecipients = externalParticipants - sessions.map { it.counterparty }
|
||||
require(missingRecipients.isEmpty()) {
|
||||
"Flow sessions were not provided for the following transaction participants: $missingRecipients"
|
||||
}
|
||||
}
|
||||
|
||||
val notarised = notariseAndRecord()
|
||||
|
||||
// Each transaction has its own set of recipients, but extra recipients get them all.
|
||||
progressTracker.currentStep = BROADCASTING
|
||||
val recipients = parties.filterNot(serviceHub.myInfo::isLegalIdentity)
|
||||
logger.info("Broadcasting transaction to parties ${recipients.map { it.name }.joinToString(", ", "[", "]")}.")
|
||||
for (party in recipients) {
|
||||
logger.info("Sending transaction to party ${party.name}.")
|
||||
val session = initiateFlow(party)
|
||||
subFlow(SendTransactionFlow(session, notarised))
|
||||
logger.info("Party ${party.name} received the transaction.")
|
||||
|
||||
if (sessions == null) {
|
||||
val recipients = externalParticipants + (extraRecipients - serviceHub.myInfo.legalIdentities)
|
||||
logger.info("Broadcasting transaction to parties ${recipients.joinToString(", ", "[", "]")}.")
|
||||
for (recipient in recipients) {
|
||||
logger.info("Sending transaction to party ${recipient.name}.")
|
||||
val session = initiateFlow(recipient)
|
||||
subFlow(SendTransactionFlow(session, notarised))
|
||||
logger.info("Party $recipient received the transaction.")
|
||||
}
|
||||
} else {
|
||||
for (session in sessions) {
|
||||
subFlow(SendTransactionFlow(session, notarised))
|
||||
logger.info("Party ${session.counterparty} received the transaction.")
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("All parties received the transaction successfully.")
|
||||
|
||||
return notarised
|
||||
}
|
||||
|
||||
private fun logCommandData() {
|
||||
if (logger.isDebugEnabled) {
|
||||
val commandDataTypes = transaction.tx.commands.asSequence().mapNotNull { it.value::class.qualifiedName }.distinct()
|
||||
logger.debug("Started finalization, commands are ${commandDataTypes.joinToString(", ", "[", "]")}.")
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun notariseAndRecord(): SignedTransaction {
|
||||
val notarised = if (needsNotarySignature(transaction)) {
|
||||
@ -98,13 +168,13 @@ class FinalityFlow(val transaction: SignedTransaction,
|
||||
|
||||
private fun hasNoNotarySignature(stx: SignedTransaction): Boolean {
|
||||
val notaryKey = stx.tx.notary?.owningKey
|
||||
val signers = stx.sigs.map { it.by }.toSet()
|
||||
val signers = stx.sigs.asSequence().map { it.by }.toSet()
|
||||
return notaryKey?.isFulfilledBy(signers) != true
|
||||
}
|
||||
|
||||
private fun getPartiesToSend(ltx: LedgerTransaction): Set<Party> {
|
||||
private fun extractExternalParticipants(ltx: LedgerTransaction): Set<Party> {
|
||||
val participants = ltx.outputStates.flatMap { it.participants } + ltx.inputStates.flatMap { it.participants }
|
||||
return groupAbstractPartyByWellKnownParty(serviceHub, participants).keys + extraRecipients
|
||||
return groupAbstractPartyByWellKnownParty(serviceHub, participants).keys - serviceHub.myInfo.legalIdentities
|
||||
}
|
||||
|
||||
private fun verifyTx(): LedgerTransaction {
|
||||
@ -116,3 +186,33 @@ class FinalityFlow(val transaction: SignedTransaction,
|
||||
return ltx
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The receiving counterpart to [FinalityFlow].
|
||||
*
|
||||
* All parties who are receiving a finalised transaction from a sender flow must subcall this flow in their own flows.
|
||||
*
|
||||
* It's typical to have already signed the transaction proposal in the same workflow using [SignTransactionFlow]. If so
|
||||
* then the transaction ID can be passed in as an extra check to ensure the finalised transaction is the one that was signed
|
||||
* before it's committed to the vault.
|
||||
*
|
||||
* @param otherSideSession The session which is providing the transaction to record.
|
||||
* @param expectedTxId Expected ID of the transaction that's about to be received. This is typically retrieved from
|
||||
* [SignTransactionFlow].
|
||||
* @param statesToRecord Which transactions to commit to the vault. Defaults to [StatesToRecord.ONLY_RELEVANT].
|
||||
*/
|
||||
class ReceiveFinalityFlow @JvmOverloads constructor(val otherSideSession: FlowSession,
|
||||
val expectedTxId: SecureHash? = null,
|
||||
val statesToRecord: StatesToRecord = ONLY_RELEVANT) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
return subFlow(object : ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = true, statesToRecord = statesToRecord) {
|
||||
override fun checkBeforeRecording(stx: SignedTransaction) {
|
||||
require(expectedTxId == null || expectedTxId == stx.id) {
|
||||
"We expected to receive transaction with ID $expectedTxId but instead got ${stx.id}. Transaction was" +
|
||||
"not recorded and nor its states sent to the vault."
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -23,9 +23,9 @@ import java.security.SignatureException
|
||||
* @property checkSufficientSignatures if true checks all required signatures are present. See [SignedTransaction.verify].
|
||||
* @property statesToRecord which transaction states should be recorded in the vault, if any.
|
||||
*/
|
||||
class ReceiveTransactionFlow @JvmOverloads constructor(private val otherSideSession: FlowSession,
|
||||
private val checkSufficientSignatures: Boolean = true,
|
||||
private val statesToRecord: StatesToRecord = StatesToRecord.NONE) : FlowLogic<SignedTransaction>() {
|
||||
open class ReceiveTransactionFlow @JvmOverloads constructor(private val otherSideSession: FlowSession,
|
||||
private val checkSufficientSignatures: Boolean = true,
|
||||
private val statesToRecord: StatesToRecord = StatesToRecord.NONE) : FlowLogic<SignedTransaction>() {
|
||||
@Suppress("KDocMissingDocumentation")
|
||||
@Suspendable
|
||||
@Throws(SignatureException::class,
|
||||
@ -40,7 +40,7 @@ class ReceiveTransactionFlow @JvmOverloads constructor(private val otherSideSess
|
||||
}
|
||||
val stx = otherSideSession.receive<SignedTransaction>().unwrap {
|
||||
it.pushToLoggingContext()
|
||||
logger.info("Received transaction acknowledgement request from party ${otherSideSession.counterparty.name}.")
|
||||
logger.info("Received transaction acknowledgement request from party ${otherSideSession.counterparty}.")
|
||||
subFlow(ResolveTransactionsFlow(it, otherSideSession))
|
||||
logger.info("Transaction dependencies resolution completed.")
|
||||
try {
|
||||
@ -54,12 +54,21 @@ class ReceiveTransactionFlow @JvmOverloads constructor(private val otherSideSess
|
||||
if (checkSufficientSignatures) {
|
||||
// We should only send a transaction to the vault for processing if we did in fact fully verify it, and
|
||||
// there are no missing signatures. We don't want partly signed stuff in the vault.
|
||||
checkBeforeRecording(stx)
|
||||
logger.info("Successfully received fully signed tx. Sending it to the vault for processing.")
|
||||
serviceHub.recordTransactions(statesToRecord, setOf(stx))
|
||||
logger.info("Successfully recorded received transaction locally.")
|
||||
}
|
||||
return stx
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to perform extra checks on the received transaction just before it's recorded. The transaction has already
|
||||
* been resolved and verified at this point.
|
||||
*/
|
||||
@Suspendable
|
||||
@Throws(FlowException::class)
|
||||
protected open fun checkBeforeRecording(stx: SignedTransaction) = Unit
|
||||
}
|
||||
|
||||
/**
|
||||
@ -74,7 +83,8 @@ class ReceiveStateAndRefFlow<out T : ContractState>(private val otherSideSession
|
||||
@Suspendable
|
||||
override fun call(): List<StateAndRef<T>> {
|
||||
return otherSideSession.receive<List<StateAndRef<T>>>().unwrap {
|
||||
subFlow(ResolveTransactionsFlow(it.map { it.ref.txhash }.toSet(), otherSideSession))
|
||||
val txHashes = it.asSequence().map { it.ref.txhash }.toSet()
|
||||
subFlow(ResolveTransactionsFlow(txHashes, otherSideSession))
|
||||
it
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.core.internal.cordapp
|
||||
|
||||
import net.corda.core.internal.PLATFORM_VERSION
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
@ -11,12 +12,12 @@ object CordappInfoResolver {
|
||||
private val logger = loggerFor<CordappInfoResolver>()
|
||||
private val cordappClasses: ConcurrentHashMap<String, Set<CordappImpl.Info>> = ConcurrentHashMap()
|
||||
|
||||
// TODO use the StackWalker API once we migrate to Java 9+
|
||||
// TODO Use the StackWalker API once we migrate to Java 9+
|
||||
private var cordappInfoResolver: () -> CordappImpl.Info? = {
|
||||
Exception().stackTrace
|
||||
.mapNotNull { cordappClasses[it.className] }
|
||||
// If there is more than one cordapp registered for a class name we can't determine the "correct" one and return null.
|
||||
.firstOrNull { it.size < 2 }?.single()
|
||||
.firstOrNull { it.size == 1 }?.single()
|
||||
}
|
||||
|
||||
/*
|
||||
@ -42,22 +43,33 @@ object CordappInfoResolver {
|
||||
* In situations where a `[CordappProvider]` is available the CorDapp context should be obtained from there.
|
||||
*
|
||||
* @return Information about the CorDapp from which the invoker is called, null if called outside a CorDapp or the
|
||||
* calling CorDapp cannot be reliably determined..
|
||||
* calling CorDapp cannot be reliably determined.
|
||||
*/
|
||||
fun getCorDappInfo(): CordappImpl.Info? = cordappInfoResolver()
|
||||
val currentCordappInfo: CordappImpl.Info? get() = cordappInfoResolver()
|
||||
|
||||
/**
|
||||
* Temporarily switch out the internal resolver for another one. For use in testing.
|
||||
* Returns the target version of the current calling CorDapp. Defaults to the current platform version if there isn't one.
|
||||
*/
|
||||
// TODO It may be the default is wrong and this should be Int? instead
|
||||
val currentTargetVersion: Int get() = currentCordappInfo?.targetPlatformVersion ?: PLATFORM_VERSION
|
||||
|
||||
/**
|
||||
* Temporarily apply a fake CorDapp.Info with the given parameters. For use in testing.
|
||||
*/
|
||||
@Synchronized
|
||||
@VisibleForTesting
|
||||
fun withCordappInfoResolution(tempResolver: () -> CordappImpl.Info?, block: () -> Unit) {
|
||||
val resolver = cordappInfoResolver
|
||||
cordappInfoResolver = tempResolver
|
||||
fun <T> withCordappInfo(shortName: String = "CordappInfoResolver.withCordappInfo",
|
||||
vendor: String = "Corda",
|
||||
version: String = "1.0",
|
||||
minimumPlatformVersion: Int = 1,
|
||||
targetPlatformVersion: Int = PLATFORM_VERSION,
|
||||
block: () -> T): T {
|
||||
val currentResolver = cordappInfoResolver
|
||||
cordappInfoResolver = { CordappImpl.Info(shortName, vendor, version, minimumPlatformVersion, targetPlatformVersion) }
|
||||
try {
|
||||
block()
|
||||
return block()
|
||||
} finally {
|
||||
cordappInfoResolver = resolver
|
||||
cordappInfoResolver = currentResolver
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,4 +77,4 @@ object CordappInfoResolver {
|
||||
internal fun clear() {
|
||||
cordappClasses.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,8 +5,6 @@ import com.natpryce.hamkrest.assertion.assert
|
||||
import net.corda.core.contracts.Command
|
||||
import net.corda.core.contracts.StateAndContract
|
||||
import net.corda.core.contracts.requireThat
|
||||
import net.corda.testing.internal.matchers.flow.willReturn
|
||||
import net.corda.testing.internal.matchers.flow.willThrow
|
||||
import net.corda.core.flows.mixins.WithContracts
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
@ -17,6 +15,8 @@ import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.internal.matchers.flow.willReturn
|
||||
import net.corda.testing.internal.matchers.flow.willThrow
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.internal.InternalMockNetwork
|
||||
@ -116,7 +116,7 @@ class CollectSignaturesFlowTests : WithContracts {
|
||||
val ptx = serviceHub.signInitialTransaction(builder)
|
||||
val sessions = excludeHostNode(serviceHub, groupAbstractPartyByWellKnownParty(serviceHub, state.owners)).map { initiateFlow(it.key) }
|
||||
val stx = subFlow(CollectSignaturesFlow(ptx, sessions, myInputKeys))
|
||||
return subFlow(FinalityFlow(stx))
|
||||
return subFlow(FinalityFlow(stx, sessions))
|
||||
}
|
||||
}
|
||||
|
||||
@ -136,8 +136,8 @@ class CollectSignaturesFlowTests : WithContracts {
|
||||
}
|
||||
}
|
||||
|
||||
val stx = subFlow(signFlow)
|
||||
waitForLedgerCommit(stx.id)
|
||||
val stxId = subFlow(signFlow).id
|
||||
subFlow(ReceiveFinalityFlow(otherSideSession, expectedTxId = stxId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,63 +4,98 @@ import com.natpryce.hamkrest.and
|
||||
import com.natpryce.hamkrest.assertion.assert
|
||||
import net.corda.core.flows.mixins.WithFinality
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.cordapp.CordappInfoResolver
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.POUNDS
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.finance.issuedBy
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.internal.matchers.flow.willReturn
|
||||
import net.corda.testing.internal.matchers.flow.willThrow
|
||||
import net.corda.testing.node.internal.InternalMockNetwork
|
||||
import net.corda.testing.node.internal.TestStartedNode
|
||||
import net.corda.testing.node.internal.cordappsForPackages
|
||||
import org.junit.AfterClass
|
||||
import net.corda.testing.node.TestCordapp
|
||||
import net.corda.testing.node.internal.*
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
||||
import org.junit.After
|
||||
import org.junit.Test
|
||||
|
||||
class FinalityFlowTests : WithFinality {
|
||||
companion object {
|
||||
private val CHARLIE = TestIdentity(CHARLIE_NAME, 90).party
|
||||
private val classMockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForPackages(
|
||||
"net.corda.finance.contracts.asset",
|
||||
"net.corda.finance.schemas"
|
||||
))
|
||||
|
||||
@JvmStatic
|
||||
@AfterClass
|
||||
fun tearDown() = classMockNet.stopNodes()
|
||||
}
|
||||
|
||||
override val mockNet = classMockNet
|
||||
override val mockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForPackages(
|
||||
"net.corda.finance.contracts.asset",
|
||||
"net.corda.finance.schemas",
|
||||
"net.corda.core.flows.mixins"
|
||||
))
|
||||
|
||||
private val aliceNode = makeNode(ALICE_NAME)
|
||||
private val bobNode = makeNode(BOB_NAME)
|
||||
|
||||
private val bob = bobNode.info.singleIdentity()
|
||||
private val notary = mockNet.defaultNotaryIdentity
|
||||
|
||||
@After
|
||||
fun tearDown() = mockNet.stopNodes()
|
||||
|
||||
@Test
|
||||
fun `finalise a simple transaction`() {
|
||||
val stx = aliceNode.signCashTransactionWith(bob)
|
||||
val bob = createBob()
|
||||
val stx = aliceNode.issuesCashTo(bob)
|
||||
|
||||
assert.that(
|
||||
aliceNode.finalise(stx),
|
||||
aliceNode.finalise(stx, bob.info.singleIdentity()),
|
||||
willReturn(
|
||||
requiredSignatures(1)
|
||||
and visibleTo(bobNode)))
|
||||
and visibleTo(bob)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reject a transaction with unknown parties`() {
|
||||
// Charlie isn't part of this network, so node A won't recognise them
|
||||
val stx = aliceNode.signCashTransactionWith(CHARLIE)
|
||||
val stx = aliceNode.issuesCashTo(CHARLIE)
|
||||
|
||||
assert.that(
|
||||
aliceNode.finalise(stx),
|
||||
willThrow<IllegalArgumentException>())
|
||||
}
|
||||
|
||||
private fun TestStartedNode.signCashTransactionWith(other: Party): SignedTransaction {
|
||||
@Test
|
||||
fun `prevent use of the old API if the CorDapp target version is 4`() {
|
||||
val bob = createBob()
|
||||
val stx = aliceNode.issuesCashTo(bob)
|
||||
val resultFuture = CordappInfoResolver.withCordappInfo(targetPlatformVersion = 4) {
|
||||
@Suppress("DEPRECATION")
|
||||
aliceNode.startFlowAndRunNetwork(FinalityFlow(stx)).resultFuture
|
||||
}
|
||||
assertThatIllegalArgumentException().isThrownBy {
|
||||
resultFuture.getOrThrow()
|
||||
}.withMessageContaining("A flow session for each external participant to the transaction must be provided.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `allow use of the old API if the CorDapp target version is 3`() {
|
||||
// We need Bob to load at least one old CorDapp so that its FinalityHandler is enabled
|
||||
val bob = createBob(cordapps = listOf(cordappForPackages("com.template").withTargetVersion(3)))
|
||||
val stx = aliceNode.issuesCashTo(bob)
|
||||
val resultFuture = CordappInfoResolver.withCordappInfo(targetPlatformVersion = 3) {
|
||||
@Suppress("DEPRECATION")
|
||||
aliceNode.startFlowAndRunNetwork(FinalityFlow(stx)).resultFuture
|
||||
}
|
||||
resultFuture.getOrThrow()
|
||||
assertThat(bob.services.validatedTransactions.getTransaction(stx.id)).isNotNull()
|
||||
}
|
||||
|
||||
private fun createBob(cordapps: List<TestCordapp> = emptyList()): TestStartedNode {
|
||||
return mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME, additionalCordapps = cordapps))
|
||||
}
|
||||
|
||||
private fun TestStartedNode.issuesCashTo(recipient: TestStartedNode): SignedTransaction {
|
||||
return issuesCashTo(recipient.info.singleIdentity())
|
||||
}
|
||||
|
||||
private fun TestStartedNode.issuesCashTo(other: Party): SignedTransaction {
|
||||
val amount = 1000.POUNDS.issuedBy(info.singleIdentity().ref(0))
|
||||
val builder = TransactionBuilder(notary)
|
||||
Cash().generateIssue(builder, amount, other, notary)
|
||||
|
@ -50,7 +50,7 @@ internal class CreateRefState : FlowLogic<SignedTransaction>() {
|
||||
addOutputState(RefState.State(ourIdentity), RefState.CONTRACT_ID)
|
||||
addCommand(RefState.Create(), listOf(ourIdentity.owningKey))
|
||||
})
|
||||
return subFlow(FinalityFlow(stx))
|
||||
return subFlow(FinalityFlow(stx, emptyList()))
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,7 +64,7 @@ internal class UpdateRefState(private val stateAndRef: StateAndRef<RefState.Stat
|
||||
addOutputState(stateAndRef.state.data.update(), RefState.CONTRACT_ID)
|
||||
addCommand(RefState.Update(), listOf(ourIdentity.owningKey))
|
||||
})
|
||||
return subFlow(FinalityFlow(stx))
|
||||
return subFlow(FinalityFlow(stx, emptyList()))
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,7 +111,7 @@ internal class UseRefState(private val linearId: UniqueIdentifier) : FlowLogic<S
|
||||
addOutputState(DummyState(), DummyContract.PROGRAM_ID)
|
||||
addCommand(DummyContract.Commands.Create(), listOf(ourIdentity.owningKey))
|
||||
})
|
||||
return subFlow(FinalityFlow(stx))
|
||||
return subFlow(FinalityFlow(stx, emptyList()))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,9 +4,7 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import com.natpryce.hamkrest.MatchResult
|
||||
import com.natpryce.hamkrest.Matcher
|
||||
import com.natpryce.hamkrest.equalTo
|
||||
import net.corda.core.flows.FinalityFlow
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.FlowStateMachine
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
@ -18,16 +16,16 @@ import net.corda.testing.node.internal.TestStartedNode
|
||||
|
||||
interface WithFinality : WithMockNet {
|
||||
//region Operations
|
||||
fun TestStartedNode.finalise(stx: SignedTransaction, vararg additionalParties: Party): FlowStateMachine<SignedTransaction> {
|
||||
return startFlowAndRunNetwork(FinalityFlow(stx, additionalParties.toSet()))
|
||||
fun TestStartedNode.finalise(stx: SignedTransaction, vararg recipients: Party): FlowStateMachine<SignedTransaction> {
|
||||
return startFlowAndRunNetwork(FinalityInvoker(stx, recipients.toSet()))
|
||||
}
|
||||
|
||||
fun TestStartedNode.getValidatedTransaction(stx: SignedTransaction): SignedTransaction {
|
||||
return services.validatedTransactions.getTransaction(stx.id)!!
|
||||
}
|
||||
|
||||
fun CordaRPCOps.finalise(stx: SignedTransaction, vararg parties: Party): FlowHandle<SignedTransaction> {
|
||||
return startFlow(::FinalityInvoker, stx, parties.toSet()).andRunNetwork()
|
||||
fun CordaRPCOps.finalise(stx: SignedTransaction, vararg recipients: Party): FlowHandle<SignedTransaction> {
|
||||
return startFlow(::FinalityInvoker, stx, recipients.toSet()).andRunNetwork()
|
||||
}
|
||||
//endregion
|
||||
|
||||
@ -40,10 +38,22 @@ interface WithFinality : WithMockNet {
|
||||
}
|
||||
//endregion
|
||||
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
class FinalityInvoker(private val transaction: SignedTransaction,
|
||||
private val extraRecipients: Set<Party>) : FlowLogic<SignedTransaction>() {
|
||||
private val recipients: Set<Party>) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction = subFlow(FinalityFlow(transaction, extraRecipients))
|
||||
override fun call(): SignedTransaction {
|
||||
val sessions = recipients.map(::initiateFlow)
|
||||
return subFlow(FinalityFlow(transaction, sessions))
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(FinalityInvoker::class)
|
||||
class FinalityResponder(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
subFlow(ReceiveFinalityFlow(otherSide))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,42 +1,37 @@
|
||||
package net.corda.core.internal.cordapp
|
||||
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class CordappInfoResolverTest {
|
||||
|
||||
@Before
|
||||
@After
|
||||
fun clearCordappInfoResolver() {
|
||||
CordappInfoResolver.clear()
|
||||
}
|
||||
|
||||
@Test()
|
||||
fun `The correct cordapp resolver is used after calling withCordappResolution`() {
|
||||
@Test
|
||||
fun `the correct cordapp resolver is used after calling withCordappInfo`() {
|
||||
val defaultTargetVersion = 222
|
||||
|
||||
CordappInfoResolver.register(listOf(javaClass.name), CordappImpl.Info("test", "test", "2", 3, defaultTargetVersion))
|
||||
assertEquals(defaultTargetVersion, returnCallingTargetVersion())
|
||||
assertEquals(defaultTargetVersion, CordappInfoResolver.currentTargetVersion)
|
||||
|
||||
val expectedTargetVersion = 555
|
||||
CordappInfoResolver.withCordappInfoResolution( { CordappImpl.Info("foo", "bar", "1", 2, expectedTargetVersion) })
|
||||
{
|
||||
val actualTargetVersion = returnCallingTargetVersion()
|
||||
CordappInfoResolver.withCordappInfo(targetPlatformVersion = expectedTargetVersion) {
|
||||
val actualTargetVersion = CordappInfoResolver.currentTargetVersion
|
||||
assertEquals(expectedTargetVersion, actualTargetVersion)
|
||||
}
|
||||
assertEquals(defaultTargetVersion, returnCallingTargetVersion())
|
||||
assertEquals(defaultTargetVersion, CordappInfoResolver.currentTargetVersion)
|
||||
}
|
||||
|
||||
@Test()
|
||||
fun `When more than one cordapp is registered for the same class, the resolver returns null`() {
|
||||
@Test
|
||||
fun `when more than one cordapp is registered for the same class, the resolver returns null`() {
|
||||
CordappInfoResolver.register(listOf(javaClass.name), CordappImpl.Info("test", "test", "2", 3, 222))
|
||||
CordappInfoResolver.register(listOf(javaClass.name), CordappImpl.Info("test1", "test1", "1", 2, 456))
|
||||
assertEquals(0, returnCallingTargetVersion())
|
||||
assertThat(CordappInfoResolver.currentCordappInfo).isNull()
|
||||
}
|
||||
|
||||
private fun returnCallingTargetVersion(): Int {
|
||||
return CordappInfoResolver.getCorDappInfo()?.targetPlatformVersion ?: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -450,6 +450,9 @@ An example is the ``@InitiatingFlow InitiatorFlow``/``@InitiatedBy ResponderFlow
|
||||
|
||||
.. note:: Initiating flows are versioned separately from their parents.
|
||||
|
||||
.. note:: The only exception to this rule is ``FinalityFlow`` which is annotated with ``@InitiatingFlow`` but is an inlined flow. This flow
|
||||
was previously initiating and the annotation exists to maintain backwards compatibility with old code.
|
||||
|
||||
Core initiating subflows
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Corda-provided initiating subflows are a little different to standard ones as they are versioned together with the
|
||||
@ -460,8 +463,6 @@ Library flows
|
||||
^^^^^^^^^^^^^
|
||||
Corda installs four initiating subflow pairs on each node by default:
|
||||
|
||||
* ``FinalityFlow``/``FinalityHandler``, which should be used to notarise and record a transaction and broadcast it to
|
||||
all relevant parties
|
||||
* ``NotaryChangeFlow``/``NotaryChangeHandler``, which should be used to change a state's notary
|
||||
* ``ContractUpgradeFlow.Initiate``/``ContractUpgradeHandler``, which should be used to change a state's contract
|
||||
* ``SwapIdentitiesFlow``/``SwapIdentitiesHandler``, which is used to exchange confidential identities with a
|
||||
@ -474,10 +475,13 @@ Corda installs four initiating subflow pairs on each node by default:
|
||||
Corda also provides a number of built-in inlined subflows that should be used for handling common tasks. The most
|
||||
important are:
|
||||
|
||||
* ``CollectSignaturesFlow`` (inlined), which should be used to collect a transaction's required signatures
|
||||
* ``SendTransactionFlow`` (inlined), which should be used to send a signed transaction if it needed to be resolved on
|
||||
* ``FinalityFlow`` which is used to notarise, record locally and then broadcast a signed transaction to its participants
|
||||
and any extra parties.
|
||||
* ``ReceiveFinalityFlow`` to receive these notarised transactions from the ``FinalityFlow`` sender and record locally.
|
||||
* ``CollectSignaturesFlow`` , which should be used to collect a transaction's required signatures
|
||||
* ``SendTransactionFlow`` , which should be used to send a signed transaction if it needed to be resolved on
|
||||
the other side.
|
||||
* ``ReceiveTransactionFlow`` (inlined), which should be used receive a signed transaction
|
||||
* ``ReceiveTransactionFlow``, which should be used receive a signed transaction
|
||||
|
||||
Let's look at some of these flows in more detail.
|
||||
|
||||
@ -516,20 +520,26 @@ We can also choose to send the transaction to additional parties who aren't one
|
||||
:end-before: DOCEND 10
|
||||
:dedent: 12
|
||||
|
||||
Only one party has to call ``FinalityFlow`` for a given transaction to be recorded by all participants. It does
|
||||
**not** need to be called by each participant individually.
|
||||
Only one party has to call ``FinalityFlow`` for a given transaction to be recorded by all participants. It **must not**
|
||||
be called by every participant. Instead, every other particpant **must** call ``ReceiveFinalityFlow`` in their responder
|
||||
flow to receive the transaction:
|
||||
|
||||
Because the transaction has already been notarised and the input states consumed, if the participants when receiving the
|
||||
transaction fail to verify it, or the receiving flow (the finality handler) fails due to some other error, we then have
|
||||
the scenario where not all parties have the correct up to date view of the ledger. To recover from this the finality handler
|
||||
is automatically sent to the flow hospital where it's suspended and retried from its last checkpoint on node restart.
|
||||
This gives the node operator the opportunity to recover from the error. Until the issue is resolved the node will continue
|
||||
to retry the flow on each startup.
|
||||
.. container:: codeset
|
||||
|
||||
.. note:: It's possible to forcibly terminate the erroring finality handler using the ``killFlow`` RPC but at the risk
|
||||
of an inconsistent view of the ledger.
|
||||
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART ReceiveFinalityFlow
|
||||
:end-before: DOCEND ReceiveFinalityFlow
|
||||
:dedent: 8
|
||||
|
||||
.. note:: A future release will allow retrying hospitalised flows without restarting the node, i.e. via RPC.
|
||||
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java
|
||||
:language: java
|
||||
:start-after: DOCSTART ReceiveFinalityFlow
|
||||
:end-before: DOCEND ReceiveFinalityFlow
|
||||
:dedent: 12
|
||||
|
||||
``idOfTxWeSigned`` is an optional parameter used to confirm that we got the right transaction. It comes from using ``SignTransactionFlow``
|
||||
which is described below.
|
||||
|
||||
CollectSignaturesFlow/SignTransactionFlow
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
@ -136,23 +136,23 @@ Use the ``ServiceHub`` ``jdbcSession`` function to obtain a JDBC connection as i
|
||||
:start-after: DOCSTART JdbcSession
|
||||
:end-before: DOCEND JdbcSession
|
||||
|
||||
JDBC sessions can be used in Flows and Service Plugins (see ":doc:`flow-state-machines`")
|
||||
JDBC sessions can be used in flows and services (see ":doc:`flow-state-machines`").
|
||||
|
||||
The following example illustrates the creation of a custom corda service using a jdbcSession:
|
||||
The following example illustrates the creation of a custom Corda service using a ``jdbcSession``:
|
||||
|
||||
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/CustomVaultQuery.kt
|
||||
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/vault/CustomVaultQuery.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART CustomVaultQuery
|
||||
:end-before: DOCEND CustomVaultQuery
|
||||
|
||||
which is then referenced within a custom flow:
|
||||
|
||||
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/CustomVaultQuery.kt
|
||||
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/vault/CustomVaultQuery.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART TopupIssuer
|
||||
:end-before: DOCEND TopupIssuer
|
||||
|
||||
For examples on testing ``@CordaService`` implementations, see the oracle example :doc:`here <oracles>`
|
||||
For examples on testing ``@CordaService`` implementations, see the oracle example :doc:`here <oracles>`.
|
||||
|
||||
JPA Support
|
||||
-----------
|
||||
|
@ -24,7 +24,7 @@ are considered to have a stake in the state. Among other things, the ``participa
|
||||
|
||||
* Need to sign any notary-change and contract-upgrade transactions involving this state
|
||||
|
||||
* Receive any finalised transactions involving this state as part of ``FinalityFlow``
|
||||
* Receive any finalised transactions involving this state as part of ``FinalityFlow`` / ``ReceiveFinalityFlow``
|
||||
|
||||
ContractState sub-interfaces
|
||||
----------------------------
|
||||
|
@ -12,9 +12,21 @@ Unreleased
|
||||
|
||||
* New "validate-configuration" sub-command to `corda.jar`, allowing to validate the actual node configuration without starting the node.
|
||||
|
||||
* Introduced new optional network bootstrapper command line option (--minimum-platform-version) to set as a network parameter
|
||||
* CorDapps now have the ability to specify a minimum platform version in their MANIFEST.MF to prevent old nodes from loading them.
|
||||
|
||||
* Introduce minimum and target platform version for CorDapps.
|
||||
* CorDapps have the ability to specify a target platform version in their MANIFEST.MF as a means of indicating to the node
|
||||
the app was designed and tested on that version.
|
||||
|
||||
* Nodes will no longer automatically reject flow initiation requests for flows they don't know about. Instead the request will remain
|
||||
un-acknowledged in the message broker. This enables the recovery scenerio whereby any missing CorDapp can be installed and retried on node
|
||||
restart. As a consequence the initiating flow will be blocked until the receiving node has resolved the issue.
|
||||
|
||||
* ``FinalityFlow`` is now an inlined flow and no longer requires a handler flow in the counterparty. This is to fix the
|
||||
security problem with the handler flow as it accepts any transaction it receives without any checks. Existing CorDapp
|
||||
binaries relying on this old behaviour will continue to function as previously. However, it is strongly recommended that
|
||||
CorDapps switch to this new API. See :doc:`upgrade-notes` for further details.
|
||||
|
||||
* Introduced new optional network bootstrapper command line option (--minimum-platform-version) to set as a network parameter
|
||||
|
||||
* BFT-Smart and Raft notary implementations have been extracted out of node into ``experimental`` CorDapps to emphasise
|
||||
their experimental nature. Moreover, the BFT-Smart notary will only work in dev mode due to its use of Java serialization.
|
||||
|
@ -85,12 +85,12 @@ flow with a full node.
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. literalinclude:: ../../docs/source/example-code/src/integration-test/kotlin/net/corda/docs/TutorialFlowAsyncOperationTest.kt
|
||||
.. literalinclude:: ../../docs/source/example-code/src/integration-test/kotlin/net/corda/docs/kotlin/tutorial/test/TutorialFlowAsyncOperationTest.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART summingWorks
|
||||
:end-before: DOCEND summingWorks
|
||||
|
||||
.. literalinclude:: ../../docs/source/example-code/src/integration-test/java/net/corda/docs/java/TutorialFlowAsyncOperationTest.java
|
||||
.. literalinclude:: ../../docs/source/example-code/src/integration-test/java/net/corda/docs/java/tutorial/test/TutorialFlowAsyncOperationTest.java
|
||||
:language: java
|
||||
:start-after: DOCSTART summingWorks
|
||||
:end-before: DOCEND summingWorks
|
||||
|
@ -1,4 +1,4 @@
|
||||
package net.corda.docs;
|
||||
package net.corda.docs.java.tutorial.test;
|
||||
|
||||
import net.corda.client.rpc.CordaRPCClient;
|
||||
import net.corda.core.concurrent.CordaFuture;
|
||||
@ -22,7 +22,9 @@ import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import rx.Observable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.Currency;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.singletonList;
|
||||
@ -48,7 +50,7 @@ public class JavaIntegrationTestingTutorial extends IntegrationTest {
|
||||
// START 1
|
||||
driver(new DriverParameters()
|
||||
.withStartNodesInProcess(true)
|
||||
.withExtraCordappPackagesToScan(Arrays.asList("net.corda.finance.contracts.asset", "net.corda.finance.schemas")), dsl -> {
|
||||
.withExtraCordappPackagesToScan(singletonList("net.corda.finance")), dsl -> {
|
||||
|
||||
User aliceUser = new User("aliceUser", "testPassword1", new HashSet<>(asList(
|
||||
startFlow(CashIssueAndPaymentFlow.class),
|
@ -1,4 +1,4 @@
|
||||
package net.corda.docs.java;
|
||||
package net.corda.docs.java.tutorial.test;
|
||||
|
||||
import kotlin.Unit;
|
||||
import net.corda.client.rpc.CordaRPCClient;
|
@ -1,152 +0,0 @@
|
||||
package net.corda.docs
|
||||
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.core.internal.concurrent.transpose
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.messaging.vaultTrackBy
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.node.services.vault.QueryCriteria
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
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.testing.core.*
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.internal.IntegrationTest
|
||||
import net.corda.testing.internal.IntegrationTestSchemas
|
||||
import net.corda.testing.internal.toDatabaseSchemaName
|
||||
import net.corda.testing.node.User
|
||||
import org.junit.ClassRule
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class IntegrationTestingTutorial : IntegrationTest() {
|
||||
companion object {
|
||||
@ClassRule
|
||||
@JvmField
|
||||
val databaseSchemas = IntegrationTestSchemas(ALICE_NAME.toDatabaseSchemaName(), BOB_NAME.toDatabaseSchemaName(),
|
||||
DUMMY_NOTARY_NAME.toDatabaseSchemaName())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `alice bob cash exchange example`() {
|
||||
// START 1
|
||||
driver(DriverParameters(
|
||||
startNodesInProcess = true,
|
||||
extraCordappPackagesToScan = listOf("net.corda.finance.contracts.asset","net.corda.finance.schemas")
|
||||
)) {
|
||||
val aliceUser = User("aliceUser", "testPassword1", permissions = setOf(
|
||||
startFlow<CashIssueFlow>(),
|
||||
startFlow<CashPaymentFlow>(),
|
||||
invokeRpc("vaultTrackBy"),
|
||||
invokeRpc(CordaRPCOps::notaryIdentities),
|
||||
invokeRpc(CordaRPCOps::networkMapFeed)
|
||||
))
|
||||
val bobUser = User("bobUser", "testPassword2", permissions = setOf(
|
||||
startFlow<CashPaymentFlow>(),
|
||||
invokeRpc("vaultTrackBy"),
|
||||
invokeRpc(CordaRPCOps::networkMapFeed)
|
||||
))
|
||||
val (alice, bob) = listOf(
|
||||
startNode(providedName = ALICE_NAME, rpcUsers = listOf(aliceUser)),
|
||||
startNode(providedName = BOB_NAME, rpcUsers = listOf(bobUser))
|
||||
).transpose().getOrThrow()
|
||||
|
||||
// END 1
|
||||
|
||||
// START 2
|
||||
val aliceClient = CordaRPCClient(alice.rpcAddress)
|
||||
val aliceProxy = aliceClient.start("aliceUser", "testPassword1").proxy
|
||||
|
||||
val bobClient = CordaRPCClient(bob.rpcAddress)
|
||||
val bobProxy = bobClient.start("bobUser", "testPassword2").proxy
|
||||
// END 2
|
||||
|
||||
// START 3
|
||||
val bobVaultUpdates = bobProxy.vaultTrackBy<Cash.State>(criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL)).updates
|
||||
val aliceVaultUpdates = aliceProxy.vaultTrackBy<Cash.State>(criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL)).updates
|
||||
// END 3
|
||||
|
||||
// START 4
|
||||
val numberOfStates = 10
|
||||
val issueRef = OpaqueBytes.of(0)
|
||||
val notaryParty = aliceProxy.notaryIdentities().first()
|
||||
(1..numberOfStates).map { i ->
|
||||
aliceProxy.startFlow(::CashIssueFlow,
|
||||
i.DOLLARS,
|
||||
issueRef,
|
||||
notaryParty
|
||||
).returnValue
|
||||
}.transpose().getOrThrow()
|
||||
// We wait for all of the issuances to run before we start making payments
|
||||
(1..numberOfStates).map { i ->
|
||||
aliceProxy.startFlow(::CashPaymentFlow,
|
||||
i.DOLLARS,
|
||||
bob.nodeInfo.singleIdentity(),
|
||||
true
|
||||
).returnValue
|
||||
}.transpose().getOrThrow()
|
||||
|
||||
bobVaultUpdates.expectEvents {
|
||||
parallel(
|
||||
(1..numberOfStates).map { i ->
|
||||
expect(
|
||||
match = { update: Vault.Update<Cash.State> ->
|
||||
update.produced.first().state.data.amount.quantity == i * 100L
|
||||
}
|
||||
) { update ->
|
||||
println("Bob vault update of $update")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
// END 4
|
||||
|
||||
// START 5
|
||||
for (i in 1..numberOfStates) {
|
||||
bobProxy.startFlow(::CashPaymentFlow, i.DOLLARS, alice.nodeInfo.singleIdentity()).returnValue.getOrThrow()
|
||||
}
|
||||
|
||||
aliceVaultUpdates.expectEvents {
|
||||
sequence(
|
||||
// issuance
|
||||
parallel(
|
||||
(1..numberOfStates).map { i ->
|
||||
expect(match = { it.moved() == -i * 100 }) { update: Vault.Update<Cash.State> ->
|
||||
assertEquals(0, update.consumed.size)
|
||||
}
|
||||
}
|
||||
),
|
||||
// move to Bob
|
||||
parallel(
|
||||
(1..numberOfStates).map { i ->
|
||||
expect(match = { it.moved() == i * 100 }) { _: Vault.Update<Cash.State> ->
|
||||
}
|
||||
}
|
||||
),
|
||||
// move back to Alice
|
||||
sequence(
|
||||
(1..numberOfStates).map { i ->
|
||||
expect(match = { it.moved() == -i * 100 }) { update: Vault.Update<Cash.State> ->
|
||||
assertEquals(update.consumed.size, 0)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
// END 5
|
||||
}
|
||||
}
|
||||
|
||||
private fun Vault.Update<Cash.State>.moved(): Int {
|
||||
val consumedSum = consumed.sumBy { it.state.data.amount.quantity.toInt() }
|
||||
val producedSum = produced.sumBy { it.state.data.amount.quantity.toInt() }
|
||||
return consumedSum - producedSum
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package net.corda.docs
|
||||
package net.corda.docs.kotlin.tutorial.test
|
||||
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.core.contracts.Amount
|
||||
@ -40,10 +40,7 @@ class KotlinIntegrationTestingTutorial : IntegrationTest() {
|
||||
@Test
|
||||
fun `alice bob cash exchange example`() {
|
||||
// START 1
|
||||
driver(DriverParameters(
|
||||
startNodesInProcess = true,
|
||||
extraCordappPackagesToScan = listOf("net.corda.finance.contracts.asset", "net.corda.finance.schemas")
|
||||
)) {
|
||||
driver(DriverParameters(startNodesInProcess = true, extraCordappPackagesToScan = listOf("net.corda.finance"))) {
|
||||
val aliceUser = User("aliceUser", "testPassword1", permissions = setOf(
|
||||
startFlow<CashIssueAndPaymentFlow>(),
|
||||
invokeRpc("vaultTrackBy")
|
@ -1,4 +1,4 @@
|
||||
package net.corda.docs
|
||||
package net.corda.docs.kotlin.tutorial.test
|
||||
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.core.messaging.startFlow
|
@ -0,0 +1,134 @@
|
||||
package net.corda.docs.java;
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable;
|
||||
import net.corda.core.flows.*;
|
||||
import net.corda.core.identity.Party;
|
||||
import net.corda.core.transactions.SignedTransaction;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import static java.util.Collections.singletonList;
|
||||
|
||||
@SuppressWarnings("ALL")
|
||||
public class FinalityFlowMigration {
|
||||
public static SignedTransaction dummyTransactionWithParticipant(Party party) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
// DOCSTART SimpleFlowUsingOldApi
|
||||
public static class SimpleFlowUsingOldApi extends FlowLogic<SignedTransaction> {
|
||||
private final Party counterparty;
|
||||
|
||||
@Suspendable
|
||||
@Override
|
||||
public SignedTransaction call() throws FlowException {
|
||||
SignedTransaction stx = dummyTransactionWithParticipant(counterparty);
|
||||
return subFlow(new FinalityFlow(stx));
|
||||
}
|
||||
// DOCEND SimpleFlowUsingOldApi
|
||||
|
||||
public SimpleFlowUsingOldApi(Party counterparty) {
|
||||
this.counterparty = counterparty;
|
||||
}
|
||||
}
|
||||
|
||||
// DOCSTART SimpleFlowUsingNewApi
|
||||
// Notice how the flow *must* now be an initiating flow even when it wasn't before.
|
||||
@InitiatingFlow
|
||||
public static class SimpleFlowUsingNewApi extends FlowLogic<SignedTransaction> {
|
||||
private final Party counterparty;
|
||||
|
||||
@Suspendable
|
||||
@Override
|
||||
public SignedTransaction call() throws FlowException {
|
||||
SignedTransaction stx = dummyTransactionWithParticipant(counterparty);
|
||||
// For each non-local participant in the transaction we must initiate a flow session with them.
|
||||
FlowSession session = initiateFlow(counterparty);
|
||||
return subFlow(new FinalityFlow(stx, session));
|
||||
}
|
||||
// DOCEND SimpleFlowUsingNewApi
|
||||
|
||||
public SimpleFlowUsingNewApi(Party counterparty) {
|
||||
this.counterparty = counterparty;
|
||||
}
|
||||
}
|
||||
// DOCSTART SimpleNewResponderFlow
|
||||
// All participants will run this flow to receive and record the finalised transaction into their vault.
|
||||
@InitiatedBy(SimpleFlowUsingNewApi.class)
|
||||
public static class SimpleNewResponderFlow extends FlowLogic<Void> {
|
||||
private final FlowSession otherSide;
|
||||
|
||||
@Suspendable
|
||||
@Override
|
||||
public Void call() throws FlowException {
|
||||
subFlow(new ReceiveFinalityFlow(otherSide));
|
||||
return null;
|
||||
}
|
||||
// DOCEND SimpleNewResponderFlow
|
||||
|
||||
public SimpleNewResponderFlow(FlowSession otherSide) {
|
||||
this.otherSide = otherSide;
|
||||
}
|
||||
}
|
||||
|
||||
// DOCSTART ExistingInitiatingFlow
|
||||
// Assuming the previous version of the flow was 1 (the default if none is specified), we increment the version number to 2
|
||||
// to allow for backwards compatibility with nodes running the old CorDapp.
|
||||
@InitiatingFlow(version = 2)
|
||||
public static class ExistingInitiatingFlow extends FlowLogic<SignedTransaction> {
|
||||
private final Party counterparty;
|
||||
|
||||
@Suspendable
|
||||
@Override
|
||||
public SignedTransaction call() throws FlowException {
|
||||
SignedTransaction partiallySignedTx = dummyTransactionWithParticipant(counterparty);
|
||||
FlowSession session = initiateFlow(counterparty);
|
||||
SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(partiallySignedTx, singletonList(session)));
|
||||
// Determine which version of the flow that other side is using.
|
||||
if (session.getCounterpartyFlowInfo().getFlowVersion() == 1) {
|
||||
// Use the old API if the other side is using the previous version of the flow.
|
||||
return subFlow(new FinalityFlow(fullySignedTx));
|
||||
} else {
|
||||
// Otherwise they're at least on version 2 and so we can send the finalised transaction on the existing session.
|
||||
return subFlow(new FinalityFlow(fullySignedTx, session));
|
||||
}
|
||||
}
|
||||
// DOCEND ExistingInitiatingFlow
|
||||
|
||||
public ExistingInitiatingFlow(Party counterparty) {
|
||||
this.counterparty = counterparty;
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(ExistingInitiatingFlow.class)
|
||||
public static class ExistingResponderFlow extends FlowLogic<Void> {
|
||||
private final FlowSession otherSide;
|
||||
|
||||
public ExistingResponderFlow(FlowSession otherSide) {
|
||||
this.otherSide = otherSide;
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@Override
|
||||
public Void call() throws FlowException {
|
||||
SignedTransaction txWeJustSigned = subFlow(new SignTransactionFlow(otherSide) {
|
||||
@Suspendable
|
||||
@Override
|
||||
protected void checkTransaction(@NotNull SignedTransaction stx) throws FlowException {
|
||||
// Do checks here
|
||||
}
|
||||
});
|
||||
// DOCSTART ExistingResponderFlow
|
||||
if (otherSide.getCounterpartyFlowInfo().getFlowVersion() >= 2) {
|
||||
// The other side is not using the old CorDapp so call ReceiveFinalityFlow to record the finalised transaction.
|
||||
// If SignTransactionFlow is used then we can verify the tranaction we receive for recording is the same one
|
||||
// that was just signed.
|
||||
subFlow(new ReceiveFinalityFlow(otherSide, txWeJustSigned.getId()));
|
||||
} else {
|
||||
// Otherwise the other side is running the old CorDapp and so we don't need to do anything further. The node
|
||||
// will automatically record the finalised transaction using the old insecure mechanism.
|
||||
}
|
||||
// DOCEND ExistingResponderFlow
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -28,6 +28,7 @@ import java.security.GeneralSecurityException;
|
||||
import java.security.PublicKey;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
@ -579,13 +580,13 @@ public class FlowCookbook {
|
||||
// We notarise the transaction and get it recorded in the vault of
|
||||
// the participants of all the transaction's states.
|
||||
// DOCSTART 09
|
||||
SignedTransaction notarisedTx1 = subFlow(new FinalityFlow(fullySignedTx, FINALISATION.childProgressTracker()));
|
||||
SignedTransaction notarisedTx1 = subFlow(new FinalityFlow(fullySignedTx, singleton(counterpartySession), FINALISATION.childProgressTracker()));
|
||||
// DOCEND 09
|
||||
// We can also choose to send it to additional parties who aren't one
|
||||
// of the state's participants.
|
||||
// DOCSTART 10
|
||||
Set<Party> additionalParties = singleton(regulator);
|
||||
SignedTransaction notarisedTx2 = subFlow(new FinalityFlow(fullySignedTx, additionalParties, FINALISATION.childProgressTracker()));
|
||||
List<FlowSession> partySessions = Arrays.asList(counterpartySession, initiateFlow(regulator));
|
||||
SignedTransaction notarisedTx2 = subFlow(new FinalityFlow(fullySignedTx, partySessions, FINALISATION.childProgressTracker()));
|
||||
// DOCEND 10
|
||||
|
||||
return null;
|
||||
@ -667,7 +668,7 @@ public class FlowCookbook {
|
||||
}
|
||||
}
|
||||
|
||||
subFlow(new SignTxFlow(counterpartySession, SignTransactionFlow.tracker()));
|
||||
SecureHash idOfTxWeSigned = subFlow(new SignTxFlow(counterpartySession, SignTransactionFlow.tracker())).getId();
|
||||
// DOCEND 16
|
||||
|
||||
/*------------------------------
|
||||
@ -675,9 +676,12 @@ public class FlowCookbook {
|
||||
------------------------------*/
|
||||
progressTracker.setCurrentStep(FINALISATION);
|
||||
|
||||
// Nothing to do here! As long as some other party calls
|
||||
// ``FinalityFlow``, the recording of the transaction on our node
|
||||
// we be handled automatically.
|
||||
// As the final step the responder waits to receive the notarised transaction from the sending party
|
||||
// Since it knows the ID of the transaction it just signed, the transaction ID is specified to ensure the correct
|
||||
// transaction is received and recorded.
|
||||
// DOCSTART ReceiveFinalityFlow
|
||||
subFlow(new ReceiveFinalityFlow(counterpartySession, idOfTxWeSigned));
|
||||
// DOCEND ReceiveFinalityFlow
|
||||
|
||||
return null;
|
||||
}
|
||||
|
@ -2,16 +2,12 @@ package net.corda.docs.java.tutorial.helloworld;
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable;
|
||||
import com.template.TemplateContract;
|
||||
import net.corda.core.flows.FlowException;
|
||||
import net.corda.core.flows.FlowLogic;
|
||||
import net.corda.core.flows.InitiatingFlow;
|
||||
import net.corda.core.flows.StartableByRPC;
|
||||
import net.corda.core.flows.*;
|
||||
import net.corda.core.utilities.ProgressTracker;
|
||||
|
||||
// DOCSTART 01
|
||||
// Add these imports:
|
||||
import net.corda.core.contracts.Command;
|
||||
import net.corda.core.flows.FinalityFlow;
|
||||
import net.corda.core.identity.Party;
|
||||
import net.corda.core.transactions.SignedTransaction;
|
||||
import net.corda.core.transactions.TransactionBuilder;
|
||||
@ -59,8 +55,11 @@ public class IOUFlow extends FlowLogic<Void> {
|
||||
// Signing the transaction.
|
||||
SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder);
|
||||
|
||||
// Finalising the transaction.
|
||||
subFlow(new FinalityFlow(signedTx));
|
||||
// Creating a session with the other party.
|
||||
FlowSession otherPartySession = initiateFlow(otherParty);
|
||||
|
||||
// We finalise the transaction and then send it to the counterparty.
|
||||
subFlow(new FinalityFlow(signedTx, otherPartySession));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
@ -0,0 +1,25 @@
|
||||
package net.corda.docs.java.tutorial.helloworld;
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable;
|
||||
import net.corda.core.flows.*;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
// DOCSTART 01
|
||||
// Replace Responder's definition with:
|
||||
@InitiatedBy(IOUFlow.class)
|
||||
public class IOUFlowResponder extends FlowLogic<Void> {
|
||||
private final FlowSession otherPartySession;
|
||||
|
||||
public IOUFlowResponder(FlowSession otherPartySession) {
|
||||
this.otherPartySession = otherPartySession;
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@Override
|
||||
public Void call() throws FlowException {
|
||||
subFlow(new ReceiveFinalityFlow(otherPartySession));
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// DOCEND 01
|
@ -69,7 +69,7 @@ public class IOUFlow extends FlowLogic<Void> {
|
||||
signedTx, Arrays.asList(otherPartySession), CollectSignaturesFlow.tracker()));
|
||||
|
||||
// Finalising the transaction.
|
||||
subFlow(new FinalityFlow(fullySignedTx));
|
||||
subFlow(new FinalityFlow(fullySignedTx, otherPartySession));
|
||||
|
||||
return null;
|
||||
// DOCEND 02
|
||||
|
@ -1,15 +1,14 @@
|
||||
package net.corda.docs.java.tutorial.twoparty;
|
||||
|
||||
// DOCSTART 01
|
||||
// Add these imports:
|
||||
import co.paralleluniverse.fibers.Suspendable;
|
||||
import net.corda.core.contracts.ContractState;
|
||||
import net.corda.core.crypto.SecureHash;
|
||||
import net.corda.core.flows.*;
|
||||
import net.corda.core.transactions.SignedTransaction;
|
||||
import net.corda.core.utilities.ProgressTracker;
|
||||
|
||||
import static net.corda.core.contracts.ContractsDSL.requireThat;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
// Define IOUFlowResponder:
|
||||
@InitiatedBy(IOUFlow.class)
|
||||
public class IOUFlowResponder extends FlowLogic<Void> {
|
||||
@ -22,9 +21,10 @@ public class IOUFlowResponder extends FlowLogic<Void> {
|
||||
@Suspendable
|
||||
@Override
|
||||
public Void call() throws FlowException {
|
||||
// DOCSTART 01
|
||||
class SignTxFlow extends SignTransactionFlow {
|
||||
private SignTxFlow(FlowSession otherPartySession, ProgressTracker progressTracker) {
|
||||
super(otherPartySession, progressTracker);
|
||||
private SignTxFlow(FlowSession otherPartySession) {
|
||||
super(otherPartySession);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -39,9 +39,11 @@ public class IOUFlowResponder extends FlowLogic<Void> {
|
||||
}
|
||||
}
|
||||
|
||||
subFlow(new SignTxFlow(otherPartySession, SignTransactionFlow.Companion.tracker()));
|
||||
SecureHash expectedTxId = subFlow(new SignTxFlow(otherPartySession)).getId();
|
||||
|
||||
subFlow(new ReceiveFinalityFlow(otherPartySession, expectedTxId));
|
||||
|
||||
return null;
|
||||
// DOCEND 01
|
||||
}
|
||||
}
|
||||
// DOCEND 01
|
@ -0,0 +1,91 @@
|
||||
@file:Suppress("DEPRECATION", "unused", "UNUSED_PARAMETER")
|
||||
|
||||
package net.corda.docs.kotlin
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
|
||||
private fun dummyTransactionWithParticipant(party: Party): SignedTransaction = TODO()
|
||||
|
||||
// DOCSTART SimpleFlowUsingOldApi
|
||||
class SimpleFlowUsingOldApi(private val counterparty: Party) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val stx = dummyTransactionWithParticipant(counterparty)
|
||||
return subFlow(FinalityFlow(stx))
|
||||
}
|
||||
}
|
||||
// DOCEND SimpleFlowUsingOldApi
|
||||
|
||||
// DOCSTART SimpleFlowUsingNewApi
|
||||
// Notice how the flow *must* now be an initiating flow even when it wasn't before.
|
||||
@InitiatingFlow
|
||||
class SimpleFlowUsingNewApi(private val counterparty: Party) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val stx = dummyTransactionWithParticipant(counterparty)
|
||||
// For each non-local participant in the transaction we must initiate a flow session with them.
|
||||
val session = initiateFlow(counterparty)
|
||||
return subFlow(FinalityFlow(stx, session))
|
||||
}
|
||||
}
|
||||
// DOCEND SimpleFlowUsingNewApi
|
||||
|
||||
// DOCSTART SimpleNewResponderFlow
|
||||
// All participants will run this flow to receive and record the finalised transaction into their vault.
|
||||
@InitiatedBy(SimpleFlowUsingNewApi::class)
|
||||
class SimpleNewResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
subFlow(ReceiveFinalityFlow(otherSide))
|
||||
}
|
||||
}
|
||||
// DOCEND SimpleNewResponderFlow
|
||||
|
||||
// DOCSTART ExistingInitiatingFlow
|
||||
// Assuming the previous version of the flow was 1 (the default if none is specified), we increment the version number to 2
|
||||
// to allow for backwards compatibility with nodes running the old CorDapp.
|
||||
@InitiatingFlow(version = 2)
|
||||
class ExistingInitiatingFlow(private val counterparty: Party) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val partiallySignedTx = dummyTransactionWithParticipant(counterparty)
|
||||
val session = initiateFlow(counterparty)
|
||||
val fullySignedTx = subFlow(CollectSignaturesFlow(partiallySignedTx, listOf(session)))
|
||||
// Determine which version of the flow that other side is using.
|
||||
return if (session.getCounterpartyFlowInfo().flowVersion == 1) {
|
||||
// Use the old API if the other side is using the previous version of the flow.
|
||||
subFlow(FinalityFlow(fullySignedTx))
|
||||
} else {
|
||||
// Otherwise they're at least on version 2 and so we can send the finalised transaction on the existing session.
|
||||
subFlow(FinalityFlow(fullySignedTx, session))
|
||||
}
|
||||
}
|
||||
}
|
||||
// DOCEND ExistingInitiatingFlow
|
||||
|
||||
@InitiatedBy(ExistingInitiatingFlow::class)
|
||||
class ExistingResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val txWeJustSigned = subFlow(object : SignTransactionFlow(otherSide) {
|
||||
@Suspendable
|
||||
override fun checkTransaction(stx: SignedTransaction) {
|
||||
// Do checks here
|
||||
}
|
||||
})
|
||||
// DOCSTART ExistingResponderFlow
|
||||
if (otherSide.getCounterpartyFlowInfo().flowVersion >= 2) {
|
||||
// The other side is not using the old CorDapp so call ReceiveFinalityFlow to record the finalised transaction.
|
||||
// If SignTransactionFlow is used then we can verify the tranaction we receive for recording is the same one
|
||||
// that was just signed.
|
||||
subFlow(ReceiveFinalityFlow(otherSide, expectedTxId = txWeJustSigned.id))
|
||||
} else {
|
||||
// Otherwise the other side is running the old CorDapp and so we don't need to do anything further. The node
|
||||
// will automatically record the finalised transaction using the old insecure mechanism.
|
||||
}
|
||||
// DOCEND ExistingResponderFlow
|
||||
}
|
||||
}
|
@ -568,13 +568,13 @@ class InitiatorFlow(val arg1: Boolean, val arg2: Int, private val counterparty:
|
||||
// We notarise the transaction and get it recorded in the vault of
|
||||
// the participants of all the transaction's states.
|
||||
// DOCSTART 09
|
||||
val notarisedTx1: SignedTransaction = subFlow(FinalityFlow(fullySignedTx, FINALISATION.childProgressTracker()))
|
||||
val notarisedTx1: SignedTransaction = subFlow(FinalityFlow(fullySignedTx, listOf(counterpartySession), FINALISATION.childProgressTracker()))
|
||||
// DOCEND 09
|
||||
// We can also choose to send it to additional parties who aren't one
|
||||
// of the state's participants.
|
||||
// DOCSTART 10
|
||||
val additionalParties: Set<Party> = setOf(regulator)
|
||||
val notarisedTx2: SignedTransaction = subFlow(FinalityFlow(fullySignedTx, additionalParties, FINALISATION.childProgressTracker()))
|
||||
val partySessions: List<FlowSession> = listOf(counterpartySession, initiateFlow(regulator))
|
||||
val notarisedTx2: SignedTransaction = subFlow(FinalityFlow(fullySignedTx, partySessions, FINALISATION.childProgressTracker()))
|
||||
// DOCEND 10
|
||||
}
|
||||
}
|
||||
@ -643,7 +643,7 @@ class ResponderFlow(val counterpartySession: FlowSession) : FlowLogic<Unit>() {
|
||||
}
|
||||
}
|
||||
|
||||
subFlow(signTransactionFlow)
|
||||
val idOfTxWeSigned = subFlow(signTransactionFlow).id
|
||||
// DOCEND 16
|
||||
|
||||
/**-----------------------------
|
||||
@ -651,8 +651,11 @@ class ResponderFlow(val counterpartySession: FlowSession) : FlowLogic<Unit>() {
|
||||
-----------------------------**/
|
||||
progressTracker.currentStep = FINALISATION
|
||||
|
||||
// Nothing to do here! As long as some other party calls
|
||||
// ``FinalityFlow``, the recording of the transaction on our node
|
||||
// we be handled automatically.
|
||||
// As the final step the responder waits to receive the notarised transaction from the sending party
|
||||
// Since it knows the ID of the transaction it just signed, the transaction ID is specified to ensure the correct
|
||||
// transaction is received and recorded.
|
||||
// DOCSTART ReceiveFinalityFlow
|
||||
subFlow(ReceiveFinalityFlow(counterpartySession, expectedTxId = idOfTxWeSigned))
|
||||
// DOCEND ReceiveFinalityFlow
|
||||
}
|
||||
}
|
||||
|
@ -160,7 +160,7 @@ class ForeignExchangeFlow(private val tradeId: String,
|
||||
}
|
||||
|
||||
// Initiate the standard protocol to notarise and distribute to the involved parties.
|
||||
subFlow(FinalityFlow(allPartySignedTx, setOf(counterparty)))
|
||||
subFlow(FinalityFlow(allPartySignedTx, counterpartySession))
|
||||
|
||||
return allPartySignedTx.id
|
||||
}
|
||||
@ -239,7 +239,8 @@ class ForeignExchangeRemoteFlow(private val source: FlowSession) : FlowLogic<Uni
|
||||
|
||||
// send the other side our signature.
|
||||
source.send(ourSignature)
|
||||
// N.B. The FinalityProtocol will be responsible for Notarising the SignedTransaction
|
||||
// and broadcasting the result to us.
|
||||
|
||||
// and then finally stored the finalised transaction into our vault
|
||||
subFlow(ReceiveFinalityFlow(source))
|
||||
}
|
||||
}
|
||||
|
@ -43,8 +43,11 @@ class IOUFlow(val iouValue: Int,
|
||||
// We sign the transaction.
|
||||
val signedTx = serviceHub.signInitialTransaction(txBuilder)
|
||||
|
||||
// We finalise the transaction.
|
||||
subFlow(FinalityFlow(signedTx))
|
||||
// Creating a session with the other party.
|
||||
val otherPartySession = initiateFlow(otherParty)
|
||||
|
||||
// We finalise the transaction and then send it to the counterparty.
|
||||
subFlow(FinalityFlow(signedTx, otherPartySession))
|
||||
}
|
||||
}
|
||||
// DOCEND 01
|
||||
|
@ -0,0 +1,20 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package net.corda.docs.kotlin.tutorial.helloworld
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.flows.ReceiveFinalityFlow
|
||||
|
||||
// DOCSTART 01
|
||||
// Replace Responder's definition with:
|
||||
@InitiatedBy(IOUFlow::class)
|
||||
class IOUFlowResponder(private val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
subFlow(ReceiveFinalityFlow(otherPartySession))
|
||||
}
|
||||
}
|
||||
// DOCEND 01
|
@ -52,7 +52,7 @@ class IOUFlow(val iouValue: Int,
|
||||
val fullySignedTx = subFlow(CollectSignaturesFlow(signedTx, listOf(otherPartySession), CollectSignaturesFlow.tracker()))
|
||||
|
||||
// Finalising the transaction.
|
||||
subFlow(FinalityFlow(fullySignedTx))
|
||||
subFlow(FinalityFlow(fullySignedTx, otherPartySession))
|
||||
// DOCEND 02
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,8 @@ import net.corda.core.transactions.SignedTransaction
|
||||
class IOUFlowResponder(val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val signTransactionFlow = object : SignTransactionFlow(otherPartySession, SignTransactionFlow.tracker()) {
|
||||
// DOCSTART 01
|
||||
val signTransactionFlow = object : SignTransactionFlow(otherPartySession) {
|
||||
override fun checkTransaction(stx: SignedTransaction) = requireThat {
|
||||
val output = stx.tx.outputs.single().data
|
||||
"This must be an IOU transaction." using (output is IOUState)
|
||||
@ -26,7 +27,9 @@ class IOUFlowResponder(val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||
}
|
||||
}
|
||||
|
||||
subFlow(signTransactionFlow)
|
||||
val expectedTxId = subFlow(signTransactionFlow).id
|
||||
|
||||
subFlow(ReceiveFinalityFlow(otherPartySession, expectedTxId))
|
||||
// DOCEND 01
|
||||
}
|
||||
}
|
||||
// DOCEND 01
|
||||
|
@ -1,6 +1,6 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package net.corda.docs.kotlin
|
||||
package net.corda.docs.kotlin.txbuild
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.*
|
||||
@ -25,7 +25,7 @@ enum class WorkflowState {
|
||||
REJECTED
|
||||
}
|
||||
|
||||
const val TRADE_APPROVAL_PROGRAM_ID = "net.corda.docs.kotlin.TradeApprovalContract"
|
||||
const val TRADE_APPROVAL_PROGRAM_ID = "net.corda.docs.kotlin.txbuild.TradeApprovalContract"
|
||||
|
||||
/**
|
||||
* Minimal contract to encode a simple workflow with one initial state and two possible eventual states.
|
||||
@ -93,6 +93,7 @@ data class TradeApprovalContract(val blank: Unit? = null) : Contract {
|
||||
* The protocol then sends a copy to the other node. We don't require the other party to sign
|
||||
* as their approval/rejection is to follow.
|
||||
*/
|
||||
@InitiatingFlow
|
||||
class SubmitTradeApprovalFlow(private val tradeId: String,
|
||||
private val counterparty: Party) : FlowLogic<StateAndRef<TradeApprovalContract.State>>() {
|
||||
@Suspendable
|
||||
@ -109,12 +110,20 @@ class SubmitTradeApprovalFlow(private val tradeId: String,
|
||||
// We can automatically sign as there is no untrusted data.
|
||||
val signedTx = serviceHub.signInitialTransaction(tx)
|
||||
// Notarise and distribute.
|
||||
subFlow(FinalityFlow(signedTx, setOf(counterparty)))
|
||||
subFlow(FinalityFlow(signedTx, initiateFlow(counterparty)))
|
||||
// Return the initial state
|
||||
return signedTx.tx.outRef(0)
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(SubmitTradeApprovalFlow::class)
|
||||
class SubmitTradeApprovalResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
subFlow(ReceiveFinalityFlow(otherSide))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple flow to complete a proposal submitted by another party and ensure both nodes
|
||||
* end up with a fully signed copy of the state either as APPROVED, or REJECTED
|
||||
@ -174,8 +183,8 @@ class SubmitCompletionFlow(private val ref: StateRef, private val verdict: Workf
|
||||
val selfSignedTx = serviceHub.signInitialTransaction(tx)
|
||||
//DOCEND 2
|
||||
// Send the signed transaction to the originator and await their signature to confirm
|
||||
val session = initiateFlow(newState.source)
|
||||
val allPartySignedTx = session.sendAndReceive<TransactionSignature>(selfSignedTx).unwrap {
|
||||
val sourceSession = initiateFlow(newState.source)
|
||||
val allPartySignedTx = sourceSession.sendAndReceive<TransactionSignature>(selfSignedTx).unwrap {
|
||||
// Add their signature to our unmodified transaction. To check they signed the same tx.
|
||||
val agreedTx = selfSignedTx + it
|
||||
// Receive back their signature and confirm that it is for an unmodified transaction
|
||||
@ -189,7 +198,7 @@ class SubmitCompletionFlow(private val ref: StateRef, private val verdict: Workf
|
||||
}
|
||||
// DOCSTART 4
|
||||
// Notarise and distribute the completed transaction.
|
||||
subFlow(FinalityFlow(allPartySignedTx, setOf(newState.source)))
|
||||
subFlow(FinalityFlow(allPartySignedTx, sourceSession))
|
||||
// DOCEND 4
|
||||
// Return back the details of the completed state/transaction.
|
||||
return allPartySignedTx.tx.outRef(0)
|
||||
@ -233,7 +242,7 @@ class RecordCompletionFlow(private val sourceSession: FlowSession) : FlowLogic<U
|
||||
val ourSignature = serviceHub.createSignature(completeTx)
|
||||
// Send our signature to the other party.
|
||||
sourceSession.send(ourSignature)
|
||||
// N.B. The FinalityProtocol will be responsible for Notarising the SignedTransaction
|
||||
// and broadcasting the result to us.
|
||||
|
||||
subFlow(ReceiveFinalityFlow(sourceSession))
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||
|
||||
package net.corda.docs.kotlin
|
||||
package net.corda.docs.kotlin.vault
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.Amount
|
||||
@ -68,7 +68,6 @@ object CustomVaultQuery {
|
||||
* This is a slightly modified version of the IssuerFlow, which uses a 3rd party custom query to
|
||||
* retrieve a list of currencies and top up amounts to be used in the issuance.
|
||||
*/
|
||||
|
||||
object TopupIssuerFlow {
|
||||
@CordaSerializable
|
||||
data class TopupRequest(val issueToParty: Party,
|
@ -1,9 +1,10 @@
|
||||
package net.corda.docs.kotlin
|
||||
package net.corda.docs.kotlin.txbuild
|
||||
|
||||
import net.corda.core.contracts.LinearState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.UniqueIdentifier
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.packageName
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.queryBy
|
||||
import net.corda.core.node.services.vault.QueryCriteria
|
||||
@ -33,7 +34,7 @@ class WorkflowTransactionBuildTutorialTest {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.docs"))
|
||||
mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf(javaClass.packageName))
|
||||
aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
||||
bobNode = mockNet.createPartyNode(BOB_NAME)
|
||||
alice = aliceNode.services.myInfo.identityFromX500Name(ALICE_NAME)
|
@ -1,13 +1,14 @@
|
||||
package net.corda.docs.kotlin
|
||||
package net.corda.docs.kotlin.vault
|
||||
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.packageName
|
||||
import net.corda.core.node.services.queryBy
|
||||
import net.corda.core.node.services.vault.*
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.docs.java.tutorial.helloworld.IOUFlow
|
||||
import net.corda.docs.kotlin.tutorial.helloworld.IOUFlow
|
||||
import net.corda.finance.*
|
||||
import net.corda.finance.contracts.getCashBalances
|
||||
import net.corda.finance.flows.CashIssueFlow
|
||||
@ -17,7 +18,7 @@ import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.StartedMockNode
|
||||
import org.assertj.core.api.Assertions.assertThatCode
|
||||
import org.junit.After
|
||||
import org.junit.Assert
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.util.*
|
||||
@ -30,7 +31,7 @@ class CustomVaultQueryTest {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.finance", "net.corda.docs", "com.template"))
|
||||
mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.finance", IOUFlow::class.packageName, javaClass.packageName, "com.template"))
|
||||
nodeA = mockNet.createPartyNode()
|
||||
nodeB = mockNet.createPartyNode()
|
||||
notary = mockNet.defaultNotaryIdentity
|
||||
@ -43,7 +44,6 @@ class CustomVaultQueryTest {
|
||||
|
||||
@Test
|
||||
fun `query by max recorded time`() {
|
||||
|
||||
nodeA.startFlow(IOUFlow(1000, nodeB.info.singleIdentity())).getOrThrow()
|
||||
nodeA.startFlow(IOUFlow(500, nodeB.info.singleIdentity())).getOrThrow()
|
||||
|
||||
@ -69,9 +69,9 @@ class CustomVaultQueryTest {
|
||||
topUpCurrencies()
|
||||
val (cashBalancesAfterTopup, _) = getBalances()
|
||||
|
||||
Assert.assertEquals(cashBalancesOriginal[GBP]?.times(2), cashBalancesAfterTopup[GBP])
|
||||
Assert.assertEquals(cashBalancesOriginal[USD]?.times(2) , cashBalancesAfterTopup[USD])
|
||||
Assert.assertEquals(cashBalancesOriginal[CHF]?.times( 2), cashBalancesAfterTopup[CHF])
|
||||
assertEquals(cashBalancesOriginal[GBP]?.times(2), cashBalancesAfterTopup[GBP])
|
||||
assertEquals(cashBalancesOriginal[USD]?.times(2) , cashBalancesAfterTopup[USD])
|
||||
assertEquals(cashBalancesOriginal[CHF]?.times( 2), cashBalancesAfterTopup[CHF])
|
||||
}
|
||||
|
||||
private fun issueCashForCurrency(amountToIssue: Amount<Currency>) {
|
||||
@ -86,7 +86,8 @@ class CustomVaultQueryTest {
|
||||
nodeA.info.singleIdentity(),
|
||||
OpaqueBytes.of(0x01),
|
||||
nodeA.info.singleIdentity(),
|
||||
notary)).getOrThrow()
|
||||
notary)
|
||||
).getOrThrow()
|
||||
}
|
||||
|
||||
private fun getBalances(): Pair<Map<Currency, Amount<Currency>>, Map<Currency, Amount<Currency>>> {
|
@ -227,8 +227,8 @@ Next, we call another subflow called ``SignTransactionFlow``. ``SignTransactionF
|
||||
|
||||
The transaction then needs to be finalized. This is the the process of sending the transaction to a notary to assert
|
||||
(with another signature) that the time-window in the transaction (if any) is valid and there are no double spends.
|
||||
In this flow, finalization is handled by the buyer, so we just wait for the signed transaction to appear in our
|
||||
transaction storage. It will have the same ID as the one we started with but more signatures.
|
||||
In this flow, finalization is handled by the buyer, we just wait for them to send it to us. It will have the same ID as
|
||||
the one we started with but more signatures.
|
||||
|
||||
Implementing the buyer
|
||||
----------------------
|
||||
@ -314,9 +314,11 @@ On the buyer side, we use ``FinalityFlow`` to finalise the transaction. It will:
|
||||
* Record the transaction in the local vault, if it is relevant (i.e. involves the owner of the node).
|
||||
* Send the fully signed transaction to the other participants for recording as well.
|
||||
|
||||
.. warning:: If the seller stops before sending the finalised transaction to the buyer, the seller is left with a
|
||||
valid transaction but the buyer isn't, so they can't spend the asset they just purchased! This sort of thing is not
|
||||
always a risk (as the seller may not gain anything from that sort of behaviour except a lawsuit), but if it is, a future
|
||||
On the seller side we use ``ReceiveFinalityFlow`` to receive and record the finalised transaction.
|
||||
|
||||
.. warning:: If the buyer stops before sending the finalised transaction to the seller, the buyer is left with a
|
||||
valid transaction but the seller isn't, so they don't get the cash! This sort of thing is not
|
||||
always a risk (as the buyer may not gain anything from that sort of behaviour except a lawsuit), but if it is, a future
|
||||
version of the platform will allow you to ask the notary to send you the transaction as well, in case your counterparty
|
||||
does not. This is not the default because it reveals more private info to the notary.
|
||||
|
||||
|
@ -21,11 +21,11 @@ require the following steps:
|
||||
|
||||
1. Building the transaction proposal for the issuance of a new IOU onto a ledger
|
||||
2. Signing the transaction proposal
|
||||
3. Recording the transaction
|
||||
4. Sending the transaction to the IOU's borrower so that they can record it too
|
||||
3. Recording the transaction and sending it to the IOU's borrower so that they can record it too
|
||||
|
||||
At this stage, we do not require the borrower to approve and sign IOU issuance transactions. We will be able to impose
|
||||
this requirement when we look at contracts in the next tutorial.
|
||||
We also need the borrower to receive the transaction and record it for itself. At this stage, we do not require the borrower
|
||||
to approve and sign IOU issuance transactions. We will be able to impose this requirement when we look at contracts in the
|
||||
next tutorial.
|
||||
|
||||
Subflows
|
||||
^^^^^^^^
|
||||
@ -34,13 +34,11 @@ forcing each developer to reimplement their own logic to handle these tasks, Cor
|
||||
to handle these tasks. We call these flows that are invoked in the context of a larger flow to handle a repeatable task
|
||||
*subflows*.
|
||||
|
||||
In our case, we can automate steps 3 and 4 of the IOU issuance flow using ``FinalityFlow``.
|
||||
|
||||
FlowLogic
|
||||
---------
|
||||
All flows must subclass ``FlowLogic``. You then define the steps taken by the flow by overriding ``FlowLogic.call``.
|
||||
|
||||
Let's define our ``IOUFlow``. Delete the existing ``Responder`` flow. Then replace the definition of ``Initiator`` with the following:
|
||||
Let's define our ``IOUFlow``. Replace the definition of ``Initiator`` with the following:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
@ -54,8 +52,9 @@ Let's define our ``IOUFlow``. Delete the existing ``Responder`` flow. Then repla
|
||||
:start-after: DOCSTART 01
|
||||
:end-before: DOCEND 01
|
||||
|
||||
If you're following along in Java, you'll also need to rename ``Initiator.java`` to ``IOUFlow.java``. Let's walk
|
||||
through this code step-by-step.
|
||||
If you're following along in Java, you'll also need to rename ``Initiator.java`` to ``IOUFlow.java``.
|
||||
|
||||
Let's walk through this code step-by-step.
|
||||
|
||||
We've defined our own ``FlowLogic`` subclass that overrides ``FlowLogic.call``. ``FlowLogic.call`` has a return type
|
||||
that must match the type parameter passed to ``FlowLogic`` - this is type returned by running the flow.
|
||||
@ -73,7 +72,7 @@ annotation out will lead to some very weird error messages!
|
||||
There are also a few more annotations, on the ``FlowLogic`` subclass itself:
|
||||
|
||||
* ``@InitiatingFlow`` means that this flow is part of a flow pair and that it triggers the other side to run the
|
||||
the counterpart flow.
|
||||
the counterpart flow (which in our case is the ``IOUFlowResponder`` defined below).
|
||||
* ``@StartableByRPC`` allows the node owner to start this flow via an RPC call
|
||||
|
||||
Let's walk through the steps of ``FlowLogic.call`` itself. This is where we actually describe the procedure for
|
||||
@ -145,15 +144,36 @@ We sign the transaction using ``ServiceHub.signInitialTransaction``, which retur
|
||||
|
||||
Finalising the transaction
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
We now have a valid signed transaction. All that's left to do is to have it recorded by all the relevant parties. By
|
||||
doing so, it will become a permanent part of the ledger. As discussed, we'll handle this process automatically using a
|
||||
built-in flow called ``FinalityFlow``. ``FinalityFlow`` completely automates the process of:
|
||||
We now have a valid signed transaction. All that's left to do is to get the notary to sign it, have that recorded
|
||||
locally and then send it to all the relevant parties. Once that happens the transaction will become a permanent part of the
|
||||
ledger. We use ``FinalityFlow`` which does all of this for the lender.
|
||||
|
||||
* Notarising the transaction if required (i.e. if the transaction contains inputs and/or a time-window)
|
||||
* Recording it in our vault
|
||||
* Sending it to the other participants (i.e. the lender) for them to record as well
|
||||
For the borrower to receive the transaction they just need a flow that responds to the seller's.
|
||||
|
||||
Creating the borrower's flow
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
The borrower has to use ``ReceiveFinalityFlow`` in order to receive and record the transaction; it needs to respond to
|
||||
the lender's flow. Let's do that by replacing ``Responder`` from the template with the following:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/helloworld/IOUFlowResponder.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 01
|
||||
:end-before: DOCEND 01
|
||||
|
||||
.. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUFlowResponder.java
|
||||
:language: java
|
||||
:start-after: DOCSTART 01
|
||||
:end-before: DOCEND 01
|
||||
|
||||
As with the ``IOUFlow``, our ``IOUFlowResponder`` flow is a ``FlowLogic`` subclass where we've overridden ``FlowLogic.call``.
|
||||
|
||||
The flow is annotated with ``InitiatedBy(IOUFlow.class)``, which means that your node will invoke
|
||||
``IOUFlowResponder.call`` when it receives a message from a instance of ``Initiator`` running on another node. This message
|
||||
will be the finalised transaction which will be recorded in the borrower's vault.
|
||||
|
||||
Progress so far
|
||||
---------------
|
||||
Our flow, and our CorDapp, are now ready! We have now defined a flow that we can start on our node to completely
|
||||
automate the process of issuing an IOU onto the ledger. All that's left is to spin up some nodes and test our CorDapp.
|
||||
automate the process of issuing an IOU onto the ledger. All that's left is to spin up some nodes and test our CorDapp.
|
||||
|
@ -65,13 +65,10 @@ transaction proposal before finalising it by adding our signature.
|
||||
Requesting the borrower's signature
|
||||
-----------------------------------
|
||||
|
||||
We now need to communicate with the borrower to request their signature over the transaction. Whenever you want to
|
||||
communicate with another party in the context of a flow, you first need to establish a flow session with them. If the
|
||||
counterparty has a ``FlowLogic`` registered to respond to the ``FlowLogic`` initiating the session, a session will be
|
||||
established. All communication between the two ``FlowLogic`` instances will then place as part of this session.
|
||||
Previously we wrote a responder flow for the borrower in order to receive the finalised transaction from the lender.
|
||||
We use this same flow to first request their signature over the transaction.
|
||||
|
||||
Once we have a session with the borrower, we gather the borrower's signature using ``CollectSignaturesFlow``, which
|
||||
takes:
|
||||
We gather the borrower's signature using ``CollectSignaturesFlow``, which takes:
|
||||
|
||||
* A transaction signed by the flow initiator
|
||||
* A list of flow-sessions between the flow initiator and the required signers
|
||||
@ -80,11 +77,11 @@ And returns a transaction signed by all the required signers.
|
||||
|
||||
We can then pass this fully-signed transaction into ``FinalityFlow``.
|
||||
|
||||
Creating the borrower's flow
|
||||
Updating the borrower's flow
|
||||
----------------------------
|
||||
On the lender's side, we used ``CollectSignaturesFlow`` to automate the collection of signatures. To allow the lender
|
||||
to respond, we need to write a response flow as well. In a new ``IOUFlowResponder.java`` file in Java, or within the
|
||||
``App.kt`` file in Kotlin, add the following class:
|
||||
On the lender's side, we used ``CollectSignaturesFlow`` to automate the collection of signatures. To allow the borrower
|
||||
to respond, we need to update its responder flow to first receive the partially signed transaction for signing. Update
|
||||
``IOUFlowResponder.call`` to be the following:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
@ -92,19 +89,13 @@ to respond, we need to write a response flow as well. In a new ``IOUFlowResponde
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 01
|
||||
:end-before: DOCEND 01
|
||||
:dedent: 8
|
||||
|
||||
.. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlowResponder.java
|
||||
:language: java
|
||||
:start-after: DOCSTART 01
|
||||
:end-before: DOCEND 01
|
||||
|
||||
As with the ``IOUFlow``, our ``IOUFlowResponder`` flow is a ``FlowLogic`` subclass where we've overridden
|
||||
``FlowLogic.call``.
|
||||
|
||||
The flow is annotated with ``InitiatedBy(IOUFlow.class)``, which means that your node will invoke
|
||||
``IOUFlowResponder.call`` when it receives a message from a instance of ``Initiator`` running on another node. What
|
||||
will this message from the ``IOUFlow`` be? If we look at the definition of ``CollectSignaturesFlow``, we can see that
|
||||
we'll be sent a ``SignedTransaction``, and are expected to send back our signature over that transaction.
|
||||
:dedent: 8
|
||||
|
||||
We could write our own flow to handle this process. However, there is also a pre-defined flow called
|
||||
``SignTransactionFlow`` that can handle the process automatically. The only catch is that ``SignTransactionFlow`` is an
|
||||
@ -128,6 +119,9 @@ signatures are contractually valid.
|
||||
Once we've defined the ``SignTransactionFlow`` subclass, we invoke it using ``FlowLogic.subFlow``, and the
|
||||
communication with the borrower's and the lender's flow is conducted automatically.
|
||||
|
||||
``SignedTransactionFlow`` returns the newly signed transaction. We pass in the transaction's ID to ``ReceiveFinalityFlow``
|
||||
to ensure we are recording the correct notarised transaction from the lender.
|
||||
|
||||
Conclusion
|
||||
----------
|
||||
We have now updated our flow to verify the transaction and gather the lender's signature, in line with the constraints
|
||||
|
@ -7,7 +7,7 @@ In the Hello, World tutorial, we built a CorDapp allowing us to model IOUs on le
|
||||
elements:
|
||||
|
||||
* An ``IOUState``, representing IOUs on the blockchain
|
||||
* An ``IOUFlow``, orchestrating the process of agreeing the creation of an IOU on-ledger
|
||||
* An ``IOUFlow`` and ``IOFlowResponder`` flow pair, orchestrating the process of agreeing the creation of an IOU on-ledger
|
||||
|
||||
However, our CorDapp did not impose any constraints on the evolution of IOUs on the blockchain over time. Anyone was free
|
||||
to create IOUs of any value, between any party.
|
||||
|
@ -81,12 +81,10 @@ To give a few more specific details consider two simplified real world
|
||||
scenarios. First, a basic foreign exchange cash transaction. This
|
||||
transaction needs to locate a set of funds to exchange. A flow
|
||||
modelling this is implemented in ``FxTransactionBuildTutorial.kt``
|
||||
(see ``docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FxTransactionBuildTutorial.kt`` in the
|
||||
`main Corda repo <https://github.com/corda/corda>`_).
|
||||
(in the `main Corda repo <https://github.com/corda/corda>`_).
|
||||
Second, a simple business model in which parties manually accept or
|
||||
reject each other's trade proposals, which is implemented in
|
||||
``WorkflowTransactionBuildTutorial.kt`` (see
|
||||
``docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/WorkflowTransactionBuildTutorial.kt`` in the
|
||||
``WorkflowTransactionBuildTutorial.kt`` (in the
|
||||
`main Corda repo <https://github.com/corda/corda>`_). To run and explore these
|
||||
examples using the IntelliJ IDE one can run/step through the respective unit
|
||||
tests in ``FxTransactionBuildTutorialTest.kt`` and
|
||||
@ -148,7 +146,7 @@ parameters to the flow to identify the states being operated upon. Thus
|
||||
code to gather the latest input state for a given ``StateRef`` would use
|
||||
the ``VaultService`` as follows:
|
||||
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/WorkflowTransactionBuildTutorial.kt
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/txbuild/WorkflowTransactionBuildTutorial.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 1
|
||||
:end-before: DOCEND 1
|
||||
@ -221,7 +219,7 @@ and convert it into a ``SignedTransaction``.
|
||||
|
||||
Examples of this process are:
|
||||
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/WorkflowTransactionBuildTutorial.kt
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/txbuild/WorkflowTransactionBuildTutorial.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 2
|
||||
:end-before: DOCEND 2
|
||||
@ -260,7 +258,7 @@ context. For example, the flow may need to check that the parties are the
|
||||
right ones, or that the ``Command`` present on the transaction is as
|
||||
expected for this specific flow. An example of this from the demo code is:
|
||||
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/WorkflowTransactionBuildTutorial.kt
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/txbuild/WorkflowTransactionBuildTutorial.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 3
|
||||
:end-before: DOCEND 3
|
||||
@ -277,7 +275,7 @@ Once all the signatures are applied to the ``SignedTransaction``, the
|
||||
final steps are notarisation and ensuring that all nodes record the fully-signed transaction. The
|
||||
code for this is standardised in the ``FinalityFlow``:
|
||||
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/WorkflowTransactionBuildTutorial.kt
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/txbuild/WorkflowTransactionBuildTutorial.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 4
|
||||
:end-before: DOCEND 4
|
||||
|
@ -21,13 +21,13 @@ a local network where all the nodes see each other and provides safe shutting do
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/KotlinIntegrationTestingTutorial.kt
|
||||
.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/kotlin/tutorial/test/KotlinIntegrationTestingTutorial.kt
|
||||
:language: kotlin
|
||||
:start-after: START 1
|
||||
:end-before: END 1
|
||||
:dedent: 8
|
||||
|
||||
.. literalinclude:: example-code/src/integration-test/java/net/corda/docs/JavaIntegrationTestingTutorial.java
|
||||
.. literalinclude:: example-code/src/integration-test/java/net/corda/docs/java/tutorial/test/JavaIntegrationTestingTutorial.java
|
||||
:language: java
|
||||
:start-after: START 1
|
||||
:end-before: END 1
|
||||
@ -49,13 +49,13 @@ the information returned; their respective ``NodeHandles`` s.
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/KotlinIntegrationTestingTutorial.kt
|
||||
.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/kotlin/tutorial/test/KotlinIntegrationTestingTutorial.kt
|
||||
:language: kotlin
|
||||
:start-after: START 2
|
||||
:end-before: END 2
|
||||
:dedent: 12
|
||||
|
||||
.. literalinclude:: example-code/src/integration-test/java/net/corda/docs/JavaIntegrationTestingTutorial.java
|
||||
.. literalinclude:: example-code/src/integration-test/java/net/corda/docs/java/tutorial/test/JavaIntegrationTestingTutorial.java
|
||||
:language: java
|
||||
:start-after: START 2
|
||||
:end-before: END 2
|
||||
@ -66,13 +66,13 @@ us to start flows and query state.
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/KotlinIntegrationTestingTutorial.kt
|
||||
.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/kotlin/tutorial/test/KotlinIntegrationTestingTutorial.kt
|
||||
:language: kotlin
|
||||
:start-after: START 3
|
||||
:end-before: END 3
|
||||
:dedent: 12
|
||||
|
||||
.. literalinclude:: example-code/src/integration-test/java/net/corda/docs/JavaIntegrationTestingTutorial.java
|
||||
.. literalinclude:: example-code/src/integration-test/java/net/corda/docs/java/tutorial/test/JavaIntegrationTestingTutorial.java
|
||||
:language: java
|
||||
:start-after: START 3
|
||||
:end-before: END 3
|
||||
@ -84,13 +84,13 @@ Now that we're all set up we can finally get some cash action going!
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/KotlinIntegrationTestingTutorial.kt
|
||||
.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/kotlin/tutorial/test/KotlinIntegrationTestingTutorial.kt
|
||||
:language: kotlin
|
||||
:start-after: START 4
|
||||
:end-before: END 4
|
||||
:dedent: 12
|
||||
|
||||
.. literalinclude:: example-code/src/integration-test/java/net/corda/docs/JavaIntegrationTestingTutorial.java
|
||||
.. literalinclude:: example-code/src/integration-test/java/net/corda/docs/java/tutorial/test/JavaIntegrationTestingTutorial.java
|
||||
:language: java
|
||||
:start-after: START 4
|
||||
:end-before: END 4
|
||||
@ -106,13 +106,13 @@ is asserting.
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/KotlinIntegrationTestingTutorial.kt
|
||||
.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/kotlin/tutorial/test/KotlinIntegrationTestingTutorial.kt
|
||||
:language: kotlin
|
||||
:start-after: START 5
|
||||
:end-before: END 5
|
||||
:dedent: 12
|
||||
|
||||
.. literalinclude:: example-code/src/integration-test/java/net/corda/docs/JavaIntegrationTestingTutorial.java
|
||||
.. literalinclude:: example-code/src/integration-test/java/net/corda/docs/java/tutorial/test/JavaIntegrationTestingTutorial.java
|
||||
:language: java
|
||||
:start-after: START 5
|
||||
:end-before: END 5
|
||||
@ -123,6 +123,6 @@ Next we want Bob to send this cash back to Alice.
|
||||
That's it! We saw how to start up several corda nodes locally, how to connect to them, and how to test some simple invariants
|
||||
about ``CashIssueAndPaymentFlow`` and ``CashPaymentFlow``.
|
||||
|
||||
You can find the complete test at ``example-code/src/integration-test/java/net/corda/docs/JavaIntegrationTestingTutorial.java``
|
||||
(Java) and ``example-code/src/integration-test/kotlin/net/corda/docs/KotlinIntegrationTestingTutorial.kt`` (Kotlin) in the
|
||||
You can find the complete test at ``example-code/src/integration-test/java/net/corda/docs/java/tutorial/test/JavaIntegrationTestingTutorial.java``
|
||||
(Java) and ``example-code/src/integration-test/kotlin/net/corda/docs/kotlin/tutorial/test/KotlinIntegrationTestingTutorial.kt`` (Kotlin) in the
|
||||
`Corda repo <https://github.com/corda/corda>`_.
|
||||
|
@ -1,3 +1,9 @@
|
||||
.. highlight:: kotlin
|
||||
.. raw:: html
|
||||
|
||||
<script type="text/javascript" src="_static/jquery.js"></script>
|
||||
<script type="text/javascript" src="_static/codesets.js"></script>
|
||||
|
||||
Upgrading a CorDapp to a new platform version
|
||||
=============================================
|
||||
|
||||
@ -6,6 +12,106 @@ These notes provide instructions for upgrading your CorDapps from previous versi
|
||||
.. contents::
|
||||
:depth: 1
|
||||
|
||||
UNRELEASED
|
||||
----------
|
||||
|
||||
FinalityFlow
|
||||
^^^^^^^^^^^^
|
||||
|
||||
The previous ``FinalityFlow`` API is insecure. It requires a handler flow in the counterparty node which accepts any and
|
||||
all signed transactions that are sent to it, without checks. It is **highly** recommended that existing CorDapps migrate
|
||||
away to the new API.
|
||||
|
||||
As an example, let's take a very simple flow that finalises a transaction without the involvement of a counterpart flow:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/FinalityFlowMigration.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART SimpleFlowUsingOldApi
|
||||
:end-before: DOCEND SimpleFlowUsingOldApi
|
||||
|
||||
.. literalinclude:: example-code/src/main/java/net/corda/docs/java/FinalityFlowMigration.java
|
||||
:language: java
|
||||
:start-after: DOCSTART SimpleFlowUsingOldApi
|
||||
:end-before: DOCEND SimpleFlowUsingOldApi
|
||||
:dedent: 4
|
||||
|
||||
To use the new API, this flow needs to be annotated with ``InitiatingFlow`` and a ``FlowSession`` to the participant of the transaction must be
|
||||
passed to ``FinalityFlow`` :
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/FinalityFlowMigration.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART SimpleFlowUsingNewApi
|
||||
:end-before: DOCEND SimpleFlowUsingNewApi
|
||||
|
||||
.. literalinclude:: example-code/src/main/java/net/corda/docs/java/FinalityFlowMigration.java
|
||||
:language: java
|
||||
:start-after: DOCSTART SimpleFlowUsingNewApi
|
||||
:end-before: DOCEND SimpleFlowUsingNewApi
|
||||
:dedent: 4
|
||||
|
||||
If there are more than one transaction participants then a session to each one must be initiated, excluding the local party
|
||||
and the notary.
|
||||
|
||||
A responder flow has to be introduced, which will automatically run on the other participants' nodes, which will call ``ReceiveFinalityFlow``
|
||||
to record the finalised transaction:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/FinalityFlowMigration.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART SimpleNewResponderFlow
|
||||
:end-before: DOCEND SimpleNewResponderFlow
|
||||
|
||||
.. literalinclude:: example-code/src/main/java/net/corda/docs/java/FinalityFlowMigration.java
|
||||
:language: java
|
||||
:start-after: DOCSTART SimpleNewResponderFlow
|
||||
:end-before: DOCEND SimpleNewResponderFlow
|
||||
:dedent: 4
|
||||
|
||||
For flows which are already initiating counterpart flows then it's a simple matter of using the existing flow session.
|
||||
Note however, the new ``FinalityFlow`` is inlined and so the sequence of sends and receives between the two flows will
|
||||
change and will be incompatible with your current flows. You can use the flow version API to write your flows in a
|
||||
backwards compatible way.
|
||||
|
||||
Here's what an upgraded initiating flow may look like:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/FinalityFlowMigration.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART ExistingInitiatingFlow
|
||||
:end-before: DOCEND ExistingInitiatingFlow
|
||||
|
||||
.. literalinclude:: example-code/src/main/java/net/corda/docs/java/FinalityFlowMigration.java
|
||||
:language: java
|
||||
:start-after: DOCSTART ExistingInitiatingFlow
|
||||
:end-before: DOCEND ExistingInitiatingFlow
|
||||
:dedent: 4
|
||||
|
||||
For the responder flow, insert a call to ``ReceiveFinalityFlow`` at the location where it's expecting to receive the
|
||||
finalised transaction. If the initiator is written in a backwards compatible way then so must the responder.
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/FinalityFlowMigration.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART ExistingResponderFlow
|
||||
:end-before: DOCEND ExistingResponderFlow
|
||||
:dedent: 8
|
||||
|
||||
.. literalinclude:: example-code/src/main/java/net/corda/docs/java/FinalityFlowMigration.java
|
||||
:language: java
|
||||
:start-after: DOCSTART ExistingResponderFlow
|
||||
:end-before: DOCEND ExistingResponderFlow
|
||||
:dedent: 12
|
||||
|
||||
The responder flow may be waiting for the finalised transaction to appear in the local node's vault using ``waitForLedgerCommit``.
|
||||
This is no longer necessary with ``ReceiveFinalityFlow`` and the call to ``waitForLedgerCommit`` can be removed.
|
||||
|
||||
Upgrading from Corda Open Source
|
||||
--------------------------------
|
||||
|
||||
|
@ -19,16 +19,6 @@ inline fun <reified T : ContractState> ServiceHub.queryStateByRef(ref: StateRef)
|
||||
return results.states.firstOrNull() ?: throw IllegalArgumentException("State (type=${T::class}) corresponding to the reference $ref not found (or is spent).")
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorthand when a single party signs a TX and then returns a result that uses the signed TX (e.g. includes the TX id)
|
||||
*/
|
||||
@Suspendable
|
||||
fun <R> FlowLogic<R>.finalize(tx: TransactionBuilder, returnWithSignedTx: (stx: SignedTransaction) -> R): R {
|
||||
val stx = serviceHub.signInitialTransaction(tx)
|
||||
subFlow(FinalityFlow(stx)) // it'll send to all participants in the state by default
|
||||
return returnWithSignedTx(stx)
|
||||
}
|
||||
|
||||
/**
|
||||
* Corda fails when it tries to store the same attachment hash twice. And it's convenient to also do nothing if no attachment is provided.
|
||||
* This doesn't fix the same-attachment problem completely but should at least help in testing with the same file.
|
||||
|
@ -2,12 +2,8 @@ package net.corda.finance.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.flows.FinalityFlow
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.NotaryException
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
@ -27,9 +23,9 @@ abstract class AbstractCashFlow<out T>(override val progressTracker: ProgressTra
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
protected fun finaliseTx(tx: SignedTransaction, extraParticipants: Set<Party>, message: String): SignedTransaction {
|
||||
protected fun finaliseTx(tx: SignedTransaction, sessions: Collection<FlowSession>, message: String): SignedTransaction {
|
||||
try {
|
||||
return subFlow(FinalityFlow(tx, extraParticipants))
|
||||
return subFlow(FinalityFlow(tx, sessions))
|
||||
} catch (e: NotaryException) {
|
||||
throw CashException(message, e)
|
||||
}
|
||||
|
@ -3,8 +3,7 @@ package net.corda.finance.flows
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.InsufficientBalanceException
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.node.services.queryBy
|
||||
import net.corda.core.node.services.vault.DEFAULT_PAGE_NUM
|
||||
import net.corda.core.node.services.vault.PageSpecification
|
||||
@ -28,6 +27,7 @@ import java.util.*
|
||||
* @param issuerRef the reference on the issued currency. Added to the node's legal identity to determine the
|
||||
* issuer.
|
||||
*/
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
class CashExitFlow(private val amount: Amount<Currency>,
|
||||
private val issuerRef: OpaqueBytes,
|
||||
@ -53,7 +53,7 @@ class CashExitFlow(private val amount: Amount<Currency>,
|
||||
.getInstance { serviceHub.jdbcSession().metaData }
|
||||
.unconsumedCashStatesForSpending(serviceHub, amount, setOf(issuer.party), builder.notary, builder.lockId, setOf(issuer.reference))
|
||||
val signers = try {
|
||||
val changeOwner = exitStates.map { it.state.data.owner }.toSet().firstOrNull() ?: throw InsufficientBalanceException(amount)
|
||||
val changeOwner = exitStates.asSequence().map { it.state.data.owner }.toSet().firstOrNull() ?: throw InsufficientBalanceException(amount)
|
||||
Cash().generateExit(
|
||||
builder,
|
||||
amount.issuedBy(issuer),
|
||||
@ -67,21 +67,31 @@ class CashExitFlow(private val amount: Amount<Currency>,
|
||||
val inputStates = serviceHub.vaultService.queryBy<Cash.State>(VaultQueryCriteria(stateRefs = builder.inputStates()),
|
||||
PageSpecification(pageNumber = DEFAULT_PAGE_NUM, pageSize = builder.inputStates().size)).states
|
||||
|
||||
// TODO: Is it safe to drop participants we don't know how to contact? Does not knowing how to contact them
|
||||
// count as a reason to fail?
|
||||
val participants: Set<Party> = inputStates
|
||||
val participantSessions = inputStates
|
||||
.asSequence()
|
||||
.mapNotNull { serviceHub.identityService.wellKnownPartyFromAnonymous(it.state.data.owner) }
|
||||
.toSet()
|
||||
.filterNot(serviceHub.myInfo::isLegalIdentity)
|
||||
.distinct()
|
||||
.map(::initiateFlow)
|
||||
.toList()
|
||||
// Sign transaction
|
||||
progressTracker.currentStep = SIGNING_TX
|
||||
val tx = serviceHub.signInitialTransaction(builder, signers)
|
||||
|
||||
// Commit the transaction
|
||||
progressTracker.currentStep = FINALISING_TX
|
||||
val notarised = finaliseTx(tx, participants, "Unable to notarise exit")
|
||||
val notarised = finaliseTx(tx, participantSessions, "Unable to notarise exit")
|
||||
return Result(notarised, null)
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
class ExitRequest(amount: Amount<Currency>, val issuerRef: OpaqueBytes) : AbstractRequest(amount)
|
||||
}
|
||||
|
||||
@InitiatedBy(CashExitFlow::class)
|
||||
class CashExitResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
subFlow(ReceiveFinalityFlow(otherSide))
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.confidential.SwapIdentitiesFlow
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.InsufficientBalanceException
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
@ -28,6 +28,7 @@ import java.util.*
|
||||
* for testing purposes.
|
||||
*/
|
||||
@StartableByRPC
|
||||
@InitiatingFlow
|
||||
open class CashPaymentFlow(
|
||||
val amount: Amount<Currency>,
|
||||
val recipient: Party,
|
||||
@ -75,7 +76,8 @@ open class CashPaymentFlow(
|
||||
|
||||
progressTracker.currentStep = FINALISING_TX
|
||||
logger.info("Finalising transaction for: ${tx.id}")
|
||||
val notarised = finaliseTx(tx, setOf(recipient), "Unable to notarise spend")
|
||||
val sessions = if (serviceHub.myInfo.isLegalIdentity(recipient)) emptyList() else listOf(initiateFlow(recipient))
|
||||
val notarised = finaliseTx(tx, sessions, "Unable to notarise spend")
|
||||
logger.info("Finalised transaction for: ${notarised.id}")
|
||||
return Result(notarised, anonymousRecipient)
|
||||
}
|
||||
@ -87,3 +89,11 @@ open class CashPaymentFlow(
|
||||
val issuerConstraint: Set<Party> = emptySet(),
|
||||
val notary: Party? = null) : AbstractRequest(amount)
|
||||
}
|
||||
|
||||
@InitiatedBy(CashPaymentFlow::class)
|
||||
class CashPaymentResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
subFlow(ReceiveFinalityFlow(otherSide))
|
||||
}
|
||||
}
|
||||
|
@ -71,7 +71,7 @@ object TwoPartyDealFlow {
|
||||
|
||||
val txId = subFlow(signTransactionFlow).id
|
||||
|
||||
return waitForLedgerCommit(txId)
|
||||
return subFlow(ReceiveFinalityFlow(otherSideSession, expectedTxId = txId))
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@ -81,8 +81,7 @@ object TwoPartyDealFlow {
|
||||
/**
|
||||
* Abstracted bilateral deal flow participant that is recipient of initial communication.
|
||||
*/
|
||||
abstract class Secondary<U>(override val progressTracker: ProgressTracker = Secondary.tracker(),
|
||||
val regulators: Set<Party> = emptySet()) : FlowLogic<SignedTransaction>() {
|
||||
abstract class Secondary<U>(override val progressTracker: ProgressTracker = Secondary.tracker()) : FlowLogic<SignedTransaction>() {
|
||||
|
||||
companion object {
|
||||
object RECEIVING : ProgressTracker.Step("Waiting for deal info.")
|
||||
@ -124,7 +123,7 @@ object TwoPartyDealFlow {
|
||||
logger.trace("Got signatures from other party, verifying ... ")
|
||||
|
||||
progressTracker.currentStep = RECORDING
|
||||
val ftx = subFlow(FinalityFlow(stx, regulators + otherSideSession.counterparty))
|
||||
val ftx = subFlow(FinalityFlow(stx, otherSideSession))
|
||||
logger.trace("Recorded transaction.")
|
||||
|
||||
return ftx
|
||||
|
@ -114,7 +114,7 @@ object TwoPartyTradeFlow {
|
||||
val txId = subFlow(signTransactionFlow).id
|
||||
// DOCEND 5
|
||||
|
||||
return waitForLedgerCommit(txId)
|
||||
return subFlow(ReceiveFinalityFlow(otherSideSession, expectedTxId = txId))
|
||||
}
|
||||
// DOCEND 4
|
||||
|
||||
@ -188,7 +188,7 @@ object TwoPartyTradeFlow {
|
||||
|
||||
// Notarise and record the transaction.
|
||||
progressTracker.currentStep = RECORDING
|
||||
return subFlow(FinalityFlow(twiceSignedTx))
|
||||
return subFlow(FinalityFlow(twiceSignedTx, sellerSession))
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
|
@ -13,6 +13,7 @@ import net.corda.testing.core.*
|
||||
import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.StartedMockNode
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@ -29,7 +30,7 @@ class CashPaymentFlowTests {
|
||||
|
||||
@Before
|
||||
fun start() {
|
||||
mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin(), cordappPackages = listOf("net.corda.finance.contracts.asset", "net.corda.finance.schemas"))
|
||||
mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin(), cordappPackages = listOf("net.corda.finance"))
|
||||
bankOfCordaNode = mockNet.createPartyNode(BOC_NAME)
|
||||
bankOfCorda = bankOfCordaNode.info.identityFromX500Name(BOC_NAME)
|
||||
aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
||||
@ -61,10 +62,10 @@ class CashPaymentFlowTests {
|
||||
// Check Bank of Corda vault updates - we take in some issued cash and split it into $500 to the notary
|
||||
// and $1,500 back to us, so we expect to consume one state, produce one state for our own vault
|
||||
vaultUpdatesBoc.expectEvents {
|
||||
expect { update ->
|
||||
require(update.consumed.size == 1) { "Expected 1 consumed states, actual: $update" }
|
||||
require(update.produced.size == 1) { "Expected 1 produced states, actual: $update" }
|
||||
val changeState = update.produced.single().state.data
|
||||
expect { (consumed, produced) ->
|
||||
assertThat(consumed).hasSize(1)
|
||||
assertThat(produced).hasSize(1)
|
||||
val changeState = produced.single().state.data
|
||||
assertEquals(expectedChange.`issued by`(bankOfCorda.ref(ref)), changeState.amount)
|
||||
}
|
||||
}
|
||||
@ -72,8 +73,8 @@ class CashPaymentFlowTests {
|
||||
// Check notary node vault updates
|
||||
vaultUpdatesBankClient.expectEvents {
|
||||
expect { (consumed, produced) ->
|
||||
require(consumed.isEmpty()) { consumed.size }
|
||||
require(produced.size == 1) { produced.size }
|
||||
assertThat(consumed).isEmpty()
|
||||
assertThat(produced).hasSize(1)
|
||||
val paymentState = produced.single().state.data
|
||||
assertEquals(expectedPayment.`issued by`(bankOfCorda.ref(ref)), paymentState.amount)
|
||||
}
|
||||
|
@ -43,7 +43,6 @@ class FlowsDrainingModeContentionTest : IntegrationTest() {
|
||||
@JvmField
|
||||
val databaseSchemas = IntegrationTestSchemas(ALICE_NAME.toDatabaseSchemaName(), BOB_NAME.toDatabaseSchemaName(), DUMMY_NOTARY_NAME.toDatabaseSchemaName())
|
||||
}
|
||||
|
||||
private val portAllocation = PortAllocation.Incremental(10000)
|
||||
private val user = User("mark", "dadada", setOf(all()))
|
||||
private val users = listOf(user)
|
||||
@ -100,7 +99,7 @@ class ProposeTransactionAndWaitForCommit(private val data: String,
|
||||
subFlow(SendTransactionFlow(session, signedTx))
|
||||
session.send(myRpcInfo)
|
||||
|
||||
return waitForLedgerCommit(signedTx.id)
|
||||
return subFlow(ReceiveFinalityFlow(session, expectedTxId = signedTx.id))
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,7 +113,7 @@ class SignTransactionTriggerDrainingModeAndFinality(private val session: FlowSes
|
||||
|
||||
triggerDrainingModeForInitiatingNode(initiatingRpcInfo)
|
||||
|
||||
subFlow(FinalityFlow(signedTx, setOf(session.counterparty)))
|
||||
subFlow(FinalityFlow(signedTx, session))
|
||||
}
|
||||
|
||||
private fun triggerDrainingModeForInitiatingNode(initiatingRpcInfo: RpcInfo) {
|
||||
|
@ -147,6 +147,6 @@ class SendMessageFlow(private val message: Message, private val notary: Party) :
|
||||
val signedTx = serviceHub.signInitialTransaction(txBuilder)
|
||||
|
||||
progressTracker.currentStep = FINALISING_TRANSACTION
|
||||
return subFlow(FinalityFlow(signedTx, FINALISING_TRANSACTION.childProgressTracker()))
|
||||
return subFlow(FinalityFlow(signedTx, emptyList(), FINALISING_TRANSACTION.childProgressTracker()))
|
||||
}
|
||||
}
|
@ -4,9 +4,7 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import com.google.common.collect.ImmutableList
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.flows.FinalityFlow
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.concurrent.transpose
|
||||
import net.corda.core.messaging.startFlow
|
||||
@ -46,6 +44,7 @@ class ScheduledFlowIntegrationTests : IntegrationTest() {
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
@InitiatingFlow
|
||||
class InsertInitialStateFlow(private val destination: Party,
|
||||
private val notary: Party,
|
||||
private val identity: Int = 1,
|
||||
@ -58,11 +57,20 @@ class ScheduledFlowIntegrationTests : IntegrationTest() {
|
||||
.addOutputState(scheduledState, DummyContract.PROGRAM_ID)
|
||||
.addCommand(dummyCommand(ourIdentity.owningKey))
|
||||
val tx = serviceHub.signInitialTransaction(builder)
|
||||
subFlow(FinalityFlow(tx))
|
||||
subFlow(FinalityFlow(tx, initiateFlow(destination)))
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(InsertInitialStateFlow::class)
|
||||
class InsertInitialStateResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
subFlow(ReceiveFinalityFlow(otherSide))
|
||||
}
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
@InitiatingFlow
|
||||
class AnotherFlow(private val identity: String) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
@ -78,7 +86,15 @@ class ScheduledFlowIntegrationTests : IntegrationTest() {
|
||||
.addOutputState(outputState, DummyContract.PROGRAM_ID)
|
||||
.addCommand(dummyCommand(ourIdentity.owningKey))
|
||||
val tx = serviceHub.signInitialTransaction(builder)
|
||||
subFlow(FinalityFlow(tx, outputState.participants.toSet()))
|
||||
subFlow(FinalityFlow(tx, initiateFlow(state.state.data.destination)))
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(AnotherFlow::class)
|
||||
class AnotherResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
subFlow(ReceiveFinalityFlow(otherSide))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,10 +2,7 @@ package net.corda.testMessage
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.flows.FinalityFlow
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowLogicRefFactory
|
||||
import net.corda.core.flows.SchedulableFlow
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.NonEmptySet
|
||||
@ -16,6 +13,7 @@ import java.util.*
|
||||
import kotlin.reflect.jvm.jvmName
|
||||
|
||||
@SchedulableFlow
|
||||
@InitiatingFlow
|
||||
class ScheduledFlow(private val stateRef: StateRef) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
@ -35,7 +33,15 @@ class ScheduledFlow(private val stateRef: StateRef) : FlowLogic<Unit>() {
|
||||
.addOutputState(newStateOutput, DummyContract.PROGRAM_ID)
|
||||
.addCommand(dummyCommand(ourIdentity.owningKey))
|
||||
val tx = serviceHub.signInitialTransaction(builder)
|
||||
subFlow(FinalityFlow(tx, setOf(scheduledState.destination)))
|
||||
subFlow(FinalityFlow(tx, initiateFlow(scheduledState.destination)))
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(ScheduledFlow::class)
|
||||
class ScheduledResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
subFlow(ReceiveFinalityFlow(otherSide))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,9 +49,9 @@ class SendMessageFlow(private val message: Message, private val notary: Party, p
|
||||
return if (reciepent != null) {
|
||||
val session = initiateFlow(reciepent)
|
||||
subFlow(SendTransactionFlow(session, signedTx))
|
||||
subFlow(FinalityFlow(signedTx, setOf(reciepent), FINALISING_TRANSACTION.childProgressTracker()))
|
||||
subFlow(FinalityFlow(signedTx, listOf(session), FINALISING_TRANSACTION.childProgressTracker()))
|
||||
} else {
|
||||
subFlow(FinalityFlow(signedTx, FINALISING_TRANSACTION.childProgressTracker()))
|
||||
subFlow(FinalityFlow(signedTx, emptyList(), FINALISING_TRANSACTION.childProgressTracker()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -663,12 +663,39 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
}
|
||||
|
||||
private fun installCoreFlows() {
|
||||
flowManager.registerInitiatedCoreFlowFactory(FinalityFlow::class, FinalityHandler::class, ::FinalityHandler)
|
||||
installFinalityHandler()
|
||||
flowManager.registerInitiatedCoreFlowFactory(NotaryChangeFlow::class, NotaryChangeHandler::class, ::NotaryChangeHandler)
|
||||
flowManager.registerInitiatedCoreFlowFactory(ContractUpgradeFlow.Initiate::class, NotaryChangeHandler::class, ::ContractUpgradeHandler)
|
||||
// TODO Make this an inlined flow (and remove this flow mapping!), which should be possible now that FinalityFlow is also inlined
|
||||
flowManager.registerInitiatedCoreFlowFactory(SwapIdentitiesFlow::class, SwapIdentitiesHandler::class, ::SwapIdentitiesHandler)
|
||||
}
|
||||
|
||||
// The FinalityHandler is insecure as it blindly accepts any and all transactions into the node's local vault without doing any checks.
|
||||
// To plug this hole, the sending-side FinalityFlow has been made inlined with an inlined ReceiveFinalityFlow counterpart. The old
|
||||
// FinalityFlow API is gated to only work with old CorDapps (those whose target platform version < 4), and the FinalityHandler will only
|
||||
// work if there is at least one old CorDapp loaded (to preserve backwards compatibility).
|
||||
//
|
||||
// If an attempt is made to send us a transaction via FinalityHandler, and it's disabled, we will reject the request at the session-init
|
||||
// level by throwing a FinalityHandlerDisabled exception. This is picked up by the flow hospital which will not send the error back
|
||||
// (immediately) and instead pause the request by keeping it un-acknowledged in the message broker. This means the request isn't lost
|
||||
// across node restarts and allows the node operator time to accept or reject the request.
|
||||
// TODO Add public API to allow the node operator to accept or reject
|
||||
private fun installFinalityHandler() {
|
||||
// Disable the insecure FinalityHandler if none of the loaded CorDapps are old enough to require it.
|
||||
val cordappsNeedingFinalityHandler = cordappLoader.cordapps.filter { it.info.targetPlatformVersion < 4 }
|
||||
if (cordappsNeedingFinalityHandler.isEmpty()) {
|
||||
log.info("FinalityHandler is disabled as there are no CorDapps loaded which require it")
|
||||
} else {
|
||||
log.warn("FinalityHandler is enabled as there are CorDapps that require it: ${cordappsNeedingFinalityHandler.map { it.info }}. " +
|
||||
"This is insecure and it is strongly recommended that newer versions of these CorDapps be used instead.")
|
||||
}
|
||||
val disabled = cordappsNeedingFinalityHandler.isEmpty()
|
||||
flowManager.registerInitiatedCoreFlowFactory(FinalityFlow::class, FinalityHandler::class) {
|
||||
if (disabled) throw SessionRejectException.FinalityHandlerDisabled()
|
||||
FinalityHandler(it)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun makeTransactionStorage(transactionCacheSizeBytes: Long): WritableTransactionStorage {
|
||||
return DBTransactionStorage(database, cacheFactory)
|
||||
}
|
||||
|
@ -7,21 +7,15 @@ import net.corda.core.contracts.requireThat
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.ContractUpgradeUtils
|
||||
import net.corda.core.transactions.ContractUpgradeWireTransaction
|
||||
import net.corda.core.node.StatesToRecord
|
||||
import net.corda.core.transactions.ContractUpgradeWireTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
|
||||
// TODO: We should have a whitelist of contracts we're willing to accept at all, and reject if the transaction
|
||||
// includes us in any outside that list. Potentially just if it includes any outside that list at all.
|
||||
// TODO: Do we want to be able to reject specific transactions on more complex rules, for example reject incoming
|
||||
// cash without from unknown parties?
|
||||
class FinalityHandler(private val sender: FlowSession) : FlowLogic<Unit>() {
|
||||
class FinalityHandler(val sender: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
subFlow(ReceiveTransactionFlow(sender, true, StatesToRecord.ONLY_RELEVANT))
|
||||
}
|
||||
|
||||
internal fun sender(): Party = sender.counterparty
|
||||
}
|
||||
|
||||
class NotaryChangeHandler(otherSideSession: FlowSession) : AbstractStateReplacementFlow.Acceptor<Party>(otherSideSession) {
|
||||
@ -54,10 +48,13 @@ class ContractUpgradeHandler(otherSide: FlowSession) : AbstractStateReplacementF
|
||||
override fun verifyProposal(stx: SignedTransaction, proposal: AbstractStateReplacementFlow.Proposal<Class<out UpgradedContract<ContractState, *>>>) {
|
||||
// Retrieve signed transaction from our side, we will apply the upgrade logic to the transaction on our side, and
|
||||
// verify outputs matches the proposed upgrade.
|
||||
val ourSTX = serviceHub.validatedTransactions.getTransaction(proposal.stateRef.txhash)
|
||||
requireNotNull(ourSTX) { "We don't have a copy of the referenced state" }
|
||||
val oldStateAndRef = ourSTX!!.resolveBaseTransaction(serviceHub).outRef<ContractState>(proposal.stateRef.index)
|
||||
val authorisedUpgrade = serviceHub.contractUpgradeService.getAuthorisedContractUpgrade(oldStateAndRef.ref) ?: throw IllegalStateException("Contract state upgrade is unauthorised. State hash : ${oldStateAndRef.ref}")
|
||||
val ourSTX = requireNotNull(serviceHub.validatedTransactions.getTransaction(proposal.stateRef.txhash)) {
|
||||
"We don't have a copy of the referenced state"
|
||||
}
|
||||
val oldStateAndRef = ourSTX.resolveBaseTransaction(serviceHub).outRef<ContractState>(proposal.stateRef.index)
|
||||
val authorisedUpgrade = checkNotNull(serviceHub.contractUpgradeService.getAuthorisedContractUpgrade(oldStateAndRef.ref)) {
|
||||
"Contract state upgrade is unauthorised. State hash : ${oldStateAndRef.ref}"
|
||||
}
|
||||
val proposedTx = stx.coreTransaction as ContractUpgradeWireTransaction
|
||||
val expectedTx = ContractUpgradeUtils.assembleUpgradeTx(oldStateAndRef, proposal.modification, proposedTx.privacySalt, serviceHub)
|
||||
requireThat {
|
||||
|
@ -12,5 +12,8 @@ open class SessionRejectException(message: String) : CordaException(message) {
|
||||
class NotAFlow(val initiatorClass: Class<*>) : SessionRejectException("${initiatorClass.name} is not a flow")
|
||||
|
||||
class NotRegistered(val initiatorFlowClass: Class<out FlowLogic<*>>) : SessionRejectException("${initiatorFlowClass.name} is not registered")
|
||||
}
|
||||
|
||||
class FinalityHandlerDisabled : SessionRejectException("Counterparty attempting to use the old insecure API of FinalityFlow. However this " +
|
||||
"API is disabled on this node since there no CorDapps installed that require it. It may be that the counterparty is running an " +
|
||||
"older verison of a CorDapp.")
|
||||
}
|
||||
|
@ -42,6 +42,9 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging, private val
|
||||
// installed on restart, at which point the message will be able proceed as normal. If not then it will need
|
||||
// to be dropped manually.
|
||||
Outcome.OVERNIGHT_OBSERVATION
|
||||
} else if (error is SessionRejectException.FinalityHandlerDisabled) {
|
||||
// TODO We need a way to be able to give the green light to such a session-init message
|
||||
Outcome.OVERNIGHT_OBSERVATION
|
||||
} else {
|
||||
Outcome.UNTREATABLE
|
||||
}
|
||||
@ -284,7 +287,8 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging, private val
|
||||
|
||||
private fun warn(flowLogic: FinalityHandler, flowFiber: FlowFiber, currentState: StateMachineState) {
|
||||
log.warn("Flow ${flowFiber.id} failed to be finalised. Manual intervention may be required before retrying " +
|
||||
"the flow by re-starting the node. State machine state: $currentState, initiating party was: ${flowLogic.sender().name}")
|
||||
"the flow by re-starting the node. State machine state: $currentState, initiating party was: " +
|
||||
"${flowLogic.sender.counterparty}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,11 +2,9 @@ package net.corda.node.modes.draining
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.flows.FinalityFlow
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowLogicRefFactory
|
||||
import net.corda.core.flows.SchedulableFlow
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.packageName
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
@ -43,7 +41,10 @@ class ScheduledFlowsDrainingModeTest {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForPackages("net.corda.testing.contracts"), threadPerNode = true)
|
||||
mockNet = InternalMockNetwork(
|
||||
cordappsForAllNodes = cordappsForPackages("net.corda.testing.contracts", javaClass.packageName),
|
||||
threadPerNode = true
|
||||
)
|
||||
aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME))
|
||||
bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME))
|
||||
notary = mockNet.defaultNotaryIdentity
|
||||
@ -112,6 +113,7 @@ class ScheduledFlowsDrainingModeTest {
|
||||
override val participants: List<Party> get() = listOf(source, destination)
|
||||
}
|
||||
|
||||
@InitiatingFlow
|
||||
class InsertInitialStateFlow(private val destination: Party, private val notary: Party) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
@ -120,11 +122,20 @@ class ScheduledFlowsDrainingModeTest {
|
||||
.addOutputState(scheduledState, DummyContract.PROGRAM_ID)
|
||||
.addCommand(dummyCommand(ourIdentity.owningKey))
|
||||
val tx = serviceHub.signInitialTransaction(builder)
|
||||
subFlow(FinalityFlow(tx))
|
||||
subFlow(FinalityFlow(tx, initiateFlow(destination)))
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(InsertInitialStateFlow::class)
|
||||
class InsertInitialStateResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
subFlow(ReceiveFinalityFlow(otherSide))
|
||||
}
|
||||
}
|
||||
|
||||
@SchedulableFlow
|
||||
@InitiatingFlow
|
||||
class ScheduledFlow(private val stateRef: StateRef) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
@ -142,7 +153,15 @@ class ScheduledFlowsDrainingModeTest {
|
||||
.addOutputState(newStateOutput, DummyContract.PROGRAM_ID)
|
||||
.addCommand(dummyCommand(ourIdentity.owningKey))
|
||||
val tx = serviceHub.signInitialTransaction(builder)
|
||||
subFlow(FinalityFlow(tx, setOf(scheduledState.destination)))
|
||||
subFlow(FinalityFlow(tx, initiateFlow(scheduledState.destination)))
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(ScheduledFlow::class)
|
||||
class ScheduledResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
subFlow(ReceiveFinalityFlow(otherSide))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,12 @@
|
||||
package net.corda.node.services
|
||||
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.contracts.TransactionVerificationException
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FinalityFlow
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.internal.cordapp.CordappInfoResolver
|
||||
import net.corda.core.internal.packageName
|
||||
import net.corda.core.toFuture
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
@ -16,8 +20,10 @@ import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.node.internal.*
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.After
|
||||
import org.junit.Test
|
||||
import rx.Observable
|
||||
|
||||
class FinalityHandlerTest {
|
||||
private val mockNet = InternalMockNetwork()
|
||||
@ -31,51 +37,115 @@ class FinalityHandlerTest {
|
||||
fun `sent to flow hospital on error and attempted retry on node restart`() {
|
||||
// Setup a network where only Alice has the finance CorDapp and it sends a cash tx to Bob who doesn't have the
|
||||
// CorDapp. Bob's FinalityHandler will error when validating the tx.
|
||||
val alice = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME, additionalCordapps = setOf(FINANCE_CORDAPP)))
|
||||
val alice = mockNet.createNode(InternalMockNodeParameters(
|
||||
legalName = ALICE_NAME,
|
||||
additionalCordapps = setOf(FINANCE_CORDAPP)
|
||||
))
|
||||
|
||||
var bob = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME))
|
||||
var bob = mockNet.createNode(InternalMockNodeParameters(
|
||||
legalName = BOB_NAME,
|
||||
// The node disables the FinalityHandler completely if there are no old CorDapps loaded, so we need to add
|
||||
// a token old CorDapp to keep the handler running.
|
||||
additionalCordapps = setOf(cordappForPackages(javaClass.packageName).withTargetVersion(3))
|
||||
))
|
||||
|
||||
val stx = TransactionBuilder(mockNet.defaultNotaryIdentity).let {
|
||||
Cash().generateIssue(
|
||||
it,
|
||||
1000.POUNDS.issuedBy(alice.info.singleIdentity().ref(0)),
|
||||
bob.info.singleIdentity(),
|
||||
mockNet.defaultNotaryIdentity
|
||||
)
|
||||
alice.services.signInitialTransaction(it)
|
||||
val stx = alice.issueCashTo(bob)
|
||||
val finalityHandlerId = bob.trackFinalityHandlerId().run {
|
||||
alice.finaliseWithOldApi(stx)
|
||||
getOrThrow()
|
||||
}
|
||||
|
||||
val finalityHandlerIdFuture = bob.smm.track()
|
||||
.updates
|
||||
.filter { it.logic is FinalityHandler }
|
||||
.map { it.logic.runId }
|
||||
.toFuture()
|
||||
|
||||
val finalisedTx = alice.services.startFlow(FinalityFlow(stx)).run {
|
||||
mockNet.runNetwork()
|
||||
resultFuture.getOrThrow()
|
||||
}
|
||||
val finalityHandlerId = finalityHandlerIdFuture.getOrThrow()
|
||||
|
||||
bob.assertFlowSentForObservation(finalityHandlerId)
|
||||
assertThat(bob.getTransaction(finalisedTx.id)).isNull()
|
||||
bob.assertFlowSentForObservationDueToConstraintError(finalityHandlerId)
|
||||
assertThat(bob.getTransaction(stx.id)).isNull()
|
||||
|
||||
bob = mockNet.restartNode(bob)
|
||||
// Since we've not done anything to fix the orignal error, we expect the finality handler to be sent to the hospital
|
||||
// again on restart
|
||||
bob.assertFlowSentForObservation(finalityHandlerId)
|
||||
assertThat(bob.getTransaction(finalisedTx.id)).isNull()
|
||||
bob.assertFlowSentForObservationDueToConstraintError(finalityHandlerId)
|
||||
assertThat(bob.getTransaction(stx.id)).isNull()
|
||||
}
|
||||
|
||||
private fun TestStartedNode.assertFlowSentForObservation(runId: StateMachineRunId) {
|
||||
val keptInForObservation = smm.flowHospital
|
||||
.track()
|
||||
.let { it.updates.startWith(it.snapshot) }
|
||||
.ofType(MedicalRecord.Flow::class.java)
|
||||
.filter { it.flowId == runId && it.outcome == Outcome.OVERNIGHT_OBSERVATION }
|
||||
@Test
|
||||
fun `disabled if there are no old CorDapps loaded`() {
|
||||
val alice = mockNet.createNode(InternalMockNodeParameters(
|
||||
legalName = ALICE_NAME,
|
||||
additionalCordapps = setOf(FINANCE_CORDAPP)
|
||||
))
|
||||
|
||||
val bob = mockNet.createNode(InternalMockNodeParameters(
|
||||
legalName = BOB_NAME,
|
||||
// Make sure the target version is 4, and not the current platform version which may be greater
|
||||
additionalCordapps = setOf(FINANCE_CORDAPP.withTargetVersion(4))
|
||||
))
|
||||
|
||||
val stx = alice.issueCashTo(bob)
|
||||
val finalityFuture = alice.finaliseWithOldApi(stx)
|
||||
|
||||
val record = bob.medicalRecordsOfType<MedicalRecord.SessionInit>()
|
||||
.toBlocking()
|
||||
.first()
|
||||
assertThat(keptInForObservation.by).contains(FinalityDoctor)
|
||||
assertThat(record.outcome).isEqualTo(Outcome.OVERNIGHT_OBSERVATION)
|
||||
assertThat(record.sender).isEqualTo(alice.info.singleIdentity())
|
||||
assertThat(record.initiatorFlowClassName).isEqualTo(FinalityFlow::class.java.name)
|
||||
|
||||
assertThat(bob.getTransaction(stx.id)).isNull()
|
||||
|
||||
// Drop the session-init so that Alice gets the error message
|
||||
assertThat(finalityFuture).isNotDone()
|
||||
bob.smm.flowHospital.dropSessionInit(record.id)
|
||||
mockNet.runNetwork()
|
||||
assertThatThrownBy {
|
||||
finalityFuture.getOrThrow()
|
||||
}.hasMessageContaining("Counterparty attempting to use the old insecure API of FinalityFlow")
|
||||
}
|
||||
|
||||
private fun TestStartedNode.issueCashTo(recipient: TestStartedNode): SignedTransaction {
|
||||
return TransactionBuilder(mockNet.defaultNotaryIdentity).let {
|
||||
Cash().generateIssue(
|
||||
it,
|
||||
1000.POUNDS.issuedBy(info.singleIdentity().ref(0)),
|
||||
recipient.info.singleIdentity(),
|
||||
mockNet.defaultNotaryIdentity
|
||||
)
|
||||
services.signInitialTransaction(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestStartedNode.trackFinalityHandlerId(): CordaFuture<StateMachineRunId> {
|
||||
return smm
|
||||
.track()
|
||||
.updates
|
||||
.filter { it.logic is FinalityHandler }
|
||||
.map { it.logic.runId }
|
||||
.toFuture()
|
||||
}
|
||||
|
||||
private fun TestStartedNode.finaliseWithOldApi(stx: SignedTransaction): CordaFuture<SignedTransaction> {
|
||||
return CordappInfoResolver.withCordappInfo(targetPlatformVersion = 3) {
|
||||
@Suppress("DEPRECATION")
|
||||
services.startFlow(FinalityFlow(stx)).resultFuture.apply {
|
||||
mockNet.runNetwork()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <reified R : MedicalRecord> TestStartedNode.medicalRecordsOfType(): Observable<R> {
|
||||
return smm
|
||||
.flowHospital
|
||||
.track()
|
||||
.let { it.updates.startWith(it.snapshot) }
|
||||
.ofType(R::class.java)
|
||||
}
|
||||
|
||||
private fun TestStartedNode.assertFlowSentForObservationDueToConstraintError(runId: StateMachineRunId) {
|
||||
val observation = medicalRecordsOfType<MedicalRecord.Flow>()
|
||||
.filter { it.flowId == runId }
|
||||
.toBlocking()
|
||||
.first()
|
||||
assertThat(observation.outcome).isEqualTo(Outcome.OVERNIGHT_OBSERVATION)
|
||||
assertThat(observation.by).contains(FinalityDoctor)
|
||||
val error = observation.errors.single()
|
||||
assertThat(error).isInstanceOf(TransactionVerificationException.ContractConstraintRejection::class.java)
|
||||
}
|
||||
|
||||
private fun TestStartedNode.getTransaction(id: SecureHash): SignedTransaction? {
|
||||
|
@ -65,7 +65,7 @@ class ServiceHubConcurrentUsageTest {
|
||||
val issuer = ourIdentity.ref(OpaqueBytes.of(0))
|
||||
Cash().generateIssue(builder, 10.DOLLARS.issuedBy(issuer), ourIdentity, notary)
|
||||
val stx = serviceHub.signInitialTransaction(builder)
|
||||
return subFlow(FinalityFlow(stx))
|
||||
return subFlow(FinalityFlow(stx, emptyList()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -156,7 +156,7 @@ class TimedFlowTests {
|
||||
setTimeWindow(services.clock.instant(), 30.seconds)
|
||||
addOutputState(DummyContract.SingleOwnerState(owner = info.singleIdentity()), DummyContract.PROGRAM_ID, AlwaysAcceptAttachmentConstraint)
|
||||
}
|
||||
val flow = FinalityFlow(issueTx)
|
||||
val flow = FinalityFlow(issueTx, emptyList())
|
||||
val progressTracker = flow.progressTracker
|
||||
assertNotEquals(ProgressTracker.DONE, progressTracker.currentStep)
|
||||
val progressTrackerDone = getDoneFuture(flow.progressTracker)
|
||||
|
@ -4,11 +4,9 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.context.InvocationOrigin
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.flows.FinalityFlow
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowLogicRefFactory
|
||||
import net.corda.core.flows.SchedulableFlow
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.packageName
|
||||
import net.corda.core.node.services.VaultService
|
||||
import net.corda.core.node.services.queryBy
|
||||
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria
|
||||
@ -60,6 +58,7 @@ class ScheduledFlowTests {
|
||||
override val participants: List<Party> get() = listOf(source, destination)
|
||||
}
|
||||
|
||||
@InitiatingFlow
|
||||
class InsertInitialStateFlow(private val destination: Party, private val notary: Party) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
@ -68,10 +67,19 @@ class ScheduledFlowTests {
|
||||
.addOutputState(scheduledState, DummyContract.PROGRAM_ID)
|
||||
.addCommand(dummyCommand(ourIdentity.owningKey))
|
||||
val tx = serviceHub.signInitialTransaction(builder)
|
||||
subFlow(FinalityFlow(tx))
|
||||
subFlow(FinalityFlow(tx, initiateFlow(destination)))
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(InsertInitialStateFlow::class)
|
||||
class InsertInitialStateResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
subFlow(ReceiveFinalityFlow(otherSide))
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatingFlow
|
||||
@SchedulableFlow
|
||||
class ScheduledFlow(private val stateRef: StateRef) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
@ -90,13 +98,21 @@ class ScheduledFlowTests {
|
||||
.addOutputState(newStateOutput, DummyContract.PROGRAM_ID)
|
||||
.addCommand(dummyCommand(ourIdentity.owningKey))
|
||||
val tx = serviceHub.signInitialTransaction(builder)
|
||||
subFlow(FinalityFlow(tx, setOf(scheduledState.destination)))
|
||||
subFlow(FinalityFlow(tx, initiateFlow(scheduledState.destination)))
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(ScheduledFlow::class)
|
||||
class ScheduledResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
subFlow(ReceiveFinalityFlow(otherSide))
|
||||
}
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForPackages("net.corda.testing.contracts"), threadPerNode = true)
|
||||
mockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForPackages("net.corda.testing.contracts", javaClass.packageName), threadPerNode = true)
|
||||
aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME))
|
||||
bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME))
|
||||
notary = mockNet.defaultNotaryIdentity
|
||||
|
@ -7,14 +7,11 @@ import co.paralleluniverse.strands.concurrent.Semaphore
|
||||
import net.corda.client.rpc.notUsed
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.FlowStateMachine
|
||||
import net.corda.core.internal.concurrent.flatMap
|
||||
import net.corda.core.internal.concurrent.map
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.messaging.MessageRecipients
|
||||
import net.corda.core.node.services.PartyInfo
|
||||
import net.corda.core.node.services.queryBy
|
||||
@ -30,7 +27,10 @@ import net.corda.core.utilities.unwrap
|
||||
import net.corda.node.services.persistence.checkpoints
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.contracts.DummyState
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.dummyCommand
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.internal.LogHelper
|
||||
import net.corda.testing.node.InMemoryMessagingNetwork.MessageTransfer
|
||||
import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin
|
||||
@ -66,7 +66,7 @@ class FlowFrameworkTests {
|
||||
@Before
|
||||
fun setUpMockNet() {
|
||||
mockNet = InternalMockNetwork(
|
||||
cordappsForAllNodes = cordappsForPackages("net.corda.testing.contracts"),
|
||||
cordappsForAllNodes = cordappsForPackages("net.corda.testing.contracts") + FINANCE_CORDAPP,
|
||||
servicePeerAllocationStrategy = RoundRobin()
|
||||
)
|
||||
|
||||
@ -266,18 +266,20 @@ class FlowFrameworkTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `wait for transaction`() {
|
||||
fun waitForLedgerCommit() {
|
||||
val ptx = TransactionBuilder(notary = notaryIdentity)
|
||||
.addOutputState(DummyState(), DummyContract.PROGRAM_ID)
|
||||
.addCommand(dummyCommand(alice.owningKey))
|
||||
val stx = aliceNode.services.signInitialTransaction(ptx)
|
||||
|
||||
val committerFiber = aliceNode.registerCordappFlowFactory(WaitingFlows.Waiter::class) {
|
||||
WaitingFlows.Committer(it)
|
||||
}.map { it.stateMachine }.map { uncheckedCast<FlowStateMachine<*>, FlowStateMachine<Any>>(it) }
|
||||
val waiterStx = bobNode.services.startFlow(WaitingFlows.Waiter(stx, alice)).resultFuture
|
||||
val committerStx = aliceNode.registerCordappFlowFactory(CommitReceiverFlow::class) {
|
||||
CommitterFlow(it)
|
||||
}.flatMap { it.stateMachine.resultFuture }
|
||||
// The waitForLedgerCommit call has to occur on separate flow
|
||||
val waiterStx = bobNode.services.startFlow(WaiterFlow(stx.id)).resultFuture
|
||||
val commitReceiverStx = bobNode.services.startFlow(CommitReceiverFlow(stx, alice)).resultFuture
|
||||
mockNet.runNetwork()
|
||||
assertThat(waiterStx.getOrThrow()).isEqualTo(committerFiber.getOrThrow().resultFuture.getOrThrow())
|
||||
assertThat(committerStx.getOrThrow()).isEqualTo(waiterStx.getOrThrow()).isEqualTo(commitReceiverStx.getOrThrow())
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -287,10 +289,8 @@ class FlowFrameworkTests {
|
||||
.addCommand(dummyCommand())
|
||||
val stx = aliceNode.services.signInitialTransaction(ptx)
|
||||
|
||||
aliceNode.registerCordappFlowFactory(WaitingFlows.Waiter::class) {
|
||||
WaitingFlows.Committer(it) { throw Exception("Error") }
|
||||
}
|
||||
val waiter = bobNode.services.startFlow(WaitingFlows.Waiter(stx, alice)).resultFuture
|
||||
aliceNode.registerCordappFlowFactory(CommitReceiverFlow::class) { CommitterFlow(it) { throw Exception("Error") } }
|
||||
val waiter = bobNode.services.startFlow(CommitReceiverFlow(stx, alice)).resultFuture
|
||||
mockNet.runNetwork()
|
||||
assertThatExceptionOfType(UnexpectedFlowEndException::class.java).isThrownBy {
|
||||
waiter.getOrThrow()
|
||||
@ -299,18 +299,10 @@ class FlowFrameworkTests {
|
||||
|
||||
@Test
|
||||
fun `verify vault query service is tokenizable by force checkpointing within a flow`() {
|
||||
val ptx = TransactionBuilder(notary = notaryIdentity)
|
||||
.addOutputState(DummyState(), DummyContract.PROGRAM_ID)
|
||||
.addCommand(dummyCommand(alice.owningKey))
|
||||
val stx = aliceNode.services.signInitialTransaction(ptx)
|
||||
|
||||
aliceNode.registerCordappFlowFactory(VaultQueryFlow::class) {
|
||||
WaitingFlows.Committer(it)
|
||||
}
|
||||
val result = bobNode.services.startFlow(VaultQueryFlow(stx, alice)).resultFuture
|
||||
|
||||
aliceNode.registerCordappFlowFactory(VaultQueryFlow::class) { InitiatedSendFlow("Hello", it) }
|
||||
val result = bobNode.services.startFlow(VaultQueryFlow(alice)).resultFuture
|
||||
mockNet.runNetwork()
|
||||
assertThat(result.getOrThrow()).isEmpty()
|
||||
result.getOrThrow()
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -492,24 +484,27 @@ class FlowFrameworkTests {
|
||||
}
|
||||
}
|
||||
|
||||
private object WaitingFlows {
|
||||
@InitiatingFlow
|
||||
class Waiter(val stx: SignedTransaction, val otherParty: Party) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val otherPartySession = initiateFlow(otherParty)
|
||||
otherPartySession.send(stx)
|
||||
return waitForLedgerCommit(stx.id)
|
||||
}
|
||||
}
|
||||
class WaiterFlow(private val txId: SecureHash) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction = waitForLedgerCommit(txId)
|
||||
}
|
||||
|
||||
class Committer(val otherPartySession: FlowSession, val throwException: (() -> Exception)? = null) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val stx = otherPartySession.receive<SignedTransaction>().unwrap { it }
|
||||
if (throwException != null) throw throwException.invoke()
|
||||
return subFlow(FinalityFlow(stx, setOf(otherPartySession.counterparty)))
|
||||
}
|
||||
@InitiatingFlow
|
||||
class CommitReceiverFlow(val stx: SignedTransaction, private val otherParty: Party) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val otherPartySession = initiateFlow(otherParty)
|
||||
otherPartySession.send(stx)
|
||||
return subFlow(ReceiveFinalityFlow(otherPartySession, expectedTxId = stx.id))
|
||||
}
|
||||
}
|
||||
|
||||
class CommitterFlow(private val otherPartySession: FlowSession, private val throwException: (() -> Exception)? = null) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val stx = otherPartySession.receive<SignedTransaction>().unwrap { it }
|
||||
if (throwException != null) throw throwException.invoke()
|
||||
return subFlow(FinalityFlow(stx, otherPartySession))
|
||||
}
|
||||
}
|
||||
|
||||
@ -527,16 +522,15 @@ class FlowFrameworkTests {
|
||||
private class IncorrectCustomSendFlow(payload: String, otherParty: Party) : CustomInterface, SendFlow(payload, otherParty)
|
||||
|
||||
@InitiatingFlow
|
||||
private class VaultQueryFlow(val stx: SignedTransaction, val otherParty: Party) : FlowLogic<List<StateAndRef<ContractState>>>() {
|
||||
private class VaultQueryFlow(val otherParty: Party) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call(): List<StateAndRef<ContractState>> {
|
||||
override fun call() {
|
||||
val otherPartySession = initiateFlow(otherParty)
|
||||
otherPartySession.send(stx)
|
||||
// hold onto reference here to force checkpoint of vaultService and thus
|
||||
// Hold onto reference here to force checkpoint of vaultService and thus
|
||||
// prove it is registered as a tokenizableService in the node
|
||||
val vaultQuerySvc = serviceHub.vaultService
|
||||
waitForLedgerCommit(stx.id)
|
||||
return vaultQuerySvc.queryBy<ContractState>().states
|
||||
otherPartySession.receive<Any>()
|
||||
vaultQuerySvc.queryBy<ContractState>().states
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
package net.corda.node.services.vault
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.FinalityFlow
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.queryBy
|
||||
@ -26,7 +24,6 @@ import java.util.concurrent.ExecutionException
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class VaultFlowTest {
|
||||
|
||||
private lateinit var mockNetwork: MockNetwork
|
||||
private lateinit var partyA: StartedMockNode
|
||||
private lateinit var partyB: StartedMockNode
|
||||
@ -72,17 +69,26 @@ class VaultFlowTest {
|
||||
partyB.services.vaultService.queryBy<DummyDealContract.State>().states.size
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatingFlow
|
||||
class Initiator(private val participants: List<Party>) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val stx = serviceHub.signInitialTransaction(TransactionBuilder(serviceHub.networkMapCache.notaryIdentities.first()).apply {
|
||||
addOutputState(UniqueDummyLinearContract.State(participants, "Dummy linear id"), UNIQUE_DUMMY_LINEAR_CONTRACT_PROGRAM_ID)
|
||||
addOutputState(DummyDealContract.State(participants, "linear id"), DUMMY_DEAL_PROGRAM_ID)
|
||||
addCommand(DummyCommandData, listOf(ourIdentity.owningKey))
|
||||
})
|
||||
subFlow(FinalityFlow(stx))
|
||||
@InitiatingFlow
|
||||
class Initiator(private val participants: List<Party>) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val stx = serviceHub.signInitialTransaction(TransactionBuilder(serviceHub.networkMapCache.notaryIdentities.first()).apply {
|
||||
addOutputState(UniqueDummyLinearContract.State(participants, "Dummy linear id"), UNIQUE_DUMMY_LINEAR_CONTRACT_PROGRAM_ID)
|
||||
addOutputState(DummyDealContract.State(participants, "linear id"), DUMMY_DEAL_PROGRAM_ID)
|
||||
addCommand(DummyCommandData, listOf(ourIdentity.owningKey))
|
||||
})
|
||||
val sessions = participants.mapNotNull { if (it != ourIdentity) initiateFlow(it) else null }
|
||||
subFlow(FinalityFlow(stx, sessions))
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(Initiator::class)
|
||||
class Responder(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
subFlow(ReceiveFinalityFlow(otherSide))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -101,12 +101,13 @@ class VaultSoftLockManagerTest {
|
||||
object CommandDataImpl : CommandData
|
||||
|
||||
class ClientLogic(nodePair: NodePair, val state: ContractState) : NodePair.AbstractClientLogic<List<ContractState>>(nodePair) {
|
||||
override fun callImpl() = run {
|
||||
subFlow(FinalityFlow(serviceHub.signInitialTransaction(TransactionBuilder(notary = ourIdentity).apply {
|
||||
override fun callImpl(): List<ContractState> {
|
||||
val stx = serviceHub.signInitialTransaction(TransactionBuilder(notary = ourIdentity).apply {
|
||||
addOutputState(state, ContractImpl::class.jvmName)
|
||||
addCommand(CommandDataImpl, ourIdentity.owningKey)
|
||||
})))
|
||||
serviceHub.vaultService.queryBy<ContractState>(VaultQueryCriteria(softLockingCondition = SoftLockingCondition(LOCKED_ONLY))).states.map {
|
||||
})
|
||||
subFlow(FinalityFlow(stx, emptyList()))
|
||||
return serviceHub.vaultService.queryBy<ContractState>(VaultQueryCriteria(softLockingCondition = SoftLockingCondition(LOCKED_ONLY))).states.map {
|
||||
it.state.data
|
||||
}
|
||||
}
|
||||
|
@ -72,7 +72,9 @@ cordapp {
|
||||
info {
|
||||
name "net/corda/perftestcordapp"
|
||||
vendor "R3"
|
||||
targetPlatformVersion corda_platform_version.toInteger()
|
||||
// TODO Update the performance test cordapp to use the new FinalityFlow API
|
||||
// targetPlatformVersion corda_platform_version.toInteger()
|
||||
targetPlatformVersion 3
|
||||
minimumPlatformVersion 1
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.MockNodeConfigOverrides
|
||||
import net.corda.testing.node.MockNodeParameters
|
||||
import net.corda.testing.node.internal.cordappForPackages
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.After
|
||||
import org.junit.Test
|
||||
@ -19,7 +20,10 @@ import java.util.Collections.nCopies
|
||||
class CashSelectionH2Test {
|
||||
private val mockNet = MockNetwork(
|
||||
threadPerNode = true,
|
||||
cordappPackages = listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset", "com.r3.corda.enterprise.perftestcordapp.schemas"))
|
||||
cordappPackages = emptyList(),
|
||||
// TODO Update the performance test cordapp to use the new FinalityFlow API
|
||||
cordappsForAllNodes = listOf(cordappForPackages("com.r3.corda.enterprise.perftestcordapp").withTargetVersion(3))
|
||||
)
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
|
@ -10,6 +10,7 @@ import net.corda.testing.core.BOC_NAME
|
||||
import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.StartedMockNode
|
||||
import net.corda.testing.node.internal.cordappForPackages
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@ -26,8 +27,12 @@ class CashExitFlowTests {
|
||||
|
||||
@Before
|
||||
fun start() {
|
||||
mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin(),
|
||||
cordappPackages = listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset", "com.r3.corda.enterprise.perftestcordapp.schemas"))
|
||||
mockNet = MockNetwork(
|
||||
servicePeerAllocationStrategy = RoundRobin(),
|
||||
cordappPackages = emptyList(),
|
||||
// TODO Update the performance test cordapp to use the new FinalityFlow API
|
||||
cordappsForAllNodes = listOf(cordappForPackages("com.r3.corda.enterprise.perftestcordapp").withTargetVersion(3))
|
||||
)
|
||||
bankOfCordaNode = mockNet.createPartyNode(BOC_NAME)
|
||||
bankOfCorda = bankOfCordaNode.info.identityFromX500Name(BOC_NAME)
|
||||
notary = mockNet.defaultNotaryIdentity
|
||||
|
@ -11,10 +11,7 @@ import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin
|
||||
import net.corda.testing.node.internal.InternalMockNetwork
|
||||
import net.corda.testing.node.internal.TestStartedNode
|
||||
import net.corda.testing.node.internal.cordappsForPackages
|
||||
import net.corda.testing.node.internal.startFlow
|
||||
import net.corda.testing.node.internal.*
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@ -30,8 +27,11 @@ class CashIssueAndPaymentFlowTests {
|
||||
|
||||
@Before
|
||||
fun start() {
|
||||
mockNet = InternalMockNetwork(servicePeerAllocationStrategy = RoundRobin(),
|
||||
cordappsForAllNodes = cordappsForPackages("com.r3.corda.enterprise.perftestcordapp.contracts.asset", "com.r3.corda.enterprise.perftestcordapp.schemas"))
|
||||
mockNet = InternalMockNetwork(
|
||||
servicePeerAllocationStrategy = RoundRobin(),
|
||||
// TODO Update the performance test cordapp to use the new FinalityFlow API
|
||||
cordappsForAllNodes = listOf(cordappForPackages("com.r3.corda.enterprise.perftestcordapp").withTargetVersion(3))
|
||||
)
|
||||
bankOfCordaNode = mockNet.createPartyNode(BOC_NAME)
|
||||
aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
||||
bankOfCorda = bankOfCordaNode.info.singleIdentity()
|
||||
|
@ -11,10 +11,7 @@ import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin
|
||||
import net.corda.testing.node.internal.InternalMockNetwork
|
||||
import net.corda.testing.node.internal.TestStartedNode
|
||||
import net.corda.testing.node.internal.cordappsForPackages
|
||||
import net.corda.testing.node.internal.startFlow
|
||||
import net.corda.testing.node.internal.*
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@ -23,7 +20,7 @@ import org.junit.runners.Parameterized
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class CashIssueAndPayNoSelectionTests(private val anonymous: Boolean) {
|
||||
class CashIssueAndPaymentNoSelectionTests(private val anonymous: Boolean) {
|
||||
companion object {
|
||||
@Parameterized.Parameters(name = "Anonymous = {0}")
|
||||
@JvmStatic
|
||||
@ -39,8 +36,11 @@ class CashIssueAndPayNoSelectionTests(private val anonymous: Boolean) {
|
||||
|
||||
@Before
|
||||
fun start() {
|
||||
mockNet = InternalMockNetwork(servicePeerAllocationStrategy = RoundRobin(),
|
||||
cordappsForAllNodes = cordappsForPackages("com.r3.corda.enterprise.perftestcordapp.contracts.asset", "com.r3.corda.enterprise.perftestcordapp.schemas"))
|
||||
mockNet = InternalMockNetwork(
|
||||
servicePeerAllocationStrategy = RoundRobin(),
|
||||
// TODO Update the performance test cordapp to use the new FinalityFlow API
|
||||
cordappsForAllNodes = listOf(cordappForPackages("com.r3.corda.enterprise.perftestcordapp").withTargetVersion(3))
|
||||
)
|
||||
bankOfCordaNode = mockNet.createPartyNode(BOC_NAME)
|
||||
aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
||||
bankOfCorda = bankOfCordaNode.info.singleIdentity()
|
@ -10,6 +10,7 @@ import net.corda.testing.core.BOC_NAME
|
||||
import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.StartedMockNode
|
||||
import net.corda.testing.node.internal.cordappForPackages
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@ -26,7 +27,10 @@ class CashIssueFlowTests {
|
||||
fun start() {
|
||||
mockNet = MockNetwork(
|
||||
servicePeerAllocationStrategy = RoundRobin(),
|
||||
cordappPackages = listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset", "com.r3.corda.enterprise.perftestcordapp.schemas"))
|
||||
cordappPackages = emptyList(),
|
||||
// TODO Update the performance test cordapp to use the new FinalityFlow API
|
||||
cordappsForAllNodes = listOf(cordappForPackages("com.r3.corda.enterprise.perftestcordapp").withTargetVersion(3))
|
||||
)
|
||||
bankOfCordaNode = mockNet.createPartyNode(BOC_NAME)
|
||||
bankOfCorda = bankOfCordaNode.info.identityFromX500Name(BOC_NAME)
|
||||
notary = mockNet.defaultNotaryIdentity
|
||||
|
@ -11,10 +11,7 @@ import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin
|
||||
import net.corda.testing.node.internal.InternalMockNetwork
|
||||
import net.corda.testing.node.internal.TestStartedNode
|
||||
import net.corda.testing.node.internal.cordappsForPackages
|
||||
import net.corda.testing.node.internal.startFlow
|
||||
import net.corda.testing.node.internal.*
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@ -33,7 +30,9 @@ class CashPaymentFlowTests {
|
||||
fun start() {
|
||||
mockNet = InternalMockNetwork(
|
||||
servicePeerAllocationStrategy = RoundRobin(),
|
||||
cordappsForAllNodes = cordappsForPackages("com.r3.corda.enterprise.perftestcordapp.contracts.asset", "com.r3.corda.enterprise.perftestcordapp.schemas"))
|
||||
// TODO Update the performance test cordapp to use the new FinalityFlow API
|
||||
cordappsForAllNodes = listOf(cordappForPackages("com.r3.corda.enterprise.perftestcordapp").withTargetVersion(3))
|
||||
)
|
||||
bankOfCordaNode = mockNet.createPartyNode(BOC_NAME)
|
||||
aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
||||
bankOfCorda = bankOfCordaNode.info.singleIdentity()
|
||||
|
@ -52,7 +52,6 @@ import net.corda.testing.dsl.TestTransactionDSLInterpreter
|
||||
import net.corda.testing.internal.LogHelper
|
||||
import net.corda.testing.internal.TEST_TX_TIME
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.internal.*
|
||||
import net.corda.testing.node.ledger
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
@ -86,14 +85,13 @@ internal fun CheckpointStorage.checkpoints(): List<SerializedBytes<Checkpoint>>
|
||||
@RunWith(Parameterized::class)
|
||||
class TwoPartyTradeFlowTests(private val anonymous: Boolean) {
|
||||
companion object {
|
||||
private val cordappPackages = listOf("com.r3.corda.enterprise.perftestcordapp.contracts", "com.r3.corda.enterprise.perftestcordapp.schemas")
|
||||
private val cordappsForAllNodes = cordappsForPackages(cordappPackages)
|
||||
// TODO Update the performance test cordapp to use the new FinalityFlow API
|
||||
private val cordappsForAllNodes = listOf(cordappForPackages("com.r3.corda.enterprise.perftestcordapp").withTargetVersion(3))
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters(name = "Anonymous = {0}")
|
||||
fun data(): Collection<Boolean> = listOf(true, false)
|
||||
|
||||
private val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20)
|
||||
private val MEGA_CORP = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party
|
||||
private val DUMMY_NOTARY get() = dummyNotary.party
|
||||
}
|
||||
|
||||
@ -117,7 +115,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) {
|
||||
// allow interruption half way through.
|
||||
mockNet = InternalMockNetwork(threadPerNode = true, cordappsForAllNodes = cordappsForAllNodes)
|
||||
val ledgerIdentityService = rigorousMock<IdentityServiceInternal>()
|
||||
MockServices(cordappPackages, MEGA_CORP.name, ledgerIdentityService).ledger(DUMMY_NOTARY) {
|
||||
mockNet.defaultNotaryNode.services.ledger(DUMMY_NOTARY) {
|
||||
val notaryNode = mockNet.defaultNotaryNode
|
||||
val aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
||||
val bobNode = mockNet.createPartyNode(BOB_NAME)
|
||||
@ -170,7 +168,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) {
|
||||
fun `trade cash for commercial paper fails using soft locking`() {
|
||||
mockNet = InternalMockNetwork(threadPerNode = true, cordappsForAllNodes = cordappsForAllNodes)
|
||||
val ledgerIdentityService = rigorousMock<IdentityServiceInternal>()
|
||||
MockServices(cordappPackages, MEGA_CORP.name, ledgerIdentityService).ledger(DUMMY_NOTARY) {
|
||||
mockNet.defaultNotaryNode.services.ledger(DUMMY_NOTARY) {
|
||||
val notaryNode = mockNet.defaultNotaryNode
|
||||
val aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
||||
val bobNode = mockNet.createPartyNode(BOB_NAME)
|
||||
@ -228,7 +226,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) {
|
||||
fun `shutdown and restore`() {
|
||||
mockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForAllNodes)
|
||||
val ledgerIdentityService = rigorousMock<IdentityServiceInternal>()
|
||||
MockServices(cordappPackages, MEGA_CORP.name, ledgerIdentityService).ledger(DUMMY_NOTARY) {
|
||||
mockNet.defaultNotaryNode.services.ledger(DUMMY_NOTARY) {
|
||||
val notaryNode = mockNet.defaultNotaryNode
|
||||
val aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
||||
var bobNode = mockNet.createPartyNode(BOB_NAME)
|
||||
@ -523,7 +521,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) {
|
||||
fun `dependency with error on buyer side`() {
|
||||
mockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForAllNodes)
|
||||
val ledgerIdentityService = rigorousMock<IdentityServiceInternal>()
|
||||
MockServices(cordappPackages, MEGA_CORP.name, ledgerIdentityService).ledger(DUMMY_NOTARY) {
|
||||
mockNet.defaultNotaryNode.services.ledger(DUMMY_NOTARY) {
|
||||
runWithError(ledgerIdentityService, true, false, "at least one cash input")
|
||||
}
|
||||
}
|
||||
@ -532,7 +530,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) {
|
||||
fun `dependency with error on seller side`() {
|
||||
mockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForAllNodes)
|
||||
val ledgerIdentityService = rigorousMock<IdentityServiceInternal>()
|
||||
MockServices(cordappPackages, MEGA_CORP.name, ledgerIdentityService).ledger(DUMMY_NOTARY) {
|
||||
mockNet.defaultNotaryNode.services.ledger(DUMMY_NOTARY) {
|
||||
runWithError(ledgerIdentityService, false, true, "Issuances have a time-window")
|
||||
}
|
||||
}
|
@ -8,16 +8,14 @@ import net.corda.core.contracts.Contract
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.TypeOnlyCommandData
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FinalityFlow
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.flows.StartableByService
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.Emoji
|
||||
import net.corda.core.internal.InputStreamAndHash
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.startTrackedFlow
|
||||
import net.corda.core.node.StatesToRecord
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
@ -106,6 +104,7 @@ private fun sender(rpc: CordaRPCOps, inputStream: InputStream, hash: SecureHash.
|
||||
}
|
||||
// DOCEND 2
|
||||
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
class AttachmentDemoFlow(private val otherSide: Party,
|
||||
private val notary: Party,
|
||||
@ -125,10 +124,19 @@ class AttachmentDemoFlow(private val otherSide: Party,
|
||||
|
||||
progressTracker.currentStep = SIGNING
|
||||
|
||||
// Send the transaction to the other recipient
|
||||
val stx = serviceHub.signInitialTransaction(ptx)
|
||||
|
||||
return subFlow(FinalityFlow(stx, setOf(otherSide)))
|
||||
// Send the transaction to the other recipient
|
||||
return subFlow(FinalityFlow(stx, initiateFlow(otherSide)))
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(AttachmentDemoFlow::class)
|
||||
class StoreAttachmentFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
// As a non-participant to the transaction we need to record all states
|
||||
subFlow(ReceiveFinalityFlow(otherSide, statesToRecord = StatesToRecord.ALL_VISIBLE))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,7 +44,7 @@ class TestCommsFlowInitiator(private val x500Name: CordaX500Name? = null) : Flow
|
||||
tx.addOutputState(CommsTestState(responses, serviceHub.myInfo.legalIdentities.first()), CommsTestContract::class.java.name)
|
||||
tx.addCommand(CommsTestCommand, serviceHub.myInfo.legalIdentities.first().owningKey)
|
||||
val signedTx = serviceHub.signInitialTransaction(tx)
|
||||
subFlow(FinalityFlow(signedTx))
|
||||
subFlow(FinalityFlow(signedTx, emptyList()))
|
||||
return responses
|
||||
}
|
||||
|
||||
|
@ -32,14 +32,14 @@ class TestNotaryFlow : FlowLogic<String>() {
|
||||
issueBuilder.addOutputState(NotaryTestState(notary.name.toString(), myIdentity), NotaryTestContract::class.java.name)
|
||||
issueBuilder.addCommand(NotaryTestCommand, myIdentity.owningKey)
|
||||
val signedTx = serviceHub.signInitialTransaction(issueBuilder)
|
||||
val issueResult = subFlow(FinalityFlow(signedTx))
|
||||
val issueResult = subFlow(FinalityFlow(signedTx, emptyList()))
|
||||
progressTracker.currentStep = ISSUED
|
||||
val destroyBuilder = TransactionBuilder()
|
||||
destroyBuilder.notary = notary
|
||||
destroyBuilder.addInputState(issueResult.tx.outRefsOfType<NotaryTestState>().first())
|
||||
destroyBuilder.addCommand(NotaryTestCommand, myIdentity.owningKey)
|
||||
val signedDestroyT = serviceHub.signInitialTransaction(destroyBuilder)
|
||||
val result = subFlow(FinalityFlow(signedDestroyT))
|
||||
val result = subFlow(FinalityFlow(signedDestroyT, emptyList()))
|
||||
progressTracker.currentStep = DESTROYING
|
||||
progressTracker.currentStep = FINALIZED
|
||||
return "notarised: ${result.notary}::${result.tx.id}"
|
||||
|
@ -3,9 +3,7 @@ package net.corda.traderdemo.flow
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FinalityFlow
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
@ -22,6 +20,7 @@ import java.util.*
|
||||
* Flow for the Bank of Corda node to issue some commercial paper to the seller's node, to sell to the buyer.
|
||||
*/
|
||||
@StartableByRPC
|
||||
@InitiatingFlow
|
||||
class CommercialPaperIssueFlow(private val amount: Amount<Currency>,
|
||||
private val issueRef: OpaqueBytes,
|
||||
private val recipient: Party,
|
||||
@ -56,7 +55,7 @@ class CommercialPaperIssueFlow(private val amount: Amount<Currency>,
|
||||
// Sign it as ourselves.
|
||||
val stx = serviceHub.signInitialTransaction(tx)
|
||||
|
||||
subFlow(FinalityFlow(stx))
|
||||
subFlow(FinalityFlow(stx, emptyList()))
|
||||
}
|
||||
|
||||
// Now make a dummy transaction that moves it to a new key, just to show that resolving dependencies works.
|
||||
@ -65,7 +64,17 @@ class CommercialPaperIssueFlow(private val amount: Amount<Currency>,
|
||||
val builder = TransactionBuilder(notary)
|
||||
CommercialPaper().generateMove(builder, issuance.tx.outRef(0), recipient)
|
||||
val stx = serviceHub.signInitialTransaction(builder)
|
||||
subFlow(FinalityFlow(stx))
|
||||
val recipientSession = initiateFlow(recipient)
|
||||
subFlow(FinalityFlow(stx, listOf(recipientSession)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(CommercialPaperIssueFlow::class)
|
||||
class CommercialPaperIssueResponderFlow(private val otherSideSession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
// Record the move transaction
|
||||
subFlow(ReceiveFinalityFlow(otherSideSession))
|
||||
}
|
||||
}
|
||||
|
@ -588,7 +588,7 @@ data class DriverParameters(
|
||||
fun withNetworkParameters(networkParameters: NetworkParameters): DriverParameters = copy(networkParameters = networkParameters)
|
||||
fun withNotaryCustomOverrides(notaryCustomOverrides: Map<String, Any?>): DriverParameters = copy(notaryCustomOverrides = notaryCustomOverrides)
|
||||
fun withInMemoryDB(inMemoryDB: Boolean): DriverParameters = copy(inMemoryDB = inMemoryDB)
|
||||
fun withCordappsForAllNodes(cordappsForAllNodes: Set<TestCordapp>?): DriverParameters = copy(cordappsForAllNodes = cordappsForAllNodes)
|
||||
fun withCordappsForAllNodes(cordappsForAllNodes: Collection<TestCordapp>?): DriverParameters = copy(cordappsForAllNodes = cordappsForAllNodes)
|
||||
|
||||
fun copy(
|
||||
isDebug: Boolean,
|
||||
|
@ -10,7 +10,7 @@ import net.corda.testing.node.internal.simplifyScanPackages
|
||||
*/
|
||||
@DoNotImplement
|
||||
interface TestCordapp {
|
||||
/** Returns the name, defaults to "test-cordapp" if not specified. */
|
||||
/** Returns the name, defaults to "test-name" if not specified. */
|
||||
val name: String
|
||||
|
||||
/** Returns the title, defaults to "test-title" if not specified. */
|
||||
@ -51,17 +51,21 @@ interface TestCordapp {
|
||||
|
||||
class Factory {
|
||||
companion object {
|
||||
/**
|
||||
* Create a [TestCordapp] object by scanning the given packages. The meta data on the CorDapp will be the
|
||||
* default values, which can be changed with the wither methods.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun fromPackages(vararg packageNames: String): TestCordapp = fromPackages(packageNames.asList())
|
||||
|
||||
/**
|
||||
* Create a [TestCordapp] object by scanning the given packages. The meta data on the CorDapp will be the
|
||||
* default values, which can be specified with the wither methods.
|
||||
* default values, which can be changed with the wither methods.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun fromPackages(packageNames: Collection<String>): TestCordapp {
|
||||
return TestCordappImpl(
|
||||
name = "test-cordapp",
|
||||
name = "test-name",
|
||||
version = "1.0",
|
||||
vendor = "test-vendor",
|
||||
title = "test-title",
|
||||
|
@ -251,12 +251,10 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe
|
||||
|
||||
@VisibleForTesting
|
||||
internal open fun createNotaries(): List<TestStartedNode> {
|
||||
val version = VersionInfo(networkParameters.minimumPlatformVersion, "Mock release", "Mock revision", "Mock Vendor")
|
||||
return notarySpecs.map { (name, validating) ->
|
||||
createNode(InternalMockNodeParameters(
|
||||
legalName = name,
|
||||
configOverrides = MockNodeConfigOverrides(notary = MockNetNotaryConfig(validating)),
|
||||
version = version
|
||||
configOverrides = MockNodeConfigOverrides(notary = MockNetNotaryConfig(validating))
|
||||
))
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user