mirror of
https://github.com/corda/corda.git
synced 2024-12-20 21:43:14 +00:00
Merge pull request #5245 from corda/shams-merge-cid-878
CID-878 Flows: Create flow sessions with anonymous parties by the introduction (and the introduction of Destination abstraction)
This commit is contained in:
commit
48b0de5e57
@ -2897,7 +2897,7 @@ public abstract class net.corda.core.identity.AbstractParty extends java.lang.Ob
|
|||||||
##
|
##
|
||||||
@DoNotImplement
|
@DoNotImplement
|
||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
public final class net.corda.core.identity.AnonymousParty extends net.corda.core.identity.AbstractParty
|
public final class net.corda.core.identity.AnonymousParty extends net.corda.core.identity.AbstractParty implements net.corda.core.flows.Destination
|
||||||
public <init>(java.security.PublicKey)
|
public <init>(java.security.PublicKey)
|
||||||
@Nullable
|
@Nullable
|
||||||
public net.corda.core.identity.CordaX500Name nameOrNull()
|
public net.corda.core.identity.CordaX500Name nameOrNull()
|
||||||
@ -2978,7 +2978,7 @@ public final class net.corda.core.identity.IdentityUtils extends java.lang.Objec
|
|||||||
##
|
##
|
||||||
@DoNotImplement
|
@DoNotImplement
|
||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
public final class net.corda.core.identity.Party extends net.corda.core.identity.AbstractParty
|
public final class net.corda.core.identity.Party extends net.corda.core.identity.AbstractParty implements net.corda.core.flows.Destination
|
||||||
public <init>(java.security.cert.X509Certificate)
|
public <init>(java.security.cert.X509Certificate)
|
||||||
public <init>(net.corda.core.identity.CordaX500Name, java.security.PublicKey)
|
public <init>(net.corda.core.identity.CordaX500Name, java.security.PublicKey)
|
||||||
@NotNull
|
@NotNull
|
||||||
|
@ -3,8 +3,11 @@ package net.corda.core.flows
|
|||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.crypto.TransactionSignature
|
import net.corda.core.crypto.TransactionSignature
|
||||||
import net.corda.core.crypto.isFulfilledBy
|
import net.corda.core.crypto.isFulfilledBy
|
||||||
|
import net.corda.core.crypto.toStringShort
|
||||||
|
import net.corda.core.identity.AnonymousParty
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.identity.groupPublicKeysByWellKnownParty
|
import net.corda.core.identity.groupPublicKeysByWellKnownParty
|
||||||
|
import net.corda.core.internal.toMultiMap
|
||||||
import net.corda.core.node.ServiceHub
|
import net.corda.core.node.ServiceHub
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.transactions.WireTransaction
|
import net.corda.core.transactions.WireTransaction
|
||||||
@ -65,7 +68,12 @@ class CollectSignaturesFlow @JvmOverloads constructor(val partiallySignedTx: Sig
|
|||||||
val sessionsToCollectFrom: Collection<FlowSession>,
|
val sessionsToCollectFrom: Collection<FlowSession>,
|
||||||
val myOptionalKeys: Iterable<PublicKey>?,
|
val myOptionalKeys: Iterable<PublicKey>?,
|
||||||
override val progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : FlowLogic<SignedTransaction>() {
|
override val progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : FlowLogic<SignedTransaction>() {
|
||||||
@JvmOverloads constructor(partiallySignedTx: SignedTransaction, sessionsToCollectFrom: Collection<FlowSession>, progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : this(partiallySignedTx, sessionsToCollectFrom, null, progressTracker)
|
@JvmOverloads
|
||||||
|
constructor(
|
||||||
|
partiallySignedTx: SignedTransaction,
|
||||||
|
sessionsToCollectFrom: Collection<FlowSession>,
|
||||||
|
progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()
|
||||||
|
) : this(partiallySignedTx, sessionsToCollectFrom, null, progressTracker)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
object COLLECTING : ProgressTracker.Step("Collecting signatures from counterparties.")
|
object COLLECTING : ProgressTracker.Step("Collecting signatures from counterparties.")
|
||||||
@ -75,7 +83,6 @@ class CollectSignaturesFlow @JvmOverloads constructor(val partiallySignedTx: Sig
|
|||||||
fun tracker() = ProgressTracker(COLLECTING, VERIFYING)
|
fun tracker() = ProgressTracker(COLLECTING, VERIFYING)
|
||||||
|
|
||||||
// TODO: Make the progress tracker adapt to the number of counterparties to collect from.
|
// TODO: Make the progress tracker adapt to the number of counterparties to collect from.
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
@ -105,14 +112,61 @@ class CollectSignaturesFlow @JvmOverloads constructor(val partiallySignedTx: Sig
|
|||||||
// If the unsigned counterparties list is empty then we don't need to collect any more signatures here.
|
// If the unsigned counterparties list is empty then we don't need to collect any more signatures here.
|
||||||
if (unsigned.isEmpty()) return partiallySignedTx
|
if (unsigned.isEmpty()) return partiallySignedTx
|
||||||
|
|
||||||
val partyToKeysMap = groupPublicKeysByWellKnownParty(serviceHub, unsigned)
|
val setOfAllSessionKeys: Map<PublicKey, FlowSession> = sessionsToCollectFrom
|
||||||
// Check that we have a session for all parties. No more, no less.
|
.groupBy {
|
||||||
require(sessionsToCollectFrom.map { it.counterparty }.toSet() == partyToKeysMap.keys) {
|
val destination = it.destination
|
||||||
|
when (destination) {
|
||||||
|
is Party -> destination.owningKey
|
||||||
|
is AnonymousParty -> destination.owningKey
|
||||||
|
else -> throw IllegalArgumentException("Signatures can only be collected from Party or AnonymousParty, not $destination")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.mapValues {
|
||||||
|
require(it.value.size == 1) { "There are multiple sessions initiated for party key ${it.key.toStringShort()}" }
|
||||||
|
it.value.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
val partyToKeysItSignsFor: Map<Party, List<PublicKey>> = groupPublicKeysByWellKnownParty(serviceHub, unsigned)
|
||||||
|
val keyToSigningParty: Map<PublicKey, Party> = partyToKeysItSignsFor
|
||||||
|
.flatMap { (wellKnown, allKeysItSignsFor) -> allKeysItSignsFor.map { it to wellKnown } }
|
||||||
|
.toMap()
|
||||||
|
|
||||||
|
val unrelatedSessions = sessionsToCollectFrom.filterNot {
|
||||||
|
if (it.destination is Party) {
|
||||||
|
// The session must have a corresponding unsigned.
|
||||||
|
it.destination in partyToKeysItSignsFor
|
||||||
|
} else {
|
||||||
|
// setOfAllSessionKeys has already checked for valid destination types so we can safely cast to AnonoymousParty here.
|
||||||
|
// This session was not initiated by a wellKnownParty so must directly exist in the unsigned.
|
||||||
|
(it.destination as AnonymousParty).owningKey in unsigned
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val keyToSessionList = unsigned.map {
|
||||||
|
val session = setOfAllSessionKeys[it]
|
||||||
|
if (session != null) {
|
||||||
|
// The unsigned key exists directly as a sessionKey, so use that session
|
||||||
|
it to session
|
||||||
|
} else {
|
||||||
|
// It might be delegated to a wellKnownParty
|
||||||
|
val wellKnownParty = checkNotNull(keyToSigningParty[it]) { "Could not find a session or wellKnown party for key ${it.toStringShort()}" }
|
||||||
|
// There is a wellKnownParty for this key, check if it has a session, and if so - use that session
|
||||||
|
val sessionForWellKnownParty = checkNotNull(setOfAllSessionKeys[wellKnownParty.owningKey]) {
|
||||||
|
"No session available to request signature for key: ${it.toStringShort()}"
|
||||||
|
}
|
||||||
|
it to sessionForWellKnownParty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now invert the map to find the keys per session
|
||||||
|
val sessionToKeysMap = keyToSessionList.map { it.second to it.first }.toMultiMap()
|
||||||
|
|
||||||
|
require(unrelatedSessions.isEmpty()) {
|
||||||
"The Initiator of CollectSignaturesFlow must pass in exactly the sessions required to sign the transaction."
|
"The Initiator of CollectSignaturesFlow must pass in exactly the sessions required to sign the transaction."
|
||||||
}
|
}
|
||||||
// Collect signatures from all counterparties and append them to the partially signed transaction.
|
// Collect signatures from all counterparties and append them to the partially signed transaction.
|
||||||
val counterpartySignatures = sessionsToCollectFrom.flatMap { session ->
|
val counterpartySignatures = sessionToKeysMap.flatMap { (session, keys) ->
|
||||||
subFlow(CollectSignatureFlow(partiallySignedTx, session, partyToKeysMap[session.counterparty]!!))
|
subFlow(CollectSignatureFlow(partiallySignedTx, session, keys))
|
||||||
}
|
}
|
||||||
val stx = partiallySignedTx + counterpartySignatures
|
val stx = partiallySignedTx + counterpartySignatures
|
||||||
|
|
||||||
@ -196,7 +250,7 @@ class CollectSignatureFlow(val partiallySignedTx: SignedTransaction, val session
|
|||||||
* @param otherSideSession The session which is providing you a transaction to sign.
|
* @param otherSideSession The session which is providing you a transaction to sign.
|
||||||
*/
|
*/
|
||||||
abstract class SignTransactionFlow @JvmOverloads constructor(val otherSideSession: FlowSession,
|
abstract class SignTransactionFlow @JvmOverloads constructor(val otherSideSession: FlowSession,
|
||||||
override val progressTracker: ProgressTracker = SignTransactionFlow.tracker()) : FlowLogic<SignedTransaction>() {
|
override val progressTracker: ProgressTracker = SignTransactionFlow.tracker()) : FlowLogic<SignedTransaction>() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
object RECEIVING : ProgressTracker.Step("Receiving transaction proposal for signing.")
|
object RECEIVING : ProgressTracker.Step("Receiving transaction proposal for signing.")
|
||||||
@ -247,12 +301,6 @@ abstract class SignTransactionFlow @JvmOverloads constructor(val otherSideSessio
|
|||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
private fun checkSignatures(stx: SignedTransaction) {
|
private fun checkSignatures(stx: SignedTransaction) {
|
||||||
// We set `ignoreUnrecognisedParties` to `true` in `groupPublicKeysByWellKnownParty`. This is because we don't
|
|
||||||
// need to recognise all keys, but just the initiator's.
|
|
||||||
val signingWellKnownIdentities = groupPublicKeysByWellKnownParty(serviceHub, stx.sigs.map(TransactionSignature::by), true)
|
|
||||||
require(otherSideSession.counterparty in signingWellKnownIdentities) {
|
|
||||||
"The Initiator of CollectSignaturesFlow must have signed the transaction. Found $signingWellKnownIdentities, expected $otherSideSession"
|
|
||||||
}
|
|
||||||
val signed = stx.sigs.map { it.by }
|
val signed = stx.sigs.map { it.by }
|
||||||
val allSigners = stx.tx.requiredSigningKeys
|
val allSigners = stx.tx.requiredSigningKeys
|
||||||
val notSigned = allSigners - signed
|
val notSigned = allSigners - signed
|
||||||
|
@ -115,6 +115,13 @@ abstract class FlowLogic<out T> {
|
|||||||
*/
|
*/
|
||||||
val serviceHub: ServiceHub get() = stateMachine.serviceHub
|
val serviceHub: ServiceHub get() = stateMachine.serviceHub
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a communication session with [destination]. Subsequently you may send/receive using this session object. How the messaging
|
||||||
|
* is routed depends on the [Destination] type, including whether this call does any initial communication.
|
||||||
|
*/
|
||||||
|
@Suspendable
|
||||||
|
fun initiateFlow(destination: Destination): FlowSession = stateMachine.initiateFlow(destination)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a communication session with [party]. Subsequently you may send/receive using this session object. Note
|
* Creates a communication session with [party]. Subsequently you may send/receive using this session object. Note
|
||||||
* that this function does not communicate in itself, the counter-flow will be kicked off by the first send/receive.
|
* that this function does not communicate in itself, the counter-flow will be kicked off by the first send/receive.
|
||||||
@ -252,7 +259,6 @@ abstract class FlowLogic<out T> {
|
|||||||
return sendAndReceiveWithRetry(R::class.java, payload)
|
return sendAndReceiveWithRetry(R::class.java, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Suspends until a message has been received for each session in the specified [sessions].
|
/** Suspends until a message has been received for each session in the specified [sessions].
|
||||||
*
|
*
|
||||||
* Consider [receiveAll(receiveType: Class<R>, sessions: List<FlowSession>): List<UntrustworthyData<R>>] when the same type is expected from all sessions.
|
* Consider [receiveAll(receiveType: Class<R>, sessions: List<FlowSession>): List<UntrustworthyData<R>>] when the same type is expected from all sessions.
|
||||||
|
@ -2,11 +2,12 @@ package net.corda.core.flows
|
|||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.DoNotImplement
|
import net.corda.core.DoNotImplement
|
||||||
|
import net.corda.core.KeepForDJVM
|
||||||
|
import net.corda.core.identity.AnonymousParty
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.utilities.UntrustworthyData
|
import net.corda.core.utilities.UntrustworthyData
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* A [FlowSession] is a handle on a communication sequence between two paired flows, possibly running on separate nodes.
|
* A [FlowSession] is a handle on a communication sequence between two paired flows, possibly running on separate nodes.
|
||||||
* It is used to send and receive messages between the flows as well as to query information about the counter-flow.
|
* It is used to send and receive messages between the flows as well as to query information about the counter-flow.
|
||||||
*
|
*
|
||||||
@ -45,8 +46,18 @@ import net.corda.core.utilities.UntrustworthyData
|
|||||||
@DoNotImplement
|
@DoNotImplement
|
||||||
abstract class FlowSession {
|
abstract class FlowSession {
|
||||||
/**
|
/**
|
||||||
* The [Party] on the other side of this session. In the case of a session created by [FlowLogic.initiateFlow]
|
* The [Destination] on the other side of this session. In the case of a session created by [FlowLogic.initiateFlow] this is the same
|
||||||
* [counterparty] is the same Party as the one passed to that function.
|
* destination as the one passed to that function.
|
||||||
|
*/
|
||||||
|
abstract val destination: Destination
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the destination on the other side of this session is a [Party] then returns that, otherwise throws [IllegalStateException].
|
||||||
|
*
|
||||||
|
* Only use this method if it's known the other side is a [Party], otherwise use [destination].
|
||||||
|
*
|
||||||
|
* @throws IllegalStateException if the other side is not a [Party].
|
||||||
|
* @see destination
|
||||||
*/
|
*/
|
||||||
abstract val counterparty: Party
|
abstract val counterparty: Party
|
||||||
|
|
||||||
@ -181,3 +192,14 @@ abstract class FlowSession {
|
|||||||
@Suspendable
|
@Suspendable
|
||||||
abstract fun send(payload: Any)
|
abstract fun send(payload: Any)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An abstraction for flow session destinations. A flow can send to and receive from objects which implement this interface. The specifics
|
||||||
|
* of how the messages are routed depend on the implementation.
|
||||||
|
*
|
||||||
|
* Corda currently only supports a fixed set of destination types, namely [Party] and [AnonymousParty]. New destination types will be added
|
||||||
|
* in future releases.
|
||||||
|
*/
|
||||||
|
@DoNotImplement
|
||||||
|
@KeepForDJVM
|
||||||
|
interface Destination
|
||||||
|
@ -3,15 +3,22 @@ package net.corda.core.identity
|
|||||||
import net.corda.core.KeepForDJVM
|
import net.corda.core.KeepForDJVM
|
||||||
import net.corda.core.contracts.PartyAndReference
|
import net.corda.core.contracts.PartyAndReference
|
||||||
import net.corda.core.crypto.toStringShort
|
import net.corda.core.crypto.toStringShort
|
||||||
|
import net.corda.core.flows.Destination
|
||||||
|
import net.corda.core.flows.FlowLogic
|
||||||
import net.corda.core.utilities.OpaqueBytes
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [AnonymousParty] class contains enough information to uniquely identify a [Party] while excluding private
|
* The [AnonymousParty] class contains enough information to uniquely identify a [Party] while excluding private
|
||||||
* information such as name. It is intended to represent a party on the distributed ledger.
|
* information such as name. It is intended to represent a party on the distributed ledger.
|
||||||
|
*
|
||||||
|
* ### Flow sessions
|
||||||
|
*
|
||||||
|
* Anonymous parties can be used to communicate using the [FlowLogic.initiateFlow] method. Message routing is simply routing to the well-known
|
||||||
|
* [Party] the anonymous party belongs to. This mechanism assumes the party initiating the communication knows who the anonymous party is.
|
||||||
*/
|
*/
|
||||||
@KeepForDJVM
|
@KeepForDJVM
|
||||||
class AnonymousParty(owningKey: PublicKey) : AbstractParty(owningKey) {
|
class AnonymousParty(owningKey: PublicKey) : Destination, AbstractParty(owningKey) {
|
||||||
override fun nameOrNull(): CordaX500Name? = null
|
override fun nameOrNull(): CordaX500Name? = null
|
||||||
override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes)
|
override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes)
|
||||||
override fun toString() = "Anonymous(${owningKey.toStringShort()})"
|
override fun toString() = "Anonymous(${owningKey.toStringShort()})"
|
||||||
|
@ -4,6 +4,8 @@ import net.corda.core.KeepForDJVM
|
|||||||
import net.corda.core.contracts.PartyAndReference
|
import net.corda.core.contracts.PartyAndReference
|
||||||
import net.corda.core.crypto.CompositeKey
|
import net.corda.core.crypto.CompositeKey
|
||||||
import net.corda.core.crypto.Crypto
|
import net.corda.core.crypto.Crypto
|
||||||
|
import net.corda.core.flows.Destination
|
||||||
|
import net.corda.core.flows.FlowLogic
|
||||||
import net.corda.core.utilities.OpaqueBytes
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
@ -25,10 +27,15 @@ import java.security.cert.X509Certificate
|
|||||||
*
|
*
|
||||||
* Note that equality is based solely on the owning key.
|
* Note that equality is based solely on the owning key.
|
||||||
*
|
*
|
||||||
|
* ### Flow sessions
|
||||||
|
*
|
||||||
|
* Communication with other parties is done using the flow framework with the [FlowLogic.initiateFlow] method. Message routing is done by
|
||||||
|
* using the network map to look up the connectivity details pertaining to the [Party].
|
||||||
|
*
|
||||||
* @see CompositeKey
|
* @see CompositeKey
|
||||||
*/
|
*/
|
||||||
@KeepForDJVM
|
@KeepForDJVM
|
||||||
class Party(val name: CordaX500Name, owningKey: PublicKey) : AbstractParty(owningKey) {
|
class Party(val name: CordaX500Name, owningKey: PublicKey) : Destination, AbstractParty(owningKey) {
|
||||||
constructor(certificate: X509Certificate)
|
constructor(certificate: X509Certificate)
|
||||||
: this(CordaX500Name.build(certificate.subjectX500Principal), Crypto.toSupportedPublicKey(certificate.publicKey))
|
: this(CordaX500Name.build(certificate.subjectX500Principal), Crypto.toSupportedPublicKey(certificate.publicKey))
|
||||||
|
|
||||||
|
@ -5,10 +5,7 @@ import net.corda.core.DeleteForDJVM
|
|||||||
import net.corda.core.DoNotImplement
|
import net.corda.core.DoNotImplement
|
||||||
import net.corda.core.concurrent.CordaFuture
|
import net.corda.core.concurrent.CordaFuture
|
||||||
import net.corda.core.context.InvocationContext
|
import net.corda.core.context.InvocationContext
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.flows.FlowSession
|
|
||||||
import net.corda.core.flows.FlowStackSnapshot
|
|
||||||
import net.corda.core.flows.StateMachineRunId
|
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.node.ServiceHub
|
import net.corda.core.node.ServiceHub
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
@ -21,7 +18,7 @@ interface FlowStateMachine<FLOWRETURN> {
|
|||||||
fun <SUSPENDRETURN : Any> suspend(ioRequest: FlowIORequest<SUSPENDRETURN>, maySkipCheckpoint: Boolean): SUSPENDRETURN
|
fun <SUSPENDRETURN : Any> suspend(ioRequest: FlowIORequest<SUSPENDRETURN>, maySkipCheckpoint: Boolean): SUSPENDRETURN
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
fun initiateFlow(party: Party): FlowSession
|
fun initiateFlow(destination: Destination): FlowSession
|
||||||
|
|
||||||
fun checkFlowPermission(permissionName: String, extraAuditData: Map<String, String>)
|
fun checkFlowPermission(permissionName: String, extraAuditData: Map<String, String>)
|
||||||
|
|
||||||
|
@ -5,6 +5,9 @@ import net.corda.core.DoNotImplement
|
|||||||
import net.corda.core.contracts.PartyAndReference
|
import net.corda.core.contracts.PartyAndReference
|
||||||
import net.corda.core.crypto.toStringShort
|
import net.corda.core.crypto.toStringShort
|
||||||
import net.corda.core.identity.*
|
import net.corda.core.identity.*
|
||||||
|
import net.corda.core.internal.hash
|
||||||
|
import net.corda.core.utilities.contextLogger
|
||||||
|
import net.corda.core.utilities.debug
|
||||||
import java.security.InvalidAlgorithmParameterException
|
import java.security.InvalidAlgorithmParameterException
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.security.cert.*
|
import java.security.cert.*
|
||||||
@ -24,6 +27,10 @@ interface IdentityService {
|
|||||||
val trustAnchor: TrustAnchor
|
val trustAnchor: TrustAnchor
|
||||||
val caCertStore: CertStore
|
val caCertStore: CertStore
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = contextLogger()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify and then store an identity.
|
* Verify and then store an identity.
|
||||||
*
|
*
|
||||||
@ -94,6 +101,7 @@ interface IdentityService {
|
|||||||
// The original version of this would return the party as-is if it was a Party (rather than AnonymousParty),
|
// The original version of this would return the party as-is if it was a Party (rather than AnonymousParty),
|
||||||
// however that means that we don't verify that we know who owns the key. As such as now enforce turning the key
|
// however that means that we don't verify that we know who owns the key. As such as now enforce turning the key
|
||||||
// into a party, and from there figure out the well known party.
|
// into a party, and from there figure out the well known party.
|
||||||
|
log.debug { "Attempting to find wellKnownParty for: ${party.owningKey.hash}" }
|
||||||
val candidate = partyFromKey(party.owningKey)
|
val candidate = partyFromKey(party.owningKey)
|
||||||
// TODO: This should be done via the network map cache, which is the authoritative source of well known identities
|
// TODO: This should be done via the network map cache, which is the authoritative source of well known identities
|
||||||
return if (candidate != null) {
|
return if (candidate != null) {
|
||||||
|
@ -6,13 +6,11 @@ import net.corda.core.contracts.Command
|
|||||||
import net.corda.core.contracts.StateAndContract
|
import net.corda.core.contracts.StateAndContract
|
||||||
import net.corda.core.contracts.requireThat
|
import net.corda.core.contracts.requireThat
|
||||||
import net.corda.core.flows.mixins.WithContracts
|
import net.corda.core.flows.mixins.WithContracts
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.*
|
||||||
import net.corda.core.identity.Party
|
|
||||||
import net.corda.core.identity.excludeHostNode
|
|
||||||
import net.corda.core.identity.groupAbstractPartyByWellKnownParty
|
|
||||||
import net.corda.core.node.services.IdentityService
|
import net.corda.core.node.services.IdentityService
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
|
import net.corda.node.services.api.IdentityServiceInternal
|
||||||
import net.corda.testing.contracts.DummyContract
|
import net.corda.testing.contracts.DummyContract
|
||||||
import net.corda.testing.core.*
|
import net.corda.testing.core.*
|
||||||
import net.corda.testing.internal.matchers.flow.willReturn
|
import net.corda.testing.internal.matchers.flow.willReturn
|
||||||
@ -23,8 +21,11 @@ import net.corda.testing.node.internal.DUMMY_CONTRACTS_CORDAPP
|
|||||||
import net.corda.testing.node.internal.InternalMockNetwork
|
import net.corda.testing.node.internal.InternalMockNetwork
|
||||||
import net.corda.testing.node.internal.TestStartedNode
|
import net.corda.testing.node.internal.TestStartedNode
|
||||||
import net.corda.testing.node.internal.enclosedCordapp
|
import net.corda.testing.node.internal.enclosedCordapp
|
||||||
|
import org.hamcrest.CoreMatchers.`is`
|
||||||
import org.junit.AfterClass
|
import org.junit.AfterClass
|
||||||
|
import org.junit.Assert
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import java.security.PublicKey
|
||||||
|
|
||||||
class CollectSignaturesFlowTests : WithContracts {
|
class CollectSignaturesFlowTests : WithContracts {
|
||||||
companion object {
|
companion object {
|
||||||
@ -55,11 +56,59 @@ class CollectSignaturesFlowTests : WithContracts {
|
|||||||
aliceNode.verifyAndRegister(bConfidentialIdentity)
|
aliceNode.verifyAndRegister(bConfidentialIdentity)
|
||||||
|
|
||||||
assertThat(
|
assertThat(
|
||||||
aliceNode.startTestFlow(alice, bConfidentialIdentity.party, charlie),
|
aliceNode.startTestFlow(alice, bConfidentialIdentity.party, charlie),
|
||||||
willReturn(requiredSignatures(3))
|
willReturn(requiredSignatures(3))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `successfully collects signatures when sessions are initiated with AnonymousParty`() {
|
||||||
|
val aConfidentialIdentity1 = aliceNode.createConfidentialIdentity(alice)
|
||||||
|
val bConfidentialIdentity1 = bobNode.createConfidentialIdentity(bob)
|
||||||
|
val bConfidentialIdentity2 = bobNode.createConfidentialIdentity(bob)
|
||||||
|
val cConfidentialIdentity1 = charlieNode.createConfidentialIdentity(charlie)
|
||||||
|
|
||||||
|
bobNode.registerInitiatedFlow(AnonymousSessionTestFlowResponder::class.java)
|
||||||
|
charlieNode.registerInitiatedFlow(AnonymousSessionTestFlowResponder::class.java)
|
||||||
|
|
||||||
|
val owners = listOf(aConfidentialIdentity1, bConfidentialIdentity1, bConfidentialIdentity2, cConfidentialIdentity1)
|
||||||
|
|
||||||
|
val future = aliceNode.startFlow(AnonymousSessionTestFlow(owners)).resultFuture
|
||||||
|
mockNet.runNetwork()
|
||||||
|
val stx = future.get()
|
||||||
|
val missingSigners = stx.getMissingSigners()
|
||||||
|
Assert.assertThat(missingSigners, `is`(emptySet()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `successfully collects signatures when sessions are initiated with both AnonymousParty and WellKnownParty`() {
|
||||||
|
val aConfidentialIdentity1 = aliceNode.createConfidentialIdentity(alice)
|
||||||
|
val bConfidentialIdentity1 = bobNode.createConfidentialIdentity(bob)
|
||||||
|
val bConfidentialIdentity2 = bobNode.createConfidentialIdentity(bob)
|
||||||
|
val cConfidentialIdentity1 = charlieNode.createConfidentialIdentity(charlie)
|
||||||
|
val cConfidentialIdentity2 = charlieNode.createConfidentialIdentity(charlie)
|
||||||
|
|
||||||
|
bobNode.registerInitiatedFlow(MixAndMatchAnonymousSessionTestFlowResponder::class.java)
|
||||||
|
charlieNode.registerInitiatedFlow(MixAndMatchAnonymousSessionTestFlowResponder::class.java)
|
||||||
|
|
||||||
|
val owners = listOf(
|
||||||
|
aConfidentialIdentity1,
|
||||||
|
bConfidentialIdentity1,
|
||||||
|
bConfidentialIdentity2,
|
||||||
|
cConfidentialIdentity1,
|
||||||
|
cConfidentialIdentity2
|
||||||
|
)
|
||||||
|
|
||||||
|
val keysToLookup = listOf(bConfidentialIdentity1.owningKey, bConfidentialIdentity2.owningKey, cConfidentialIdentity1.owningKey)
|
||||||
|
val keysToKeepAnonymous = listOf(cConfidentialIdentity2.owningKey)
|
||||||
|
|
||||||
|
val future = aliceNode.startFlow(MixAndMatchAnonymousSessionTestFlow(owners, keysToLookup.toSet(), keysToKeepAnonymous.toSet())).resultFuture
|
||||||
|
mockNet.runNetwork()
|
||||||
|
val stx = future.get()
|
||||||
|
val missingSigners = stx.getMissingSigners()
|
||||||
|
Assert.assertThat(missingSigners, `is`(emptySet()))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `no need to collect any signatures`() {
|
fun `no need to collect any signatures`() {
|
||||||
val ptx = aliceNode.signDummyContract(alice.ref(1))
|
val ptx = aliceNode.signDummyContract(alice.ref(1))
|
||||||
@ -97,10 +146,10 @@ class CollectSignaturesFlowTests : WithContracts {
|
|||||||
//region Operators
|
//region Operators
|
||||||
private fun TestStartedNode.startTestFlow(vararg party: Party) =
|
private fun TestStartedNode.startTestFlow(vararg party: Party) =
|
||||||
startFlowAndRunNetwork(
|
startFlowAndRunNetwork(
|
||||||
TestFlow.Initiator(DummyContract.MultiOwnerState(
|
TestFlow.Initiator(DummyContract.MultiOwnerState(
|
||||||
MAGIC_NUMBER,
|
MAGIC_NUMBER,
|
||||||
listOf(*party)),
|
listOf(*party)),
|
||||||
mockNet.defaultNotaryIdentity))
|
mockNet.defaultNotaryIdentity))
|
||||||
|
|
||||||
//region Test Flow
|
//region Test Flow
|
||||||
// With this flow, the initiator starts the "CollectTransactionFlow". It is then the responders responsibility to
|
// With this flow, the initiator starts the "CollectTransactionFlow". It is then the responders responsibility to
|
||||||
@ -144,3 +193,82 @@ class CollectSignaturesFlowTests : WithContracts {
|
|||||||
}
|
}
|
||||||
//region
|
//region
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@InitiatingFlow
|
||||||
|
class AnonymousSessionTestFlow(private val cis: List<PartyAndCertificate>) : FlowLogic<SignedTransaction>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call(): SignedTransaction {
|
||||||
|
|
||||||
|
for (ci in cis) {
|
||||||
|
if (ci.name != ourIdentity.name) {
|
||||||
|
(serviceHub.identityService as IdentityServiceInternal).verifyAndRegisterIdentity(ci)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val state = DummyContract.MultiOwnerState(owners = cis.map { AnonymousParty(it.owningKey) })
|
||||||
|
val create = net.corda.testing.contracts.DummyContract.Commands.Create()
|
||||||
|
val txBuilder = TransactionBuilder(notary = serviceHub.networkMapCache.notaryIdentities.first())
|
||||||
|
.addOutputState(state)
|
||||||
|
.addCommand(create, cis.map { it.owningKey })
|
||||||
|
|
||||||
|
|
||||||
|
val ourKey = cis.single { it.name == ourIdentity.name }.owningKey
|
||||||
|
val signedByUsTx = serviceHub.signInitialTransaction(txBuilder, ourKey)
|
||||||
|
val sessionsToCollectFrom = cis.filter { it.name != ourIdentity.name }.map { initiateFlow(AnonymousParty(it.owningKey)) }
|
||||||
|
return subFlow(CollectSignaturesFlow(signedByUsTx, sessionsToCollectFrom, myOptionalKeys = listOf(ourKey)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@InitiatedBy(AnonymousSessionTestFlow::class)
|
||||||
|
class AnonymousSessionTestFlowResponder(private val otherSideSession: FlowSession) : FlowLogic<Unit>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
val signFlow = object : SignTransactionFlow(otherSideSession) {
|
||||||
|
@Suspendable
|
||||||
|
override fun checkTransaction(stx: SignedTransaction) = requireThat {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
subFlow(signFlow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@InitiatingFlow
|
||||||
|
class MixAndMatchAnonymousSessionTestFlow(private val cis: List<PartyAndCertificate>,
|
||||||
|
private val keysToLookUp: Set<PublicKey>,
|
||||||
|
private val keysToKeepAnonymous: Set<PublicKey>) : FlowLogic<SignedTransaction>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call(): SignedTransaction {
|
||||||
|
|
||||||
|
for (ci in cis) {
|
||||||
|
if (ci.name != ourIdentity.name) {
|
||||||
|
(serviceHub.identityService as IdentityServiceInternal).verifyAndRegisterIdentity(ci)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val state = DummyContract.MultiOwnerState(owners = cis.map { AnonymousParty(it.owningKey) })
|
||||||
|
val create = net.corda.testing.contracts.DummyContract.Commands.Create()
|
||||||
|
val txBuilder = TransactionBuilder(notary = serviceHub.networkMapCache.notaryIdentities.first())
|
||||||
|
.addOutputState(state)
|
||||||
|
.addCommand(create, cis.map { it.owningKey })
|
||||||
|
|
||||||
|
|
||||||
|
val ourKey = cis.single { it.name == ourIdentity.name }.owningKey
|
||||||
|
val signedByUsTx = serviceHub.signInitialTransaction(txBuilder, ourKey)
|
||||||
|
|
||||||
|
val resolvedParties = keysToLookUp.map { serviceHub.identityService.wellKnownPartyFromAnonymous(AnonymousParty(it))!! }.toSet()
|
||||||
|
val anonymousParties = keysToKeepAnonymous.map { AnonymousParty(it) }
|
||||||
|
val sessionsToCollectFrom = (resolvedParties + anonymousParties).map { initiateFlow(it as Destination) }
|
||||||
|
return subFlow(CollectSignaturesFlow(signedByUsTx, sessionsToCollectFrom, myOptionalKeys = listOf(ourKey)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@InitiatedBy(MixAndMatchAnonymousSessionTestFlow::class)
|
||||||
|
class MixAndMatchAnonymousSessionTestFlowResponder(private val otherSideSession: FlowSession) : FlowLogic<Unit>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
val signFlow = object : SignTransactionFlow(otherSideSession) {
|
||||||
|
@Suspendable
|
||||||
|
override fun checkTransaction(stx: SignedTransaction) = requireThat {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
subFlow(signFlow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -9,6 +9,10 @@ release, see :doc:`app-upgrade-notes`.
|
|||||||
Version 5.0
|
Version 5.0
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
|
* Introduced a new ``Destination`` abstraction for communicating with non-Party destinations using the new ``FlowLogic.initateFlow(Destination)``
|
||||||
|
method. ``Party`` and ``AnonymousParty`` have been retrofitted to implement ``Destination``. Initiating a flow to an ``AnonymousParty``
|
||||||
|
means resolving to the well-known identity ``Party`` and then communicating with that.
|
||||||
|
|
||||||
* Removed ``finance-workflows`` dependency on jackson library. The functions that used jackson (e.g. ``FinanceJSONSupport``) have been moved
|
* Removed ``finance-workflows`` dependency on jackson library. The functions that used jackson (e.g. ``FinanceJSONSupport``) have been moved
|
||||||
into IRS Demo.
|
into IRS Demo.
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import net.corda.confidential.SwapIdentitiesFlow
|
|||||||
import net.corda.core.contracts.Amount
|
import net.corda.core.contracts.Amount
|
||||||
import net.corda.core.contracts.InsufficientBalanceException
|
import net.corda.core.contracts.InsufficientBalanceException
|
||||||
import net.corda.core.flows.*
|
import net.corda.core.flows.*
|
||||||
|
import net.corda.core.identity.AbstractParty
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
@ -52,7 +53,7 @@ open class CashPaymentFlow(
|
|||||||
progressTracker.currentStep = GENERATING_ID
|
progressTracker.currentStep = GENERATING_ID
|
||||||
val recipientSession = initiateFlow(recipient)
|
val recipientSession = initiateFlow(recipient)
|
||||||
recipientSession.send(anonymous)
|
recipientSession.send(anonymous)
|
||||||
val anonymousRecipient = if (anonymous) {
|
val anonymousRecipient: AbstractParty = if (anonymous) {
|
||||||
subFlow(SwapIdentitiesFlow(recipientSession))[recipient]!!
|
subFlow(SwapIdentitiesFlow(recipientSession))[recipient]!!
|
||||||
} else {
|
} else {
|
||||||
recipient
|
recipient
|
||||||
|
@ -154,7 +154,7 @@ object CashUtils {
|
|||||||
// If anonymous is true, generate a new identity that change will be sent to for confidentiality purposes. This means that a
|
// If anonymous is true, generate a new identity that change will be sent to for confidentiality purposes. This means that a
|
||||||
// third party with a copy of the transaction (such as the notary) cannot identify who the change was
|
// third party with a copy of the transaction (such as the notary) cannot identify who the change was
|
||||||
// sent to
|
// sent to
|
||||||
val changeIdentity = if (anonymous) services.keyManagementService.freshKeyAndCert(ourIdentity, revocationEnabled).party.anonymise() else ourIdentity.party
|
val changeIdentity: AbstractParty = if (anonymous) services.keyManagementService.freshKeyAndCert(ourIdentity, revocationEnabled).party.anonymise() else ourIdentity.party
|
||||||
return OnLedgerAsset.generateSpend(
|
return OnLedgerAsset.generateSpend(
|
||||||
tx,
|
tx,
|
||||||
payments,
|
payments,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.corda.node.services.api
|
package net.corda.node.services.api
|
||||||
|
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.identity.PartyAndCertificate
|
import net.corda.core.identity.PartyAndCertificate
|
||||||
import net.corda.core.internal.CertRole
|
import net.corda.core.internal.CertRole
|
||||||
import net.corda.core.node.services.IdentityService
|
import net.corda.core.node.services.IdentityService
|
||||||
@ -8,6 +9,7 @@ import net.corda.core.utilities.contextLogger
|
|||||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||||
import net.corda.nodeapi.internal.crypto.x509Certificates
|
import net.corda.nodeapi.internal.crypto.x509Certificates
|
||||||
import java.security.InvalidAlgorithmParameterException
|
import java.security.InvalidAlgorithmParameterException
|
||||||
|
import java.security.PublicKey
|
||||||
import java.security.cert.CertPathValidatorException
|
import java.security.cert.CertPathValidatorException
|
||||||
import java.security.cert.CertificateExpiredException
|
import java.security.cert.CertificateExpiredException
|
||||||
import java.security.cert.CertificateNotYetValidException
|
import java.security.cert.CertificateNotYetValidException
|
||||||
|
@ -9,6 +9,7 @@ import net.corda.core.serialization.SingletonSerializeAsToken
|
|||||||
import net.corda.core.utilities.MAX_HASH_HEX_SIZE
|
import net.corda.core.utilities.MAX_HASH_HEX_SIZE
|
||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import net.corda.core.utilities.debug
|
import net.corda.core.utilities.debug
|
||||||
|
import net.corda.core.utilities.toBase58String
|
||||||
import net.corda.node.services.api.IdentityServiceInternal
|
import net.corda.node.services.api.IdentityServiceInternal
|
||||||
import net.corda.node.utilities.AppendOnlyPersistentMap
|
import net.corda.node.utilities.AppendOnlyPersistentMap
|
||||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||||
@ -170,7 +171,9 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We give the caller a copy of the data set to avoid any locking problems
|
// We give the caller a copy of the data set to avoid any locking problems
|
||||||
override fun getAllIdentities(): Iterable<PartyAndCertificate> = database.transaction { keyToParties.allPersisted().map { it.second }.asIterable() }
|
override fun getAllIdentities(): Iterable<PartyAndCertificate> = database.transaction {
|
||||||
|
keyToParties.allPersisted().map { it.second }.asIterable()
|
||||||
|
}
|
||||||
|
|
||||||
override fun wellKnownPartyFromX500Name(name: CordaX500Name): Party? = certificateFromCordaX500Name(name)?.party
|
override fun wellKnownPartyFromX500Name(name: CordaX500Name): Party? = certificateFromCordaX500Name(name)?.party
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.corda.node.services.statemachine
|
package net.corda.node.services.statemachine
|
||||||
|
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
|
import net.corda.core.flows.Destination
|
||||||
import net.corda.core.flows.StateMachineRunId
|
import net.corda.core.flows.StateMachineRunId
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.FlowAsyncOperation
|
import net.corda.core.internal.FlowAsyncOperation
|
||||||
@ -19,10 +20,10 @@ sealed class Action {
|
|||||||
data class TrackTransaction(val hash: SecureHash) : Action()
|
data class TrackTransaction(val hash: SecureHash) : Action()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send an initial session message to [party].
|
* Send an initial session message to [destination].
|
||||||
*/
|
*/
|
||||||
data class SendInitial(
|
data class SendInitial(
|
||||||
val party: Party,
|
val destination: Destination,
|
||||||
val initialise: InitialSessionMessage,
|
val initialise: InitialSessionMessage,
|
||||||
val deduplicationId: SenderDeduplicationId
|
val deduplicationId: SenderDeduplicationId
|
||||||
) : Action()
|
) : Action()
|
||||||
|
@ -168,7 +168,7 @@ class ActionExecutorImpl(
|
|||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
private fun executeSendInitial(action: Action.SendInitial) {
|
private fun executeSendInitial(action: Action.SendInitial) {
|
||||||
flowMessaging.sendSessionMessage(action.party, action.initialise, action.deduplicationId)
|
flowMessaging.sendSessionMessage(action.destination, action.initialise, action.deduplicationId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package net.corda.node.services.statemachine
|
package net.corda.node.services.statemachine
|
||||||
|
|
||||||
|
import net.corda.core.flows.Destination
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.FlowIORequest
|
import net.corda.core.internal.FlowIORequest
|
||||||
@ -69,9 +70,8 @@ sealed class Event {
|
|||||||
*
|
*
|
||||||
* Initiate a flow. This causes a new session object to be created and returned to the flow. Note that no actual
|
* Initiate a flow. This causes a new session object to be created and returned to the flow. Note that no actual
|
||||||
* communication takes place at this time, only on the first send/receive operation on the session.
|
* communication takes place at this time, only on the first send/receive operation on the session.
|
||||||
* @param party the [Party] to create a session with.
|
|
||||||
*/
|
*/
|
||||||
data class InitiateFlow(val party: Party) : Event()
|
data class InitiateFlow(val destination: Destination) : Event()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Signal the entering into a subflow.
|
* Signal the entering into a subflow.
|
||||||
|
@ -3,7 +3,9 @@ package net.corda.node.services.statemachine
|
|||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import com.esotericsoftware.kryo.KryoException
|
import com.esotericsoftware.kryo.KryoException
|
||||||
import net.corda.core.context.InvocationOrigin
|
import net.corda.core.context.InvocationOrigin
|
||||||
|
import net.corda.core.flows.Destination
|
||||||
import net.corda.core.flows.FlowException
|
import net.corda.core.flows.FlowException
|
||||||
|
import net.corda.core.identity.AnonymousParty
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.serialization.SerializedBytes
|
import net.corda.core.serialization.SerializedBytes
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
@ -20,11 +22,10 @@ import java.io.NotSerializableException
|
|||||||
*/
|
*/
|
||||||
interface FlowMessaging {
|
interface FlowMessaging {
|
||||||
/**
|
/**
|
||||||
* Send [message] to [party] using [deduplicationId]. Optionally [acknowledgementHandler] may be specified to
|
* Send [message] to [destination] using [deduplicationId].
|
||||||
* listen on the send acknowledgement.
|
|
||||||
*/
|
*/
|
||||||
@Suspendable
|
@Suspendable
|
||||||
fun sendSessionMessage(party: Party, message: SessionMessage, deduplicationId: SenderDeduplicationId)
|
fun sendSessionMessage(destination: Destination, message: SessionMessage, deduplicationId: SenderDeduplicationId)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the messaging using the [onMessage] message handler.
|
* Start the messaging using the [onMessage] message handler.
|
||||||
@ -49,10 +50,20 @@ class FlowMessagingImpl(val serviceHub: ServiceHubInternal): FlowMessaging {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun sendSessionMessage(party: Party, message: SessionMessage, deduplicationId: SenderDeduplicationId) {
|
override fun sendSessionMessage(destination: Destination, message: SessionMessage, deduplicationId: SenderDeduplicationId) {
|
||||||
log.trace { "Sending message $deduplicationId $message to party $party" }
|
val party = if (destination is Party) {
|
||||||
|
log.trace { "Sending message $deduplicationId $message to $destination" }
|
||||||
|
destination
|
||||||
|
} else {
|
||||||
|
// We assume that the destination type has already been checked by initiateFlow
|
||||||
|
val wellKnown = requireNotNull(serviceHub.identityService.wellKnownPartyFromAnonymous(destination as AnonymousParty)) {
|
||||||
|
"We do not know who $destination belongs to"
|
||||||
|
}
|
||||||
|
log.trace { "Sending message $deduplicationId $message to $wellKnown on behalf of $destination" }
|
||||||
|
wellKnown
|
||||||
|
}
|
||||||
val networkMessage = serviceHub.networkService.createMessage(sessionTopic, serializeSessionMessage(message).bytes, deduplicationId, message.additionalHeaders(party))
|
val networkMessage = serviceHub.networkService.createMessage(sessionTopic, serializeSessionMessage(message).bytes, deduplicationId, message.additionalHeaders(party))
|
||||||
val partyInfo = serviceHub.networkMapCache.getPartyInfo(party) ?: throw IllegalArgumentException("Don't know about $party")
|
val partyInfo = requireNotNull(serviceHub.networkMapCache.getPartyInfo(party)) { "Don't know about $party" }
|
||||||
val address = serviceHub.networkService.getAddressOfParty(partyInfo)
|
val address = serviceHub.networkService.getAddressOfParty(partyInfo)
|
||||||
val sequenceKey = when (message) {
|
val sequenceKey = when (message) {
|
||||||
is InitialSessionMessage -> message.initiatorSessionId
|
is InitialSessionMessage -> message.initiatorSessionId
|
||||||
|
@ -2,43 +2,42 @@ package net.corda.node.services.statemachine
|
|||||||
|
|
||||||
import co.paralleluniverse.fibers.Fiber
|
import co.paralleluniverse.fibers.Fiber
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import net.corda.core.flows.Destination
|
||||||
import net.corda.core.flows.FlowInfo
|
import net.corda.core.flows.FlowInfo
|
||||||
import net.corda.core.flows.FlowSession
|
import net.corda.core.flows.FlowSession
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.FlowIORequest
|
import net.corda.core.internal.FlowIORequest
|
||||||
import net.corda.core.internal.FlowStateMachine
|
import net.corda.core.internal.FlowStateMachine
|
||||||
|
import net.corda.core.internal.checkPayloadIs
|
||||||
import net.corda.core.serialization.SerializationDefaults
|
import net.corda.core.serialization.SerializationDefaults
|
||||||
import net.corda.core.serialization.SerializedBytes
|
import net.corda.core.serialization.SerializedBytes
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.core.utilities.NonEmptySet
|
import net.corda.core.utilities.NonEmptySet
|
||||||
import net.corda.core.utilities.UntrustworthyData
|
import net.corda.core.utilities.UntrustworthyData
|
||||||
import net.corda.core.internal.checkPayloadIs
|
|
||||||
|
|
||||||
class FlowSessionImpl(
|
class FlowSessionImpl(
|
||||||
override val counterparty: Party,
|
override val destination: Destination,
|
||||||
val sourceSessionId: SessionId
|
val sourceSessionId: SessionId
|
||||||
) : FlowSession() {
|
) : FlowSession() {
|
||||||
|
|
||||||
override fun toString() = "FlowSessionImpl(counterparty=$counterparty, sourceSessionId=$sourceSessionId)"
|
override val counterparty: Party get() = checkNotNull(destination as? Party) { "$destination is not a Party" }
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun toString(): String = "FlowSessionImpl(destination=$destination, sourceSessionId=$sourceSessionId)"
|
||||||
return (other as? FlowSessionImpl)?.sourceSessionId == sourceSessionId
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode() = sourceSessionId.hashCode()
|
override fun equals(other: Any?): Boolean = other === this || other is FlowSessionImpl && other.sourceSessionId == sourceSessionId
|
||||||
|
|
||||||
private fun getFlowStateMachine(): FlowStateMachine<*> {
|
override fun hashCode(): Int = sourceSessionId.hashCode()
|
||||||
return Fiber.currentFiber() as FlowStateMachine<*>
|
|
||||||
}
|
private val flowStateMachine: FlowStateMachine<*> get() = Fiber.currentFiber() as FlowStateMachine<*>
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun getCounterpartyFlowInfo(maySkipCheckpoint: Boolean): FlowInfo {
|
override fun getCounterpartyFlowInfo(maySkipCheckpoint: Boolean): FlowInfo {
|
||||||
val request = FlowIORequest.GetFlowInfo(NonEmptySet.of(this))
|
val request = FlowIORequest.GetFlowInfo(NonEmptySet.of(this))
|
||||||
return getFlowStateMachine().suspend(request, maySkipCheckpoint)[this]!!
|
return flowStateMachine.suspend(request, maySkipCheckpoint).getValue(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun getCounterpartyFlowInfo() = getCounterpartyFlowInfo(maySkipCheckpoint = false)
|
override fun getCounterpartyFlowInfo(): FlowInfo = getCounterpartyFlowInfo(maySkipCheckpoint = false)
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun <R : Any> sendAndReceive(
|
override fun <R : Any> sendAndReceive(
|
||||||
@ -51,8 +50,8 @@ class FlowSessionImpl(
|
|||||||
sessionToMessage = mapOf(this to payload.serialize(context = SerializationDefaults.P2P_CONTEXT)),
|
sessionToMessage = mapOf(this to payload.serialize(context = SerializationDefaults.P2P_CONTEXT)),
|
||||||
shouldRetrySend = false
|
shouldRetrySend = false
|
||||||
)
|
)
|
||||||
val responseValues: Map<FlowSession, SerializedBytes<Any>> = getFlowStateMachine().suspend(request, maySkipCheckpoint)
|
val responseValues: Map<FlowSession, SerializedBytes<Any>> = flowStateMachine.suspend(request, maySkipCheckpoint)
|
||||||
val responseForCurrentSession = responseValues[this]!!
|
val responseForCurrentSession = responseValues.getValue(this)
|
||||||
|
|
||||||
return responseForCurrentSession.checkPayloadIs(receiveType)
|
return responseForCurrentSession.checkPayloadIs(receiveType)
|
||||||
}
|
}
|
||||||
@ -64,7 +63,7 @@ class FlowSessionImpl(
|
|||||||
override fun <R : Any> receive(receiveType: Class<R>, maySkipCheckpoint: Boolean): UntrustworthyData<R> {
|
override fun <R : Any> receive(receiveType: Class<R>, maySkipCheckpoint: Boolean): UntrustworthyData<R> {
|
||||||
enforceNotPrimitive(receiveType)
|
enforceNotPrimitive(receiveType)
|
||||||
val request = FlowIORequest.Receive(NonEmptySet.of(this))
|
val request = FlowIORequest.Receive(NonEmptySet.of(this))
|
||||||
return getFlowStateMachine().suspend(request, maySkipCheckpoint)[this]!!.checkPayloadIs(receiveType)
|
return flowStateMachine.suspend(request, maySkipCheckpoint).getValue(this).checkPayloadIs(receiveType)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
@ -75,7 +74,7 @@ class FlowSessionImpl(
|
|||||||
val request = FlowIORequest.Send(
|
val request = FlowIORequest.Send(
|
||||||
sessionToMessage = mapOf(this to payload.serialize(context = SerializationDefaults.P2P_CONTEXT))
|
sessionToMessage = mapOf(this to payload.serialize(context = SerializationDefaults.P2P_CONTEXT))
|
||||||
)
|
)
|
||||||
return getFlowStateMachine().suspend(request, maySkipCheckpoint)
|
return flowStateMachine.suspend(request, maySkipCheckpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
|
@ -10,6 +10,7 @@ import net.corda.core.concurrent.CordaFuture
|
|||||||
import net.corda.core.context.InvocationContext
|
import net.corda.core.context.InvocationContext
|
||||||
import net.corda.core.cordapp.Cordapp
|
import net.corda.core.cordapp.Cordapp
|
||||||
import net.corda.core.flows.*
|
import net.corda.core.flows.*
|
||||||
|
import net.corda.core.identity.AnonymousParty
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.*
|
import net.corda.core.internal.*
|
||||||
import net.corda.core.serialization.internal.CheckpointSerializationContext
|
import net.corda.core.serialization.internal.CheckpointSerializationContext
|
||||||
@ -230,7 +231,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
|||||||
"Transaction context is missing. This might happen if a suspendable method is not annotated with @Suspendable annotation."
|
"Transaction context is missing. This might happen if a suspendable method is not annotated with @Suspendable annotation."
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
require(contextTransactionOrNull == null){"Transaction is marked as not present, but is not null"}
|
require(contextTransactionOrNull == null) { "Transaction is marked as not present, but is not null" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -354,9 +355,10 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun initiateFlow(party: Party): FlowSession {
|
override fun initiateFlow(destination: Destination): FlowSession {
|
||||||
|
require(destination is Party || destination is AnonymousParty) { "Unsupported destination type ${destination.javaClass.name}" }
|
||||||
val resume = processEventImmediately(
|
val resume = processEventImmediately(
|
||||||
Event.InitiateFlow(party),
|
Event.InitiateFlow(destination),
|
||||||
isDbTransactionOpenOnEntry = true,
|
isDbTransactionOpenOnEntry = true,
|
||||||
isDbTransactionOpenOnExit = true
|
isDbTransactionOpenOnExit = true
|
||||||
) as FlowContinuation.Resume
|
) as FlowContinuation.Resume
|
||||||
@ -440,7 +442,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
|||||||
isDbTransactionOpenOnEntry = true,
|
isDbTransactionOpenOnEntry = true,
|
||||||
isDbTransactionOpenOnExit = false
|
isDbTransactionOpenOnExit = false
|
||||||
)
|
)
|
||||||
require(continuation == FlowContinuation.ProcessEvents){"Expected a continuation of type ${FlowContinuation.ProcessEvents}, found $continuation "}
|
require(continuation == FlowContinuation.ProcessEvents) { "Expected a continuation of type ${FlowContinuation.ProcessEvents}, found $continuation " }
|
||||||
unpark(SERIALIZER_BLOCKER)
|
unpark(SERIALIZER_BLOCKER)
|
||||||
}
|
}
|
||||||
return uncheckedCast(processEventsUntilFlowIsResumed(
|
return uncheckedCast(processEventsUntilFlowIsResumed(
|
||||||
|
@ -2,6 +2,7 @@ package net.corda.node.services.statemachine
|
|||||||
|
|
||||||
import net.corda.core.context.InvocationContext
|
import net.corda.core.context.InvocationContext
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
|
import net.corda.core.flows.Destination
|
||||||
import net.corda.core.flows.FlowInfo
|
import net.corda.core.flows.FlowInfo
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
@ -98,7 +99,7 @@ sealed class SessionState {
|
|||||||
* We haven't yet sent the initialisation message
|
* We haven't yet sent the initialisation message
|
||||||
*/
|
*/
|
||||||
data class Uninitiated(
|
data class Uninitiated(
|
||||||
val party: Party,
|
val destination: Destination,
|
||||||
val initiatingSubFlow: SubFlow.Initiating,
|
val initiatingSubFlow: SubFlow.Initiating,
|
||||||
val sourceSessionId: SessionId,
|
val sourceSessionId: SessionId,
|
||||||
val additionalEntropy: Long
|
val additionalEntropy: Long
|
||||||
|
@ -222,7 +222,7 @@ class StartedFlowTransition(
|
|||||||
deduplicationSeed = sessionState.deduplicationSeed
|
deduplicationSeed = sessionState.deduplicationSeed
|
||||||
)
|
)
|
||||||
val deduplicationId = DeduplicationId.createForNormal(checkpoint, index++, newSessionState)
|
val deduplicationId = DeduplicationId.createForNormal(checkpoint, index++, newSessionState)
|
||||||
actions.add(Action.SendInitial(sessionState.party, initialMessage, SenderDeduplicationId(deduplicationId, startingState.senderUUID)))
|
actions.add(Action.SendInitial(sessionState.destination, initialMessage, SenderDeduplicationId(deduplicationId, startingState.senderUUID)))
|
||||||
newSessions[sourceSessionId] = newSessionState
|
newSessions[sourceSessionId] = newSessionState
|
||||||
}
|
}
|
||||||
currentState = currentState.copy(checkpoint = checkpoint.copy(sessions = newSessions))
|
currentState = currentState.copy(checkpoint = checkpoint.copy(sessions = newSessions))
|
||||||
@ -256,7 +256,7 @@ class StartedFlowTransition(
|
|||||||
when (existingSessionState) {
|
when (existingSessionState) {
|
||||||
is SessionState.Uninitiated -> {
|
is SessionState.Uninitiated -> {
|
||||||
val initialMessage = createInitialSessionMessage(existingSessionState.initiatingSubFlow, sourceSessionId, existingSessionState.additionalEntropy, message)
|
val initialMessage = createInitialSessionMessage(existingSessionState.initiatingSubFlow, sourceSessionId, existingSessionState.additionalEntropy, message)
|
||||||
actions.add(Action.SendInitial(existingSessionState.party, initialMessage, SenderDeduplicationId(deduplicationId, startingState.senderUUID)))
|
actions.add(Action.SendInitial(existingSessionState.destination, initialMessage, SenderDeduplicationId(deduplicationId, startingState.senderUUID)))
|
||||||
newSessions[sourceSessionId] = SessionState.Initiating(
|
newSessions[sourceSessionId] = SessionState.Initiating(
|
||||||
bufferedMessages = emptyList(),
|
bufferedMessages = emptyList(),
|
||||||
rejectionError = null,
|
rejectionError = null,
|
||||||
|
@ -235,8 +235,8 @@ class TopLevelTransition(
|
|||||||
return@builder FlowContinuation.ProcessEvents
|
return@builder FlowContinuation.ProcessEvents
|
||||||
}
|
}
|
||||||
val sourceSessionId = SessionId.createRandom(context.secureRandom)
|
val sourceSessionId = SessionId.createRandom(context.secureRandom)
|
||||||
val sessionImpl = FlowSessionImpl(event.party, sourceSessionId)
|
val sessionImpl = FlowSessionImpl(event.destination, sourceSessionId)
|
||||||
val newSessions = checkpoint.sessions + (sourceSessionId to SessionState.Uninitiated(event.party, initiatingSubFlow, sourceSessionId, context.secureRandom.nextLong()))
|
val newSessions = checkpoint.sessions + (sourceSessionId to SessionState.Uninitiated(event.destination, initiatingSubFlow, sourceSessionId, context.secureRandom.nextLong()))
|
||||||
currentState = currentState.copy(checkpoint = checkpoint.copy(sessions = newSessions))
|
currentState = currentState.copy(checkpoint = checkpoint.copy(sessions = newSessions))
|
||||||
actions.add(Action.AddSessionBinding(context.id, sourceSessionId))
|
actions.add(Action.AddSessionBinding(context.id, sourceSessionId))
|
||||||
FlowContinuation.Resume(sessionImpl)
|
FlowContinuation.Resume(sessionImpl)
|
||||||
|
@ -36,8 +36,7 @@ import net.corda.testing.internal.LogHelper
|
|||||||
import net.corda.testing.node.InMemoryMessagingNetwork.MessageTransfer
|
import net.corda.testing.node.InMemoryMessagingNetwork.MessageTransfer
|
||||||
import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin
|
import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin
|
||||||
import net.corda.testing.node.internal.*
|
import net.corda.testing.node.internal.*
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.*
|
||||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
|
||||||
import org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType
|
import org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType
|
||||||
import org.assertj.core.api.Condition
|
import org.assertj.core.api.Condition
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
@ -463,6 +462,28 @@ class FlowFrameworkTests {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `initiating flow using unknown AnonymousParty`() {
|
||||||
|
val anonymousBob = bobNode.services.keyManagementService.freshKeyAndCert(bobNode.info.legalIdentitiesAndCerts.single(), false).party.anonymise()
|
||||||
|
bobNode.registerCordappFlowFactory(SendAndReceiveFlow::class) { SingleInlinedSubFlow(it) }
|
||||||
|
val result = aliceNode.services.startFlow(SendAndReceiveFlow(anonymousBob, "Hello")).resultFuture
|
||||||
|
mockNet.runNetwork()
|
||||||
|
assertThatIllegalArgumentException()
|
||||||
|
.isThrownBy { result.getOrThrow() }
|
||||||
|
.withMessage("We do not know who $anonymousBob belongs to")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `initiating flow using known AnonymousParty`() {
|
||||||
|
val anonymousBob = bobNode.services.keyManagementService.freshKeyAndCert(bobNode.info.legalIdentitiesAndCerts.single(), false)
|
||||||
|
aliceNode.services.identityService.verifyAndRegisterIdentity(anonymousBob)
|
||||||
|
val bobResponderFlow = bobNode.registerCordappFlowFactory(SendAndReceiveFlow::class) { SingleInlinedSubFlow(it) }
|
||||||
|
val result = aliceNode.services.startFlow(SendAndReceiveFlow(anonymousBob.party.anonymise(), "Hello")).resultFuture
|
||||||
|
mockNet.runNetwork()
|
||||||
|
bobResponderFlow.getOrThrow()
|
||||||
|
assertThat(result.getOrThrow()).isEqualTo("HelloHello")
|
||||||
|
}
|
||||||
|
|
||||||
//region Helpers
|
//region Helpers
|
||||||
|
|
||||||
private val normalEnd = ExistingSessionMessage(SessionId(0), EndSessionMessage) // NormalSessionEnd(0)
|
private val normalEnd = ExistingSessionMessage(SessionId(0), EndSessionMessage) // NormalSessionEnd(0)
|
||||||
@ -762,12 +783,12 @@ internal class MyFlowException(override val message: String) : FlowException() {
|
|||||||
internal class MyPeerFlowException(override val message: String, val peer: Party) : FlowException()
|
internal class MyPeerFlowException(override val message: String, val peer: Party) : FlowException()
|
||||||
|
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
internal class SendAndReceiveFlow(private val otherParty: Party, private val payload: Any, private val otherPartySession: FlowSession? = null) : FlowLogic<Any>() {
|
internal class SendAndReceiveFlow(private val destination: Destination, private val payload: Any, private val otherPartySession: FlowSession? = null) : FlowLogic<Any>() {
|
||||||
constructor(otherPartySession: FlowSession, payload: Any) : this(otherPartySession.counterparty, payload, otherPartySession)
|
constructor(otherPartySession: FlowSession, payload: Any) : this(otherPartySession.counterparty, payload, otherPartySession)
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call(): Any {
|
override fun call(): Any {
|
||||||
return (otherPartySession ?: initiateFlow(otherParty)).sendAndReceive<Any>(payload).unwrap { it }
|
return (otherPartySession ?: initiateFlow(destination)).sendAndReceive<Any>(payload).unwrap { it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,7 +142,8 @@ class RetryFlowMockTest {
|
|||||||
val alice = TestIdentity(CordaX500Name.parse("L=London,O=Alice Ltd,OU=Trade,C=GB")).party
|
val alice = TestIdentity(CordaX500Name.parse("L=London,O=Alice Ltd,OU=Trade,C=GB")).party
|
||||||
val records = nodeA.smm.flowHospital.track().updates.toBlocking().toIterable().iterator()
|
val records = nodeA.smm.flowHospital.track().updates.toBlocking().toIterable().iterator()
|
||||||
val flow: FlowStateMachine<Unit> = nodeA.services.startFlow(FinalityHandler(object : FlowSession() {
|
val flow: FlowStateMachine<Unit> = nodeA.services.startFlow(FinalityHandler(object : FlowSession() {
|
||||||
override val counterparty = alice
|
override val destination: Destination get() = alice
|
||||||
|
override val counterparty: Party get() = alice
|
||||||
|
|
||||||
override fun getCounterpartyFlowInfo(maySkipCheckpoint: Boolean): FlowInfo {
|
override fun getCounterpartyFlowInfo(maySkipCheckpoint: Boolean): FlowInfo {
|
||||||
TODO("not implemented")
|
TODO("not implemented")
|
||||||
|
Loading…
Reference in New Issue
Block a user