Collect Signatures now works with encryption

This commit is contained in:
adam.houston 2022-02-25 16:59:57 +00:00
parent 16a4c92677
commit 9e46423465
11 changed files with 422 additions and 12 deletions

View File

@ -222,7 +222,9 @@ private class WireTransactionSerializer : JsonSerializer<WireTransaction>() {
value.attachments,
value.references,
value.privacySalt,
value.networkParametersHash
value.networkParametersHash,
value.inputsStates,
value.referenceStates
))
}
}
@ -238,7 +240,9 @@ private class WireTransactionDeserializer : JsonDeserializer<WireTransaction>()
wrapper.notary,
wrapper.timeWindow,
wrapper.references,
wrapper.networkParametersHash
wrapper.networkParametersHash,
wrapper.inputStates,
wrapper.referenceStates
)
return WireTransaction(componentGroups, wrapper.privacySalt, wrapper.digestService ?: DigestService.sha2_256)
}
@ -254,7 +258,9 @@ private class WireTransactionJson(@get:JsonInclude(Include.NON_NULL) val digestS
val attachments: List<SecureHash>,
val references: List<StateRef>,
val privacySalt: PrivacySalt,
val networkParametersHash: SecureHash?)
val networkParametersHash: SecureHash?,
val inputStates: List<StateAndRef<ContractState>>,
val referenceStates: List<StateAndRef<ContractState>>)
private interface TransactionStateMixin {
@get:JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)

View File

@ -13,5 +13,7 @@ enum class ComponentGroupEnum {
TIMEWINDOW_GROUP, // ordinal = 5.
SIGNERS_GROUP, // ordinal = 6.
REFERENCES_GROUP, // ordinal = 7.
PARAMETERS_GROUP // ordinal = 8.
PARAMETERS_GROUP, // ordinal = 8.
INPUT_STATES_GROUP, // ordinal = 9.
REFERENCE_STATES_GROUP, // ordinal = 10.
}

View File

