Back porting clean up of FlowFrameworkTests.kt made in ENT (#4218)

This commit is contained in:
Shams Asari 2018-11-12 18:38:47 +00:00 committed by GitHub
parent 369f23e306
commit 1c012f6403
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 488 additions and 446 deletions

View File

@ -9,9 +9,9 @@ import net.corda.confidential.SwapIdentitiesHandler
import net.corda.core.CordaException import net.corda.core.CordaException
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.crypto.internal.AliasPrivateKey
import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.internal.AliasPrivateKey
import net.corda.core.crypto.newSecureRandom import net.corda.core.crypto.newSecureRandom
import net.corda.core.flows.* import net.corda.core.flows.*
import net.corda.core.identity.AbstractParty import net.corda.core.identity.AbstractParty
@ -122,14 +122,14 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
cacheFactoryPrototype: BindableNamedCacheFactory, cacheFactoryPrototype: BindableNamedCacheFactory,
protected val versionInfo: VersionInfo, protected val versionInfo: VersionInfo,
protected val flowManager: FlowManager, protected val flowManager: FlowManager,
protected val serverThread: AffinityExecutor.ServiceAffinityExecutor, val serverThread: AffinityExecutor.ServiceAffinityExecutor,
private val busyNodeLatch: ReusableLatch = ReusableLatch()) : SingletonSerializeAsToken() { val busyNodeLatch: ReusableLatch = ReusableLatch()) : SingletonSerializeAsToken() {
protected abstract val log: Logger protected abstract val log: Logger
@Suppress("LeakingThis") @Suppress("LeakingThis")
private var tokenizableServices: MutableList<Any>? = mutableListOf(platformClock, this) private var tokenizableServices: MutableList<Any>? = mutableListOf(platformClock, this)
protected val metricRegistry = MetricRegistry() val metricRegistry = MetricRegistry()
protected val cacheFactory = cacheFactoryPrototype.bindWithConfig(configuration).bindWithMetrics(metricRegistry).tokenize() protected val cacheFactory = cacheFactoryPrototype.bindWithConfig(configuration).bindWithMetrics(metricRegistry).tokenize()
val monitoringService = MonitoringService(metricRegistry).tokenize() val monitoringService = MonitoringService(metricRegistry).tokenize()
@ -146,7 +146,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
} }
} }
protected val cordappLoader: CordappLoader = makeCordappLoader(configuration, versionInfo) val cordappLoader: CordappLoader = makeCordappLoader(configuration, versionInfo)
val schemaService = NodeSchemaService(cordappLoader.cordappSchemas).tokenize() val schemaService = NodeSchemaService(cordappLoader.cordappSchemas).tokenize()
val identityService = PersistentIdentityService(cacheFactory).tokenize() val identityService = PersistentIdentityService(cacheFactory).tokenize()
val database: CordaPersistence = createCordaPersistence( val database: CordaPersistence = createCordaPersistence(
@ -1008,7 +1008,6 @@ class FlowStarterImpl(private val smm: StateMachineManager, private val flowLogi
private val _future = openFuture<FlowStateMachine<T>>() private val _future = openFuture<FlowStateMachine<T>>()
override val future: CordaFuture<FlowStateMachine<T>> override val future: CordaFuture<FlowStateMachine<T>>
get() = _future get() = _future
} }
return startFlow(startFlowEvent) return startFlow(startFlowEvent)
} }

View File

