diff --git a/CHANGELOG.md b/CHANGELOG.md index a78bbbd0..08551447 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ * Rebased the `workflows` to use the new `confidential-identities` module. You will need to add: `TestCordapp.findCordapp("com.r3.corda.lib.ci")` to your tests. +This is also important to version your initiating flows using inlined flows from `token-sdk` with a version equal or more than 2. +Add `@InitiatingFlow(version = 2)` to your initiating flows using new `confiential-identities`, for more information see: +[Flow versioning](https://docs.corda.net/upgrading-cordapps.html#flow-versioning) #### General diff --git a/workflows/build.gradle b/workflows/build.gradle index 0f70f62e..9d0d01df 100644 --- a/workflows/build.gradle +++ b/workflows/build.gradle @@ -46,10 +46,13 @@ dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" // Corda dependencies. - cordaCompile ("$corda_release_group:corda-core:$corda_release_version") { + cordaCompile("$corda_release_group:corda-core:$corda_release_version") { changing = true } cordaCompile "$confidential_id_release_group:ci-workflows:$confidential_id_release_version" + cordaCompile("$corda_release_group:corda-confidential-identities:$corda_release_version") { + changing = true + } // Logging. testCompile "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}" diff --git a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/redeem/AbstractRedeemTokensFlow.kt b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/redeem/AbstractRedeemTokensFlow.kt index d4f4aa47..bfcce800 100644 --- a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/redeem/AbstractRedeemTokensFlow.kt +++ b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/redeem/AbstractRedeemTokensFlow.kt @@ -1,9 +1,9 @@ package com.r3.corda.lib.tokens.workflows.flows.redeem import co.paralleluniverse.fibers.Suspendable -import com.r3.corda.lib.ci.workflows.SyncKeyMappingFlow import com.r3.corda.lib.tokens.workflows.internal.flows.finality.ObserverAwareFinalityFlow import com.r3.corda.lib.tokens.workflows.internal.flows.finality.TransactionRole +import com.r3.corda.lib.tokens.workflows.internal.flows.syncKeyVersion import com.r3.corda.lib.tokens.workflows.utilities.ourSigningKeys import net.corda.core.flows.CollectSignaturesFlow import net.corda.core.flows.FlowLogic @@ -52,7 +52,7 @@ abstract class AbstractRedeemTokensFlow : FlowLogic() { // First synchronise identities between issuer and our states. // TODO: Only do this if necessary. progressTracker.currentStep = SYNC_IDS - subFlow(SyncKeyMappingFlow(issuerSession, txBuilder.toWireTransaction(serviceHub))) + syncKeyVersion(issuerSession, txBuilder) val ourSigningKeys = txBuilder.toLedgerTransaction(serviceHub).ourSigningKeys(serviceHub) val partialStx = serviceHub.signInitialTransaction(txBuilder, ourSigningKeys) // Call collect signatures flow, issuer should perform all the checks for redeeming states. diff --git a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/redeem/ConfidentialRedeemFungibleTokensFlow.kt b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/redeem/ConfidentialRedeemFungibleTokensFlow.kt index eef5ce87..e418ad06 100644 --- a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/redeem/ConfidentialRedeemFungibleTokensFlow.kt +++ b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/redeem/ConfidentialRedeemFungibleTokensFlow.kt @@ -1,9 +1,9 @@ package com.r3.corda.lib.tokens.workflows.flows.redeem import co.paralleluniverse.fibers.Suspendable -import com.r3.corda.lib.ci.workflows.ProvideKeyFlow import com.r3.corda.lib.tokens.contracts.types.TokenType import com.r3.corda.lib.tokens.workflows.internal.flows.finality.TransactionRole +import com.r3.corda.lib.tokens.workflows.internal.flows.provideKeyVersion import net.corda.core.contracts.Amount import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowSession @@ -33,7 +33,7 @@ constructor( // Send anonymous identity to the issuer. issuerSession.send(TransactionRole.PARTICIPANT) observerSessions.forEach { it.send(TransactionRole.OBSERVER) } - val changeOwner = subFlow(ProvideKeyFlow(issuerSession)) + val changeOwner = provideKeyVersion(issuerSession) return subFlow(RedeemFungibleTokensFlow( amount = amount, issuerSession = issuerSession, diff --git a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/redeem/ConfidentialRedeemFungibleTokensFlowHandler.kt b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/redeem/ConfidentialRedeemFungibleTokensFlowHandler.kt index dde2ca85..787dbf69 100644 --- a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/redeem/ConfidentialRedeemFungibleTokensFlowHandler.kt +++ b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/redeem/ConfidentialRedeemFungibleTokensFlowHandler.kt @@ -3,6 +3,7 @@ package com.r3.corda.lib.tokens.workflows.flows.redeem import co.paralleluniverse.fibers.Suspendable import com.r3.corda.lib.ci.workflows.RequestKeyFlow import com.r3.corda.lib.tokens.workflows.internal.flows.finality.TransactionRole +import com.r3.corda.lib.tokens.workflows.internal.flows.requestKeyVersion import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowSession import net.corda.core.utilities.unwrap @@ -15,7 +16,7 @@ class ConfidentialRedeemFungibleTokensFlowHandler(val otherSession: FlowSession) override fun call() { val role = otherSession.receive().unwrap { it } if (role == TransactionRole.PARTICIPANT) { - subFlow(RequestKeyFlow(otherSession)) + requestKeyVersion(otherSession) } // Perform checks that the change owner is well known and belongs to the party that inititated the flow subFlow(RedeemTokensFlowHandler(otherSession)) diff --git a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/redeem/RedeemTokensFlowHandler.kt b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/redeem/RedeemTokensFlowHandler.kt index 54e8c3d6..5b3e0ab1 100644 --- a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/redeem/RedeemTokensFlowHandler.kt +++ b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/redeem/RedeemTokensFlowHandler.kt @@ -1,13 +1,13 @@ package com.r3.corda.lib.tokens.workflows.flows.redeem import co.paralleluniverse.fibers.Suspendable -import com.r3.corda.lib.ci.workflows.SyncKeyMappingFlowHandler import com.r3.corda.lib.tokens.contracts.states.AbstractToken import com.r3.corda.lib.tokens.workflows.internal.checkOwner import com.r3.corda.lib.tokens.workflows.internal.checkSameIssuer import com.r3.corda.lib.tokens.workflows.internal.checkSameNotary import com.r3.corda.lib.tokens.workflows.internal.flows.finality.ObserverAwareFinalityFlowHandler import com.r3.corda.lib.tokens.workflows.internal.flows.finality.TransactionRole +import com.r3.corda.lib.tokens.workflows.internal.flows.syncKeyVersionHandler import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowSession import net.corda.core.flows.SignTransactionFlow @@ -26,7 +26,7 @@ class RedeemTokensFlowHandler(val otherSession: FlowSession) : FlowLogic() if (role == TransactionRole.PARTICIPANT) { // Synchronise all confidential identities, issuer isn't involved in move transactions, so states holders may // not be known to this node. - subFlow(SyncKeyMappingFlowHandler(otherSession)) + syncKeyVersionHandler(otherSession) // Perform all the checks to sign the transaction. subFlow(object : SignTransactionFlow(otherSession) { override fun checkTransaction(stx: SignedTransaction) { diff --git a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/rpc/IssueTokens.kt b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/rpc/IssueTokens.kt index 829bc525..5dec7981 100644 --- a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/rpc/IssueTokens.kt +++ b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/rpc/IssueTokens.kt @@ -54,7 +54,7 @@ class IssueTokensHandler(val otherSession: FlowSession) : FlowLogic() { */ @StartableByService @StartableByRPC -@InitiatingFlow +@InitiatingFlow(version = 2) class ConfidentialIssueTokens @JvmOverloads constructor( diff --git a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/rpc/MoveTokens.kt b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/rpc/MoveTokens.kt index 82096ea5..4b9488c1 100644 --- a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/rpc/MoveTokens.kt +++ b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/rpc/MoveTokens.kt @@ -116,7 +116,7 @@ class MoveNonFungibleTokensHandler(val otherSession: FlowSession) : FlowLogic>, val observers: List, @@ -170,7 +170,7 @@ class ConfidentialMoveFungibleTokensHandler(val otherSession: FlowSession) : Flo */ @StartableByService @StartableByRPC -@InitiatingFlow +@InitiatingFlow(version = 2) class ConfidentialMoveNonFungibleTokens( val partyAndToken: PartyAndToken, val observers: List, diff --git a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/rpc/RedeemTokens.kt b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/rpc/RedeemTokens.kt index 90672b6d..20561e7a 100644 --- a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/rpc/RedeemTokens.kt +++ b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/rpc/RedeemTokens.kt @@ -22,7 +22,7 @@ import net.corda.core.transactions.SignedTransaction @StartableByService @StartableByRPC -@InitiatingFlow +@InitiatingFlow(version = 2) class RedeemFungibleTokens @JvmOverloads constructor( @@ -49,7 +49,7 @@ class RedeemFungibleTokensHandler(val otherSession: FlowSession) : FlowLogic.counterpartyPlatformVersion(session: FlowSession): Int { + val nodeInfo = serviceHub.networkMapCache.getNodeByLegalIdentity(session.counterparty) + return nodeInfo?.platformVersion ?: 0 +} + +// Internal utilities after introducing new confidential identities. To handle different versions. +@Suspendable +internal fun FlowLogic<*>.provideKeyVersion(session: FlowSession): AbstractParty { + val otherFlowVersion = session.getCounterpartyFlowInfo().flowVersion + val topLevelName = FlowLogic.currentTopLevel?.let { it::class.java.canonicalName } ?: "" + // This will work only for initiating flow + return if (otherFlowVersion == 1 || counterpartyPlatformVersion(session) < 5) { + if (!topLevelName.startsWith(INITIATING_TOKENS_FLOW)) { + logger.warn("Your CorDapp is using new confidential identities, but other party flow has version 1 or runs " + + "on platformVersion less than 5. Falling back to old CI." + + "If this is not intended behaviour version your flows with version >= 2") + } + // Old Confidential Identities case + subFlow(RequestConfidentialIdentityFlowHandler(session)) + } else { + // New Confidential Identities + subFlow(ProvideKeyFlow(session)) + } +} + +@Suspendable +internal fun FlowLogic<*>.requestKeyVersion(session: FlowSession): AnonymousParty { + val otherFlowVersion = session.getCounterpartyFlowInfo().flowVersion + val topLevelName = FlowLogic.currentTopLevel?.let { it::class.java.canonicalName } ?: "" + // This will work only for initiating flow + return if (otherFlowVersion == 1 || counterpartyPlatformVersion(session) < 5) { + if (!topLevelName.startsWith(INITIATING_TOKENS_FLOW)) { + logger.warn("Your CorDapp is using new confidential identities, but other party flow has version 1 or runs " + + "on platformVersion less than 5. Falling back to old CI." + + "If this is not intended behaviour version your flows with version >= 2") + } + // Old Confidential Identities case + val key = subFlow(RequestConfidentialIdentityFlow(session)).owningKey + AnonymousParty(key) + } else { + // New Confidential Identities + subFlow(RequestKeyFlow(session)) + } +} + +@Suspendable +internal fun FlowLogic<*>.syncKeyVersionHandler(session: FlowSession) { + val otherFlowVersion = session.getCounterpartyFlowInfo().flowVersion + val topLevelName = FlowLogic.currentTopLevel?.let { it::class.java.canonicalName } ?: "" + // This will work only for initiating flow + if (otherFlowVersion == 1 || counterpartyPlatformVersion(session) < 5) { + if (!topLevelName.startsWith(INITIATING_TOKENS_FLOW)) { + logger.warn("Your CorDapp is using new confidential identities, but other party flow has version 1 or runs " + + "on platformVersion less than 5. Falling back to old CI." + + "If this is not intended behaviour version your flows with version >= 2") + } + // Old Confidential Identities case + subFlow(IdentitySyncFlow.Receive(session)) + } else { + // New Confidential Identities + subFlow(SyncKeyMappingFlowHandler(session)) + } +} + +@Suspendable +internal fun FlowLogic<*>.syncKeyVersion(session: FlowSession, txBuilder: TransactionBuilder) { + val otherFlowVersion = session.getCounterpartyFlowInfo().flowVersion + val topLevelName = FlowLogic.currentTopLevel?.let { it::class.java.canonicalName } ?: "" + // This will work only for initiating flow + if (otherFlowVersion == 1 && counterpartyPlatformVersion(session) < 5) { + if (!topLevelName.startsWith(INITIATING_TOKENS_FLOW)) { + logger.warn("Your CorDapp is using new confidential identities, but other party flow has version 1 and runs " + + "on platformVersion less than 5. Falling back to old CI." + + "If this is not intended behaviour version your flows with version >= 2") + } + // Old Confidential Identities case + subFlow(IdentitySyncFlow.Send(session, txBuilder.toWireTransaction(serviceHub))) + } else { + // New Confidential Identities + subFlow(SyncKeyMappingFlow(session, txBuilder.toWireTransaction(serviceHub))) + } +} + +@CordaSerializable +internal class ConfidentialIdentityRequest + +@CordaSerializable +internal data class IdentityWithSignature(val identity: PartyAndCertificate, val signature: DigitalSignature) + +/** + * Data class used only in the context of asserting that the owner of the private key for the listed key wants to use it + * to represent the named entity. This is paired with an X.509 certificate (which asserts the signing identity says + * the key represents the named entity) and protects against a malicious party incorrectly claiming others' + * keys. + */ +@CordaSerializable +internal data class CertificateOwnershipAssertion(val name: CordaX500Name, val owningKey: PublicKey) + +internal class RequestConfidentialIdentityFlow(val session: FlowSession) : FlowLogic() { + @Suspendable + override fun call(): PartyAndCertificate { + return session.sendAndReceive(ConfidentialIdentityRequest()).unwrap { theirIdentWithSig -> + validateAndRegisterIdentity( + serviceHub = serviceHub, + otherSide = session.counterparty, + theirAnonymousIdentity = theirIdentWithSig.identity, + signature = theirIdentWithSig.signature + ) + } + } +} + +internal class RequestConfidentialIdentityFlowHandler(val otherSession: FlowSession) : FlowLogic() { + @Suspendable + override fun call(): AnonymousParty { + otherSession.receive().unwrap { it } + val ourAnonymousIdentity: PartyAndCertificate = serviceHub.keyManagementService.freshKeyAndCert( + identity = ourIdentityAndCert, + revocationEnabled = false + ) + val data: ByteArray = buildDataToSign(ourAnonymousIdentity) + val signature: DigitalSignature = serviceHub.keyManagementService.sign( + bytes = data, + publicKey = ourAnonymousIdentity.owningKey + ).withoutKey() + val ourIdentityWithSig = IdentityWithSignature(ourAnonymousIdentity, signature) + otherSession.send(ourIdentityWithSig) + return ourAnonymousIdentity.party.anonymise() + } +} + +/** + * Verifies the confidential identity cert chain and if vaild then stores the identity mapping in the [IdentityService]. + */ +@Suspendable +internal fun validateAndRegisterIdentity( + serviceHub: ServiceHub, + otherSide: Party, + theirAnonymousIdentity: PartyAndCertificate, + signature: DigitalSignature +): PartyAndCertificate { + if (theirAnonymousIdentity.name != otherSide.name) { + throw Exception("Certificate subject must match counterparty's well known identity.") + } + try { + theirAnonymousIdentity.owningKey.verify(buildDataToSign(theirAnonymousIdentity), signature) + } catch (ex: SignatureException) { + throw Exception("Signature does not match the expected identity ownership assertion.", ex) + } + // Validate then store their identity so that we can prove the key in the transaction is held by the counterparty. + serviceHub.identityService.verifyAndRegisterIdentity(theirAnonymousIdentity) + return theirAnonymousIdentity +} + +internal fun buildDataToSign(identity: PartyAndCertificate): ByteArray { + return CertificateOwnershipAssertion(identity.name, identity.owningKey).serialize().bytes +} diff --git a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/internal/flows/confidential/AnonymisePartiesFlow.kt b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/internal/flows/confidential/AnonymisePartiesFlow.kt index c1356471..341a6166 100644 --- a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/internal/flows/confidential/AnonymisePartiesFlow.kt +++ b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/internal/flows/confidential/AnonymisePartiesFlow.kt @@ -2,6 +2,7 @@ package com.r3.corda.lib.tokens.workflows.internal.flows.confidential import co.paralleluniverse.fibers.Suspendable import com.r3.corda.lib.ci.workflows.RequestKeyFlow +import com.r3.corda.lib.tokens.workflows.internal.flows.requestKeyVersion import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowSession import net.corda.core.identity.AnonymousParty @@ -25,7 +26,7 @@ class AnonymisePartiesFlow( val party = session.counterparty if (party in parties) { session.send(ActionRequest.CREATE_NEW_KEY) - val anonParty = subFlow(RequestKeyFlow(session)) + val anonParty = requestKeyVersion(session) Pair(party, anonParty) } else { session.send(ActionRequest.DO_NOTHING) diff --git a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/internal/flows/confidential/AnonymisePartiesFlowHandler.kt b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/internal/flows/confidential/AnonymisePartiesFlowHandler.kt index 3db8c7e1..bb086c11 100644 --- a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/internal/flows/confidential/AnonymisePartiesFlowHandler.kt +++ b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/internal/flows/confidential/AnonymisePartiesFlowHandler.kt @@ -2,6 +2,7 @@ package com.r3.corda.lib.tokens.workflows.internal.flows.confidential import co.paralleluniverse.fibers.Suspendable import com.r3.corda.lib.ci.workflows.ProvideKeyFlow +import com.r3.corda.lib.tokens.workflows.internal.flows.provideKeyVersion import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowSession import net.corda.core.utilities.unwrap @@ -11,7 +12,7 @@ class AnonymisePartiesFlowHandler(val otherSession: FlowSession) : FlowLogic().unwrap { it } if (action == ActionRequest.CREATE_NEW_KEY) { - subFlow(ProvideKeyFlow(otherSession)) + provideKeyVersion(otherSession) } } } \ No newline at end of file diff --git a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/internal/schemas/DistributionRecord.kt b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/internal/schemas/DistributionRecord.kt index 633fa559..de9cd34f 100644 --- a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/internal/schemas/DistributionRecord.kt +++ b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/internal/schemas/DistributionRecord.kt @@ -2,10 +2,14 @@ package com.r3.corda.lib.tokens.workflows.internal.schemas import net.corda.core.identity.Party import net.corda.core.schemas.MappedSchema -import net.corda.core.serialization.CordaSerializable import org.hibernate.annotations.Type import java.util.* -import javax.persistence.* +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.GeneratedValue +import javax.persistence.Id +import javax.persistence.Index +import javax.persistence.Table object DistributionRecordSchema @@ -15,7 +19,6 @@ object DistributionRecordSchemaV1 : MappedSchema( mappedTypes = listOf(DistributionRecord::class.java) ) -@CordaSerializable @Entity @Table(name = "distribution_record", indexes = [Index(name = "dist_record_idx", columnList = "linear_id")]) class DistributionRecord( diff --git a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/internal/testflows/TestFlows.kt b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/internal/testflows/TestFlows.kt index 1990e3c6..800390cf 100644 --- a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/internal/testflows/TestFlows.kt +++ b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/internal/testflows/TestFlows.kt @@ -17,7 +17,6 @@ import com.r3.corda.lib.tokens.workflows.internal.flows.distribution.UpdateDistr import com.r3.corda.lib.tokens.workflows.internal.flows.distribution.getDistributionList import com.r3.corda.lib.tokens.workflows.internal.flows.finality.ObserverAwareFinalityFlow import com.r3.corda.lib.tokens.workflows.internal.flows.finality.ObserverAwareFinalityFlowHandler -import com.r3.corda.lib.tokens.workflows.internal.schemas.DistributionRecord import com.r3.corda.lib.tokens.workflows.utilities.getPreferredNotary import com.r3.corda.lib.tokens.workflows.utilities.ourSigningKeys import net.corda.core.contracts.Amount @@ -37,6 +36,7 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.seconds import net.corda.core.utilities.unwrap import java.time.Duration +import java.util.* // This is very simple test flow for DvP. @CordaSerializable @@ -94,11 +94,14 @@ class DvPFlowHandler(val otherSession: FlowSession) : FlowLogic() { } } +@CordaSerializable +data class DistributionRecordSerial(val linearId: UUID, val party: Party) + @StartableByRPC -class GetDistributionList(val housePtr: TokenPointer) : FlowLogic>() { +class GetDistributionList(val housePtr: TokenPointer) : FlowLogic>() { @Suspendable - override fun call(): List { - return getDistributionList(serviceHub, housePtr.pointer.pointer) + override fun call(): List { + return getDistributionList(serviceHub, housePtr.pointer.pointer).map { DistributionRecordSerial(it.linearId, it.party) } } }