Clean up IssuerFlow

* Switch to using anonymous party as recipient
* Enable anonymisation for issuance as well as move in issuer flows.
* Pass notary into issuer flow rather than taking a notary at random from the network map.
* Enable anonymisation in Bank of Corda RPC test
* Parameterize issuer flow tests into anonymous and deanonymised versions
This commit is contained in:
Ross Nicoll 2017-07-12 15:31:33 +01:00
parent f6aa672215
commit 773aa28873
14 changed files with 95 additions and 44 deletions

View File

@ -9,6 +9,7 @@ import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowInitiator
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StateMachineRunId
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.node.NodeInfo
import net.corda.core.node.services.NetworkMapCache
@ -303,6 +304,13 @@ interface CordaRPCOps : RPCOps {
/** Enumerates the class names of the flows that this node knows about. */
fun registeredFlows(): List<String>
/**
* Returns a node's identity from the network map cache, where known.
*
* @return the node info if available.
*/
fun nodeIdentityFromParty(party: AbstractParty): NodeInfo?
}
inline fun <reified T : ContractState> CordaRPCOps.vaultQueryBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(),
@ -371,6 +379,17 @@ inline fun <T : Any, A, B, C, D, E, reified R : FlowLogic<T>> CordaRPCOps.startF
arg4: E
): FlowHandle<T> = startFlowDynamic(R::class.java, arg0, arg1, arg2, arg3, arg4)
inline fun <T : Any, A, B, C, D, E, F, reified R : FlowLogic<T>> CordaRPCOps.startFlow(
@Suppress("UNUSED_PARAMETER")
flowConstructor: (A, B, C, D, E, F) -> R,
arg0: A,
arg1: B,
arg2: C,
arg3: D,
arg4: E,
arg5: F
): FlowHandle<T> = startFlowDynamic(R::class.java, arg0, arg1, arg2, arg3, arg4, arg5)
/**
* Same again, except this time with progress-tracking enabled.
*/

View File