@ -0,0 +1,166 @@
package net.corda.node.services.statemachine
import net.corda.core.crypto.random63BitValue
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.registerCordappFlowFactory
import net.corda.core.identity.Party
import net.corda.core.utilities.getOrThrow
import net.corda.node.services.persistence.checkpoints
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.internal.LogHelper
import net.corda.testing.node.InMemoryMessagingNetwork
import net.corda.testing.node.internal.*
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import rx.Observable
import java.util.*
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class FlowFrameworkPersistenceTests {
companion object {
init {
LogHelper.setLevel("+net.corda.flow")
}
}
private lateinit var mockNet: InternalMockNetwork
private val receivedSessionMessages = ArrayList<SessionTransfer>()
private lateinit var aliceNode: TestStartedNode
private lateinit var bobNode: TestStartedNode
private lateinit var notaryIdentity: Party
private lateinit var alice: Party
private lateinit var bob: Party
private lateinit var aliceFlowManager: MockNodeFlowManager
private lateinit var bobFlowManager: MockNodeFlowManager
@Before
fun start() {
mockNet = InternalMockNetwork(
cordappsForAllNodes = cordappsForPackages("net.corda.finance.contracts", "net.corda.testing.contracts"),
servicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin()
)
aliceFlowManager = MockNodeFlowManager()
bobFlowManager = MockNodeFlowManager()
aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME, flowManager = aliceFlowManager))
bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME, flowManager = bobFlowManager))
receivedSessionMessagesObservable().forEach { receivedSessionMessages += it }
// Extract identities
alice = aliceNode.info.singleIdentity()
bob = bobNode.info.singleIdentity()
notaryIdentity = mockNet.defaultNotaryIdentity
}
@After
fun cleanUp() {
mockNet.stopNodes()
receivedSessionMessages.clear()
}
@Test
fun `newly added flow is preserved on restart`() {
aliceNode.services.startFlow(NoOpFlow(nonTerminating = true))
aliceNode.internals.acceptableLiveFiberCountOnStop = 1
val restoredFlow = aliceNode.restartAndGetRestoredFlow<NoOpFlow>()
assertThat(restoredFlow.flowStarted).isTrue()
}
@Test
fun `flow restarted just after receiving payload`() {
bobNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it)
.nonTerminating() }
aliceNode.services.startFlow(SendFlow("Hello", bob))
// We push through just enough messages to get only the payload sent
bobNode.pumpReceive()
bobNode.internals.disableDBCloseOnStop()
bobNode.internals.acceptableLiveFiberCountOnStop = 1
bobNode.dispose()
mockNet.runNetwork()
val restoredFlow = bobNode.restartAndGetRestoredFlow<InitiatedReceiveFlow>()
assertThat(restoredFlow.receivedPayloads[0]).isEqualTo("Hello")
}
@Test
fun `flow loaded from checkpoint will respond to messages from before start`() {
aliceNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) }
bobNode.services.startFlow(ReceiveFlow(alice).nonTerminating()) // Prepare checkpointed receive flow
val restoredFlow = bobNode.restartAndGetRestoredFlow<ReceiveFlow>()
assertThat(restoredFlow.receivedPayloads[0]).isEqualTo("Hello")
}
@Ignore("Some changes in startup order make this test's assumptions fail.")
@Test
fun `flow with send will resend on interrupted restart`() {
val payload = random63BitValue()
val payload2 = random63BitValue()
var sentCount = 0
mockNet.messagingNetwork.sentMessages.toSessionTransfers().filter { it.isPayloadTransfer }.forEach { sentCount++ }
val charlieNode = mockNet.createNode(InternalMockNodeParameters(legalName = CHARLIE_NAME))
val secondFlow = charlieNode.registerCordappFlowFactory(PingPongFlow::class) { PingPongFlow(it, payload2) }
mockNet.runNetwork()
val charlie = charlieNode.info.singleIdentity()
// Kick off first send and receive
bobNode.services.startFlow(PingPongFlow(charlie, payload))
bobNode.database.transaction {
assertEquals(1, bobNode.internals.checkpointStorage.checkpoints().size)
}
// Make sure the add() has finished initial processing.
bobNode.internals.disableDBCloseOnStop()
// Restart node and thus reload the checkpoint and resend the message with same UUID
bobNode.dispose()
bobNode.database.transaction {
assertEquals(1, bobNode.internals.checkpointStorage.checkpoints().size) // confirm checkpoint
bobNode.services.networkMapCache.clearNetworkMapCache()
}
val node2b = mockNet.createNode(InternalMockNodeParameters(bobNode.internals.id))
bobNode.internals.manuallyCloseDB()
val (firstAgain, fut1) = node2b.getSingleFlow<PingPongFlow>()
// Run the network which will also fire up the second flow. First message should get deduped. So message data stays in sync.
mockNet.runNetwork()
fut1.getOrThrow()
val receivedCount = receivedSessionMessages.count { it.isPayloadTransfer }
// Check flows completed cleanly and didn't get out of phase
assertEquals(4, receivedCount, "Flow should have exchanged 4 unique messages")// Two messages each way
// can't give a precise value as every addMessageHandler re-runs the undelivered messages
assertTrue(sentCount > receivedCount, "Node restart should have retransmitted messages")
node2b.database.transaction {
assertEquals(0, node2b.internals.checkpointStorage.checkpoints().size, "Checkpoints left after restored flow should have ended")
}
charlieNode.database.transaction {
assertEquals(0, charlieNode.internals.checkpointStorage.checkpoints().size, "Checkpoints left after restored flow should have ended")
}
assertEquals(payload2, firstAgain.receivedPayload, "Received payload does not match the first value on Node 3")
assertEquals(payload2 + 1, firstAgain.receivedPayload2, "Received payload does not match the expected second value on Node 3")
assertEquals(payload, secondFlow.getOrThrow().receivedPayload, "Received payload does not match the (restarted) first value on Node 2")
assertEquals(payload + 1, secondFlow.getOrThrow().receivedPayload2, "Received payload does not match the expected second value on Node 2")
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////
//region Helpers
private inline fun <reified P : FlowLogic<*>> TestStartedNode.restartAndGetRestoredFlow(): P {
val newNode = mockNet.restartNode(this)
newNode.internals.acceptableLiveFiberCountOnStop = 1
mockNet.runNetwork()
return newNode.getSingleFlow<P>().first
}
private fun receivedSessionMessagesObservable(): Observable<SessionTransfer> {
return mockNet.messagingNetwork.receivedMessages.toSessionTransfers()
}
//endregion Helpers
}

View File

