mirror of
https://github.com/corda/corda.git
synced 2025-04-04 18:09:17 +00:00
Merge branch 'master' into shams-os-merge-201217
This commit is contained in:
commit
97fac8e988
BIN
docs/source/design/businessNetwork/businessNetwork.png
Normal file
BIN
docs/source/design/businessNetwork/businessNetwork.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 108 KiB |
235
docs/source/design/businessNetwork/design.md
Normal file
235
docs/source/design/businessNetwork/design.md
Normal file
@ -0,0 +1,235 @@
|
||||

|
||||
|
||||
# Business Network design
|
||||
|
||||
DOCUMENT MANAGEMENT
|
||||
---
|
||||
|
||||
## Document Control
|
||||
|
||||
| Title | |
|
||||
| -------------------- | ---------------------------------------- |
|
||||
| Date | 14-Dec-2017 |
|
||||
| Authors | David Lee and Viktor Kolomeyko |
|
||||
| Distribution | Design Review Board, Product Management, Services - Technical (Consulting), Platform Delivery |
|
||||
| Corda target version | Enterprise |
|
||||
| JIRA reference | [Sample Business Network implementation](https://r3-cev.atlassian.net/browse/R3NET-546) |
|
||||
|
||||
## Approvals
|
||||
|
||||
#### Document Sign-off
|
||||
|
||||
| Author | |
|
||||
| ----------------- | ---------------------------------------- |
|
||||
| Reviewer(s) | David Lee, Viktor Kolomeyko, Mike Hearn and James Carlyle |
|
||||
| Final approver(s) | Richard G. Brown |
|
||||
|
||||
|
||||
HIGH LEVEL DESIGN
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Business Networks are introduced in order to segregate Corda Nodes that do not need to transact with each other or indeed
|
||||
even know of each other existence.
|
||||
|
||||
The key concept of Business Network is **Business Network Operator ('BNO') node** as a mean for BNOs to serve reference data to members
|
||||
of their business network(s), as required by CorDapp(s) specific to that business network. This includes allowing BNO
|
||||
nodes to supplement the `NodeInfo`-vending capabilities of the global network map, serving network map entries which are
|
||||
not listed in the global network map. This allows BNOs to ensure strict privacy of their members and control which IP addresses are used in relation to which CorDapps.
|
||||
|
||||
## Background
|
||||
|
||||
Multiple prospective clients of Corda Connect expressed concerns about privacy of the nodes and CorDapps they are going to run.
|
||||
Ability to discover every node in Compatibility Zone through a global network map was not seen as a good design as it
|
||||
will allow competing firms to have an insight into on-boarding of the new clients, businesses and flows.
|
||||
|
||||
In order to address those privacy concerns Business Networks were introduced as a way to partition nodes into groups
|
||||
based on a need-to-know principle.
|
||||
|
||||
This design document reflects on what was previously discussed on this Confluence page
|
||||
[Business Network Membership](https://r3-cev.atlassian.net/wiki/spaces/CCD/pages/131972363/Business+Network+Membership)
|
||||
|
||||
## Scope
|
||||
|
||||
### Goals
|
||||
|
||||
* Allow Corda Connect participants to create private business networks and allow the owner of the network decide which
|
||||
parties will be included into it;
|
||||
|
||||
* Not to allow parties outside of the Business Network to discover the content of the Business Network;
|
||||
|
||||
* Provide a reference implementation for a BNO node which enables the node operator to perform actions stated above.
|
||||
|
||||
### Non-goals
|
||||
|
||||
* To mandate Business Networks. Business Networks are offered as on optional extra which some of the CorDapps may chose to use;
|
||||
|
||||
* To constrain all BNOs to adopt a single consistent CorDapp, for which the design (flows, persistence etc.) is controlled by R3;
|
||||
|
||||
* To define inclusion/exclusion or authorisation criteria for admitting a node into a Business Network.
|
||||
|
||||
## Timeline
|
||||
|
||||
This is a long-term solution initially aimed to support Project Agent go-live implementation in Q2 2018.
|
||||
|
||||
## Requirements
|
||||
|
||||
See [Identity high-level requirements](https://r3-cev.atlassian.net/wiki/spaces/CCD/pages/131746442/Identity+high-level+requirements)
|
||||
for the full set of requirements discussed to date.
|
||||
|
||||
Given the following roles:
|
||||
|
||||
| Role name | Definition |
|
||||
| --------------------------- | ---------- |
|
||||
| Compatibility Zone Operator | Responsible for administering the overall Compatibility Zone. Usually R3. |
|
||||
| Business Network Operator | Responsible for a specific business network within the Corda Connect Compatibility Zone. |
|
||||
| Node User | Uses a Corda node to carry out transactions on their own behalf. In the case of a self-owned deployment, the Node User can also be the Node Operator. |
|
||||
| Node Operator | Responsible for management of a particular corda node (deployment, configuration etc.) on behalf of the Node User. |
|
||||
|
||||
The following requirements are addressed as follows:
|
||||
|
||||
| Role | Requirement | How design satisfies requirement |
|
||||
| --------------------------- | -------------------------------------------------------------------------------------------------- | -------------------------------- |
|
||||
| Compatibility Zone Operator | To support multiple business networks operating across the CZ. | Current design supports any number of membership lists, each of which defines a business network. |
|
||||
| Compatibility Zone Operator | To permission a Business Network Operator to provision my services to its members. | Individual CordApps will be responsible for checking Business Network membership constrains. |
|
||||
| Compatibility Zone Operator | To revoke the ability for a specific Business Network Operator to operate within my CZ. | The membership lists are configurable and business networks can have their membership lists removed when needed. |
|
||||
| Business Network Operator | To be able to permission nodes to access my Business Network. | The BNO will ensure that the CorDapp is designed to keep each node's copy of the membership list up-to-date on a timely basis as new members are added to the business network. |
|
||||
| Business Network Operator | To revoke access to my business network by any member. | The BNO will ensure that the CorDapp is designed to keep each node's copy of the membership list up-to-date on a timely basis as members are removed from the business network. The BNO will be able to decide, through the timing of the membership list checks in the CorDapp flow design, whether in-flight flows should be exited upon revocation, or simply to avoid starting new flows. |
|
||||
| Business Network Operator | To protect knowledge of the membership of my Business Network from parties who don't need to know. | The BNO node upon receiving an enquiry to deliver membership list will first check identity of the caller against membership list. If the identity does not belong to the membership list the content of the membership will not be served. Optionally, Business Network membership list may be made available for discovery by the nodes outside of membership list. |
|
||||
| Business Network Operator | To help members of my Business Network to discover each other. | The BNO node will be able to serve the full list of membership participants at a minimum and may also have an API to perform the fuzzy matches. |
|
||||
| Node User | To request the termination of my identity within the CZ / business network. | A member may ask a BNO to exclude them from a business network, and it will be in the BNO's commercial interest to do so. They can be obliged to do so reliably under the terms of the R3Net agreement. |
|
||||
| Node Operator | To control the IP address on which my node receives messages in relation to a specific CorDapp. | Nodes may choose to publish different `NodeInfos` to different business networks. Business network-specific CorDapps will ensure that that the `NodeInfo` served by the business network is used for addressing that node. |
|
||||
|
||||
## Design Decisions
|
||||
|
||||
* Per [New Network Map](https://r3-cev.atlassian.net/wiki/spaces/AWG/pages/127710793/New+Network+Map):
|
||||
* R3 will serve a file containing NetworkParameters to all members of the Corda Connect Compatibility Zone, made publicly available via CDN.
|
||||
*NetworkParameters are only served by the network map and cannot be overwritten by BNOs*.
|
||||
* The network map file will continue to serve references to downloadable entries
|
||||
(Signed `NodeInfo` file with the node's certificate chain, IP addresses etc. for a given node) for any nodes which wish to be publicly discoverable by all CZ members.
|
||||
|
||||
* Nodes **may individually choose** to publish to the global network map by posting their entry to an R3-provided endpoint,
|
||||
but are **not required to do so**.
|
||||
|
||||
* BNO nodes expose a range of services to their membership over normal Corda flows (via a custom BNO CorDapp deployed on their BNO node).
|
||||
The BNO is free to define whatever services apply in the context of their business network; these will typically include:
|
||||
* Managing requests to join/leave the business network;
|
||||
* Accepting uploads of network map entries (signed `NodeInfos`) from business network members.
|
||||
*A node may choose to publish different `NodeInfos` to different business networks in order to indicate that they want
|
||||
to be addressed on a given IP in relation to a given set of traffic.*
|
||||
* Vending a membership list of distinguished names (DNs) that a given party within the business network is allowed
|
||||
to see / transact with, for use in 'address book' / 'drop-down' type UI functionality.
|
||||
*The structure of the membership list may be tailored according to the needs of the BNO and, according to the needs
|
||||
of the CorDapp, it may either be expressed as a subclass of the `NetworkMapCache` itself or as a standalone data structure in its own right.*
|
||||
* Vending `NodeInfos` in response to lookup requests by a business network member for a particular DN
|
||||
(more sophisticated query options may also be offered), which are integrated into the node's local `NetworkMapCache`.
|
||||
*A node may acquire multiple `NodeInfos` in relation to the same DN via its association with multiple business networks
|
||||
and needs a mechanism to (a) retain distinctions between them, (b) when initiating a flow by a specific CorDapp,
|
||||
preferentially address messages to the IP(s) referenced in the `NodeInfo` served by the associated business network.*
|
||||
|
||||
* For each **Business Network-specific CorDapp**, the CorDapp developer will include features to restrict such that the
|
||||
CorDapp cannot be used to transact with non-members. Namely:
|
||||
* any 'address-book' features in that CorDapp are filtered according to the membership list;
|
||||
* any `InitiatedBy` flow will first check that the initiating party is on the membership list, and throw a `FlowException` if it is not.
|
||||
|
||||
## Target Solution
|
||||
|
||||
*Illustration of services exposed by both network map and BNO node. Interface names shown are indicative and may be changed as needed.*
|
||||
|
||||

|
||||
|
||||
## Complementary solutions
|
||||
|
||||
* No requirement to change the Network Map design currently proposed in
|
||||
[New Network Map](https://r3-cev.atlassian.net/wiki/spaces/AWG/pages/127710793/New+Network+Map);
|
||||
* Data model for `NetworkMapCache` needs to be able to manage potential conflicts between `NodeInfos` downloaded from different
|
||||
network maps, and supply the correct one when needed.
|
||||
|
||||
## Final recommendation
|
||||
|
||||
* Proceed with reference implementation as detailed by [this Jira](https://r3-cev.atlassian.net/browse/R3NET-546).
|
||||
* Provision of mechanisms to bind accessibility of CorDapp flows to named parties on a membership list.
|
||||
|
||||
TECHNICAL DESIGN (this section is WIP)
|
||||
---
|
||||
|
||||
## Interfaces
|
||||
|
||||
* No impact on Public APIs
|
||||
* Internal APIs impacted
|
||||
* Modules impacted
|
||||
|
||||
* Illustrate with Software Component diagrams
|
||||
|
||||
## Functional
|
||||
|
||||
* UI requirements
|
||||
|
||||
* Illustrate with UI Mockups and/or Wireframes
|
||||
|
||||
* (Subsystem) Components descriptions and interactions)
|
||||
|
||||
Consider and list existing impacted components and services within Corda:
|
||||
|
||||
* Doorman
|
||||
* Network Map
|
||||
* Public API's (ServiceHub, RPCOps)
|
||||
* Vault
|
||||
* Notaries
|
||||
* Identity services
|
||||
* Flow framework
|
||||
* Attachments
|
||||
* Core data structures, libraries or utilities
|
||||
* Testing frameworks
|
||||
* Pluggable infrastructure: DBs, Message Brokers, LDAP
|
||||
|
||||
* Data model & serialization impact and changes required
|
||||
|
||||
* Illustrate with ERD diagrams
|
||||
|
||||
* Infrastructure services: persistence (schemas), messaging
|
||||
|
||||
## Non-Functional
|
||||
|
||||
* Performance
|
||||
* Scalability
|
||||
* High Availability
|
||||
|
||||
## Operational
|
||||
|
||||
* Deployment
|
||||
|
||||
* Versioning
|
||||
|
||||
* Maintenance
|
||||
|
||||
* Upgradability, migration
|
||||
|
||||
* Management
|
||||
|
||||
* Audit, alerting, monitoring, backup/recovery, archiving
|
||||
|
||||
## Security
|
||||
|
||||
* Data privacy
|
||||
* Authentication
|
||||
* Access control
|
||||
|
||||
## Software Development Tools and Programming Standards to be adopted.
|
||||
|
||||
* languages
|
||||
* frameworks
|
||||
* 3rd party libraries
|
||||
* architectural / design patterns
|
||||
* supporting tools
|
||||
|
||||
## Testability
|
||||
|
||||
* Unit
|
||||
* Integration
|
||||
* Smoke
|
||||
* Non-functional (performance)
|
||||
|
||||
APPENDICES
|
||||
---
|
@ -23,7 +23,7 @@ import net.corda.core.utilities.loggerFor
|
||||
import net.corda.nodeapi.internal.crypto.*
|
||||
import net.corda.nodeapi.internal.network.NetworkParameters
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.serialization.KRYO_P2P_CONTEXT
|
||||
import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT
|
||||
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
|
||||
import net.corda.nodeapi.internal.serialization.amqp.AMQPClientSerializationScheme
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||
@ -301,7 +301,7 @@ fun main(args: Array<String>) {
|
||||
}
|
||||
|
||||
private fun initialiseSerialization() {
|
||||
val context = KRYO_P2P_CONTEXT
|
||||
val context = AMQP_P2P_CONTEXT
|
||||
nodeSerializationEnv = SerializationEnvironmentImpl(
|
||||
SerializationFactoryImpl().apply {
|
||||
registerScheme(AMQPClientSerializationScheme())
|
||||
|
@ -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
|
@ -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. */
|
@ -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 {
|
@ -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
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
@ -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()
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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")
|
@ -1,4 +1,4 @@
|
||||
package net.corda.sample.businessnetwork.membership
|
||||
package net.corda.sample.businessnetwork.membership.flow
|
||||
|
||||
import net.corda.core.identity.AbstractParty
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
/**
|
||||
|
@ -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)
|
||||
}
|
@ -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}")
|
||||
}
|
||||
|
||||
@ -185,6 +195,7 @@ class ExplorerSimulation(private val options: OptionSet) {
|
||||
private fun startErrorFlowsSimulation() {
|
||||
println("Running flows with errors simulation mode ...")
|
||||
setUpRPC()
|
||||
notary = aliceNode.rpc.notaryIdentities().first()
|
||||
val eventGenerator = ErrorFlowsEventGenerator(
|
||||
parties = parties.map { it.first },
|
||||
notary = notary,
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user