Wave 3 of Business Network changes. (#193)

* R3NET-546: Re-arrange independent flows into separate packages. Functionally this is a NOP change.

* R3NET-546: Start BNO as a separate Corda node and improve GUI experience for IOU.

* R3NET-546: Move all the membership checks to the Business Network Owner node side, creating "InitiatedBy" flows as necessary.

* R3NET-546: Make MembershipViolationException AMQP serializable.

* R3NET-546: Improve GUI error reporting in case of membership violation.

* R3NET-546: Code changes following review by: @shamsasari

* R3NET-546: Code changes following review by: @shamsasari

* R3NET-546: Added a dedicated InvalidMembershipListNameException.
This commit is contained in:
Viktor Kolomeyko 2017-12-20 12:01:32 +00:00 committed by GitHub
parent 246142173d
commit c0e997c1dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 181 additions and 112 deletions

View File

@ -1,4 +1,4 @@
package net.corda.sample.businessnetwork
package net.corda.sample.businessnetwork.iou
import net.corda.core.contracts.CommandData
import net.corda.core.contracts.Contract

View File

@ -1,4 +1,4 @@
package net.corda.sample.businessnetwork
package net.corda.sample.businessnetwork.iou
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.Command
@ -9,19 +9,19 @@ import net.corda.core.identity.Party
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.ProgressTracker
import net.corda.sample.businessnetwork.membership.MembershipAware
import net.corda.sample.businessnetwork.membership.CheckMembershipFlow
import net.corda.sample.businessnetwork.membership.CheckMembershipResult
import net.corda.sample.businessnetwork.membership.flow.CheckMembershipFlow
import net.corda.sample.businessnetwork.membership.flow.CheckMembershipResult
import kotlin.reflect.jvm.jvmName
@InitiatingFlow
@StartableByRPC
class IOUFlow(val iouValue: Int,
val otherParty: Party) : FlowLogic<SignedTransaction>(), MembershipAware {
val otherParty: Party) : FlowLogic<SignedTransaction>() {
companion object {
// TODO: Derive membership name from CorDapp config.
val allowedMembershipName =
CordaX500Name("AliceBobMembershipList", "AliceBob", "Washington", "US")
CordaX500Name("AliceBobMembershipList", "Oslo", "NO")
}
/** The progress tracker provides checkpoints indicating the progress of the flow to observers. */

View File

@ -1,4 +1,4 @@
package net.corda.sample.businessnetwork
package net.corda.sample.businessnetwork.iou
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.requireThat
@ -7,14 +7,14 @@ import net.corda.core.flows.FlowSession
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.SignTransactionFlow
import net.corda.core.transactions.SignedTransaction
import net.corda.sample.businessnetwork.membership.MembershipAware
import net.corda.sample.businessnetwork.membership.flow.CheckMembershipFlow
import net.corda.sample.businessnetwork.membership.flow.CheckMembershipResult
@InitiatedBy(IOUFlow::class)
class IOUFlowResponder(val otherPartySession: FlowSession) : FlowLogic<Unit>(), MembershipAware {
class IOUFlowResponder(val otherPartySession: FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
otherPartySession.counterparty.checkMembership(IOUFlow.allowedMembershipName, this)
check(subFlow(CheckMembershipFlow(otherPartySession.counterparty, IOUFlow.allowedMembershipName)) == CheckMembershipResult.PASS)
subFlow(object : SignTransactionFlow(otherPartySession, SignTransactionFlow.tracker()) {
override fun checkTransaction(stx: SignedTransaction) = requireThat {

View File

@ -1,4 +1,4 @@
package net.corda.sample.businessnetwork
package net.corda.sample.businessnetwork.iou
import net.corda.core.contracts.ContractState
import net.corda.core.identity.Party

View File

@ -1,38 +0,0 @@
package net.corda.sample.businessnetwork.membership
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSession
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.InitiatingFlow
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.unwrap
@CordaSerializable
enum class CheckMembershipResult {
PASS,
FAIL
}
@InitiatingFlow
class CheckMembershipFlow(private val otherParty: Party, private val membershipName: CordaX500Name) : FlowLogic<CheckMembershipResult>(), MembershipAware {
@Suspendable
override fun call(): CheckMembershipResult {
otherParty.checkMembership(membershipName, this)
// This will trigger CounterpartyCheckMembershipFlow
val untrustworthyData = initiateFlow(otherParty).sendAndReceive<CheckMembershipResult>(membershipName)
return untrustworthyData.unwrap { it }
}
}
@InitiatedBy(CheckMembershipFlow::class)
class CounterpartyCheckMembershipFlow(private val otherPartySession: FlowSession) : FlowLogic<Unit>(), MembershipAware {
@Suspendable
override fun call() {
val membershipName = otherPartySession.receive<CordaX500Name>().unwrap { it }
otherPartySession.counterparty.checkMembership(membershipName, this)
otherPartySession.send(CheckMembershipResult.PASS)
}
}

View File

@ -1,26 +0,0 @@
package net.corda.sample.businessnetwork.membership
import net.corda.core.flows.FlowException
import net.corda.core.flows.FlowLogic
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.node.ServiceHub
import net.corda.sample.businessnetwork.membership.internal.MembershipListProvider
interface MembershipAware {
/**
* Checks that party has at least one common membership list with current node.
* TODO: This functionality ought to be moved into a dedicated CordaService.
*/
fun <T> AbstractParty.checkMembership(membershipName: CordaX500Name, initiatorFlow: FlowLogic<T>) {
val membershipList = getMembershipList(membershipName, initiatorFlow.serviceHub)
if (this !in membershipList) {
val msg = "'$this' doesn't belong to membership list: ${membershipName.commonName}"
throw MembershipViolationException(msg)
}
}
fun getMembershipList(listName: CordaX500Name, serviceHub: ServiceHub): MembershipList = MembershipListProvider.obtainMembershipList(listName, serviceHub.networkMapCache)
}
class MembershipViolationException(msg: String) : FlowException(msg)

View File

@ -1,16 +0,0 @@
package net.corda.sample.businessnetwork.membership
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.CordaX500Name
/**
* Flow to obtain content of the membership lists this node belongs to.
*/
@StartableByRPC
class ObtainMembershipListContentFlow(private val membershipListName: CordaX500Name) : FlowLogic<Set<AbstractParty>>(), MembershipAware {
@Suspendable
override fun call(): Set<AbstractParty> = getMembershipList(membershipListName, serviceHub).content()
}

View File

@ -0,0 +1,42 @@
package net.corda.sample.businessnetwork.membership.flow
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSession
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.InitiatingFlow
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.unwrap
@CordaSerializable
enum class CheckMembershipResult {
PASS,
FAIL
}
@InitiatingFlow
class CheckMembershipFlow(private val otherParty: Party, private val membershipName: CordaX500Name) : FlowLogic<CheckMembershipResult>() {
@Suspendable
override fun call(): CheckMembershipResult {
val bnoParty = serviceHub.networkMapCache.getPeerByLegalName(membershipName)
return if (bnoParty != null) {
// This will trigger CounterpartyCheckMembershipFlow
val untrustworthyData = initiateFlow(bnoParty).sendAndReceive<CheckMembershipResult>(otherParty)
untrustworthyData.unwrap { it }
} else {
throw InvalidMembershipListNameException(membershipName)
}
}
}
@InitiatedBy(CheckMembershipFlow::class)
class OwnerSideCheckMembershipFlow(private val initiatingPartySession: FlowSession) : FlowLogic<Unit>(), MembershipAware {
@Suspendable
override fun call() {
val partyToCheck = initiatingPartySession.receive<Party>().unwrap { it }
partyToCheck.checkMembership(ourIdentity.name, this)
initiatingPartySession.send(CheckMembershipResult.PASS)
}
}

View File

@ -0,0 +1,32 @@
package net.corda.sample.businessnetwork.membership.flow
import net.corda.core.flows.FlowException
import net.corda.core.flows.FlowLogic
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.node.ServiceHub
import net.corda.sample.businessnetwork.membership.internal.MembershipListProvider
import org.slf4j.LoggerFactory
interface MembershipAware {
/**
* Checks that party is included into the specified membership list.
*/
fun <T> AbstractParty.checkMembership(membershipName: CordaX500Name, initiatorFlow: FlowLogic<T>) {
LoggerFactory.getLogger(javaClass).debug("Checking membership of party '${this.nameOrNull()}' in membership list '$membershipName'")
val membershipList = getMembershipList(membershipName, initiatorFlow.serviceHub)
if (this !in membershipList) {
val msg = "'$this' doesn't belong to membership list: ${membershipName.organisation}"
throw MembershipViolationException(msg)
}
}
fun getMembershipList(listName: CordaX500Name, serviceHub: ServiceHub): MembershipList {
LoggerFactory.getLogger(javaClass).debug("Obtaining membership list for name '$listName'")
return MembershipListProvider.obtainMembershipList(listName, serviceHub.networkMapCache)
}
}
class MembershipViolationException(val msg: String) : FlowException(msg)
class InvalidMembershipListNameException(val membershipListName: CordaX500Name) : FlowException("Business Network owner node not found for: $membershipListName")

View File

@ -1,4 +1,4 @@
package net.corda.sample.businessnetwork.membership
package net.corda.sample.businessnetwork.membership.flow
import net.corda.core.identity.AbstractParty

View File

@ -0,0 +1,34 @@
package net.corda.sample.businessnetwork.membership.flow
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.flows.*
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.utilities.unwrap
/**
* Flow to obtain content of the membership lists this node belongs to.
*/
@StartableByRPC
@InitiatingFlow
class ObtainMembershipListContentFlow(private val membershipListName: CordaX500Name) : FlowLogic<Set<AbstractParty>>() {
@Suspendable
override fun call(): Set<AbstractParty> {
val bnoParty = serviceHub.networkMapCache.getPeerByLegalName(membershipListName) ?:
throw InvalidMembershipListNameException(membershipListName)
val untrustworthyData = initiateFlow(bnoParty).receive<Set<AbstractParty>>()
return untrustworthyData.unwrap { it }
}
}
@InitiatedBy(ObtainMembershipListContentFlow::class)
class OwnerSideObtainMembershipListContentFlow(private val initiatingPartySession: FlowSession) : FlowLogic<Unit>(), MembershipAware {
@Suspendable
override fun call() {
// Checking whether the calling party is a member. If not it is not even in position to enquire about membership list content.
initiatingPartySession.counterparty.checkMembership(ourIdentity.name, this)
val membershipListContent: Set<AbstractParty> = getMembershipList(ourIdentity.name, serviceHub).content()
initiatingPartySession.send(membershipListContent)
}
}

View File

@ -4,7 +4,7 @@ import com.opencsv.CSVReaderBuilder
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.node.services.NetworkMapCache
import net.corda.sample.businessnetwork.membership.MembershipList
import net.corda.sample.businessnetwork.membership.flow.MembershipList
import java.io.InputStream
/**

View File

@ -2,9 +2,9 @@ package net.corda.sample.businessnetwork.membership.internal
import net.corda.core.identity.CordaX500Name
import net.corda.core.node.services.NetworkMapCache
import net.corda.sample.businessnetwork.membership.MembershipList
import net.corda.sample.businessnetwork.membership.flow.MembershipList
object MembershipListProvider {
fun obtainMembershipList(listName: CordaX500Name, networkMapCache: NetworkMapCache): MembershipList =
CsvMembershipList(MembershipListProvider::class.java.getResourceAsStream("${listName.commonName}.csv"), networkMapCache)
CsvMembershipList(MembershipListProvider::class.java.getResourceAsStream("${listName.organisation}.csv"), networkMapCache)
}

View File

@ -14,8 +14,8 @@ import net.corda.core.messaging.FlowHandle
import net.corda.core.messaging.startFlow
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.sample.businessnetwork.IOUFlow
import net.corda.sample.businessnetwork.membership.ObtainMembershipListContentFlow
import net.corda.sample.businessnetwork.iou.IOUFlow
import net.corda.sample.businessnetwork.membership.flow.ObtainMembershipListContentFlow
import net.corda.finance.GBP
import net.corda.finance.USD
import net.corda.finance.contracts.asset.Cash
@ -32,8 +32,14 @@ import net.corda.testing.driver.PortAllocation
import net.corda.testing.driver.driver
import java.time.Instant
import java.util.*
import kotlin.reflect.KClass
class ExplorerSimulation(private val options: OptionSet) {
private companion object {
fun packagesOfClasses(vararg classes: KClass<*>): List<String> = classes.map { it.java.`package`.name }
}
private val user = User("user1", "test", permissions = setOf(
startFlow<CashPaymentFlow>(),
startFlow<CashConfigDataFlow>(),
@ -52,6 +58,7 @@ class ExplorerSimulation(private val options: OptionSet) {
private lateinit var bobNode: NodeHandle
private lateinit var issuerNodeGBP: NodeHandle
private lateinit var issuerNodeUSD: NodeHandle
private lateinit var bnoNode: NodeHandle
private lateinit var notary: Party
private val RPCConnections = ArrayList<CordaRPCConnection>()
@ -65,7 +72,8 @@ class ExplorerSimulation(private val options: OptionSet) {
fun startDemoNodes() {
val portAllocation = PortAllocation.Incremental(20000)
driver(portAllocation = portAllocation, extraCordappPackagesToScan = listOf("net.corda.finance", IOUFlow::class.java.`package`.name),
driver(portAllocation = portAllocation,
extraCordappPackagesToScan = packagesOfClasses(CashPaymentFlow::class, IOUFlow::class, ObtainMembershipListContentFlow::class),
isDebug = true, waitForAllNodesToFinish = true, jmxPolicy = JmxPolicy(true)) {
// TODO : Supported flow should be exposed somehow from the node instead of set of ServiceInfo.
val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user))
@ -76,14 +84,16 @@ class ExplorerSimulation(private val options: OptionSet) {
customOverrides = mapOf("issuableCurrencies" to listOf("GBP")))
val issuerUSD = startNode(providedName = usaBankName, rpcUsers = listOf(manager),
customOverrides = mapOf("issuableCurrencies" to listOf("USD")))
val bno = startNode(providedName = IOUFlow.allowedMembershipName, rpcUsers = listOf(user))
notaryNode = defaultNotaryNode.get()
aliceNode = alice.get()
bobNode = bob.get()
issuerNodeGBP = issuerGBP.get()
issuerNodeUSD = issuerUSD.get()
bnoNode = bno.get()
arrayOf(notaryNode, aliceNode, bobNode, issuerNodeGBP, issuerNodeUSD).forEach {
arrayOf(notaryNode, aliceNode, bobNode, issuerNodeGBP, issuerNodeUSD, bnoNode).forEach {
println("${it.nodeInfo.legalIdentities.first()} started on ${it.configuration.rpcAddress}")
}

View File

@ -9,8 +9,8 @@ import net.corda.client.jfx.utils.map
import net.corda.core.identity.AbstractParty
import net.corda.core.messaging.startFlow
import net.corda.core.utilities.getOrThrow
import net.corda.sample.businessnetwork.IOUFlow
import net.corda.sample.businessnetwork.membership.ObtainMembershipListContentFlow
import net.corda.sample.businessnetwork.iou.IOUFlow
import net.corda.sample.businessnetwork.membership.flow.ObtainMembershipListContentFlow
class MembershipListModel {
private val proxy by observableValue(NodeMonitorModel::proxyObservable)

View File

@ -83,9 +83,10 @@ class Network : CordaView() {
.map { it.stateAndRef.state.data }.getParties()
val outputParties = it.transaction.tx.outputStates.observable().getParties()
val signingParties = it.transaction.sigs.map { it.by.toKnownParty() }
// Input parties fire a bullets to all output parties, and to the signing parties. !! This is a rough guess of how the message moves in the network.
// Input parties fire a bullets to all output parties, then to the signing parties and then signing parties to output parties.
// !! This is a rough guess of how the message moves in the network.
// TODO : Expose artemis queue to get real message information.
inputParties.cross(outputParties) + inputParties.cross(signingParties)
inputParties.cross(outputParties) + inputParties.cross(signingParties) + signingParties.cross(outputParties)
}
}

View File

@ -28,7 +28,7 @@ import net.corda.core.identity.AbstractParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.utilities.toBase58String
import net.corda.sample.businessnetwork.IOUState
import net.corda.sample.businessnetwork.iou.IOUState
import net.corda.explorer.AmountDiff
import net.corda.explorer.formatters.AmountFormatter
import net.corda.explorer.formatters.Formatter

View File

@ -2,10 +2,17 @@ package net.corda.explorer.views.cordapps.iou
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView
import javafx.beans.binding.Bindings
import javafx.geometry.Pos
import javafx.scene.input.MouseButton
import javafx.scene.layout.BorderPane
import net.corda.client.jfx.model.TransactionDataModel
import net.corda.client.jfx.model.observableList
import net.corda.client.jfx.utils.map
import net.corda.core.utilities.Try
import net.corda.explorer.model.CordaView
import net.corda.explorer.model.CordaWidget
import net.corda.sample.businessnetwork.iou.IOUState
import net.corda.explorer.model.MembershipListModel
import tornadofx.*
@ -13,6 +20,7 @@ class IOUViewer : CordaView("IOU") {
// Inject UI elements.
override val root: BorderPane by fxml()
override val icon: FontAwesomeIcon = FontAwesomeIcon.CHEVRON_CIRCLE_RIGHT
override val widgets = listOf(CordaWidget(title, IOUWidget(), icon)).observable()
// Wire up UI
init {
@ -28,8 +36,22 @@ class IOUViewer : CordaView("IOU") {
}
fun isEnabledForNode(): Boolean = Try.on {
// Assuming if the model can be initialized - the CorDapp is installed
val allParties = MembershipListModel().allParties
allParties[0]
}.isSuccess
// Assuming if the model can be initialized - the CorDapp is installed
val allParties = MembershipListModel().allParties
allParties[0]
}.isSuccess
private class IOUWidget : BorderPane() {
private val partiallyResolvedTransactions by observableList(TransactionDataModel::partiallyResolvedTransactions)
private val iouTransactions = partiallyResolvedTransactions.filtered { t -> t.transaction.tx.outputs.any({ ts -> ts.data is IOUState }) }
init {
right {
label {
textProperty().bind(Bindings.size(iouTransactions).map(Number::toString))
BorderPane.setAlignment(this, Pos.BOTTOM_RIGHT)
}
}
}
}
}

View File

@ -1,6 +1,7 @@
package net.corda.explorer.views.cordapps.iou
import com.google.common.base.Splitter
import com.sun.javafx.collections.ImmutableObservableList
import javafx.beans.binding.Bindings
import javafx.beans.binding.BooleanBinding
import javafx.collections.FXCollections
@ -15,13 +16,15 @@ import net.corda.client.jfx.model.*
import net.corda.client.jfx.utils.isNotNull
import net.corda.client.jfx.utils.map
import net.corda.core.flows.FlowException
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.messaging.FlowHandle
import net.corda.core.messaging.startFlow
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.getOrThrow
import net.corda.sample.businessnetwork.IOUFlow
import net.corda.core.utilities.loggerFor
import net.corda.sample.businessnetwork.iou.IOUFlow
import net.corda.explorer.formatters.PartyNameFormatter
import net.corda.explorer.model.MembershipListModel
import net.corda.explorer.views.bigDecimalFormatter
@ -46,7 +49,12 @@ class NewTransaction : Fragment() {
fun show(window: Window) {
// Every time re-query from the server side
val elementsFromServer = MembershipListModel().allParties
val elementsFromServer = try {
MembershipListModel().allParties
} catch (ex: Exception) {
loggerFor<NewTransaction>().error("Unexpected error fetching membership list content", ex)
ImmutableObservableList<AbstractParty>()
}
partyBChoiceBox.apply {
items = FXCollections.observableList(parties.map { it.chooseIdentityAndCert() }).filtered { elementsFromServer.contains(it.party) }.sorted()