@ -40,16 +40,13 @@ import org.assertj.core.api.Assertions.assertThatThrownBy
import org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType import org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Ignore
import org.junit.Test import org.junit.Test
import rx.Notification import rx.Notification
import rx.Observable import rx.Observable
import java.time.Instant import java.time.Instant
import java.util.* import java.util.*
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
class FlowFrameworkTests { class FlowFrameworkTests {
companion object { companion object {
@ -449,373 +446,10 @@ class FlowFrameworkTests {
private val normalEnd = ExistingSessionMessage(SessionId(0), EndSessionMessage) // NormalSessionEnd(0) private val normalEnd = ExistingSessionMessage(SessionId(0), EndSessionMessage) // NormalSessionEnd(0)
private fun TestStartedNode.sendSessionMessage(message: SessionMessage, destination: Party) {
services.networkService.apply {
val address = getAddressOfParty(PartyInfo.SingleNode(destination, emptyList()))
send(createMessage(FlowMessagingImpl.sessionTopic, message.serialize().bytes), address)
}
}
private fun assertSessionTransfers(vararg expected: SessionTransfer) { private fun assertSessionTransfers(vararg expected: SessionTransfer) {
assertThat(receivedSessionMessages).containsExactly(*expected) assertThat(receivedSessionMessages).containsExactly(*expected)
} }
//endregion Helpers
}
class FlowFrameworkTripartyTests {
companion object {
init {
LogHelper.setLevel("+net.corda.flow")
}
private lateinit var mockNet: InternalMockNetwork
private lateinit var aliceNode: TestStartedNode
private lateinit var bobNode: TestStartedNode
private lateinit var charlieNode: TestStartedNode
private lateinit var alice: Party
private lateinit var bob: Party
private lateinit var charlie: Party
private lateinit var notaryIdentity: Party
private val receivedSessionMessages = ArrayList<SessionTransfer>()
}
@Before
fun setUpGlobalMockNet() {
mockNet = InternalMockNetwork(
cordappsForAllNodes = cordappsForPackages("net.corda.finance.contracts", "net.corda.testing.contracts"),
servicePeerAllocationStrategy = RoundRobin()
)
aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME))
bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME))
charlieNode = mockNet.createNode(InternalMockNodeParameters(legalName = CHARLIE_NAME))
// Extract identities
alice = aliceNode.info.singleIdentity()
bob = bobNode.info.singleIdentity()
charlie = charlieNode.info.singleIdentity()
notaryIdentity = mockNet.defaultNotaryIdentity
receivedSessionMessagesObservable().forEach { receivedSessionMessages += it }
}
@After
fun cleanUp() {
mockNet.stopNodes()
receivedSessionMessages.clear()
}
private fun receivedSessionMessagesObservable(): Observable<SessionTransfer> {
return mockNet.messagingNetwork.receivedMessages.toSessionTransfers()
}
@Test
fun `sending to multiple parties`() {
bobNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() }
charlieNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() }
val payload = "Hello World"
aliceNode.services.startFlow(SendFlow(payload, bob, charlie))
mockNet.runNetwork()
bobNode.internals.acceptableLiveFiberCountOnStop = 1
charlieNode.internals.acceptableLiveFiberCountOnStop = 1
val bobFlow = bobNode.getSingleFlow<InitiatedReceiveFlow>().first
val charlieFlow = charlieNode.getSingleFlow<InitiatedReceiveFlow>().first
assertThat(bobFlow.receivedPayloads[0]).isEqualTo(payload)
assertThat(charlieFlow.receivedPayloads[0]).isEqualTo(payload)
assertSessionTransfers(bobNode,
aliceNode sent sessionInit(SendFlow::class, payload = payload) to bobNode,
bobNode sent sessionConfirm() to aliceNode,
aliceNode sent normalEnd to bobNode
//There's no session end from the other flows as they're manually suspended
)
assertSessionTransfers(charlieNode,
aliceNode sent sessionInit(SendFlow::class, payload = payload) to charlieNode,
charlieNode sent sessionConfirm() to aliceNode,
aliceNode sent normalEnd to charlieNode
//There's no session end from the other flows as they're manually suspended
)
}
@Test
fun `receiving from multiple parties`() {
val bobPayload = "Test 1"
val charliePayload = "Test 2"
bobNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(bobPayload, it) }
charlieNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(charliePayload, it) }
val multiReceiveFlow = ReceiveFlow(bob, charlie).nonTerminating()
aliceNode.services.startFlow(multiReceiveFlow)
aliceNode.internals.acceptableLiveFiberCountOnStop = 1
mockNet.runNetwork()
assertThat(multiReceiveFlow.receivedPayloads[0]).isEqualTo(bobPayload)
assertThat(multiReceiveFlow.receivedPayloads[1]).isEqualTo(charliePayload)
assertSessionTransfers(bobNode,
aliceNode sent sessionInit(ReceiveFlow::class) to bobNode,
bobNode sent sessionConfirm() to aliceNode,
bobNode sent sessionData(bobPayload) to aliceNode,
bobNode sent normalEnd to aliceNode
)
assertSessionTransfers(charlieNode,
aliceNode sent sessionInit(ReceiveFlow::class) to charlieNode,
charlieNode sent sessionConfirm() to aliceNode,
charlieNode sent sessionData(charliePayload) to aliceNode,
charlieNode sent normalEnd to aliceNode
)
}
@Test
fun `FlowException only propagated to parent`() {
charlieNode.registerCordappFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Chain") } }
bobNode.registerCordappFlowFactory(ReceiveFlow::class) { ReceiveFlow(charlie) }
val receivingFiber = aliceNode.services.startFlow(ReceiveFlow(bob))
mockNet.runNetwork()
assertThatExceptionOfType(UnexpectedFlowEndException::class.java)
.isThrownBy { receivingFiber.resultFuture.getOrThrow() }
}
@Test
fun `FlowException thrown and there is a 3rd unrelated party flow`() {
// Bob will send its payload and then block waiting for the receive from Alice. Meanwhile Alice will move
// onto Charlie which will throw the exception
val node2Fiber = bobNode
.registerCordappFlowFactory(ReceiveFlow::class) { SendAndReceiveFlow(it, "Hello") }
.map { it.stateMachine }
charlieNode.registerCordappFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Nothing useful") } }
val aliceFiber = aliceNode.services.startFlow(ReceiveFlow(bob, charlie)) as FlowStateMachineImpl
mockNet.runNetwork()
// Alice will terminate with the error it received from Charlie but it won't propagate that to Bob (as it's
// not relevant to it) but it will end its session with it
assertThatExceptionOfType(MyFlowException::class.java).isThrownBy {
aliceFiber.resultFuture.getOrThrow()
}
val bobResultFuture = node2Fiber.getOrThrow().resultFuture
assertThatExceptionOfType(UnexpectedFlowEndException::class.java).isThrownBy {
bobResultFuture.getOrThrow()
}
assertSessionTransfers(bobNode,
aliceNode sent sessionInit(ReceiveFlow::class) to bobNode,
bobNode sent sessionConfirm() to aliceNode,
bobNode sent sessionData("Hello") to aliceNode,
aliceNode sent errorMessage() to bobNode
)
}
private val normalEnd = ExistingSessionMessage(SessionId(0), EndSessionMessage) // NormalSessionEnd(0)
private fun assertSessionTransfers(node: TestStartedNode, vararg expected: SessionTransfer): List<SessionTransfer> {
val actualForNode = receivedSessionMessages.filter { it.from == node.internals.id || it.to == node.network.myAddress }
assertThat(actualForNode).containsExactly(*expected)
return actualForNode
}
}
class FlowFrameworkPersistenceTests {
companion object {
init {
LogHelper.setLevel("+net.corda.flow")
}
}
private lateinit var mockNet: InternalMockNetwork
private val receivedSessionMessages = ArrayList<SessionTransfer>()
private lateinit var aliceNode: TestStartedNode
private lateinit var bobNode: TestStartedNode
private lateinit var notaryIdentity: Party
private lateinit var alice: Party
private lateinit var bob: Party
private lateinit var aliceFlowManager: MockNodeFlowManager
private lateinit var bobFlowManager: MockNodeFlowManager
@Before
fun start() {
mockNet = InternalMockNetwork(
cordappsForAllNodes = cordappsForPackages("net.corda.finance.contracts", "net.corda.testing.contracts"),
servicePeerAllocationStrategy = RoundRobin()
)
aliceFlowManager = MockNodeFlowManager()
bobFlowManager = MockNodeFlowManager()
aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME, flowManager = aliceFlowManager))
bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME, flowManager = bobFlowManager))
receivedSessionMessagesObservable().forEach { receivedSessionMessages += it }
// Extract identities
alice = aliceNode.info.singleIdentity()
bob = bobNode.info.singleIdentity()
notaryIdentity = mockNet.defaultNotaryIdentity
}
@After
fun cleanUp() {
mockNet.stopNodes()
receivedSessionMessages.clear()
}
@Test
fun `newly added flow is preserved on restart`() {
aliceNode.services.startFlow(NoOpFlow(nonTerminating = true))
aliceNode.internals.acceptableLiveFiberCountOnStop = 1
val restoredFlow = aliceNode.restartAndGetRestoredFlow<NoOpFlow>()
assertThat(restoredFlow.flowStarted).isTrue()
}
@Test
fun `flow restarted just after receiving payload`() {
bobNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() }
aliceNode.services.startFlow(SendFlow("Hello", bob))
// We push through just enough messages to get only the payload sent
bobNode.pumpReceive()
bobNode.internals.disableDBCloseOnStop()
bobNode.internals.acceptableLiveFiberCountOnStop = 1
bobNode.dispose()
mockNet.runNetwork()
val restoredFlow = bobNode.restartAndGetRestoredFlow<InitiatedReceiveFlow>()
assertThat(restoredFlow.receivedPayloads[0]).isEqualTo("Hello")
}
@Test
fun `flow loaded from checkpoint will respond to messages from before start`() {
aliceNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) }
bobNode.services.startFlow(ReceiveFlow(alice).nonTerminating()) // Prepare checkpointed receive flow
val restoredFlow = bobNode.restartAndGetRestoredFlow<ReceiveFlow>()
assertThat(restoredFlow.receivedPayloads[0]).isEqualTo("Hello")
}
@Ignore("Some changes in startup order make this test's assumptions fail.")
@Test
fun `flow with send will resend on interrupted restart`() {
val payload = random63BitValue()
val payload2 = random63BitValue()
var sentCount = 0
mockNet.messagingNetwork.sentMessages.toSessionTransfers().filter { it.isPayloadTransfer }.forEach { sentCount++ }
val charlieNode = mockNet.createNode(InternalMockNodeParameters(legalName = CHARLIE_NAME))
val secondFlow = charlieNode.registerCordappFlowFactory(PingPongFlow::class) { PingPongFlow(it, payload2) }
mockNet.runNetwork()
val charlie = charlieNode.info.singleIdentity()
// Kick off first send and receive
bobNode.services.startFlow(PingPongFlow(charlie, payload))
bobNode.database.transaction {
assertEquals(1, bobNode.internals.checkpointStorage.checkpoints().size)
}
// Make sure the add() has finished initial processing.
bobNode.internals.disableDBCloseOnStop()
// Restart node and thus reload the checkpoint and resend the message with same UUID
bobNode.dispose()
bobNode.database.transaction {
assertEquals(1, bobNode.internals.checkpointStorage.checkpoints().size) // confirm checkpoint
bobNode.services.networkMapCache.clearNetworkMapCache()
}
val node2b = mockNet.createNode(InternalMockNodeParameters(bobNode.internals.id))
bobNode.internals.manuallyCloseDB()
val (firstAgain, fut1) = node2b.getSingleFlow<PingPongFlow>()
// Run the network which will also fire up the second flow. First message should get deduped. So message data stays in sync.
mockNet.runNetwork()
fut1.getOrThrow()
val receivedCount = receivedSessionMessages.count { it.isPayloadTransfer }
// Check flows completed cleanly and didn't get out of phase
assertEquals(4, receivedCount, "Flow should have exchanged 4 unique messages")// Two messages each way
// can't give a precise value as every addMessageHandler re-runs the undelivered messages
assertTrue(sentCount > receivedCount, "Node restart should have retransmitted messages")
node2b.database.transaction {
assertEquals(0, node2b.internals.checkpointStorage.checkpoints().size, "Checkpoints left after restored flow should have ended")
}
charlieNode.database.transaction {
assertEquals(0, charlieNode.internals.checkpointStorage.checkpoints().size, "Checkpoints left after restored flow should have ended")
}
assertEquals(payload2, firstAgain.receivedPayload, "Received payload does not match the first value on Node 3")
assertEquals(payload2 + 1, firstAgain.receivedPayload2, "Received payload does not match the expected second value on Node 3")
assertEquals(payload, secondFlow.getOrThrow().receivedPayload, "Received payload does not match the (restarted) first value on Node 2")
assertEquals(payload + 1, secondFlow.getOrThrow().receivedPayload2, "Received payload does not match the expected second value on Node 2")
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////
//region Helpers
private inline fun <reified P : FlowLogic<*>> TestStartedNode.restartAndGetRestoredFlow(): P {
val newNode = mockNet.restartNode(this)
newNode.internals.acceptableLiveFiberCountOnStop = 1
mockNet.runNetwork()
return newNode.getSingleFlow<P>().first
}
private fun receivedSessionMessagesObservable(): Observable<SessionTransfer> {
return mockNet.messagingNetwork.receivedMessages.toSessionTransfers()
}
//endregion Helpers
}
private fun sessionConfirm(flowVersion: Int = 1) = ExistingSessionMessage(SessionId(0), ConfirmSessionMessage(SessionId(0), FlowInfo(flowVersion, "")))
private inline fun <reified P : FlowLogic<*>> TestStartedNode.getSingleFlow(): Pair<P, CordaFuture<*>> {
return smm.findStateMachines(P::class.java).single()
}
private fun sanitise(message: SessionMessage) = when (message) {
is InitialSessionMessage -> message.copy(initiatorSessionId = SessionId(0), initiationEntropy = 0, appName = "")
is ExistingSessionMessage -> {
val payload = message.payload
message.copy(
recipientSessionId = SessionId(0),
payload = when (payload) {
is ConfirmSessionMessage -> payload.copy(
initiatedSessionId = SessionId(0),
initiatedFlowInfo = payload.initiatedFlowInfo.copy(appName = "")
)
is ErrorSessionMessage -> payload.copy(
errorId = 0
)
else -> payload
}
)
}
}
private fun Observable<MessageTransfer>.toSessionTransfers(): Observable<SessionTransfer> {
return filter { it.getMessage().topic == FlowMessagingImpl.sessionTopic }.map {
val from = it.sender.id
val message = it.messageData.deserialize<SessionMessage>()
SessionTransfer(from, sanitise(message), it.recipients)
}
}
private fun errorMessage(errorResponse: FlowException? = null) = ExistingSessionMessage(SessionId(0), ErrorSessionMessage(errorResponse, 0))
private infix fun TestStartedNode.sent(message: SessionMessage): Pair<Int, SessionMessage> = Pair(internals.id, message)
private infix fun Pair<Int, SessionMessage>.to(node: TestStartedNode): SessionTransfer = SessionTransfer(first, second, node.network.myAddress)
private data class SessionTransfer(val from: Int, val message: SessionMessage, val to: MessageRecipients) {
val isPayloadTransfer: Boolean
get() =
message is ExistingSessionMessage && message.payload is DataSessionMessage ||
message is InitialSessionMessage && message.firstPayload != null
override fun toString(): String = "$from sent $message to $to"
}
private fun sessionInit(clientFlowClass: KClass<out FlowLogic<*>>, flowVersion: Int = 1, payload: Any? = null): InitialSessionMessage {
return InitialSessionMessage(SessionId(0), 0, clientFlowClass.java.name, flowVersion, "", payload?.serialize())
}
private fun sessionData(payload: Any) = ExistingSessionMessage(SessionId(0), DataSessionMessage(payload.serialize()))
private val FlowLogic<*>.progressSteps: CordaFuture<List<Notification<ProgressTracker.Step>>> private val FlowLogic<*>.progressSteps: CordaFuture<List<Notification<ProgressTracker.Step>>>
get() { get() {
return progressTracker!!.changes return progressTracker!!.changes
@ -840,23 +474,6 @@ private class WaitForOtherSideEndBeforeSendAndReceive(val otherParty: Party,
} }
} }
@InitiatingFlow
private open class SendFlow(val payload: Any, vararg val otherParties: Party) : FlowLogic<FlowInfo>() {
init {
require(otherParties.isNotEmpty())
}
@Suspendable
override fun call(): FlowInfo {
val flowInfos = otherParties.map {
val session = initiateFlow(it)
session.send(payload)
session.getCounterpartyFlowInfo()
}.toList()
return flowInfos.first()
}
}
// we need brand new class for a flow to fail, so here it is // we need brand new class for a flow to fail, so here it is
@InitiatingFlow @InitiatingFlow
private open class NeverRegisteredFlow(val payload: Any, vararg val otherParties: Party) : FlowLogic<FlowInfo>() { private open class NeverRegisteredFlow(val payload: Any, vararg val otherParties: Party) : FlowLogic<FlowInfo>() {
@ -896,55 +513,12 @@ private object WaitingFlows {
} }
} }
private class NoOpFlow(val nonTerminating: Boolean = false) : FlowLogic<Unit>() {
@Transient
var flowStarted = false
@Suspendable
override fun call() {
flowStarted = true
if (nonTerminating) {
Fiber.park()
}
}
}
private class InitiatedReceiveFlow(val otherPartySession: FlowSession) : FlowLogic<Unit>() {
object START_STEP : ProgressTracker.Step("Starting")
object RECEIVED_STEP : ProgressTracker.Step("Received")
override val progressTracker: ProgressTracker = ProgressTracker(START_STEP, RECEIVED_STEP)
private var nonTerminating: Boolean = false
@Transient
var receivedPayloads: List<String> = emptyList()
@Suspendable
override fun call() {
progressTracker.currentStep = START_STEP
receivedPayloads = listOf(otherPartySession.receive<String>().unwrap { it })
progressTracker.currentStep = RECEIVED_STEP
if (nonTerminating) {
Fiber.park()
}
}
fun nonTerminating(): InitiatedReceiveFlow {
nonTerminating = true
return this
}
}
private class LazyServiceHubAccessFlow : FlowLogic<Unit>() { private class LazyServiceHubAccessFlow : FlowLogic<Unit>() {
val lazyTime: Instant by lazy { serviceHub.clock.instant() } val lazyTime: Instant by lazy { serviceHub.clock.instant() }
@Suspendable @Suspendable
override fun call() = Unit override fun call() = Unit
} }
private open class InitiatedSendFlow(val payload: Any, val otherPartySession: FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() = otherPartySession.send(payload)
}
private interface CustomInterface private interface CustomInterface
private class CustomSendFlow(payload: String, otherParty: Party) : CustomInterface, SendFlow(payload, otherParty) private class CustomSendFlow(payload: String, otherParty: Party) : CustomInterface, SendFlow(payload, otherParty)
@ -952,41 +526,6 @@ private class CustomSendFlow(payload: String, otherParty: Party) : CustomInterfa
@InitiatingFlow @InitiatingFlow
private class IncorrectCustomSendFlow(payload: String, otherParty: Party) : CustomInterface, SendFlow(payload, otherParty) private class IncorrectCustomSendFlow(payload: String, otherParty: Party) : CustomInterface, SendFlow(payload, otherParty)
@InitiatingFlow
private class ReceiveFlow(vararg val otherParties: Party) : FlowLogic<Unit>() {
object START_STEP : ProgressTracker.Step("Starting")
object RECEIVED_STEP : ProgressTracker.Step("Received")
init {
require(otherParties.isNotEmpty())
}
override val progressTracker: ProgressTracker = ProgressTracker(START_STEP, RECEIVED_STEP)
private var nonTerminating: Boolean = false
@Transient
var receivedPayloads: List<String> = emptyList()
@Suspendable
override fun call() {
progressTracker.currentStep = START_STEP
receivedPayloads = otherParties.map { initiateFlow(it).receive<String>().unwrap { it } }
progressTracker.currentStep = RECEIVED_STEP
if (nonTerminating) {
Fiber.park()
}
}
fun nonTerminating(): ReceiveFlow {
nonTerminating = true
return this
}
}
private class MyFlowException(override val message: String) : FlowException() {
override fun equals(other: Any?): Boolean = other is MyFlowException && other.message == this.message
override fun hashCode(): Int = message.hashCode()
}
@InitiatingFlow @InitiatingFlow
private class VaultQueryFlow(val stx: SignedTransaction, val otherParty: Party) : FlowLogic<List<StateAndRef<ContractState>>>() { private class VaultQueryFlow(val stx: SignedTransaction, val otherParty: Party) : FlowLogic<List<StateAndRef<ContractState>>>() {
@Suspendable @Suspendable
@ -1032,22 +571,182 @@ private class DoubleInlinedSubFlow(val otherPartySession: FlowSession) : FlowLog
private data class NonSerialisableData(val a: Int) private data class NonSerialisableData(val a: Int)
private class NonSerialisableFlowException(@Suppress("unused") val data: NonSerialisableData) : FlowException() private class NonSerialisableFlowException(@Suppress("unused") val data: NonSerialisableData) : FlowException()
@InitiatingFlow
private class SendAndReceiveFlow(val otherParty: Party, val payload: Any, val otherPartySession: FlowSession? = null) : FlowLogic<Any>() {
constructor(otherPartySession: FlowSession, payload: Any) : this(otherPartySession.counterparty, payload, otherPartySession)
@Suspendable
override fun call(): Any = (otherPartySession
?: initiateFlow(otherParty)).sendAndReceive<Any>(payload).unwrap { it }
}
private class InlinedSendFlow(val payload: String, val otherPartySession: FlowSession) : FlowLogic<Unit>() { private class InlinedSendFlow(val payload: String, val otherPartySession: FlowSession) : FlowLogic<Unit>() {
@Suspendable @Suspendable
override fun call() = otherPartySession.send(payload) override fun call() = otherPartySession.send(payload)
} }
//endregion Helpers
}
internal fun sessionConfirm(flowVersion: Int = 1) = ExistingSessionMessage(SessionId(0), ConfirmSessionMessage(SessionId(0), FlowInfo(flowVersion, "")))
internal inline fun <reified P : FlowLogic<*>> TestStartedNode.getSingleFlow(): Pair<P, CordaFuture<*>> {
return smm.findStateMachines(P::class.java).single()
}
private fun sanitise(message: SessionMessage) = when (message) {
is InitialSessionMessage -> message.copy(initiatorSessionId = SessionId(0), initiationEntropy = 0, appName = "")
is ExistingSessionMessage -> {
val payload = message.payload
message.copy(
recipientSessionId = SessionId(0),
payload = when (payload) {
is ConfirmSessionMessage -> payload.copy(
initiatedSessionId = SessionId(0),
initiatedFlowInfo = payload.initiatedFlowInfo.copy(appName = "")
)
is ErrorSessionMessage -> payload.copy(
errorId = 0
)
else -> payload
}
)
}
}
internal fun Observable<MessageTransfer>.toSessionTransfers(): Observable<SessionTransfer> {
return filter { it.getMessage().topic == FlowMessagingImpl.sessionTopic }.map {
val from = it.sender.id
val message = it.messageData.deserialize<SessionMessage>()
SessionTransfer(from, sanitise(message), it.recipients)
}
}
internal fun TestStartedNode.sendSessionMessage(message: SessionMessage, destination: Party) {
services.networkService.apply {
val address = getAddressOfParty(PartyInfo.SingleNode(destination, emptyList()))
send(createMessage(FlowMessagingImpl.sessionTopic, message.serialize().bytes), address)
}
}
internal fun errorMessage(errorResponse: FlowException? = null) = ExistingSessionMessage(SessionId(0), ErrorSessionMessage(errorResponse, 0))
internal infix fun TestStartedNode.sent(message: SessionMessage): Pair<Int, SessionMessage> = Pair(internals.id, message)
internal infix fun Pair<Int, SessionMessage>.to(node: TestStartedNode): SessionTransfer = SessionTransfer(first, second, node.network.myAddress)
internal data class SessionTransfer(val from: Int, val message: SessionMessage, val to: MessageRecipients) {
val isPayloadTransfer: Boolean
get() =
message is ExistingSessionMessage && message.payload is DataSessionMessage ||
message is InitialSessionMessage && message.firstPayload != null
override fun toString(): String = "$from sent $message to $to"
}
internal fun sessionInit(clientFlowClass: KClass<out FlowLogic<*>>, flowVersion: Int = 1, payload: Any? = null): InitialSessionMessage {
return InitialSessionMessage(SessionId(0), 0, clientFlowClass.java.name, flowVersion, "", payload?.serialize())
}
internal fun sessionData(payload: Any) = ExistingSessionMessage(SessionId(0), DataSessionMessage(payload.serialize()))
@InitiatingFlow @InitiatingFlow
private class PingPongFlow(val otherParty: Party, val payload: Long, val otherPartySession: FlowSession? = null) : FlowLogic<Unit>() { internal open class SendFlow(private val payload: Any, private vararg val otherParties: Party) : FlowLogic<FlowInfo>() {
init {
require(otherParties.isNotEmpty())
}
@Suspendable
override fun call(): FlowInfo {
val flowInfos = otherParties.map {
val session = initiateFlow(it)
session.send(payload)
session.getCounterpartyFlowInfo()
}.toList()
return flowInfos.first()
}
}
internal class NoOpFlow(val nonTerminating: Boolean = false) : FlowLogic<Unit>() {
@Transient
var flowStarted = false
@Suspendable
override fun call() {
flowStarted = true
if (nonTerminating) {
Fiber.park()
}
}
}
internal class InitiatedReceiveFlow(private val otherPartySession: FlowSession) : FlowLogic<Unit>() {
object START_STEP : ProgressTracker.Step("Starting")
object RECEIVED_STEP : ProgressTracker.Step("Received")
override val progressTracker: ProgressTracker = ProgressTracker(START_STEP, RECEIVED_STEP)
private var nonTerminating: Boolean = false
@Transient
var receivedPayloads: List<String> = emptyList()
@Suspendable
override fun call() {
progressTracker.currentStep = START_STEP
receivedPayloads = listOf(otherPartySession.receive<String>().unwrap { it })
progressTracker.currentStep = RECEIVED_STEP
if (nonTerminating) {
Fiber.park()
}
}
fun nonTerminating(): InitiatedReceiveFlow {
nonTerminating = true
return this
}
}
internal open class InitiatedSendFlow(private val payload: Any, private val otherPartySession: FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() = otherPartySession.send(payload)
}
@InitiatingFlow
internal class ReceiveFlow(private vararg val otherParties: Party) : FlowLogic<Unit>() {
object START_STEP : ProgressTracker.Step("Starting")
object RECEIVED_STEP : ProgressTracker.Step("Received")
init {
require(otherParties.isNotEmpty())
}
override val progressTracker: ProgressTracker = ProgressTracker(START_STEP, RECEIVED_STEP)
private var nonTerminating: Boolean = false
@Transient
var receivedPayloads: List<String> = emptyList()
@Suspendable
override fun call() {
progressTracker.currentStep = START_STEP
receivedPayloads = otherParties.map { initiateFlow(it).receive<String>().unwrap { it } }
progressTracker.currentStep = RECEIVED_STEP
if (nonTerminating) {
Fiber.park()
}
}
fun nonTerminating(): ReceiveFlow {
nonTerminating = true
return this
}
}
internal class MyFlowException(override val message: String) : FlowException() {
override fun equals(other: Any?): Boolean = other is MyFlowException && other.message == this.message
override fun hashCode(): Int = message.hashCode()
}
@InitiatingFlow
internal class SendAndReceiveFlow(private val otherParty: Party, private val payload: Any, private val otherPartySession: FlowSession? = null) : FlowLogic<Any>() {
constructor(otherPartySession: FlowSession, payload: Any) : this(otherPartySession.counterparty, payload, otherPartySession)
@Suspendable
override fun call(): Any {
return (otherPartySession ?: initiateFlow(otherParty)).sendAndReceive<Any>(payload).unwrap { it }
}
}
@InitiatingFlow
internal class PingPongFlow(private val otherParty: Party, private val payload: Long, private val otherPartySession: FlowSession? = null) : FlowLogic<Unit>() {
constructor(otherPartySession: FlowSession, payload: Long) : this(otherPartySession.counterparty, payload, otherPartySession) constructor(otherPartySession: FlowSession, payload: Long) : this(otherPartySession.counterparty, payload, otherPartySession)
@Transient @Transient
@ -1063,7 +762,7 @@ private class PingPongFlow(val otherParty: Party, val payload: Long, val otherPa
} }
} }
private class ExceptionFlow<E : Exception>(val exception: () -> E) : FlowLogic<Nothing>() { internal class ExceptionFlow<E : Exception>(val exception: () -> E) : FlowLogic<Nothing>() {
object START_STEP : ProgressTracker.Step("Starting") object START_STEP : ProgressTracker.Step("Starting")
override val progressTracker: ProgressTracker = ProgressTracker(START_STEP) override val progressTracker: ProgressTracker = ProgressTracker(START_STEP)

View File

@ -0,0 +1,178 @@
package net.corda.node.services.statemachine
import net.corda.core.flows.UnexpectedFlowEndException
import net.corda.core.flows.registerCordappFlowFactory
import net.corda.core.identity.Party
import net.corda.core.internal.concurrent.map
import net.corda.core.utilities.getOrThrow
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.internal.LogHelper
import net.corda.testing.node.InMemoryMessagingNetwork
import net.corda.testing.node.internal.*
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.AssertionsForClassTypes
import org.junit.After
import org.junit.Before
import org.junit.Test
import rx.Observable
import java.util.*
class FlowFrameworkTripartyTests {
companion object {
init {
LogHelper.setLevel("+net.corda.flow")
}
private lateinit var mockNet: InternalMockNetwork
private lateinit var aliceNode: TestStartedNode
private lateinit var bobNode: TestStartedNode
private lateinit var charlieNode: TestStartedNode
private lateinit var alice: Party
private lateinit var bob: Party
private lateinit var charlie: Party
private lateinit var notaryIdentity: Party
private val receivedSessionMessages = ArrayList<SessionTransfer>()
}
@Before
fun setUpGlobalMockNet() {
mockNet = InternalMockNetwork(
cordappsForAllNodes = cordappsForPackages("net.corda.finance.contracts", "net.corda.testing.contracts"),
servicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin()
)
aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME))
bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME))
charlieNode = mockNet.createNode(InternalMockNodeParameters(legalName = CHARLIE_NAME))
// Extract identities
alice = aliceNode.info.singleIdentity()
bob = bobNode.info.singleIdentity()
charlie = charlieNode.info.singleIdentity()
notaryIdentity = mockNet.defaultNotaryIdentity
receivedSessionMessagesObservable().forEach { receivedSessionMessages += it }
}
@After
fun cleanUp() {
mockNet.stopNodes()
receivedSessionMessages.clear()
}
private fun receivedSessionMessagesObservable(): Observable<SessionTransfer> {
return mockNet.messagingNetwork.receivedMessages.toSessionTransfers()
}
@Test
fun `sending to multiple parties`() {
bobNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it)
.nonTerminating() }
charlieNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it)
.nonTerminating() }
val payload = "Hello World"
aliceNode.services.startFlow(SendFlow(payload, bob, charlie))
mockNet.runNetwork()
bobNode.internals.acceptableLiveFiberCountOnStop = 1
charlieNode.internals.acceptableLiveFiberCountOnStop = 1
val bobFlow = bobNode.getSingleFlow<InitiatedReceiveFlow>().first
val charlieFlow = charlieNode.getSingleFlow<InitiatedReceiveFlow>().first
assertThat(bobFlow.receivedPayloads[0]).isEqualTo(payload)
assertThat(charlieFlow.receivedPayloads[0]).isEqualTo(payload)
assertSessionTransfers(bobNode,
aliceNode sent sessionInit(SendFlow::class, payload = payload) to bobNode,
bobNode sent sessionConfirm() to aliceNode,
aliceNode sent normalEnd to bobNode
//There's no session end from the other flows as they're manually suspended
)
assertSessionTransfers(charlieNode,
aliceNode sent sessionInit(SendFlow::class, payload = payload) to charlieNode,
charlieNode sent sessionConfirm() to aliceNode,
aliceNode sent normalEnd to charlieNode
//There's no session end from the other flows as they're manually suspended
)
}
@Test
fun `receiving from multiple parties`() {
val bobPayload = "Test 1"
val charliePayload = "Test 2"
bobNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(bobPayload, it) }
charlieNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(charliePayload, it) }
val multiReceiveFlow = ReceiveFlow(bob, charlie).nonTerminating()
aliceNode.services.startFlow(multiReceiveFlow)
aliceNode.internals.acceptableLiveFiberCountOnStop = 1
mockNet.runNetwork()
assertThat(multiReceiveFlow.receivedPayloads[0]).isEqualTo(bobPayload)
assertThat(multiReceiveFlow.receivedPayloads[1]).isEqualTo(charliePayload)
assertSessionTransfers(bobNode,
aliceNode sent sessionInit(ReceiveFlow::class) to bobNode,
bobNode sent sessionConfirm() to aliceNode,
bobNode sent sessionData(bobPayload) to aliceNode,
bobNode sent normalEnd to aliceNode
)
assertSessionTransfers(charlieNode,
aliceNode sent sessionInit(ReceiveFlow::class) to charlieNode,
charlieNode sent sessionConfirm() to aliceNode,
charlieNode sent sessionData(charliePayload) to aliceNode,
charlieNode sent normalEnd to aliceNode
)
}
@Test
fun `FlowException only propagated to parent`() {
charlieNode.registerCordappFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Chain") } }
bobNode.registerCordappFlowFactory(ReceiveFlow::class) { ReceiveFlow(charlie) }
val receivingFiber = aliceNode.services.startFlow(ReceiveFlow(bob))
mockNet.runNetwork()
AssertionsForClassTypes.assertThatExceptionOfType(UnexpectedFlowEndException::class.java)
.isThrownBy { receivingFiber.resultFuture.getOrThrow() }
}
@Test
fun `FlowException thrown and there is a 3rd unrelated party flow`() {
// Bob will send its payload and then block waiting for the receive from Alice. Meanwhile Alice will move
// onto Charlie which will throw the exception
val node2Fiber = bobNode
.registerCordappFlowFactory(ReceiveFlow::class) { SendAndReceiveFlow(it, "Hello") }
.map { it.stateMachine }
charlieNode.registerCordappFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Nothing useful") } }
val aliceFiber = aliceNode.services.startFlow(ReceiveFlow(bob, charlie)) as FlowStateMachineImpl
mockNet.runNetwork()
// Alice will terminate with the error it received from Charlie but it won't propagate that to Bob (as it's
// not relevant to it) but it will end its session with it
AssertionsForClassTypes.assertThatExceptionOfType(MyFlowException::class.java)
.isThrownBy {
aliceFiber.resultFuture.getOrThrow()
}
val bobResultFuture = node2Fiber.getOrThrow().resultFuture
AssertionsForClassTypes.assertThatExceptionOfType(UnexpectedFlowEndException::class.java).isThrownBy {
bobResultFuture.getOrThrow()
}
assertSessionTransfers(bobNode,
aliceNode sent sessionInit(ReceiveFlow::class) to bobNode,
bobNode sent sessionConfirm() to aliceNode,
bobNode sent sessionData("Hello") to aliceNode,
aliceNode sent errorMessage() to bobNode
)
}
private val normalEnd = ExistingSessionMessage(SessionId(0), EndSessionMessage) // NormalSessionEnd(0)
private fun assertSessionTransfers(node: TestStartedNode, vararg expected: SessionTransfer): List<SessionTransfer> {
val actualForNode = receivedSessionMessages.filter { it.from == node.internals.id || it.to == node.network.myAddress }
assertThat(actualForNode).containsExactly(*expected)
return actualForNode
}
}