@ -1,6 +1,7 @@
package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable
import com.sun.org.apache.xpath.internal.operations.Bool
import net.corda.core.crypto.TransactionSignature
import net.corda.core.crypto.isFulfilledBy
import net.corda.core.crypto.toStringShort
@ -8,7 +9,9 @@ import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party
import net.corda.core.identity.groupPublicKeysByWellKnownParty
import net.corda.core.internal.dependencies
import net.corda.core.node.ServiceHub
import net.corda.core.transactions.RawDependency
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.ProgressTracker
@ -203,7 +206,7 @@ class CollectSignatureFlow(val partiallySignedTx: SignedTransaction, val session
@Suspendable
override fun call(): List<TransactionSignature> {
// SendTransactionFlow allows counterparty to access our data to resolve the transaction.
subFlow(SendTransactionFlow(session, partiallySignedTx))
subFlow(SendTransactionFlow(session, partiallySignedTx, encrypted = true))
// Send the key we expect the counterparty to sign with - this is important where they may have several
// keys to sign with, as it makes it faster for them to identify the key to sign with, and more straight forward
// for us to check we have the expected signature returned.
@ -259,7 +262,7 @@ class CollectSignatureFlow(val partiallySignedTx: SignedTransaction, val session
* @param otherSideSession The session which is providing you a transaction to sign.
*/
abstract class SignTransactionFlow @JvmOverloads constructor(val otherSideSession: FlowSession,
override val progressTracker: ProgressTracker = SignTransactionFlow.tracker()) : FlowLogic<SignedTransaction>() {
override val progressTracker: ProgressTracker = SignTransactionFlow.tracker(), val encrypted : Boolean = false) : FlowLogic<SignedTransaction>() {
companion object {
object RECEIVING : ProgressTracker.Step("Receiving transaction proposal for signing.")
@ -274,10 +277,10 @@ abstract class SignTransactionFlow @JvmOverloads constructor(val otherSideSessio
override fun call(): SignedTransaction {
progressTracker.currentStep = RECEIVING
// Receive transaction and resolve dependencies, check sufficient signatures is disabled as we don't have all signatures.
val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false))
val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false, encrypted = true))
// Receive the signing key that the party requesting the signature expects us to sign with. Having this provided
// means we only have to check we own that one key, rather than matching all keys in the transaction against all
// keys we own.
// keys we own.t
val signingKeys = otherSideSession.receive<List<PublicKey>>().unwrap { keys ->
// TODO: We should have a faster way of verifying we own a single key
serviceHub.keyManagementService.filterMyKeys(keys)
@ -287,7 +290,53 @@ abstract class SignTransactionFlow @JvmOverloads constructor(val otherSideSessio
checkMySignaturesRequired(stx, signingKeys)
// Check the signatures which have already been provided. Usually the Initiators and possibly an Oracle's.
checkSignatures(stx)
stx.tx.toLedgerTransaction(serviceHub).verify()
if (encrypted) {
val encryptionService = serviceHub.encryptedTransactionService
val validatedTxSvc = serviceHub.validatedTransactions
val encryptedTxs = stx.dependencies.mapNotNull {
validatedTxId ->
validatedTxSvc.getEncryptedTransaction(validatedTxId)?.let { etx ->
etx.id to etx
}
}.toMap()
val signedTxs = stx.dependencies.mapNotNull {
validatedTxId ->
validatedTxSvc.getTransaction(validatedTxId)?.let { stx ->
stx.id to stx
}
}.toMap()
val networkParameters = stx.dependencies.mapNotNull { depTxId ->
val npHash = when {
encryptedTxs[depTxId] != null -> serviceHub.encryptedTransactionService.getNetworkParameterHash(encryptedTxs[depTxId]!!)
?: serviceHub.networkParametersService.defaultHash
signedTxs[depTxId] != null -> signedTxs[depTxId]!!.networkParametersHash
?: serviceHub.networkParametersService.defaultHash
else -> null
}
npHash?.let { depTxId to npHash }
}.associate {
netParams ->
netParams.first to serviceHub.networkParametersService.lookup(netParams.second)
}
val rawDependencies = stx.dependencies.associate {
txId ->
txId to RawDependency(
encryptedTxs[txId],
signedTxs[txId],
networkParameters[txId]
)
}
encryptionService.verifyTransaction(stx, serviceHub, false, rawDependencies)
} else {
stx.tx.toLedgerTransaction(serviceHub).verify()
}
// Perform some custom verification over the transaction.
try {
checkTransaction(stx)

View File

@ -153,7 +153,9 @@ fun createComponentGroups(inputs: List<StateRef>,
notary: Party?,
timeWindow: TimeWindow?,
references: List<StateRef>,
networkParametersHash: SecureHash?): List<ComponentGroup> {
networkParametersHash: SecureHash?,
inputStates: List<StateAndRef<ContractState>> = emptyList(),
referenceStates: List<StateAndRef<ContractState>> = emptyList()): List<ComponentGroup> {
val serializationFactory = SerializationFactory.defaultFactory
val serializationContext = serializationFactory.defaultContext
val serialize = { value: Any, _: Int -> value.serialize(serializationFactory, serializationContext) }
@ -170,6 +172,8 @@ fun createComponentGroups(inputs: List<StateRef>,
// a FilteredTransaction can now verify it sees all the commands it should sign.
if (commands.isNotEmpty()) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.SIGNERS_GROUP.ordinal, commands.map { it.signers }.lazyMapped(serialize)))
if (networkParametersHash != null) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.PARAMETERS_GROUP.ordinal, listOf(networkParametersHash.serialize())))
if (inputStates.isNotEmpty()) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.INPUT_STATES_GROUP.ordinal, inputStates.lazyMapped(serialize)))
if (referenceStates.isNotEmpty()) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.REFERENCE_STATES_GROUP.ordinal, referenceStates.lazyMapped(serialize)))
return componentGroupMap
}