@ -24,6 +24,7 @@ object IssuerFlow {
data class IssuanceRequestState(val amount: Amount<Currency>,
val issueToParty: Party,
val issuerPartyRef: OpaqueBytes,
val notaryParty: Party,
val anonymous: Boolean)
/**
@ -39,11 +40,12 @@ object IssuerFlow {
val issueToParty: Party,
val issueToPartyRef: OpaqueBytes,
val issuerBankParty: Party,
val notaryParty: Party,
val anonymous: Boolean) : FlowLogic<AbstractCashFlow.Result>() {
@Suspendable
@Throws(CashException::class)
override fun call(): AbstractCashFlow.Result {
val issueRequest = IssuanceRequestState(amount, issueToParty, issueToPartyRef, anonymous)
val issueRequest = IssuanceRequestState(amount, issueToParty, issueToPartyRef, notaryParty, anonymous)
return sendAndReceive<AbstractCashFlow.Result>(issuerBankParty, issueRequest).unwrap { res ->
val tx = res.stx.tx
val expectedAmount = Amount(amount.quantity, Issued(issuerBankParty.ref(issueToPartyRef), amount.token))
@ -86,7 +88,7 @@ object IssuerFlow {
it
}
// TODO: parse request to determine Asset to issue
val txn = issueCashTo(issueRequest.amount, issueRequest.issueToParty, issueRequest.issuerPartyRef, issueRequest.anonymous)
val txn = issueCashTo(issueRequest.amount, issueRequest.issueToParty, issueRequest.issuerPartyRef, issueRequest.notaryParty, issueRequest.anonymous)
progressTracker.currentStep = SENDING_CONFIRM
send(otherParty, txn)
return txn.stx
@ -96,13 +98,12 @@ object IssuerFlow {
private fun issueCashTo(amount: Amount<Currency>,
issueTo: Party,
issuerPartyRef: OpaqueBytes,
notaryParty: Party,
anonymous: Boolean): AbstractCashFlow.Result {
// TODO: pass notary in as request parameter
val notaryParty = serviceHub.networkMapCache.notaryNodes[0].notaryIdentity
// invoke Cash subflow to issue Asset
progressTracker.currentStep = ISSUING
val issueRecipient = serviceHub.myInfo.legalIdentity
val issueCashFlow = CashIssueFlow(amount, issuerPartyRef, issueRecipient, notaryParty, anonymous = false)
val issueCashFlow = CashIssueFlow(amount, issuerPartyRef, issueRecipient, notaryParty, anonymous)
val issueTx = subFlow(issueCashFlow)
// NOTE: issueCashFlow performs a Broadcast (which stores a local copy of the txn to the ledger)
// short-circuit when issuing to self

View File

@ -5,6 +5,7 @@ import net.corda.contracts.asset.sumCashBy
import net.corda.core.contracts.*
import net.corda.core.flows.FlowException
import net.corda.core.flows.FlowLogic
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party
import net.corda.core.node.NodeInfo
@ -49,14 +50,14 @@ object TwoPartyTradeFlow {
data class SellerTradeInfo(
val assetForSale: StateAndRef<OwnableState>,
val price: Amount<Currency>,
val sellerOwnerKey: PublicKey
val sellerOwner: AbstractParty
)
open class Seller(val otherParty: Party,
val notaryNode: NodeInfo,
val assetToSell: StateAndRef<OwnableState>,
val price: Amount<Currency>,
val myKey: PublicKey,
val me: AbstractParty,
override val progressTracker: ProgressTracker = Seller.tracker()) : FlowLogic<SignedTransaction>() {
companion object {
@ -75,7 +76,7 @@ object TwoPartyTradeFlow {
override fun call(): SignedTransaction {
progressTracker.currentStep = AWAITING_PROPOSAL
// Make the first message we'll send to kick off the flow.
val hello = SellerTradeInfo(assetToSell, price, myKey)
val hello = SellerTradeInfo(assetToSell, price, me)
// What we get back from the other side is a transaction that *might* be valid and acceptable to us,
// but we must check it out thoroughly before we sign!
send(otherParty, hello)
@ -85,7 +86,7 @@ object TwoPartyTradeFlow {
// DOCSTART 5
val signTransactionFlow = object : SignTransactionFlow(otherParty, VERIFYING_AND_SIGNING.childProgressTracker()) {
override fun checkTransaction(stx: SignedTransaction) {
if (stx.tx.outputs.map { it.data }.sumCashBy(AnonymousParty(myKey)).withoutIssuer() != price)
if (stx.tx.outputs.map { it.data }.sumCashBy(me).withoutIssuer() != price)
throw FlowException("Transaction is not sending us the right amount of cash")
}
}
@ -181,7 +182,7 @@ object TwoPartyTradeFlow {
val ptx = TransactionType.General.Builder(notary)
// Add input and output states for the movement of cash, by using the Cash contract to generate the states
val (tx, cashSigningPubKeys) = serviceHub.vaultService.generateSpend(ptx, tradeRequest.price, AnonymousParty(tradeRequest.sellerOwnerKey))
val (tx, cashSigningPubKeys) = serviceHub.vaultService.generateSpend(ptx, tradeRequest.price, tradeRequest.sellerOwner)
// Add inputs/outputs/a command for the movement of the asset.
tx.addInputState(tradeRequest.assetForSale)

View File

@ -22,10 +22,21 @@ import net.corda.testing.node.MockNetwork.MockNode
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import java.util.*
import kotlin.test.assertFailsWith
class IssuerFlowTest {
@RunWith(Parameterized::class)
class IssuerFlowTest(val anonymous: Boolean) {
companion object {
@Parameterized.Parameters
@JvmStatic
fun data(): Collection<Array<Boolean>> {
return listOf(arrayOf(false), arrayOf(true))
}
}
lateinit var mockNet: MockNetwork
lateinit var notaryNode: MockNode
lateinit var bankOfCordaNode: MockNode
@ -46,6 +57,7 @@ class IssuerFlowTest {
@Test
fun `test issuer flow`() {
val notary = notaryNode.services.myInfo.notaryIdentity
val (vaultUpdatesBoc, vaultUpdatesBankClient) = bankOfCordaNode.database.transaction {
// Register for vault updates
val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL)
@ -54,7 +66,7 @@ class IssuerFlowTest {
// using default IssueTo Party Reference
val issuerResult = runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, 1000000.DOLLARS,
bankClientNode.info.legalIdentity, OpaqueBytes.of(123))
bankClientNode.info.legalIdentity, OpaqueBytes.of(123), notary)
issuerResult.get()
Pair(vaultUpdatesBoc, vaultUpdatesBankClient)
@ -68,8 +80,7 @@ class IssuerFlowTest {
require(update.consumed.isEmpty()) { "Expected 0 consumed states, actual: $update" }
require(update.produced.size == 1) { "Expected 1 produced states, actual: $update" }
val issued = update.produced.single().state.data as Cash.State
require(issued.owner == bankOfCordaNode.info.legalIdentity)
require(issued.owner != bankClientNode.info.legalIdentity)
require(issued.owner.owningKey in bankOfCordaNode.services.keyManagementService.keys)
},
// MOVE
expect { update ->
@ -86,29 +97,31 @@ class IssuerFlowTest {
require(update.consumed.isEmpty()) { update.consumed.size }
require(update.produced.size == 1) { update.produced.size }
val paidState = update.produced.single().state.data as Cash.State
require(paidState.owner == bankClientNode.info.legalIdentity)
require(paidState.owner.owningKey in bankClientNode.services.keyManagementService.keys)
}
}
}
@Test
fun `test issuer flow rejects restricted`() {
val notary = notaryNode.services.myInfo.notaryIdentity
// try to issue an amount of a restricted currency
assertFailsWith<FlowException> {
runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, Amount(100000L, currency("BRL")),
bankClientNode.info.legalIdentity, OpaqueBytes.of(123)).getOrThrow()
bankClientNode.info.legalIdentity, OpaqueBytes.of(123), notary).getOrThrow()
}
}
@Test
fun `test issue flow to self`() {
val notary = notaryNode.services.myInfo.notaryIdentity
val vaultUpdatesBoc = bankOfCordaNode.database.transaction {
val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL)
val (_, vaultUpdatesBoc) = bankOfCordaNode.services.vaultQueryService.trackBy<Cash.State>(criteria)
// using default IssueTo Party Reference
runIssuerAndIssueRequester(bankOfCordaNode, bankOfCordaNode, 1000000.DOLLARS,
bankOfCordaNode.info.legalIdentity, OpaqueBytes.of(123)).getOrThrow()
bankOfCordaNode.info.legalIdentity, OpaqueBytes.of(123), notary).getOrThrow()
vaultUpdatesBoc
}
@ -126,12 +139,13 @@ class IssuerFlowTest {
@Test
fun `test concurrent issuer flow`() {
val notary = notaryNode.services.myInfo.notaryIdentity
// this test exercises the Cashflow issue and move subflows to ensure consistent spending of issued states
val amount = 10000.DOLLARS
val amounts = calculateRandomlySizedAmounts(10000.DOLLARS, 10, 10, Random())
val handles = amounts.map { pennies ->
runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, Amount(pennies, amount.token),
bankClientNode.info.legalIdentity, OpaqueBytes.of(123))
bankClientNode.info.legalIdentity, OpaqueBytes.of(123), notary)
}
handles.forEach {
require(it.get().stx is SignedTransaction)
@ -141,11 +155,12 @@ class IssuerFlowTest {
private fun runIssuerAndIssueRequester(issuerNode: MockNode,
issueToNode: MockNode,
amount: Amount<Currency>,
party: Party,
ref: OpaqueBytes): ListenableFuture<AbstractCashFlow.Result> {
val issueToPartyAndRef = party.ref(ref)
val issueRequest = IssuanceRequester(amount, party, issueToPartyAndRef.reference, issuerNode.info.legalIdentity,
anonymous = false)
issueToParty: Party,
ref: OpaqueBytes,
notaryParty: Party): ListenableFuture<AbstractCashFlow.Result> {
val issueToPartyAndRef = issueToParty.ref(ref)
val issueRequest = IssuanceRequester(amount, issueToParty, issueToPartyAndRef.reference, issuerNode.info.legalIdentity, notaryParty,
anonymous)
return issueToNode.services.startFlow(issueRequest).resultFuture
}
}

View File

@ -8,6 +8,7 @@ import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowInitiator
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.messaging.*
import net.corda.core.node.NodeInfo
@ -178,6 +179,7 @@ class CordaRPCOpsImpl(
override fun partyFromName(name: String) = services.identityService.partyFromName(name)
override fun partyFromX500Name(x500Name: X500Name) = services.identityService.partyFromX500Name(x500Name)
override fun partiesFromName(query: String, exactMatch: Boolean): Set<Party> = services.identityService.partiesFromName(query, exactMatch)
override fun nodeIdentityFromParty(party: AbstractParty): NodeInfo? = services.networkMapCache.getNodeByLegalIdentity(party)
override fun registeredFlows(): List<String> = services.rpcFlows.map { it.name }.sorted()

View File

@ -509,12 +509,13 @@ class TwoPartyTradeFlowTests {
@Suspendable
override fun call(): SignedTransaction {
send(buyer, Pair(notary.notaryIdentity, price))
val key = serviceHub.keyManagementService.freshKey()
return subFlow(Seller(
buyer,
notary,
assetToSell,
price,
serviceHub.legalIdentityKey))
AnonymousParty(key)))
}
}

View File

@ -5,9 +5,9 @@ import net.corda.bank.api.BankOfCordaClientApi
import net.corda.bank.api.BankOfCordaWebApi.IssueRequestParams
import net.corda.core.getOrThrow
import net.corda.core.node.services.ServiceInfo
import net.corda.testing.driver.driver
import net.corda.node.services.transactions.SimpleNotaryService
import net.corda.testing.BOC
import net.corda.testing.driver.driver
import org.junit.Test
import kotlin.test.assertTrue
@ -19,9 +19,9 @@ class BankOfCordaHttpAPITest {
startNode(BOC.name, setOf(ServiceInfo(SimpleNotaryService.type))),
startNode(BIGCORP_LEGAL_NAME)
).getOrThrow()
val anonymous = true
val anonymous = false
val nodeBankOfCordaApiAddr = startWebserver(nodeBankOfCorda).getOrThrow().listenAddress
assertTrue(BankOfCordaClientApi(nodeBankOfCordaApiAddr).requestWebIssue(IssueRequestParams(1000, "USD", BIGCORP_LEGAL_NAME, "1", BOC.name, anonymous)))
assertTrue(BankOfCordaClientApi(nodeBankOfCordaApiAddr).requestWebIssue(IssueRequestParams(1000, "USD", BIGCORP_LEGAL_NAME, "1", BOC.name, BOC.name, anonymous)))
}, isDebug = true)
}
}

View File

@ -44,14 +44,14 @@ class BankOfCordaRPCClientTest {
val (_, vaultUpdatesBigCorp) = bigCorpProxy.vaultTrackByCriteria<Cash.State>(Cash.State::class.java, criteria)
// Kick-off actual Issuer Flow
// TODO: Update checks below to reflect states consumed/produced under anonymisation
val anonymous = false
val anonymous = true
bocProxy.startFlow(
::IssuanceRequester,
1000.DOLLARS,
nodeBigCorporation.nodeInfo.legalIdentity,
BIG_CORP_PARTY_REF,
nodeBankOfCorda.nodeInfo.legalIdentity,
nodeBankOfCorda.nodeInfo.notaryIdentity,
anonymous).returnValue.getOrThrow()
// Check Bank of Corda Vault Updates

View File

@ -68,7 +68,7 @@ private class BankOfCordaDriver {
} else {
try {
val anonymous = true
val requestParams = IssueRequestParams(options.valueOf(quantity), options.valueOf(currency), BIGCORP_LEGAL_NAME, "1", BOC.name, anonymous)
val requestParams = IssueRequestParams(options.valueOf(quantity), options.valueOf(currency), BIGCORP_LEGAL_NAME, "1", BOC.name, DUMMY_NOTARY.name, anonymous)
when (role) {
Role.ISSUE_CASH_RPC -> {
println("Requesting Cash via RPC ...")

View File

@ -40,11 +40,14 @@ class BankOfCordaClientApi(val hostAndPort: NetworkHostAndPort) {
?: throw Exception("Unable to locate ${params.issueToPartyName} in Network Map Service")
val issuerBankParty = proxy.partyFromX500Name(params.issuerBankName)
?: throw Exception("Unable to locate ${params.issuerBankName} in Network Map Service")
val notaryParty = proxy.partyFromX500Name(params.notaryName)
?: throw Exception("Unable to locate ${params.notaryName} in Network Map Service")
val amount = Amount(params.amount, currency(params.currency))
val issuerToPartyRef = OpaqueBytes.of(params.issueToPartyRefAsString.toByte())
return proxy.startFlow(::IssuanceRequester, amount, issueToParty, issuerToPartyRef, issuerBankParty, params.anonymous).returnValue.getOrThrow().stx
return proxy.startFlow(::IssuanceRequester, amount, issueToParty, issuerToPartyRef, issuerBankParty, notaryParty, params.anonymous)
.returnValue.getOrThrow().stx
}
}
}

View File

@ -2,7 +2,6 @@ package net.corda.bank.api
import net.corda.core.contracts.Amount
import net.corda.core.contracts.currency
import net.corda.core.flows.FlowException
import net.corda.core.getOrThrow
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.startFlow
@ -21,6 +20,7 @@ class BankOfCordaWebApi(val rpc: CordaRPCOps) {
data class IssueRequestParams(val amount: Long, val currency: String,
val issueToPartyName: X500Name, val issueToPartyRefAsString: String,
val issuerBankName: X500Name,
val notaryName: X500Name,
val anonymous: Boolean)
private companion object {
@ -43,9 +43,11 @@ class BankOfCordaWebApi(val rpc: CordaRPCOps) {
fun issueAssetRequest(params: IssueRequestParams): Response {
// Resolve parties via RPC
val issueToParty = rpc.partyFromX500Name(params.issueToPartyName)
?: throw Exception("Unable to locate ${params.issueToPartyName} in Network Map Service")
?: return Response.status(Response.Status.FORBIDDEN).entity("Unable to locate ${params.issueToPartyName} in Network Map Service").build()
val issuerBankParty = rpc.partyFromX500Name(params.issuerBankName)
?: throw Exception("Unable to locate ${params.issuerBankName} in Network Map Service")
?: return Response.status(Response.Status.FORBIDDEN).entity("Unable to locate ${params.issuerBankName} in Network Map Service").build()
val notaryParty = rpc.partyFromX500Name(params.notaryName)
?: return Response.status(Response.Status.FORBIDDEN).entity("Unable to locate ${params.notaryName} in Network Map Service").build()
val amount = Amount(params.amount, currency(params.currency))
val issuerToPartyRef = OpaqueBytes.of(params.issueToPartyRefAsString.toByte())
@ -53,13 +55,13 @@ class BankOfCordaWebApi(val rpc: CordaRPCOps) {
// invoke client side of Issuer Flow: IssuanceRequester
// The line below blocks and waits for the future to resolve.
val status = try {
rpc.startFlow(::IssuanceRequester, amount, issueToParty, issuerToPartyRef, issuerBankParty, anonymous).returnValue.getOrThrow()
return try {
rpc.startFlow(::IssuanceRequester, amount, issueToParty, issuerToPartyRef, issuerBankParty, notaryParty, anonymous).returnValue.getOrThrow()
logger.info("Issue request completed successfully: $params")
Response.Status.CREATED
} catch (e: FlowException) {
Response.Status.BAD_REQUEST
Response.status(Response.Status.CREATED).build()
} catch (e: Exception) {
logger.error("Issue request failed: ${e}", e)
Response.status(Response.Status.FORBIDDEN).build()
}
return Response.status(status).build()
}
}

View File

@ -17,6 +17,7 @@ import net.corda.core.utilities.Emoji
import net.corda.core.utilities.loggerFor
import net.corda.flows.IssuerFlow.IssuanceRequester
import net.corda.testing.BOC
import net.corda.testing.DUMMY_NOTARY
import net.corda.traderdemo.flow.SellerFlow
import org.bouncycastle.asn1.x500.X500Name
import java.util.*
@ -45,12 +46,16 @@ class TraderDemoClientApi(val rpc: CordaRPCOps) {
fun runBuyer(amount: Amount<Currency> = 30000.DOLLARS, anonymous: Boolean = true) {
val bankOfCordaParty = rpc.partyFromX500Name(BOC.name)
?: throw Exception("Unable to locate ${BOC.name} in Network Map Service")
?: throw IllegalStateException("Unable to locate ${BOC.name} in Network Map Service")
val notaryLegalIdentity = rpc.partyFromX500Name(DUMMY_NOTARY.name)
?: throw IllegalStateException("Unable to locate ${DUMMY_NOTARY.name} in Network Map Service")
val notaryNode = rpc.nodeIdentityFromParty(notaryLegalIdentity)
?: throw IllegalStateException("Unable to locate notary node in network map cache")
val me = rpc.nodeIdentity()
val amounts = calculateRandomlySizedAmounts(amount, 3, 10, Random())
// issuer random amounts of currency totaling 30000.DOLLARS in parallel
val resultFutures = amounts.map { pennies ->
rpc.startFlow(::IssuanceRequester, Amount(pennies, amount.token), me.legalIdentity, OpaqueBytes.of(1), bankOfCordaParty, anonymous).returnValue
rpc.startFlow(::IssuanceRequester, Amount(pennies, amount.token), me.legalIdentity, OpaqueBytes.of(1), bankOfCordaParty, notaryNode.notaryIdentity, anonymous).returnValue
}
Futures.allAsList(resultFutures).getOrThrow()

View File

@ -11,6 +11,7 @@ import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party
import net.corda.core.node.NodeInfo
import net.corda.core.seconds
@ -50,7 +51,7 @@ class SellerFlow(val otherParty: Party,
progressTracker.currentStep = SELF_ISSUING
val notary: NodeInfo = serviceHub.networkMapCache.notaryNodes[0]
val cpOwnerKey = serviceHub.legalIdentityKey
val cpOwnerKey = serviceHub.keyManagementService.freshKey()
val commercialPaper = selfIssueSomeCommercialPaper(serviceHub.myInfo.legalIdentity, notary)
progressTracker.currentStep = TRADING
@ -62,7 +63,7 @@ class SellerFlow(val otherParty: Party,
notary,
commercialPaper,
amount,
cpOwnerKey,
AnonymousParty(cpOwnerKey),
progressTracker.getChildProgressTracker(TRADING)!!)
return subFlow(seller)
}

View File

@ -100,6 +100,7 @@ class NewTransaction : Fragment() {
command.recipient,
command.issueRef,
myIdentity.value!!.legalIdentity,
command.notary,
command.anonymous)
} else {
command.startFlow(rpcProxy.value!!)