View File

@ -133,6 +133,9 @@ private class Validator(private val ltx: LedgerTransaction, private val transact
validateStatesAgainstContract()
// 5. Final step will be to run the contract code.
// Encryption PoC
}
private fun checkTransactionWithTimeWindowIsNotarised() {
@ -510,6 +513,10 @@ class TransactionVerifier(private val transactionClassLoader: ClassLoader) : Fun
}
}
}
private fun checkInputs() {
}
}
// BOB

View File

@ -89,6 +89,18 @@ class EncryptedTransactionService() : SingletonSerializeAsToken() {
val dependencies = extractDependencies(signedTransaction.inputs + signedTransaction.references, rawDependencies)
(signedTransaction.tx.inputsStates + signedTransaction.tx.referenceStates).forEach {
val dependency = dependencies[it.ref.txhash] ?: throw IllegalArgumentException("Dependency transaction not found")
val dependencyState = dependency.inputsAndRefs[it.ref] ?: throw IllegalArgumentException("Dependency state not found")
val dependentState = dependencyState.data
val suppliedState = it.state.data
require(dependencyState.data == it.state.data) {
"Supplied input/ref on transaction did not match it's stateRef"
}
}
// will throw if cannot verify
signedTransaction.toLedgerTransaction(serviceHub, checkSufficientSignatures, dependencies).verify()
}

View File

@ -24,6 +24,10 @@ abstract class CoreTransaction : BaseTransaction() {
* was created on older version of Corda (before 4), resolution will default to initial parameters.
*/
abstract val networkParametersHash: SecureHash?
// Encryption PoC - optionally bundle the states
open val inputsStates: List<StateAndRef<ContractState>> = emptyList()
open val referenceStates: List<StateAndRef<ContractState>> = emptyList()
}
/** A transaction with fully resolved components, such as input states. */

View File

@ -90,6 +90,7 @@ open class TransactionBuilder(
private val inputsWithTransactionState = arrayListOf<StateAndRef<ContractState>>()
private val referencesWithTransactionState = arrayListOf<TransactionState<ContractState>>()
private val referencesWithTransactionStateAndRef = arrayListOf<StateAndRef<ContractState>>()
private val excludedAttachments = arrayListOf<AttachmentId>()
/**
@ -109,6 +110,7 @@ open class TransactionBuilder(
)
t.inputsWithTransactionState.addAll(this.inputsWithTransactionState)
t.referencesWithTransactionState.addAll(this.referencesWithTransactionState)
t.referencesWithTransactionStateAndRef.addAll(this.referencesWithTransactionStateAndRef)
return t
}
@ -215,7 +217,9 @@ open class TransactionBuilder(
notary,
window,
referenceStates,
services.networkParametersService.currentHash),
services.networkParametersService.currentHash,
inputsWithTransactionState,
referencesWithTransactionStateAndRef),
privacySalt,
services.digestService
)
@ -758,6 +762,7 @@ open class TransactionBuilder(
open fun addReferenceState(referencedStateAndRef: ReferencedStateAndRef<*>) = apply {
val stateAndRef = referencedStateAndRef.stateAndRef
referencesWithTransactionState.add(stateAndRef.state)
referencesWithTransactionStateAndRef.add(stateAndRef)
// It is likely the case that users of reference states do not have permission to change the notary assigned
// to a reference state. Even if users _did_ have this permission the result would likely be a bunch of

View File

@ -18,6 +18,7 @@ import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
import net.corda.core.serialization.SerializationFactory
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.internal.AttachmentsClassLoaderCache
import net.corda.core.serialization.serialize
import net.corda.core.utilities.OpaqueBytes
@ -89,6 +90,22 @@ constructor(componentGroups: List<ComponentGroup>, val privacySalt: PrivacySalt,
/** The transaction id is represented by the root hash of Merkle tree over the transaction components. */
override val id: SecureHash get() = merkleTree.hash
// Encryption PoC
val serializationFactory = SerializationFactory.defaultFactory
val serializationContext = serializationFactory.defaultContext
override val inputsStates: List<StateAndRef<ContractState>> = componentGroups
.singleOrNull{ it.groupIndex == ComponentGroupEnum.INPUT_STATES_GROUP.ordinal }
?.let { group ->
group.components.map { (it as SerializedBytes<StateAndRef<ContractState>>).deserialize(serializationFactory, serializationContext) }
} ?: emptyList()
override val referenceStates : List<StateAndRef<ContractState>> = componentGroups
.singleOrNull{ it.groupIndex == ComponentGroupEnum.REFERENCE_STATES_GROUP.ordinal }
?.let { group ->
group.components.map {(it as SerializedBytes<StateAndRef<ContractState>>).deserialize(serializationFactory, serializationContext) }
} ?: emptyList()
/** Public keys that need to be fulfilled by signatures in order for the transaction to be valid. */
val requiredSigningKeys: Set<PublicKey>
get() {

View File

@ -128,7 +128,7 @@ abstract class OnLedgerAsset<T : Any, out C : CommandData, S : FungibleAsset<T>>
// Select a subset of the available states we were given that sums up to >= totalSendAmount.
val (gathered, gatheredAmount) = gatherCoins(acceptableStates, totalSendAmount)
check(gatheredAmount >= totalSendAmount)
val keysUsed = gathered.map { it.state.data.owner.owningKey }
val keysUsed = gathered.map { it.state.data.owner.owningKey } //+ payments.map { it.party.owningKey }
// Now calculate the output states. This is complicated by the fact that a single payment may require
// multiple output states, due to the need to keep states separated by issuer. We start by figuring out

View File

@ -0,0 +1,304 @@
package net.corda.node.services.encryptedtx
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.BelongsToContract
import net.corda.core.contracts.CommandData
import net.corda.core.contracts.Contract
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateAndRef
import net.corda.core.flows.CollectSignaturesFlow
import net.corda.core.flows.FinalityFlow
import net.corda.core.flows.FlowException
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSession
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.ReceiveFinalityFlow
import net.corda.core.flows.SignTransactionFlow
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.toHexString
import net.corda.core.utilities.unwrap
import net.corda.node.services.persistence.DBCheckpointStorage
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.BOC_NAME
import net.corda.testing.core.singleIdentity
import net.corda.testing.node.InMemoryMessagingNetwork
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.MockNetworkParameters
import net.corda.testing.node.StartedMockNode
import net.corda.testing.node.internal.enclosedCordapp
import org.junit.Before
import org.junit.Test
import java.lang.IllegalArgumentException
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class EncryptedBackchainTests {
private lateinit var mockNet: MockNetwork
private lateinit var bankOfCordaNode: StartedMockNode
private lateinit var bankOfCorda: Party
private lateinit var aliceNode: StartedMockNode
private lateinit var alice: Party
private lateinit var bobNode: StartedMockNode
private lateinit var bob: Party
@Before
fun start() {
mockNet = MockNetwork(MockNetworkParameters(servicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin(), cordappsForAllNodes = listOf(enclosedCordapp())))
bankOfCordaNode = mockNet.createPartyNode(BOC_NAME)
bankOfCorda = bankOfCordaNode.info.identityFromX500Name(BOC_NAME)
aliceNode = mockNet.createPartyNode(ALICE_NAME)
alice = aliceNode.info.singleIdentity()
bobNode = mockNet.createPartyNode(BOB_NAME)
bob = bobNode.info.singleIdentity()
}
@Test
fun `issue and move`() {
val issuanceTx = bankOfCordaNode.execFlow(IssueFlow(300))
val aliceTx = bankOfCordaNode.execFlow(MoveFlow(bankOfCordaNode.getAllTokens(), 200, alice))
val bobTx = aliceNode.execFlow(MoveFlow(aliceNode.getAllTokens(), 100, bob))
printTxs( listOf(
"Bank issue to self: " to issuanceTx,
"Bank pays Alice: " to aliceTx,
"Alice pays Bob: " to bobTx)
)
}
@Test
fun `issue and move with remote signer`() {
val issuanceTx = bankOfCordaNode.execFlow(IssueFlow(300))
val aliceTx = bankOfCordaNode.execFlow(MoveFlow(bankOfCordaNode.getAllTokens(), 200, alice, true))
val bobTx = aliceNode.execFlow(MoveFlow(aliceNode.getAllTokens(), 100, bob, true))
printTxs( listOf(
"Bank issue to self: " to issuanceTx,
"Bank pays Alice: " to aliceTx,
"Alice pays Bob: " to bobTx)
)
}
@Test
fun `bank of Corda cannot pay bob`() {
bankOfCordaNode.execFlow(IssueFlow(300))
val exception = assertFailsWith<FlowException>{
bankOfCordaNode.execFlow(MoveFlow(bankOfCordaNode.getAllTokens(), 200, bob, true))
}
assertEquals("java.lang.IllegalArgumentException: Bank of Corda cannot move money to Bob", exception.message)
}
private fun printTxs(txHashes : List<Pair<String,SignedTransaction>>) {
listOf(bankOfCordaNode, aliceNode, bobNode).forEach { node ->
println("------------------------")
println("${node.info.singleIdentity()}")
println("------------------------")
txHashes.forEach { labelToStx ->
val label = labelToStx.first
val stx = labelToStx.second
println("$label (${stx.id})")
println("> FOUND UNENCRYPTED: ${node.services.validatedTransactions.getTransaction(stx.id)}")
println("> FOUND ENCRYPTED: ${node.services.validatedTransactions.getVerifiedEncryptedTransaction(stx.id)?.let {
"${shortStringDesc(it.bytes.toHexString())} signature ${it.verifierSignature.toHexString()}"
}}")
println()
}
println()
}
}
private fun <T> StartedMockNode.execFlow(flow : FlowLogic<T>) : T {
val future = startFlow(flow)
mockNet.runNetwork()
return future.getOrThrow()
}
private fun StartedMockNode.getAllTokens() : List<StateAndRef<BasicToken>> {
val allStates = services.vaultService.queryBy(BasicToken::class.java).states
println(this.info.singleIdentity())
allStates.forEach {
println(it.state.data)
}
return allStates.filter {
it.state.data.holder.owningKey == this.info.singleIdentity().owningKey
}
}
private fun shortStringDesc(longString : String) : String {
return "EncryptedTransaction(${longString.take(15)}...${longString.takeLast(15)})"
}
@CordaSerializable
enum class SignaturesRequired {
ALL,
SENDER_ONLY
}
class BasicTokenContract: Contract {
companion object {
val contractId = this::class.java.enclosingClass.canonicalName
}
override fun verify(tx: LedgerTransaction) {
val command = tx.commandsOfType(BasicTokenCommand::class.java).single()
when (command.value) {
is Issue -> {
val inputs = tx.inputsOfType<BasicToken>()
val outputs = tx.outputsOfType<BasicToken>()
require(inputs.isEmpty()) { "No input states allowed" }
require(outputs.isNotEmpty()) { "At least one BasicToken input state is required" }
require(outputs.all { it.amount > 0 }) { "Outputs must have amounts greater than zero" }
}
is Move -> {
val inputs = tx.inputsOfType<BasicToken>()
val outputs = tx.outputsOfType<BasicToken>()
require(inputs.isNotEmpty() && outputs.isNotEmpty()) { "Input and output states are required" }
require(inputs.sumBy { it.amount } == outputs.sumBy { it.amount }) { "Inputs and outputs must have the same value"}
require(command.signers.containsAll(inputs.map { it.holder.owningKey })) { "All holders must sign the tx" }
require(inputs.all { it.amount > 0 }) { "Inputs must have amounts greater than zero" }
require(outputs.all { it.amount > 0 }) { "Outputs must have amounts greater than zero" }
// no restriction on mixing issuers
}
}
}
open class BasicTokenCommand : CommandData
class Issue : BasicTokenCommand()
class Move : BasicTokenCommand()
}
@BelongsToContract(BasicTokenContract::class)
class BasicToken(
val amount: Int,
val holder: AbstractParty,
override val participants : List<AbstractParty> = listOf(holder)) : ContractState {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as BasicToken
if (amount != other.amount) return false
if (holder != other.holder) return false
return true
}
}
class IssueFlow(val amount: Int): FlowLogic<SignedTransaction>() {
@Suspendable
override fun call() : SignedTransaction {
val ourKey = ourIdentity.owningKey
val tx = TransactionBuilder(serviceHub.networkMapCache.notaryIdentities.first())
.addCommand(BasicTokenContract.Issue(), ourKey)
.addOutputState(BasicToken(amount, ourIdentity))
val stx = serviceHub.signInitialTransaction(tx, ourKey)
return subFlow(FinalityFlow(stx, emptyList()))
}
}
@InitiatingFlow
class MoveFlow(val inputs : List<StateAndRef<BasicToken>>,
val amount: Int,
val moveTo: AbstractParty,
val allMustSign : Boolean = false): FlowLogic<SignedTransaction>() {
@Suspendable
override fun call() : SignedTransaction{
val ourKey = ourIdentity.owningKey
val allMustSignStatus = if(allMustSign) SignaturesRequired.ALL else SignaturesRequired.SENDER_ONLY
val signingKeys = if(allMustSign) listOf(ourKey, moveTo.owningKey) else listOf(ourKey)
val changeAmount = inputs.sumBy { it.state.data.amount } - amount
val tx = TransactionBuilder(serviceHub.networkMapCache.notaryIdentities.first())
.addCommand(BasicTokenContract.Move(), signingKeys)
.addOutputState(BasicToken(amount, moveTo))
if (changeAmount > 0) {
tx.addOutputState(BasicToken(changeAmount, ourIdentity))
}
inputs.forEach {
tx.addInputState(it)
}
tx.verify(serviceHub)
var stx = serviceHub.signInitialTransaction(tx, ourKey)
val wtx = stx.tx
val sessions = listOfNotNull(
serviceHub.identityService.wellKnownPartyFromAnonymous(moveTo)
).filter { it.owningKey != ourKey }.map { initiateFlow(it) }
sessions.forEach {
it.send(allMustSignStatus)
if (allMustSign) {
stx = subFlow(CollectSignaturesFlow(stx , sessions))
}
}
return subFlow(FinalityFlow(stx, sessions))
}
}
@InitiatedBy(MoveFlow::class)
class MoveHandler(val otherSession: FlowSession): FlowLogic<Unit>() {
@Suspendable
override fun call() {
if (!serviceHub.myInfo.isLegalIdentity(otherSession.counterparty)) {
val requiresSignature = otherSession.receive(SignaturesRequired::class.java).unwrap { it }
if (requiresSignature == SignaturesRequired.ALL) {
subFlow(object : SignTransactionFlow(otherSession, encrypted = true) {
override fun checkTransaction(stx: SignedTransaction) {
val inputs = stx.tx.inputsStates.filterIsInstance<StateAndRef<BasicToken>>()
val outputs = stx.tx.outputsOfType(BasicToken::class.java)
// a test condition we can use to trigger a signature failure
require(!(inputs.any { serviceHub.identityService.wellKnownPartyFromAnonymous(it.state.data.holder)?.name == BOC_NAME } &&
outputs.any { serviceHub.identityService.wellKnownPartyFromAnonymous(it.holder)?.name == BOB_NAME })){
"Bank of Corda cannot move money to Bob"
}
}
}
)
}
subFlow(ReceiveFinalityFlow(otherSideSession = otherSession))
}
}
}
}