Change CashIssueFlow to use anonymous identity

* Add functions for:
    * Retrieving nodes via their legal identity
    * Filtering a set of public keys down to those the node has corresponding private keys for
* Modify contract upgrade flows to handle identifying participants after an anomymisation step
* Correct terminology: "party who" -> "party which"
* Modify CashIssueFlow and CashPaymentFlow to optionally use an anonymous identity for the recipient.
This commit is contained in:
Ross Nicoll
2017-06-28 13:47:50 +01:00
committed by GitHub
parent e5395fe1b7
commit 1a4965c294
57 changed files with 464 additions and 205 deletions

View File

@ -113,11 +113,13 @@ class NodeMonitorModelTest : DriverBasedTest() {
@Test @Test
fun `cash issue works end to end`() { fun `cash issue works end to end`() {
val anonymous = false
rpc.startFlow(::CashIssueFlow, rpc.startFlow(::CashIssueFlow,
Amount(100, USD), Amount(100, USD),
OpaqueBytes(ByteArray(1, { 1 })), OpaqueBytes(ByteArray(1, { 1 })),
aliceNode.legalIdentity, aliceNode.legalIdentity,
notaryNode.notaryIdentity notaryNode.notaryIdentity,
anonymous
) )
vaultUpdates.expectEvents(isStrict = false) { vaultUpdates.expectEvents(isStrict = false) {
@ -138,8 +140,9 @@ class NodeMonitorModelTest : DriverBasedTest() {
@Test @Test
fun `cash issue and move`() { fun `cash issue and move`() {
rpc.startFlow(::CashIssueFlow, 100.DOLLARS, OpaqueBytes.of(1), aliceNode.legalIdentity, notaryNode.notaryIdentity).returnValue.getOrThrow() val anonymous = false
rpc.startFlow(::CashPaymentFlow, 100.DOLLARS, bobNode.legalIdentity).returnValue.getOrThrow() rpc.startFlow(::CashIssueFlow, 100.DOLLARS, OpaqueBytes.of(1), aliceNode.legalIdentity, notaryNode.notaryIdentity, anonymous).returnValue.getOrThrow()
rpc.startFlow(::CashPaymentFlow, 100.DOLLARS, bobNode.legalIdentity, anonymous).returnValue.getOrThrow()
var issueSmId: StateMachineRunId? = null var issueSmId: StateMachineRunId? = null
var moveSmId: StateMachineRunId? = null var moveSmId: StateMachineRunId? = null

View File

@ -26,7 +26,7 @@ open class EventGenerator(val parties: List<Party>, val currencies: List<Currenc
protected val issueCashGenerator = amountGenerator.combine(partyGenerator, issueRefGenerator, currencyGenerator) { amount, to, issueRef, ccy -> protected val issueCashGenerator = amountGenerator.combine(partyGenerator, issueRefGenerator, currencyGenerator) { amount, to, issueRef, ccy ->
addToMap(ccy, amount) addToMap(ccy, amount)
CashFlowCommand.IssueCash(Amount(amount, ccy), issueRef, to, notary) CashFlowCommand.IssueCash(Amount(amount, ccy), issueRef, to, notary, anonymous = true)
} }
protected val exitCashGenerator = amountGenerator.combine(issueRefGenerator, currencyGenerator) { amount, issueRef, ccy -> protected val exitCashGenerator = amountGenerator.combine(issueRefGenerator, currencyGenerator) { amount, issueRef, ccy ->
@ -35,7 +35,7 @@ open class EventGenerator(val parties: List<Party>, val currencies: List<Currenc
} }
open val moveCashGenerator = amountGenerator.combine(partyGenerator, currencyGenerator) { amountIssued, recipient, currency -> open val moveCashGenerator = amountGenerator.combine(partyGenerator, currencyGenerator) { amountIssued, recipient, currency ->
CashFlowCommand.PayCash(Amount(amountIssued, currency), recipient) CashFlowCommand.PayCash(Amount(amountIssued, currency), recipient, anonymous = true)
} }
open val issuerGenerator = Generator.frequency(listOf( open val issuerGenerator = Generator.frequency(listOf(
@ -71,11 +71,11 @@ class ErrorFlowsEventGenerator(parties: List<Party>, currencies: List<Currency>,
} }
val normalMoveGenerator = amountGenerator.combine(partyGenerator, currencyGenerator) { amountIssued, recipient, currency -> val normalMoveGenerator = amountGenerator.combine(partyGenerator, currencyGenerator) { amountIssued, recipient, currency ->
CashFlowCommand.PayCash(Amount(amountIssued, currency), recipient) CashFlowCommand.PayCash(Amount(amountIssued, currency), recipient, anonymous = true)
} }
val errorMoveGenerator = partyGenerator.combine(currencyGenerator) { recipient, currency -> val errorMoveGenerator = partyGenerator.combine(currencyGenerator) { recipient, currency ->
CashFlowCommand.PayCash(Amount(currencyMap[currency]!! * 2, currency), recipient) CashFlowCommand.PayCash(Amount(currencyMap[currency]!! * 2, currency), recipient, anonymous = true)
} }
override val moveCashGenerator = Generator.frequency(listOf( override val moveCashGenerator = Generator.frequency(listOf(

View File

@ -0,0 +1,16 @@
package net.corda.flows
import net.corda.core.identity.AnonymousParty
import net.corda.core.serialization.CordaSerializable
import org.bouncycastle.cert.X509CertificateHolder
import java.security.PublicKey
import java.security.cert.CertPath
@CordaSerializable
data class AnonymisedIdentity(
val certPath: CertPath,
val certificate: X509CertificateHolder,
val identity: AnonymousParty) {
constructor(certPath: CertPath, certificate: X509CertificateHolder, identity: PublicKey)
: this(certPath, certificate, AnonymousParty(identity))
}

View File

@ -350,6 +350,16 @@ inline fun <T : Any, A, B, C, D, reified R : FlowLogic<T>> CordaRPCOps.startFlow
arg3: D arg3: D
): FlowHandle<T> = startFlowDynamic(R::class.java, arg0, arg1, arg2, arg3) ): FlowHandle<T> = startFlowDynamic(R::class.java, arg0, arg1, arg2, arg3)
inline fun <T : Any, A, B, C, D, E, reified R : FlowLogic<T>> CordaRPCOps.startFlow(
@Suppress("UNUSED_PARAMETER")
flowConstructor: (A, B, C, D, E) -> R,
arg0: A,
arg1: B,
arg2: C,
arg3: D,
arg4: E
): FlowHandle<T> = startFlowDynamic(R::class.java, arg0, arg1, arg2, arg3, arg4)
/** /**
* Same again, except this time with progress-tracking enabled. * Same again, except this time with progress-tracking enabled.
*/ */

View File

@ -2,9 +2,11 @@ package net.corda.core.node.services
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.contracts.Contract import net.corda.core.contracts.Contract
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.messaging.DataFeed import net.corda.core.messaging.DataFeed
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.node.ServiceHub
import net.corda.core.randomOrNull import net.corda.core.randomOrNull
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
@ -63,6 +65,17 @@ interface NetworkMapCache {
*/ */
fun getRecommended(type: ServiceType, contract: Contract, vararg party: Party): NodeInfo? = getNodesWithService(type).firstOrNull() fun getRecommended(type: ServiceType, contract: Contract, vararg party: Party): NodeInfo? = getNodesWithService(type).firstOrNull()
/**
* Look up the node info for a specific party. Will attempt to de-anonymise the party if applicable; if the party
* is anonymised and the well known party cannot be resolved, it is impossible ot identify the node and therefore this
* returns null.
*
* @param party party to retrieve node information for.
* @return the node for the identity, or null if the node could not be found. This does not necessarily mean there is
* no node for the party, only that this cache is unaware of it.
*/
fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo?
/** Look up the node info for a legal name. */ /** Look up the node info for a legal name. */
fun getNodeByLegalName(principal: X500Name): NodeInfo? = partyNodes.singleOrNull { it.legalIdentity.name == principal } fun getNodeByLegalName(principal: X500Name): NodeInfo? = partyNodes.singleOrNull { it.legalIdentity.name == principal }

View File

@ -21,6 +21,7 @@ import net.corda.core.toFuture
import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction import net.corda.core.transactions.WireTransaction
import net.corda.flows.AnonymisedIdentity
import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.cert.X509CertificateHolder
import rx.Observable import rx.Observable
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
@ -486,9 +487,17 @@ interface KeyManagementService {
* @return X.509 certificate and path to the trust root. * @return X.509 certificate and path to the trust root.
*/ */
@Suspendable @Suspendable
fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): Pair<X509CertificateHolder, CertPath> fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): AnonymisedIdentity
/** Using the provided signing [PublicKey] internally looks up the matching [PrivateKey] and signs the data. /**
* Filter some keys down to the set that this node owns (has private keys for).
*
* @param candidateKeys keys which this node may own.
*/
fun filterMyKeys(candidateKeys: Iterable<PublicKey>): Iterable<PublicKey>
/**
* Using the provided signing [PublicKey] internally looks up the matching [PrivateKey] and signs the data.
* @param bytes The data to sign over using the chosen key. * @param bytes The data to sign over using the chosen key.
* @param publicKey The [PublicKey] partner to an internally held [PrivateKey], either derived from the node's primary identity, * @param publicKey The [PublicKey] partner to an internally held [PrivateKey], either derived from the node's primary identity,
* or previously generated via the [freshKey] method. * or previously generated via the [freshKey] method.

View File

@ -73,7 +73,7 @@ object DefaultKryoCustomizer {
noReferencesWithin<WireTransaction>() noReferencesWithin<WireTransaction>()
register(sun.security.ec.ECPublicKeyImpl::class.java, PublicKeySerializer) register(sun.security.ec.ECPublicKeyImpl::class.java, ECPublicKeyImplSerializer)
register(EdDSAPublicKey::class.java, Ed25519PublicKeySerializer) register(EdDSAPublicKey::class.java, Ed25519PublicKeySerializer)
register(EdDSAPrivateKey::class.java, Ed25519PrivateKeySerializer) register(EdDSAPrivateKey::class.java, Ed25519PrivateKeySerializer)
@ -113,6 +113,7 @@ object DefaultKryoCustomizer {
register(BCRSAPublicKey::class.java, PublicKeySerializer) register(BCRSAPublicKey::class.java, PublicKeySerializer)
register(BCSphincs256PrivateKey::class.java, PrivateKeySerializer) register(BCSphincs256PrivateKey::class.java, PrivateKeySerializer)
register(BCSphincs256PublicKey::class.java, PublicKeySerializer) register(BCSphincs256PublicKey::class.java, PublicKeySerializer)
register(sun.security.ec.ECPublicKeyImpl::class.java, PublicKeySerializer)
val customization = KryoSerializationCustomization(this) val customization = KryoSerializationCustomization(this)
pluginRegistries.forEach { it.customizeSerialization(customization) } pluginRegistries.forEach { it.customizeSerialization(customization) }

View File

@ -387,6 +387,20 @@ object Ed25519PublicKeySerializer : Serializer<EdDSAPublicKey>() {
} }
} }
/** For serialising an ed25519 public key */
@ThreadSafe
object ECPublicKeyImplSerializer : Serializer<sun.security.ec.ECPublicKeyImpl>() {
override fun write(kryo: Kryo, output: Output, obj: sun.security.ec.ECPublicKeyImpl) {
output.writeBytesWithLength(obj.encoded)
}
override fun read(kryo: Kryo, input: Input, type: Class<sun.security.ec.ECPublicKeyImpl>): sun.security.ec.ECPublicKeyImpl {
val A = input.readBytesWithLength()
val der = sun.security.util.DerValue(A)
return sun.security.ec.ECPublicKeyImpl.parse(der) as sun.security.ec.ECPublicKeyImpl
}
}
// TODO Implement standardized serialization of CompositeKeys. See JIRA issue: CORDA-249. // TODO Implement standardized serialization of CompositeKeys. See JIRA issue: CORDA-249.
@ThreadSafe @ThreadSafe
object CompositeKeySerializer : Serializer<CompositeKey>() { object CompositeKeySerializer : Serializer<CompositeKey>() {

View File

@ -9,6 +9,7 @@ import net.corda.core.crypto.isFulfilledBy
import net.corda.core.flows.FlowException import net.corda.core.flows.FlowException
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.identity.AbstractParty import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
@ -33,6 +34,15 @@ abstract class AbstractStateReplacementFlow {
@CordaSerializable @CordaSerializable
data class Proposal<out M>(val stateRef: StateRef, val modification: M, val stx: SignedTransaction) data class Proposal<out M>(val stateRef: StateRef, val modification: M, val stx: SignedTransaction)
/**
* The assembled transaction for upgrading a contract.
*
* @param stx signed transaction to do the upgrade.
* @param participants the parties involved in the upgrade transaction.
* @param myKey key
*/
data class UpgradeTx(val stx: SignedTransaction, val participants: Iterable<PublicKey>, val myKey: PublicKey)
/** /**
* The [Instigator] assembles the transaction for state replacement and sends out change proposals to all participants * The [Instigator] assembles the transaction for state replacement and sends out change proposals to all participants
* ([Acceptor]) of that state. If participants agree to the proposed change, they each sign the transaction. * ([Acceptor]) of that state. If participants agree to the proposed change, they each sign the transaction.
@ -57,17 +67,14 @@ abstract class AbstractStateReplacementFlow {
@Suspendable @Suspendable
@Throws(StateReplacementException::class) @Throws(StateReplacementException::class)
override fun call(): StateAndRef<T> { override fun call(): StateAndRef<T> {
val (stx, participants) = assembleTx() val (stx, participantKeys, myKey) = assembleTx()
progressTracker.currentStep = SIGNING progressTracker.currentStep = SIGNING
val myKey = serviceHub.myInfo.legalIdentity val signatures = if (participantKeys.singleOrNull() == myKey) {
val me = listOf(myKey)
val signatures = if (participants == me) {
getNotarySignatures(stx) getNotarySignatures(stx)
} else { } else {
collectSignatures((participants - me).map { it.owningKey }, stx) collectSignatures(participantKeys - myKey, stx)
} }
val finalTx = stx + signatures val finalTx = stx + signatures
@ -75,7 +82,13 @@ abstract class AbstractStateReplacementFlow {
return finalTx.tx.outRef(0) return finalTx.tx.outRef(0)
} }
abstract protected fun assembleTx(): Pair<SignedTransaction, Iterable<AbstractParty>> /**
* Build the upgrade transaction.
*
* @return a triple of the transaction, the public keys of all participants, and the participating public key of
* this node.
*/
abstract protected fun assembleTx(): UpgradeTx
@Suspendable @Suspendable
private fun collectSignatures(participants: Iterable<PublicKey>, stx: SignedTransaction): List<DigitalSignature.WithKey> { private fun collectSignatures(participants: Iterable<PublicKey>, stx: SignedTransaction): List<DigitalSignature.WithKey> {

View File

@ -58,9 +58,12 @@ class ContractUpgradeFlow<OldState : ContractState, out NewState : ContractState
} }
} }
override fun assembleTx(): Pair<SignedTransaction, Iterable<AbstractParty>> { override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx {
val baseTx = assembleBareTx(originalState, modification) val baseTx = assembleBareTx(originalState, modification)
val stx = serviceHub.signInitialTransaction(baseTx) val participantKeys = originalState.state.data.participants.map { it.owningKey }.toSet()
return stx to originalState.state.data.participants // TODO: We need a much faster way of finding our key in the transaction
val myKey = serviceHub.keyManagementService.filterMyKeys(participantKeys).single()
val stx = serviceHub.signInitialTransaction(baseTx, myKey)
return AbstractStateReplacementFlow.UpgradeTx(stx, participantKeys, myKey)
} }
} }

View File

@ -7,6 +7,7 @@ import net.corda.core.identity.Party
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker
import java.security.PublicKey
/** /**
* A flow to be used for changing a state's Notary. This is required since all input states to a transaction * A flow to be used for changing a state's Notary. This is required since all input states to a transaction
@ -24,24 +25,25 @@ class NotaryChangeFlow<out T : ContractState>(
progressTracker: ProgressTracker = tracker()) progressTracker: ProgressTracker = tracker())
: AbstractStateReplacementFlow.Instigator<T, T, Party>(originalState, newNotary, progressTracker) { : AbstractStateReplacementFlow.Instigator<T, T, Party>(originalState, newNotary, progressTracker) {
override fun assembleTx(): Pair<SignedTransaction, Iterable<AbstractParty>> { override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx {
val state = originalState.state val state = originalState.state
val tx = TransactionType.NotaryChange.Builder(originalState.state.notary) val tx = TransactionType.NotaryChange.Builder(originalState.state.notary)
val participants: Iterable<AbstractParty> val participants: Iterable<AbstractParty> = if (state.encumbrance == null) {
if (state.encumbrance == null) {
val modifiedState = TransactionState(state.data, modification) val modifiedState = TransactionState(state.data, modification)
tx.addInputState(originalState) tx.addInputState(originalState)
tx.addOutputState(modifiedState) tx.addOutputState(modifiedState)
participants = state.data.participants state.data.participants
} else { } else {
participants = resolveEncumbrances(tx) resolveEncumbrances(tx)
} }
val stx = serviceHub.signInitialTransaction(tx) val stx = serviceHub.signInitialTransaction(tx)
val participantKeys = participants.map { it.owningKey }
// TODO: We need a much faster way of finding our key in the transaction
val myKey = serviceHub.keyManagementService.filterMyKeys(participantKeys).single()
return Pair(stx, participants) return AbstractStateReplacementFlow.UpgradeTx(stx, participantKeys, myKey)
} }
/** /**

View File

@ -5,13 +5,10 @@ import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatedBy import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.unwrap import net.corda.core.utilities.unwrap
import org.bouncycastle.cert.X509CertificateHolder
import java.security.cert.CertPath
/** /**
* Very basic flow which exchanges transaction key and certificate paths between two parties in a transaction. * Very basic flow which exchanges transaction key and certificate paths between two parties in a transaction.
@ -19,11 +16,11 @@ import java.security.cert.CertPath
*/ */
object TxKeyFlow { object TxKeyFlow {
abstract class AbstractIdentityFlow<out T>(val otherSide: Party, val revocationEnabled: Boolean): FlowLogic<T>() { abstract class AbstractIdentityFlow<out T>(val otherSide: Party, val revocationEnabled: Boolean): FlowLogic<T>() {
fun validateIdentity(untrustedIdentity: AnonymousIdentity): AnonymousIdentity { fun validateIdentity(untrustedIdentity: AnonymisedIdentity): AnonymisedIdentity {
val (certPath, theirCert, txIdentity) = untrustedIdentity val (certPath, theirCert, txIdentity) = untrustedIdentity
if (theirCert.subject == otherSide.name) { if (theirCert.subject == otherSide.name) {
serviceHub.identityService.registerAnonymousIdentity(txIdentity, otherSide, certPath) serviceHub.identityService.registerAnonymousIdentity(txIdentity, otherSide, certPath)
return AnonymousIdentity(certPath, theirCert, txIdentity) return AnonymisedIdentity(certPath, theirCert, txIdentity)
} else } else
throw IllegalStateException("Expected certificate subject to be ${otherSide.name} but found ${theirCert.subject}") throw IllegalStateException("Expected certificate subject to be ${otherSide.name} but found ${theirCert.subject}")
} }
@ -32,7 +29,7 @@ object TxKeyFlow {
@StartableByRPC @StartableByRPC
@InitiatingFlow @InitiatingFlow
class Requester(otherSide: Party, class Requester(otherSide: Party,
override val progressTracker: ProgressTracker) : AbstractIdentityFlow<Map<Party, AnonymousIdentity>>(otherSide, false) { override val progressTracker: ProgressTracker) : AbstractIdentityFlow<TxIdentities>(otherSide, false) {
constructor(otherSide: Party) : this(otherSide, tracker()) constructor(otherSide: Party) : this(otherSide, tracker())
companion object { companion object {
object AWAITING_KEY : ProgressTracker.Step("Awaiting key") object AWAITING_KEY : ProgressTracker.Step("Awaiting key")
@ -41,23 +38,29 @@ object TxKeyFlow {
} }
@Suspendable @Suspendable
override fun call(): Map<Party, AnonymousIdentity> { override fun call(): TxIdentities {
progressTracker.currentStep = AWAITING_KEY progressTracker.currentStep = AWAITING_KEY
val myIdentityFragment = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, revocationEnabled) val myIdentity = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, revocationEnabled)
val myIdentity = AnonymousIdentity(myIdentityFragment) serviceHub.identityService.registerAnonymousIdentity(myIdentity.identity, serviceHub.myInfo.legalIdentity, myIdentity.certPath)
val theirIdentity = receive<AnonymousIdentity>(otherSide).unwrap { validateIdentity(it) }
// Special case that if we're both parties, a single identity is generated
return if (otherSide == serviceHub.myInfo.legalIdentity) {
TxIdentities(Pair(otherSide, myIdentity))
} else {
val theirIdentity = receive<AnonymisedIdentity>(otherSide).unwrap { validateIdentity(it) }
send(otherSide, myIdentity) send(otherSide, myIdentity)
return mapOf(Pair(otherSide, myIdentity), TxIdentities(Pair(otherSide, myIdentity),
Pair(serviceHub.myInfo.legalIdentity, theirIdentity)) Pair(serviceHub.myInfo.legalIdentity, theirIdentity))
} }
} }
}
/** /**
* Flow which waits for a key request from a counterparty, generates a new key and then returns it to the * Flow which waits for a key request from a counterparty, generates a new key and then returns it to the
* counterparty and as the result from the flow. * counterparty and as the result from the flow.
*/ */
@InitiatedBy(Requester::class) @InitiatedBy(Requester::class)
class Provider(otherSide: Party) : AbstractIdentityFlow<Map<Party, AnonymousIdentity>>(otherSide, false) { class Provider(otherSide: Party) : AbstractIdentityFlow<TxIdentities>(otherSide, false) {
companion object { companion object {
object SENDING_KEY : ProgressTracker.Step("Sending key") object SENDING_KEY : ProgressTracker.Step("Sending key")
} }
@ -65,25 +68,24 @@ object TxKeyFlow {
override val progressTracker: ProgressTracker = ProgressTracker(SENDING_KEY) override val progressTracker: ProgressTracker = ProgressTracker(SENDING_KEY)
@Suspendable @Suspendable
override fun call(): Map<Party, AnonymousIdentity> { override fun call(): TxIdentities {
val revocationEnabled = false val revocationEnabled = false
progressTracker.currentStep = SENDING_KEY progressTracker.currentStep = SENDING_KEY
val myIdentityFragment = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, revocationEnabled) val myIdentity = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, revocationEnabled)
val myIdentity = AnonymousIdentity(myIdentityFragment)
send(otherSide, myIdentity) send(otherSide, myIdentity)
val theirIdentity = receive<AnonymousIdentity>(otherSide).unwrap { validateIdentity(it) } val theirIdentity = receive<AnonymisedIdentity>(otherSide).unwrap { validateIdentity(it) }
return mapOf(Pair(otherSide, myIdentity), return TxIdentities(Pair(otherSide, myIdentity),
Pair(serviceHub.myInfo.legalIdentity, theirIdentity)) Pair(serviceHub.myInfo.legalIdentity, theirIdentity))
} }
} }
@CordaSerializable @CordaSerializable
data class AnonymousIdentity( data class TxIdentities(val identities: List<Pair<Party, AnonymisedIdentity>>) {
val certPath: CertPath, constructor(vararg identities: Pair<Party, AnonymisedIdentity>) : this(identities.toList())
val certificate: X509CertificateHolder, init {
val identity: AnonymousParty) { require(identities.size == identities.map { it.first }.toSet().size) { "Identities must be unique: ${identities.map { it.first }}" }
constructor(myIdentity: Pair<X509CertificateHolder, CertPath>) : this(myIdentity.second, }
myIdentity.first, fun forParty(party: Party): AnonymisedIdentity = identities.single { it.first == party }.second
AnonymousParty(myIdentity.second.certificates.first().publicKey)) fun toMap(): Map<Party, AnonymisedIdentity> = this.identities.toMap()
} }
} }

View File

@ -173,9 +173,11 @@ class ContractUpgradeFlowTest {
@Test @Test
fun `upgrade Cash to v2`() { fun `upgrade Cash to v2`() {
// Create some cash. // Create some cash.
val result = a.services.startFlow(CashIssueFlow(Amount(1000, USD), OpaqueBytes.of(1), a.info.legalIdentity, notary)).resultFuture val anonymous = false
val result = a.services.startFlow(CashIssueFlow(Amount(1000, USD), OpaqueBytes.of(1), a.info.legalIdentity, notary, anonymous)).resultFuture
mockNet.runNetwork() mockNet.runNetwork()
val stateAndRef = result.getOrThrow().tx.outRef<Cash.State>(0) val stx = result.getOrThrow().stx
val stateAndRef = stx.tx.outRef<Cash.State>(0)
val baseState = a.database.transaction { a.vault.unconsumedStates<ContractState>().single() } val baseState = a.database.transaction { a.vault.unconsumedStates<ContractState>().single() }
assertTrue(baseState.state.data is Cash.State, "Contract state is old version.") assertTrue(baseState.state.data is Cash.State, "Contract state is old version.")
// Starts contract upgrade flow. // Starts contract upgrade flow.

View File

@ -40,7 +40,7 @@ class TxKeyFlowTests {
val requesterFlow = aliceNode.services.startFlow(TxKeyFlow.Requester(bob)) val requesterFlow = aliceNode.services.startFlow(TxKeyFlow.Requester(bob))
// Get the results // Get the results
val actual: Map<Party, TxKeyFlow.AnonymousIdentity> = requesterFlow.resultFuture.getOrThrow() val actual: Map<Party, AnonymisedIdentity> = requesterFlow.resultFuture.getOrThrow().toMap()
assertEquals(2, actual.size) assertEquals(2, actual.size)
// Verify that the generated anonymous identities do not match the well known identities // Verify that the generated anonymous identities do not match the well known identities
val aliceAnonymousIdentity = actual[alice] ?: throw IllegalStateException() val aliceAnonymousIdentity = actual[alice] ?: throw IllegalStateException()

View File

@ -67,7 +67,8 @@ class IntegrationTestingTutorial {
i.DOLLARS, i.DOLLARS,
issueRef, issueRef,
bob.nodeInfo.legalIdentity, bob.nodeInfo.legalIdentity,
notary.nodeInfo.notaryIdentity notary.nodeInfo.notaryIdentity,
false // Not anonymised
).returnValue) ).returnValue)
} }
}.forEach(Thread::join) // Ensure the stack of futures is populated. }.forEach(Thread::join) // Ensure the stack of futures is populated.
@ -90,7 +91,7 @@ class IntegrationTestingTutorial {
// START 5 // START 5
for (i in 1..10) { for (i in 1..10) {
bobProxy.startFlow(::CashPaymentFlow, i.DOLLARS, alice.nodeInfo.legalIdentity).returnValue.getOrThrow() bobProxy.startFlow(::CashPaymentFlow, i.DOLLARS, alice.nodeInfo.legalIdentity, false).returnValue.getOrThrow()
} }
aliceVaultUpdates.expectEvents { aliceVaultUpdates.expectEvents {

View File

@ -13,7 +13,10 @@ import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.InitiatingFlow
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.ServiceHub import net.corda.core.node.ServiceHub
import net.corda.core.node.services.Vault
import net.corda.core.node.services.queryBy
import net.corda.core.node.services.unconsumedStates import net.corda.core.node.services.unconsumedStates
import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.unwrap import net.corda.core.utilities.unwrap
@ -39,14 +42,15 @@ private fun gatherOurInputs(serviceHub: ServiceHub,
amountRequired: Amount<Issued<Currency>>, amountRequired: Amount<Issued<Currency>>,
notary: Party?): Pair<List<StateAndRef<Cash.State>>, Long> { notary: Party?): Pair<List<StateAndRef<Cash.State>>, Long> {
// Collect cash type inputs // Collect cash type inputs
val cashStates = serviceHub.vaultService.unconsumedStates<Cash.State>() val queryCriteria = QueryCriteria.VaultQueryCriteria(Vault.StateStatus.UNCONSUMED, setOf(Cash.State::class.java))
val cashStates = serviceHub.vaultQueryService.queryBy<Cash.State>(queryCriteria).states
// extract our identity for convenience // extract our identity for convenience
val ourIdentity = serviceHub.myInfo.legalIdentity val ourKeys = serviceHub.keyManagementService.keys
// Filter down to our own cash states with right currency and issuer // Filter down to our own cash states with right currency and issuer
val suitableCashStates = cashStates.filter { val suitableCashStates = cashStates.filter {
val state = it.state.data val state = it.state.data
(state.owner == ourIdentity) // TODO: We may want to have the list of our states pre-cached somewhere for performance
&& (state.amount.token == amountRequired.token) (state.owner.owningKey in ourKeys) && (state.amount.token == amountRequired.token)
} }
require(!suitableCashStates.isEmpty()) { "Insufficient funds" } require(!suitableCashStates.isEmpty()) { "Insufficient funds" }
var remaining = amountRequired.quantity var remaining = amountRequired.quantity
@ -132,9 +136,6 @@ class ForeignExchangeFlow(val tradeId: String,
require(it.inputs.all { it.state.notary == notary }) { require(it.inputs.all { it.state.notary == notary }) {
"notary of remote states must be same as for our states" "notary of remote states must be same as for our states"
} }
require(it.inputs.all { it.state.data.owner == remoteRequestWithNotary.owner }) {
"The inputs are not owned by the correct counterparty"
}
require(it.inputs.all { it.state.data.amount.token == remoteRequestWithNotary.amount.token }) { require(it.inputs.all { it.state.data.amount.token == remoteRequestWithNotary.amount.token }) {
"Inputs not of the correct currency" "Inputs not of the correct currency"
} }
@ -200,7 +201,7 @@ class ForeignExchangeFlow(val tradeId: String,
// We have already validated their response and trust our own data // We have already validated their response and trust our own data
// so we can sign. Note the returned SignedTransaction is still not fully signed // so we can sign. Note the returned SignedTransaction is still not fully signed
// and would not pass full verification yet. // and would not pass full verification yet.
return serviceHub.signInitialTransaction(builder) return serviceHub.signInitialTransaction(builder, ourSigners.single())
} }
// DOCEND 3 // DOCEND 3
} }
@ -234,10 +235,11 @@ class ForeignExchangeRemoteFlow(val source: Party) : FlowLogic<Unit>() {
val ourResponse = prepareOurInputsAndOutputs(serviceHub, request) val ourResponse = prepareOurInputsAndOutputs(serviceHub, request)
// Send back our proposed states and await the full transaction to verify // Send back our proposed states and await the full transaction to verify
val ourKey = serviceHub.keyManagementService.filterMyKeys(ourResponse.inputs.flatMap { it.state.data.participants }.map { it.owningKey }).single()
val proposedTrade = sendAndReceive<SignedTransaction>(source, ourResponse).unwrap { val proposedTrade = sendAndReceive<SignedTransaction>(source, ourResponse).unwrap {
val wtx = it.tx val wtx = it.tx
// check all signatures are present except our own and the notary // check all signatures are present except our own and the notary
it.verifySignatures(serviceHub.myInfo.legalIdentity.owningKey, wtx.notary!!.owningKey) it.verifySignatures(ourKey, wtx.notary!!.owningKey)
// We need to fetch their complete input states and dependencies so that verify can operate // We need to fetch their complete input states and dependencies so that verify can operate
checkDependencies(it) checkDependencies(it)
@ -251,7 +253,7 @@ class ForeignExchangeRemoteFlow(val source: Party) : FlowLogic<Unit>() {
} }
// assuming we have completed state and business level validation we can sign the trade // assuming we have completed state and business level validation we can sign the trade
val ourSignature = serviceHub.createSignature(proposedTrade) val ourSignature = serviceHub.createSignature(proposedTrade, ourKey)
// send the other side our signature. // send the other side our signature.
send(source, ourSignature) send(source, ourSignature)

View File

@ -48,7 +48,8 @@ class FxTransactionBuildTutorialTest {
val flowHandle1 = nodeA.services.startFlow(CashIssueFlow(DOLLARS(1000), val flowHandle1 = nodeA.services.startFlow(CashIssueFlow(DOLLARS(1000),
OpaqueBytes.of(0x01), OpaqueBytes.of(0x01),
nodeA.info.legalIdentity, nodeA.info.legalIdentity,
notaryNode.info.notaryIdentity)) notaryNode.info.notaryIdentity,
false))
// Wait for the flow to stop and print // Wait for the flow to stop and print
flowHandle1.resultFuture.getOrThrow() flowHandle1.resultFuture.getOrThrow()
printBalances() printBalances()
@ -57,7 +58,8 @@ class FxTransactionBuildTutorialTest {
val flowHandle2 = nodeB.services.startFlow(CashIssueFlow(POUNDS(1000), val flowHandle2 = nodeB.services.startFlow(CashIssueFlow(POUNDS(1000),
OpaqueBytes.of(0x01), OpaqueBytes.of(0x01),
nodeB.info.legalIdentity, nodeB.info.legalIdentity,
notaryNode.info.notaryIdentity)) notaryNode.info.notaryIdentity,
false))
// Wait for flow to come to an end and print // Wait for flow to come to an end and print
flowHandle2.resultFuture.getOrThrow() flowHandle2.resultFuture.getOrThrow()
printBalances() printBalances()

View File

@ -429,7 +429,7 @@ class Obligation<P : Any> : Contract {
/** /**
* Generate a transaction performing close-out netting of two or more states. * Generate a transaction performing close-out netting of two or more states.
* *
* @param signer the party who will sign the transaction. Must be one of the obligor or beneficiary. * @param signer the party which will sign the transaction. Must be one of the obligor or beneficiary.
* @param states two or more states, which must be compatible for bilateral netting (same issuance definitions, * @param states two or more states, which must be compatible for bilateral netting (same issuance definitions,
* and same parties involved). * and same parties involved).
*/ */
@ -458,7 +458,7 @@ class Obligation<P : Any> : Contract {
* @param amountIssued the amount to be exited, represented as a quantity of issued currency. * @param amountIssued the amount to be exited, represented as a quantity of issued currency.
* @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is * @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is
* the responsibility of the caller to check that they do not exit funds held by others. * the responsibility of the caller to check that they do not exit funds held by others.
* @return the public keys who must sign the transaction for it to be valid. * @return the public keys which must sign the transaction for it to be valid.
*/ */
@Suppress("unused") @Suppress("unused")
fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<Terms<P>>>, fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<Terms<P>>>,

View File

@ -207,13 +207,15 @@ abstract class OnLedgerAsset<T : Any, C : CommandData, S : FungibleAsset<T>> : C
@JvmStatic @JvmStatic
fun <S : FungibleAsset<T>, T: Any> generateIssue(tx: TransactionBuilder, fun <S : FungibleAsset<T>, T: Any> generateIssue(tx: TransactionBuilder,
transactionState: TransactionState<S>, transactionState: TransactionState<S>,
issueCommand: CommandData) { issueCommand: CommandData): Set<PublicKey> {
check(tx.inputStates().isEmpty()) check(tx.inputStates().isEmpty())
check(tx.outputStates().map { it.data }.filterIsInstance(transactionState.javaClass).isEmpty()) check(tx.outputStates().map { it.data }.filterIsInstance(transactionState.javaClass).isEmpty())
require(transactionState.data.amount.quantity > 0) require(transactionState.data.amount.quantity > 0)
val at = transactionState.data.amount.token.issuer val at = transactionState.data.amount.token.issuer
val commandSigner = at.party.owningKey
tx.addOutputState(transactionState) tx.addOutputState(transactionState)
tx.addCommand(issueCommand, at.party.owningKey) tx.addCommand(issueCommand, commandSigner)
return setOf(commandSigner)
} }
} }

View File

@ -3,30 +3,44 @@ package net.corda.flows
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.core.flows.FlowException import net.corda.core.flows.FlowException
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker
/** /**
* Initiates a flow that produces an Issue/Move or Exit Cash transaction. * Initiates a flow that produces an Issue/Move or Exit Cash transaction.
*/ */
abstract class AbstractCashFlow(override val progressTracker: ProgressTracker) : FlowLogic<SignedTransaction>() { abstract class AbstractCashFlow<T>(override val progressTracker: ProgressTracker) : FlowLogic<T>() {
companion object { companion object {
object GENERATING_ID : ProgressTracker.Step("Generating anonymous identities")
object GENERATING_TX : ProgressTracker.Step("Generating transaction") object GENERATING_TX : ProgressTracker.Step("Generating transaction")
object SIGNING_TX : ProgressTracker.Step("Signing transaction") object SIGNING_TX : ProgressTracker.Step("Signing transaction")
object FINALISING_TX : ProgressTracker.Step("Finalising transaction") object FINALISING_TX : ProgressTracker.Step("Finalising transaction")
fun tracker() = ProgressTracker(GENERATING_TX, SIGNING_TX, FINALISING_TX) fun tracker() = ProgressTracker(GENERATING_ID, GENERATING_TX, SIGNING_TX, FINALISING_TX)
} }
@Suspendable @Suspendable
internal fun finaliseTx(participants: Set<Party>, tx: SignedTransaction, message: String) { protected fun finaliseTx(participants: Set<Party>, tx: SignedTransaction, message: String) {
try { try {
subFlow(FinalityFlow(tx, participants)) subFlow(FinalityFlow(tx, participants))
} catch (e: NotaryException) { } catch (e: NotaryException) {
throw CashException(message, e) throw CashException(message, e)
} }
} }
/**
* Combined signed transaction and identity lookup map, which is the resulting data from regular cash flows.
* Specialised flows for unit tests differ from this.
*
* @param stx the signed transaction.
* @param identities a mapping from the original identities of the parties to the anonymised equivalents.
*/
@CordaSerializable
data class Result(val stx: SignedTransaction, val identities: TxKeyFlow.TxIdentities)
} }
class CashException(message: String, cause: Throwable) : FlowException(message, cause) class CashException(message: String, cause: Throwable) : FlowException(message, cause)

View File

@ -9,7 +9,6 @@ import net.corda.core.contracts.issuedBy
import net.corda.core.flows.StartableByRPC import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.OpaqueBytes
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker
import java.util.* import java.util.*
@ -22,16 +21,20 @@ import java.util.*
* issuer. * issuer.
*/ */
@StartableByRPC @StartableByRPC
class CashExitFlow(val amount: Amount<Currency>, val issueRef: OpaqueBytes, progressTracker: ProgressTracker) : AbstractCashFlow(progressTracker) { class CashExitFlow(val amount: Amount<Currency>, val issueRef: OpaqueBytes, progressTracker: ProgressTracker) : AbstractCashFlow<AbstractCashFlow.Result>(progressTracker) {
constructor(amount: Amount<Currency>, issueRef: OpaqueBytes) : this(amount, issueRef, tracker()) constructor(amount: Amount<Currency>, issueRef: OpaqueBytes) : this(amount, issueRef, tracker())
companion object { companion object {
fun tracker() = ProgressTracker(GENERATING_TX, SIGNING_TX, FINALISING_TX) fun tracker() = ProgressTracker(GENERATING_TX, SIGNING_TX, FINALISING_TX)
} }
/**
* @return the signed transaction, and a mapping of parties to new anonymous identities generated
* (for this flow this map is always empty).
*/
@Suspendable @Suspendable
@Throws(CashException::class) @Throws(CashException::class)
override fun call(): SignedTransaction { override fun call(): AbstractCashFlow.Result {
progressTracker.currentStep = GENERATING_TX progressTracker.currentStep = GENERATING_TX
val builder: TransactionBuilder = TransactionType.General.Builder(notary = null as Party?) val builder: TransactionBuilder = TransactionType.General.Builder(notary = null as Party?)
val issuer = serviceHub.myInfo.legalIdentity.ref(issueRef) val issuer = serviceHub.myInfo.legalIdentity.ref(issueRef)
@ -67,6 +70,6 @@ class CashExitFlow(val amount: Amount<Currency>, val issueRef: OpaqueBytes, prog
// Commit the transaction // Commit the transaction
progressTracker.currentStep = FINALISING_TX progressTracker.currentStep = FINALISING_TX
finaliseTx(participants, tx, "Unable to notarise exit") finaliseTx(participants, tx, "Unable to notarise exit")
return tx return Result(tx, TxKeyFlow.TxIdentities())
} }
} }

View File

@ -6,14 +6,13 @@ import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.FlowHandle import net.corda.core.messaging.FlowHandle
import net.corda.core.messaging.startFlow import net.corda.core.messaging.startFlow
import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.OpaqueBytes
import net.corda.core.transactions.SignedTransaction
import java.util.* import java.util.*
/** /**
* A command to initiate the cash flow with. * A command to initiate the cash flow with.
*/ */
sealed class CashFlowCommand { sealed class CashFlowCommand {
abstract fun startFlow(proxy: CordaRPCOps): FlowHandle<SignedTransaction> abstract fun startFlow(proxy: CordaRPCOps): FlowHandle<AbstractCashFlow.Result>
/** /**
* A command to initiate the Cash flow with. * A command to initiate the Cash flow with.
@ -21,8 +20,9 @@ sealed class CashFlowCommand {
data class IssueCash(val amount: Amount<Currency>, data class IssueCash(val amount: Amount<Currency>,
val issueRef: OpaqueBytes, val issueRef: OpaqueBytes,
val recipient: Party, val recipient: Party,
val notary: Party) : CashFlowCommand() { val notary: Party,
override fun startFlow(proxy: CordaRPCOps) = proxy.startFlow(::CashIssueFlow, amount, issueRef, recipient, notary) val anonymous: Boolean) : CashFlowCommand() {
override fun startFlow(proxy: CordaRPCOps) = proxy.startFlow(::CashIssueFlow, amount, issueRef, recipient, notary, anonymous)
} }
/** /**
@ -31,8 +31,9 @@ sealed class CashFlowCommand {
* @param amount the amount of currency to issue on to the ledger. * @param amount the amount of currency to issue on to the ledger.
* @param recipient the party to issue the cash to. * @param recipient the party to issue the cash to.
*/ */
data class PayCash(val amount: Amount<Currency>, val recipient: Party, val issuerConstraint: Party? = null) : CashFlowCommand() { data class PayCash(val amount: Amount<Currency>, val recipient: Party, val issuerConstraint: Party? = null,
override fun startFlow(proxy: CordaRPCOps) = proxy.startFlow(::CashPaymentFlow, amount, recipient) val anonymous: Boolean) : CashFlowCommand() {
override fun startFlow(proxy: CordaRPCOps) = proxy.startFlow(::CashPaymentFlow, amount, recipient, anonymous)
} }
/** /**

View File

@ -5,10 +5,9 @@ import net.corda.contracts.asset.Cash
import net.corda.core.contracts.Amount import net.corda.core.contracts.Amount
import net.corda.core.contracts.TransactionType import net.corda.core.contracts.TransactionType
import net.corda.core.contracts.issuedBy import net.corda.core.contracts.issuedBy
import net.corda.core.identity.Party
import net.corda.core.flows.StartableByRPC import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.Party
import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.OpaqueBytes
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker
import java.util.* import java.util.*
@ -26,23 +25,39 @@ class CashIssueFlow(val amount: Amount<Currency>,
val issueRef: OpaqueBytes, val issueRef: OpaqueBytes,
val recipient: Party, val recipient: Party,
val notary: Party, val notary: Party,
progressTracker: ProgressTracker) : AbstractCashFlow(progressTracker) { val anonymous: Boolean,
progressTracker: ProgressTracker) : AbstractCashFlow<AbstractCashFlow.Result>(progressTracker) {
constructor(amount: Amount<Currency>, constructor(amount: Amount<Currency>,
issueRef: OpaqueBytes, issueRef: OpaqueBytes,
recipient: Party, recipient: Party,
notary: Party) : this(amount, issueRef, recipient, notary, tracker()) notary: Party) : this(amount, issueRef, recipient, notary, true, tracker())
constructor(amount: Amount<Currency>,
issueRef: OpaqueBytes,
recipient: Party,
notary: Party,
anonymous: Boolean) : this(amount, issueRef, recipient, notary, anonymous, tracker())
@Suspendable @Suspendable
override fun call(): SignedTransaction { override fun call(): AbstractCashFlow.Result {
progressTracker.currentStep = GENERATING_ID
val txIdentities = if (anonymous) {
subFlow(TxKeyFlow.Requester(recipient))
} else {
TxKeyFlow.TxIdentities(emptyList())
}
val anonymousRecipient = if (anonymous) {
txIdentities.forParty(recipient).identity
} else {
recipient
}
progressTracker.currentStep = GENERATING_TX progressTracker.currentStep = GENERATING_TX
val builder: TransactionBuilder = TransactionType.General.Builder(notary = notary) val builder: TransactionBuilder = TransactionType.General.Builder(notary = notary)
val issuer = serviceHub.myInfo.legalIdentity.ref(issueRef) val issuer = serviceHub.myInfo.legalIdentity.ref(issueRef)
// TODO: Get a transaction key, don't just re-use the owning key val signers = Cash().generateIssue(builder, amount.issuedBy(issuer), anonymousRecipient, notary)
Cash().generateIssue(builder, amount.issuedBy(issuer), recipient, notary)
progressTracker.currentStep = SIGNING_TX progressTracker.currentStep = SIGNING_TX
val tx = serviceHub.signInitialTransaction(builder) val tx = serviceHub.signInitialTransaction(builder, signers)
progressTracker.currentStep = FINALISING_TX progressTracker.currentStep = FINALISING_TX
subFlow(FinalityFlow(tx)) subFlow(FinalityFlow(tx))
return tx return Result(tx, txIdentities)
} }
} }

View File

@ -6,7 +6,6 @@ import net.corda.core.contracts.InsufficientBalanceException
import net.corda.core.contracts.TransactionType import net.corda.core.contracts.TransactionType
import net.corda.core.flows.StartableByRPC import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker
import java.util.* import java.util.*
@ -17,18 +16,34 @@ import java.util.*
* @param amount the amount of a currency to pay to the recipient. * @param amount the amount of a currency to pay to the recipient.
* @param recipient the party to pay the currency to. * @param recipient the party to pay the currency to.
* @param issuerConstraint if specified, the payment will be made using only cash issued by the given parties. * @param issuerConstraint if specified, the payment will be made using only cash issued by the given parties.
* @param anonymous whether to anonymous the recipient party. Should be true for normal usage, but may be false
* for testing purposes.
*/ */
@StartableByRPC @StartableByRPC
open class CashPaymentFlow( open class CashPaymentFlow(
val amount: Amount<Currency>, val amount: Amount<Currency>,
val recipient: Party, val recipient: Party,
val anonymous: Boolean,
progressTracker: ProgressTracker, progressTracker: ProgressTracker,
val issuerConstraint: Set<Party>? = null) : AbstractCashFlow(progressTracker) { val issuerConstraint: Set<Party>? = null) : AbstractCashFlow<AbstractCashFlow.Result>(progressTracker) {
/** A straightforward constructor that constructs spends using cash states of any issuer. */ /** A straightforward constructor that constructs spends using cash states of any issuer. */
constructor(amount: Amount<Currency>, recipient: Party) : this(amount, recipient, tracker()) constructor(amount: Amount<Currency>, recipient: Party) : this(amount, recipient, true, tracker())
/** A straightforward constructor that constructs spends using cash states of any issuer. */
constructor(amount: Amount<Currency>, recipient: Party, anonymous: Boolean) : this(amount, recipient, anonymous, tracker())
@Suspendable @Suspendable
override fun call(): SignedTransaction { override fun call(): AbstractCashFlow.Result {
progressTracker.currentStep = GENERATING_ID
val txIdentities = if (anonymous) {
subFlow(TxKeyFlow.Requester(recipient))
} else {
TxKeyFlow.TxIdentities(emptyList())
}
val anonymousRecipient = if (anonymous) {
txIdentities.forParty(recipient).identity
} else {
recipient
}
progressTracker.currentStep = GENERATING_TX progressTracker.currentStep = GENERATING_TX
val builder: TransactionBuilder = TransactionType.General.Builder(null as Party?) val builder: TransactionBuilder = TransactionType.General.Builder(null as Party?)
// TODO: Have some way of restricting this to states the caller controls // TODO: Have some way of restricting this to states the caller controls
@ -36,8 +51,7 @@ open class CashPaymentFlow(
serviceHub.vaultService.generateSpend( serviceHub.vaultService.generateSpend(
builder, builder,
amount, amount,
// TODO: Get a transaction key, don't just re-use the owning key anonymousRecipient,
recipient,
issuerConstraint) issuerConstraint)
} catch (e: InsufficientBalanceException) { } catch (e: InsufficientBalanceException) {
throw CashException("Insufficient cash for spend: ${e.message}", e) throw CashException("Insufficient cash for spend: ${e.message}", e)
@ -48,6 +62,6 @@ open class CashPaymentFlow(
progressTracker.currentStep = FINALISING_TX progressTracker.currentStep = FINALISING_TX
finaliseTx(setOf(recipient), tx, "Unable to notarise spend") finaliseTx(setOf(recipient), tx, "Unable to notarise spend")
return tx return Result(tx, txIdentities)
} }
} }

View File

@ -1,8 +1,10 @@
package net.corda.flows package net.corda.flows
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.contracts.asset.Cash
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.flows.* import net.corda.core.flows.*
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.OpaqueBytes
@ -20,21 +22,45 @@ import java.util.*
*/ */
object IssuerFlow { object IssuerFlow {
@CordaSerializable @CordaSerializable
data class IssuanceRequestState(val amount: Amount<Currency>, val issueToParty: Party, val issuerPartyRef: OpaqueBytes) data class IssuanceRequestState(val amount: Amount<Currency>,
val issueToParty: Party,
val issuerPartyRef: OpaqueBytes,
val anonymous: Boolean)
/** /**
* IssuanceRequester should be used by a client to ask a remote node to issue some [FungibleAsset] with the given details. * IssuanceRequester should be used by a client to ask a remote node to issue some [FungibleAsset] with the given details.
* Returns the transaction created by the Issuer to move the cash to the Requester. * Returns the transaction created by the Issuer to move the cash to the Requester.
*
* @param anonymous true if the issued asset should be sent to a new confidential identity, false to send it to the
* well known identity (generally this is only used in testing).
*/ */
@InitiatingFlow @InitiatingFlow
@StartableByRPC @StartableByRPC
class IssuanceRequester(val amount: Amount<Currency>, val issueToParty: Party, val issueToPartyRef: OpaqueBytes, class IssuanceRequester(val amount: Amount<Currency>,
val issuerBankParty: Party) : FlowLogic<SignedTransaction>() { val issueToParty: Party,
val issueToPartyRef: OpaqueBytes,
val issuerBankParty: Party,
val anonymous: Boolean) : FlowLogic<AbstractCashFlow.Result>() {
@Suspendable @Suspendable
@Throws(CashException::class) @Throws(CashException::class)
override fun call(): SignedTransaction { override fun call(): AbstractCashFlow.Result {
val issueRequest = IssuanceRequestState(amount, issueToParty, issueToPartyRef) val issueRequest = IssuanceRequestState(amount, issueToParty, issueToPartyRef, anonymous)
return sendAndReceive<SignedTransaction>(issuerBankParty, issueRequest).unwrap { it } return sendAndReceive<AbstractCashFlow.Result>(issuerBankParty, issueRequest).unwrap { res ->
val tx = res.stx.tx
val recipient = if (anonymous) {
res.identities.forParty(issueToParty).identity
} else {
issueToParty
}
val expectedAmount = Amount(amount.quantity, Issued(issuerBankParty.ref(issueToPartyRef), amount.token))
val cashOutputs = tx.outputs
.map { it.data}
.filterIsInstance<Cash.State>()
.filter { state -> state.owner == recipient }
require(cashOutputs.size == 1) { "Require a single cash output paying $recipient, found ${tx.outputs}" }
require(cashOutputs.single().amount == expectedAmount) { "Require payment of $expectedAmount"}
res
}
} }
} }
@ -66,22 +92,23 @@ object IssuerFlow {
it it
} }
// TODO: parse request to determine Asset to issue // TODO: parse request to determine Asset to issue
val txn = issueCashTo(issueRequest.amount, issueRequest.issueToParty, issueRequest.issuerPartyRef) val txn = issueCashTo(issueRequest.amount, issueRequest.issueToParty, issueRequest.issuerPartyRef, issueRequest.anonymous)
progressTracker.currentStep = SENDING_CONFIRM progressTracker.currentStep = SENDING_CONFIRM
send(otherParty, txn) send(otherParty, txn)
return txn return txn.stx
} }
@Suspendable @Suspendable
private fun issueCashTo(amount: Amount<Currency>, private fun issueCashTo(amount: Amount<Currency>,
issueTo: Party, issueTo: Party,
issuerPartyRef: OpaqueBytes): SignedTransaction { issuerPartyRef: OpaqueBytes,
anonymous: Boolean): AbstractCashFlow.Result {
// TODO: pass notary in as request parameter // TODO: pass notary in as request parameter
val notaryParty = serviceHub.networkMapCache.notaryNodes[0].notaryIdentity val notaryParty = serviceHub.networkMapCache.notaryNodes[0].notaryIdentity
// invoke Cash subflow to issue Asset // invoke Cash subflow to issue Asset
progressTracker.currentStep = ISSUING progressTracker.currentStep = ISSUING
val bankOfCordaParty = serviceHub.myInfo.legalIdentity val issueRecipient = serviceHub.myInfo.legalIdentity
val issueCashFlow = CashIssueFlow(amount, issuerPartyRef, bankOfCordaParty, notaryParty) val issueCashFlow = CashIssueFlow(amount, issuerPartyRef, issueRecipient, notaryParty, anonymous = false)
val issueTx = subFlow(issueCashFlow) val issueTx = subFlow(issueCashFlow)
// NOTE: issueCashFlow performs a Broadcast (which stores a local copy of the txn to the ledger) // NOTE: issueCashFlow performs a Broadcast (which stores a local copy of the txn to the ledger)
// short-circuit when issuing to self // short-circuit when issuing to self
@ -89,7 +116,7 @@ object IssuerFlow {
return issueTx return issueTx
// now invoke Cash subflow to Move issued assetType to issue requester // now invoke Cash subflow to Move issued assetType to issue requester
progressTracker.currentStep = TRANSFERRING progressTracker.currentStep = TRANSFERRING
val moveCashFlow = CashPaymentFlow(amount, issueTo) val moveCashFlow = CashPaymentFlow(amount, issueTo, anonymous)
val moveTx = subFlow(moveCashFlow) val moveTx = subFlow(moveCashFlow)
// NOTE: CashFlow PayCash calls FinalityFlow which performs a Broadcast (which stores a local copy of the txn to the ledger) // NOTE: CashFlow PayCash calls FinalityFlow which performs a Broadcast (which stores a local copy of the txn to the ledger)
return moveTx return moveTx

View File

@ -3,8 +3,8 @@ package net.corda.flows
import net.corda.contracts.asset.Cash import net.corda.contracts.asset.Cash
import net.corda.core.contracts.DOLLARS import net.corda.core.contracts.DOLLARS
import net.corda.core.contracts.`issued by` import net.corda.core.contracts.`issued by`
import net.corda.core.identity.Party
import net.corda.core.getOrThrow import net.corda.core.getOrThrow
import net.corda.core.identity.Party
import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.OpaqueBytes
import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin
import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork
@ -51,7 +51,7 @@ class CashExitFlowTests {
val future = bankOfCordaNode.services.startFlow(CashExitFlow(exitAmount, val future = bankOfCordaNode.services.startFlow(CashExitFlow(exitAmount,
ref)).resultFuture ref)).resultFuture
mockNet.runNetwork() mockNet.runNetwork()
val exitTx = future.getOrThrow().tx val exitTx = future.getOrThrow().stx.tx
val expected = (initialBalance - exitAmount).`issued by`(bankOfCorda.ref(ref)) val expected = (initialBalance - exitAmount).`issued by`(bankOfCorda.ref(ref))
assertEquals(1, exitTx.inputs.size) assertEquals(1, exitTx.inputs.size)
assertEquals(1, exitTx.outputs.size) assertEquals(1, exitTx.outputs.size)

View File

@ -3,8 +3,8 @@ package net.corda.flows
import net.corda.contracts.asset.Cash import net.corda.contracts.asset.Cash
import net.corda.core.contracts.DOLLARS import net.corda.core.contracts.DOLLARS
import net.corda.core.contracts.`issued by` import net.corda.core.contracts.`issued by`
import net.corda.core.identity.Party
import net.corda.core.getOrThrow import net.corda.core.getOrThrow
import net.corda.core.identity.Party
import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.OpaqueBytes
import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin
import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork
@ -46,7 +46,7 @@ class CashIssueFlowTests {
bankOfCorda, bankOfCorda,
notary)).resultFuture notary)).resultFuture
mockNet.runNetwork() mockNet.runNetwork()
val issueTx = future.getOrThrow() val issueTx = future.getOrThrow().stx
val output = issueTx.tx.outputs.single().data as Cash.State val output = issueTx.tx.outputs.single().data as Cash.State
assertEquals(expected.`issued by`(bankOfCorda.ref(ref)), output.amount) assertEquals(expected.`issued by`(bankOfCorda.ref(ref)), output.amount)
} }

View File

@ -32,7 +32,9 @@ class CashPaymentFlowTests {
notary = notaryNode.info.notaryIdentity notary = notaryNode.info.notaryIdentity
bankOfCorda = bankOfCordaNode.info.legalIdentity bankOfCorda = bankOfCordaNode.info.legalIdentity
mockNet.runNetwork() notaryNode.registerInitiatedFlow(TxKeyFlow.Provider::class.java)
notaryNode.identity.registerIdentity(bankOfCordaNode.info.legalIdentityAndCert)
bankOfCordaNode.identity.registerIdentity(notaryNode.info.legalIdentityAndCert)
val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref, val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref,
bankOfCorda, bankOfCorda,
notary)).resultFuture notary)).resultFuture
@ -53,11 +55,11 @@ class CashPaymentFlowTests {
val future = bankOfCordaNode.services.startFlow(CashPaymentFlow(expectedPayment, val future = bankOfCordaNode.services.startFlow(CashPaymentFlow(expectedPayment,
payTo)).resultFuture payTo)).resultFuture
mockNet.runNetwork() mockNet.runNetwork()
val paymentTx = future.getOrThrow() val (paymentTx, identities) = future.getOrThrow()
val states = paymentTx.tx.outputs.map { it.data }.filterIsInstance<Cash.State>() val states = paymentTx.tx.outputs.map { it.data }.filterIsInstance<Cash.State>()
val ourState = states.single { it.owner.owningKey != payTo.owningKey } val paymentState: Cash.State = states.single { it.owner == identities.forParty(payTo).identity }
val paymentState = states.single { it.owner.owningKey == payTo.owningKey } val changeState: Cash.State = states.single { it != paymentState }
assertEquals(expectedChange.`issued by`(bankOfCorda.ref(ref)), ourState.amount) assertEquals(expectedChange.`issued by`(bankOfCorda.ref(ref)), changeState.amount)
assertEquals(expectedPayment.`issued by`(bankOfCorda.ref(ref)), paymentState.amount) assertEquals(expectedPayment.`issued by`(bankOfCorda.ref(ref)), paymentState.amount)
} }

View File

@ -39,6 +39,12 @@ class IssuerFlowTest {
notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name)
bankOfCordaNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name) bankOfCordaNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name)
bankClientNode = mockNet.createPartyNode(notaryNode.network.myAddress, MEGA_CORP.name) bankClientNode = mockNet.createPartyNode(notaryNode.network.myAddress, MEGA_CORP.name)
val nodes = listOf(notaryNode, bankOfCordaNode, bankClientNode)
nodes.forEach { node ->
nodes.map { it.info.legalIdentityAndCert }.forEach(node.services.identityService::registerIdentity)
node.registerInitiatedFlow(TxKeyFlow.Provider::class.java)
}
} }
@After @After
@ -51,7 +57,7 @@ class IssuerFlowTest {
// using default IssueTo Party Reference // using default IssueTo Party Reference
val (issuer, issuerResult) = runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, 1000000.DOLLARS, val (issuer, issuerResult) = runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, 1000000.DOLLARS,
bankClientNode.info.legalIdentity, OpaqueBytes.of(123)) bankClientNode.info.legalIdentity, OpaqueBytes.of(123))
assertEquals(issuerResult.get(), issuer.get().resultFuture.get()) assertEquals(issuerResult.get().stx, issuer.get().resultFuture.get())
// try to issue an amount of a restricted currency // try to issue an amount of a restricted currency
assertFailsWith<FlowException> { assertFailsWith<FlowException> {
@ -65,7 +71,7 @@ class IssuerFlowTest {
// using default IssueTo Party Reference // using default IssueTo Party Reference
val (issuer, issuerResult) = runIssuerAndIssueRequester(bankOfCordaNode, bankOfCordaNode, 1000000.DOLLARS, val (issuer, issuerResult) = runIssuerAndIssueRequester(bankOfCordaNode, bankOfCordaNode, 1000000.DOLLARS,
bankOfCordaNode.info.legalIdentity, OpaqueBytes.of(123)) bankOfCordaNode.info.legalIdentity, OpaqueBytes.of(123))
assertEquals(issuerResult.get(), issuer.get().resultFuture.get()) assertEquals(issuerResult.get().stx, issuer.get().resultFuture.get())
} }
@Test @Test
@ -78,7 +84,7 @@ class IssuerFlowTest {
bankClientNode.info.legalIdentity, OpaqueBytes.of(123)) bankClientNode.info.legalIdentity, OpaqueBytes.of(123))
} }
handles.forEach { handles.forEach {
require(it.issueRequestResult.get() is SignedTransaction) require(it.issueRequestResult.get().stx is SignedTransaction)
} }
} }
@ -91,7 +97,8 @@ class IssuerFlowTest {
val issuerFlows: Observable<IssuerFlow.Issuer> = issuerNode.registerInitiatedFlow(IssuerFlow.Issuer::class.java) val issuerFlows: Observable<IssuerFlow.Issuer> = issuerNode.registerInitiatedFlow(IssuerFlow.Issuer::class.java)
val firstIssuerFiber = issuerFlows.toFuture().map { it.stateMachine } val firstIssuerFiber = issuerFlows.toFuture().map { it.stateMachine }
val issueRequest = IssuanceRequester(amount, party, issueToPartyAndRef.reference, issuerNode.info.legalIdentity) val issueRequest = IssuanceRequester(amount, party, issueToPartyAndRef.reference, issuerNode.info.legalIdentity,
anonymous = false)
val issueRequestResultFuture = issueToNode.services.startFlow(issueRequest).resultFuture val issueRequestResultFuture = issueToNode.services.startFlow(issueRequest).resultFuture
return IssuerFlowTest.RunResult(firstIssuerFiber, issueRequestResultFuture) return IssuerFlowTest.RunResult(firstIssuerFiber, issueRequestResultFuture)
@ -99,6 +106,6 @@ class IssuerFlowTest {
private data class RunResult( private data class RunResult(
val issuer: ListenableFuture<FlowStateMachine<*>>, val issuer: ListenableFuture<FlowStateMachine<*>>,
val issueRequestResult: ListenableFuture<SignedTransaction> val issueRequestResult: ListenableFuture<AbstractCashFlow.Result>
) )
} }

View File

@ -260,6 +260,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
rpcFlows = emptyList() rpcFlows = emptyList()
} }
// TODO: Investigate having class path scanning find this flow
registerInitiatedFlow(TxKeyFlow.Provider::class.java)
// TODO Remove this once the cash stuff is in its own CorDapp // TODO Remove this once the cash stuff is in its own CorDapp
registerInitiatedFlow(IssuerFlow.Issuer::class.java) registerInitiatedFlow(IssuerFlow.Issuer::class.java)
@ -459,7 +461,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
val storageServices = initialiseStorageService(configuration.baseDirectory) val storageServices = initialiseStorageService(configuration.baseDirectory)
storage = storageServices.first storage = storageServices.first
checkpointStorage = storageServices.second checkpointStorage = storageServices.second
netMapCache = InMemoryNetworkMapCache() netMapCache = InMemoryNetworkMapCache(services)
network = makeMessagingService() network = makeMessagingService()
schemas = makeSchemaService() schemas = makeSchemaService()
vault = makeVaultService(configuration.dataSourceProperties) vault = makeVaultService(configuration.dataSourceProperties)

View File

@ -5,11 +5,11 @@ import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.generateKeyPair
import net.corda.core.crypto.keys import net.corda.core.crypto.keys
import net.corda.core.crypto.sign import net.corda.core.crypto.sign
import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate import net.corda.core.identity.PartyAndCertificate
import net.corda.core.node.services.IdentityService import net.corda.core.node.services.IdentityService
import net.corda.core.node.services.KeyManagementService import net.corda.core.node.services.KeyManagementService
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.flows.AnonymisedIdentity
import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.operator.ContentSigner import org.bouncycastle.operator.ContentSigner
import java.security.KeyPair import java.security.KeyPair
@ -58,7 +58,7 @@ class E2ETestKeyManagementService(val identityService: IdentityService,
return keyPair.public return keyPair.public
} }
override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): Pair<X509CertificateHolder, CertPath> { override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): AnonymisedIdentity {
return freshCertificate(identityService, freshKey(), identity, getSigner(identity.owningKey), revocationEnabled) return freshCertificate(identityService, freshKey(), identity, getSigner(identity.owningKey), revocationEnabled)
} }
@ -71,6 +71,10 @@ class E2ETestKeyManagementService(val identityService: IdentityService,
} }
} }
override fun filterMyKeys(candidateKeys: Iterable<PublicKey>): Iterable<PublicKey> {
return mutex.locked { candidateKeys.filter { it in this.keys } }
}
override fun sign(bytes: ByteArray, publicKey: PublicKey): DigitalSignature.WithKey { override fun sign(bytes: ByteArray, publicKey: PublicKey): DigitalSignature.WithKey {
val keyPair = getSigningKeyPair(publicKey) val keyPair = getSigningKeyPair(publicKey)
val signature = keyPair.sign(bytes) val signature = keyPair.sign(bytes)

View File

@ -4,6 +4,7 @@ import net.corda.core.crypto.*
import net.corda.core.identity.AnonymousParty import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.PartyAndCertificate import net.corda.core.identity.PartyAndCertificate
import net.corda.core.node.services.IdentityService import net.corda.core.node.services.IdentityService
import net.corda.flows.AnonymisedIdentity
import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.operator.ContentSigner import org.bouncycastle.operator.ContentSigner
import java.security.KeyPair import java.security.KeyPair
@ -30,7 +31,7 @@ fun freshCertificate(identityService: IdentityService,
subjectPublicKey: PublicKey, subjectPublicKey: PublicKey,
issuer: PartyAndCertificate, issuer: PartyAndCertificate,
issuerSigner: ContentSigner, issuerSigner: ContentSigner,
revocationEnabled: Boolean = false): Pair<X509CertificateHolder, CertPath> { revocationEnabled: Boolean = false): AnonymisedIdentity {
val issuerCertificate = issuer.certificate val issuerCertificate = issuer.certificate
val window = X509Utilities.getCertificateValidityWindow(Duration.ZERO, Duration.ofDays(10 * 365), issuerCertificate) val window = X509Utilities.getCertificateValidityWindow(Duration.ZERO, Duration.ofDays(10 * 365), issuerCertificate)
val ourCertificate = Crypto.createCertificate(CertificateType.IDENTITY, issuerCertificate.subject, issuerSigner, issuer.name, subjectPublicKey, window) val ourCertificate = Crypto.createCertificate(CertificateType.IDENTITY, issuerCertificate.subject, issuerSigner, issuer.name, subjectPublicKey, window)
@ -39,7 +40,7 @@ fun freshCertificate(identityService: IdentityService,
identityService.registerAnonymousIdentity(AnonymousParty(subjectPublicKey), identityService.registerAnonymousIdentity(AnonymousParty(subjectPublicKey),
issuer.party, issuer.party,
ourCertPath) ourCertPath)
return Pair(issuerCertificate, ourCertPath) return AnonymisedIdentity(ourCertPath, issuerCertificate, subjectPublicKey)
} }
fun getSigner(issuerKeyPair: KeyPair): ContentSigner { fun getSigner(issuerKeyPair: KeyPair): ContentSigner {

View File

@ -9,6 +9,7 @@ import net.corda.core.identity.PartyAndCertificate
import net.corda.core.node.services.IdentityService import net.corda.core.node.services.IdentityService
import net.corda.core.node.services.KeyManagementService import net.corda.core.node.services.KeyManagementService
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.flows.AnonymisedIdentity
import net.corda.node.utilities.* import net.corda.node.utilities.*
import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.operator.ContentSigner import org.bouncycastle.operator.ContentSigner
@ -60,6 +61,10 @@ class PersistentKeyManagementService(val identityService: IdentityService,
override val keys: Set<PublicKey> get() = mutex.locked { keys.keys } override val keys: Set<PublicKey> get() = mutex.locked { keys.keys }
override fun filterMyKeys(candidateKeys: Iterable<PublicKey>): Iterable<PublicKey> {
return mutex.locked { candidateKeys.filter { it in this.keys } }
}
override fun freshKey(): PublicKey { override fun freshKey(): PublicKey {
val keyPair = generateKeyPair() val keyPair = generateKeyPair()
mutex.locked { mutex.locked {
@ -68,7 +73,7 @@ class PersistentKeyManagementService(val identityService: IdentityService,
return keyPair.public return keyPair.public
} }
override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): Pair<X509CertificateHolder, CertPath> { override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): AnonymisedIdentity {
return freshCertificate(identityService, freshKey(), identity, getSigner(identity.owningKey), revocationEnabled) return freshCertificate(identityService, freshKey(), identity, getSigner(identity.owningKey), revocationEnabled)
} }

View File

@ -4,12 +4,15 @@ import com.google.common.annotations.VisibleForTesting
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture import com.google.common.util.concurrent.SettableFuture
import net.corda.core.bufferUntilSubscribed import net.corda.core.bufferUntilSubscribed
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.map import net.corda.core.map
import net.corda.core.messaging.DataFeed import net.corda.core.messaging.DataFeed
import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.messaging.SingleMessageRecipient
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.node.ServiceHub
import net.corda.core.node.services.DEFAULT_SESSION_ID import net.corda.core.node.services.DEFAULT_SESSION_ID
import net.corda.core.node.services.IdentityService
import net.corda.core.node.services.NetworkMapCache.MapChange import net.corda.core.node.services.NetworkMapCache.MapChange
import net.corda.core.node.services.PartyInfo import net.corda.core.node.services.PartyInfo
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
@ -35,9 +38,13 @@ import javax.annotation.concurrent.ThreadSafe
/** /**
* Extremely simple in-memory cache of the network map. * Extremely simple in-memory cache of the network map.
*
* @param serviceHub an optional service hub from which we'll take the identity service. We take a service hub rather
* than the identity service directly, as this avoids problems with service start sequence (network map cache
* and identity services depend on each other). Should always be provided except for unit test cases.
*/ */
@ThreadSafe @ThreadSafe
open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCacheInternal { open class InMemoryNetworkMapCache(private val serviceHub: ServiceHub?) : SingletonSerializeAsToken(), NetworkMapCacheInternal {
companion object { companion object {
val logger = loggerFor<InMemoryNetworkMapCache>() val logger = loggerFor<InMemoryNetworkMapCache>()
} }
@ -71,6 +78,17 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach
} }
override fun getNodeByLegalIdentityKey(identityKey: PublicKey): NodeInfo? = registeredNodes[identityKey] override fun getNodeByLegalIdentityKey(identityKey: PublicKey): NodeInfo? = registeredNodes[identityKey]
override fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo? {
val wellKnownParty = if (serviceHub != null) {
serviceHub.identityService.partyFromAnonymous(party)
} else {
party
}
return wellKnownParty?.let {
getNodeByLegalIdentityKey(it.owningKey)
}
}
override fun track(): DataFeed<List<NodeInfo>, MapChange> { override fun track(): DataFeed<List<NodeInfo>, MapChange> {
synchronized(_changed) { synchronized(_changed) {

View File

@ -7,6 +7,7 @@ import net.corda.core.crypto.isFulfilledBy
import net.corda.core.crypto.keys import net.corda.core.crypto.keys
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StateMachineRunId import net.corda.core.flows.StateMachineRunId
import net.corda.core.getOrThrow
import net.corda.core.messaging.* import net.corda.core.messaging.*
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.core.node.services.Vault import net.corda.core.node.services.Vault
@ -86,18 +87,11 @@ class CordaRPCOpsImplTest {
} }
// Tell the monitoring service node to issue some cash // Tell the monitoring service node to issue some cash
val anonymous = false
val recipient = aliceNode.info.legalIdentity val recipient = aliceNode.info.legalIdentity
rpc.startFlow(::CashIssueFlow, Amount(quantity, GBP), ref, recipient, notaryNode.info.notaryIdentity) val result = rpc.startFlow(::CashIssueFlow, Amount(quantity, GBP), ref, recipient, notaryNode.info.notaryIdentity, anonymous)
mockNet.runNetwork() mockNet.runNetwork()
val expectedState = Cash.State(Amount(quantity,
Issued(aliceNode.info.legalIdentity.ref(ref), GBP)),
recipient)
// Query vault via RPC
val cash = rpc.vaultQueryBy<Cash.State>()
assertEquals(expectedState, cash.states.first().state.data)
var issueSmId: StateMachineRunId? = null var issueSmId: StateMachineRunId? = null
stateMachineUpdates.expectEvents { stateMachineUpdates.expectEvents {
sequence( sequence(
@ -111,11 +105,14 @@ class CordaRPCOpsImplTest {
) )
} }
transactions.expectEvents { val tx = result.returnValue.getOrThrow()
expect { tx -> val expectedState = Cash.State(Amount(quantity,
assertEquals(expectedState, tx.tx.outputs.single().data) Issued(aliceNode.info.legalIdentity.ref(ref), GBP)),
} recipient)
}
// Query vault via RPC
val cash = rpc.vaultQueryBy<Cash.State>()
assertEquals(expectedState, cash.states.first().state.data)
// TODO: deprecated // TODO: deprecated
vaultUpdates.expectEvents { vaultUpdates.expectEvents {
@ -135,22 +132,24 @@ class CordaRPCOpsImplTest {
@Test @Test
fun `issue and move`() { fun `issue and move`() {
rpc.startFlow(::CashIssueFlow, val anonymous = false
val result = rpc.startFlow(::CashIssueFlow,
Amount(100, USD), Amount(100, USD),
OpaqueBytes(ByteArray(1, { 1 })), OpaqueBytes(ByteArray(1, { 1 })),
aliceNode.info.legalIdentity, aliceNode.info.legalIdentity,
notaryNode.info.notaryIdentity notaryNode.info.notaryIdentity,
false
) )
mockNet.runNetwork() mockNet.runNetwork()
rpc.startFlow(::CashPaymentFlow, Amount(100, USD), aliceNode.info.legalIdentity) rpc.startFlow(::CashPaymentFlow, Amount(100, USD), aliceNode.info.legalIdentity, anonymous)
mockNet.runNetwork() mockNet.runNetwork()
var issueSmId: StateMachineRunId? = null var issueSmId: StateMachineRunId? = null
var moveSmId: StateMachineRunId? = null var moveSmId: StateMachineRunId? = null
stateMachineUpdates.expectEvents { stateMachineUpdates.expectEvents() {
sequence( sequence(
// ISSUE // ISSUE
expect { add: StateMachineUpdate.Added -> expect { add: StateMachineUpdate.Added ->
@ -169,6 +168,7 @@ class CordaRPCOpsImplTest {
) )
} }
val tx = result.returnValue.getOrThrow()
transactions.expectEvents { transactions.expectEvents {
sequence( sequence(
// ISSUE // ISSUE
@ -233,7 +233,8 @@ class CordaRPCOpsImplTest {
Amount(100, USD), Amount(100, USD),
OpaqueBytes(ByteArray(1, { 1 })), OpaqueBytes(ByteArray(1, { 1 })),
aliceNode.info.legalIdentity, aliceNode.info.legalIdentity,
notaryNode.info.notaryIdentity notaryNode.info.notaryIdentity,
false
) )
} }
} }

View File

@ -27,7 +27,7 @@ open class MockServiceHubInternal(
val network: MessagingService? = null, val network: MessagingService? = null,
val identity: IdentityService? = MOCK_IDENTITY_SERVICE, val identity: IdentityService? = MOCK_IDENTITY_SERVICE,
val storage: TxWritableStorageService? = MockStorageService(), val storage: TxWritableStorageService? = MockStorageService(),
val mapCache: NetworkMapCacheInternal? = MockNetworkMapCache(), val mapCache: NetworkMapCacheInternal? = null,
val scheduler: SchedulerService? = null, val scheduler: SchedulerService? = null,
val overrideClock: Clock? = NodeClock(), val overrideClock: Clock? = NodeClock(),
val schemas: SchemaService? = NodeSchemaService(), val schemas: SchemaService? = NodeSchemaService(),
@ -46,7 +46,7 @@ open class MockServiceHubInternal(
override val networkService: MessagingService override val networkService: MessagingService
get() = network ?: throw UnsupportedOperationException() get() = network ?: throw UnsupportedOperationException()
override val networkMapCache: NetworkMapCacheInternal override val networkMapCache: NetworkMapCacheInternal
get() = mapCache ?: throw UnsupportedOperationException() get() = mapCache ?: MockNetworkMapCache(this)
override val storageService: StorageService override val storageService: StorageService
get() = storage ?: throw UnsupportedOperationException() get() = storage ?: throw UnsupportedOperationException()
override val schedulerService: SchedulerService override val schedulerService: SchedulerService

View File

@ -60,7 +60,8 @@ class ArtemisMessagingTests {
var messagingClient: NodeMessagingClient? = null var messagingClient: NodeMessagingClient? = null
var messagingServer: ArtemisMessagingServer? = null var messagingServer: ArtemisMessagingServer? = null
val networkMapCache = InMemoryNetworkMapCache() // TODO: We should have a dummy service hub rather than change behaviour in tests
val networkMapCache = InMemoryNetworkMapCache(serviceHub = null)
val rpcOps = object : RPCOps { val rpcOps = object : RPCOps {
override val protocolVersion: Int get() = throw UnsupportedOperationException() override val protocolVersion: Int get() = throw UnsupportedOperationException()

View File

@ -6,6 +6,7 @@ import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate import net.corda.core.identity.PartyAndCertificate
import net.corda.core.node.services.IdentityService import net.corda.core.node.services.IdentityService
import net.corda.core.utilities.* import net.corda.core.utilities.*
import net.corda.flows.AnonymisedIdentity
import net.corda.flows.TxKeyFlow import net.corda.flows.TxKeyFlow
import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.identity.InMemoryIdentityService
import net.corda.testing.ALICE_PUBKEY import net.corda.testing.ALICE_PUBKEY
@ -136,14 +137,14 @@ class InMemoryIdentityServiceTests {
} }
} }
private fun createParty(x500Name: X500Name, ca: CertificateAndKeyPair): Pair<PartyAndCertificate, TxKeyFlow.AnonymousIdentity> { private fun createParty(x500Name: X500Name, ca: CertificateAndKeyPair): Pair<PartyAndCertificate, AnonymisedIdentity> {
val certFactory = CertificateFactory.getInstance("X509") val certFactory = CertificateFactory.getInstance("X509")
val issuerKeyPair = generateKeyPair() val issuerKeyPair = generateKeyPair()
val issuer = getTestPartyAndCertificate(x500Name, issuerKeyPair.public, ca) val issuer = getTestPartyAndCertificate(x500Name, issuerKeyPair.public, ca)
val txKey = Crypto.generateKeyPair() val txKey = Crypto.generateKeyPair()
val txCert = X509Utilities.createCertificate(CertificateType.IDENTITY, issuer.certificate, issuerKeyPair, x500Name, txKey.public) val txCert = X509Utilities.createCertificate(CertificateType.IDENTITY, issuer.certificate, issuerKeyPair, x500Name, txKey.public)
val txCertPath = certFactory.generateCertPath(listOf(txCert.cert) + issuer.certPath.certificates) val txCertPath = certFactory.generateCertPath(listOf(txCert.cert) + issuer.certPath.certificates)
return Pair(issuer, TxKeyFlow.AnonymousIdentity(txCertPath, txCert, AnonymousParty(txKey.public))) return Pair(issuer, AnonymisedIdentity(txCertPath, txCert, AnonymousParty(txKey.public)))
} }
/** /**

View File

@ -1,11 +1,13 @@
package net.corda.node.services.network package net.corda.node.services.network
import net.corda.core.getOrThrow import net.corda.core.getOrThrow
import net.corda.core.node.services.NetworkMapCache
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.core.utilities.ALICE import net.corda.core.utilities.ALICE
import net.corda.core.utilities.BOB import net.corda.core.utilities.BOB
import net.corda.node.utilities.transaction import net.corda.node.utilities.transaction
import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork
import org.junit.After
import org.junit.Test import org.junit.Test
import java.math.BigInteger import java.math.BigInteger
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -13,6 +15,11 @@ import kotlin.test.assertEquals
class InMemoryNetworkMapCacheTest { class InMemoryNetworkMapCacheTest {
private val mockNet = MockNetwork() private val mockNet = MockNetwork()
@After
fun teardown() {
mockNet.stopNodes()
}
@Test @Test
fun registerWithNetwork() { fun registerWithNetwork() {
val (n0, n1) = mockNet.createTwoNodes() val (n0, n1) = mockNet.createTwoNodes()
@ -28,6 +35,8 @@ class InMemoryNetworkMapCacheTest {
val nodeB = mockNet.createNode(null, -1, MockNetwork.DefaultFactory, true, BOB.name, null, entropy, ServiceInfo(NetworkMapService.type)) val nodeB = mockNet.createNode(null, -1, MockNetwork.DefaultFactory, true, BOB.name, null, entropy, ServiceInfo(NetworkMapService.type))
assertEquals(nodeA.info.legalIdentity, nodeB.info.legalIdentity) assertEquals(nodeA.info.legalIdentity, nodeB.info.legalIdentity)
mockNet.runNetwork()
// Node A currently knows only about itself, so this returns node A // Node A currently knows only about itself, so this returns node A
assertEquals(nodeA.netMapCache.getNodeByLegalIdentityKey(nodeA.info.legalIdentity.owningKey), nodeA.info) assertEquals(nodeA.netMapCache.getNodeByLegalIdentityKey(nodeA.info.legalIdentity.owningKey), nodeA.info)
@ -37,4 +46,17 @@ class InMemoryNetworkMapCacheTest {
// The details of node B write over those for node A // The details of node B write over those for node A
assertEquals(nodeA.netMapCache.getNodeByLegalIdentityKey(nodeA.info.legalIdentity.owningKey), nodeB.info) assertEquals(nodeA.netMapCache.getNodeByLegalIdentityKey(nodeA.info.legalIdentity.owningKey), nodeB.info)
} }
@Test
fun `getNodeByLegalIdentity`() {
val (n0, n1) = mockNet.createTwoNodes()
val node0Cache: NetworkMapCache = n0.services.networkMapCache
val expected = n1.info
mockNet.runNetwork()
val actual = node0Cache.getNodeByLegalIdentity(n1.info.legalIdentity)
assertEquals(expected, actual)
// TODO: Should have a test case with anonymous lookup
}
} }

View File

@ -328,10 +328,11 @@ class FlowFrameworkTests {
2000.DOLLARS, 2000.DOLLARS,
OpaqueBytes.of(0x01), OpaqueBytes.of(0x01),
node1.info.legalIdentity, node1.info.legalIdentity,
notary1.info.notaryIdentity)) notary1.info.notaryIdentity,
anonymous = false))
// We pay a couple of times, the notary picking should go round robin // We pay a couple of times, the notary picking should go round robin
for (i in 1..3) { for (i in 1..3) {
node1.services.startFlow(CashPaymentFlow(500.DOLLARS, node2.info.legalIdentity)) node1.services.startFlow(CashPaymentFlow(500.DOLLARS, node2.info.legalIdentity, anonymous = false))
mockNet.runNetwork() mockNet.runNetwork()
} }
val endpoint = mockNet.messagingNetwork.endpoint(notary1.network.myAddress as InMemoryMessagingNetwork.PeerHandle)!! val endpoint = mockNet.messagingNetwork.endpoint(notary1.network.myAddress as InMemoryMessagingNetwork.PeerHandle)!!

View File

@ -19,8 +19,9 @@ class BankOfCordaHttpAPITest {
startNode(BOC.name, setOf(ServiceInfo(SimpleNotaryService.type))), startNode(BOC.name, setOf(ServiceInfo(SimpleNotaryService.type))),
startNode(BIGCORP_LEGAL_NAME) startNode(BIGCORP_LEGAL_NAME)
).getOrThrow() ).getOrThrow()
val anonymous = true
val nodeBankOfCordaApiAddr = startWebserver(nodeBankOfCorda).getOrThrow().listenAddress val nodeBankOfCordaApiAddr = startWebserver(nodeBankOfCorda).getOrThrow().listenAddress
assertTrue(BankOfCordaClientApi(nodeBankOfCordaApiAddr).requestWebIssue(IssueRequestParams(1000, "USD", BIGCORP_LEGAL_NAME, "1", BOC.name))) assertTrue(BankOfCordaClientApi(nodeBankOfCordaApiAddr).requestWebIssue(IssueRequestParams(1000, "USD", BIGCORP_LEGAL_NAME, "1", BOC.name, anonymous)))
}, isDebug = true) }, isDebug = true)
} }
} }

View File

@ -39,25 +39,28 @@ class BankOfCordaRPCClientTest {
val vaultUpdatesBigCorp = bigCorpProxy.vaultAndUpdates().second val vaultUpdatesBigCorp = bigCorpProxy.vaultAndUpdates().second
// Kick-off actual Issuer Flow // Kick-off actual Issuer Flow
// TODO: Update checks below to reflect states consumed/produced under anonymisation
val anonymous = false
bocProxy.startFlow( bocProxy.startFlow(
::IssuanceRequester, ::IssuanceRequester,
1000.DOLLARS, 1000.DOLLARS,
nodeBigCorporation.nodeInfo.legalIdentity, nodeBigCorporation.nodeInfo.legalIdentity,
BIG_CORP_PARTY_REF, BIG_CORP_PARTY_REF,
nodeBankOfCorda.nodeInfo.legalIdentity).returnValue.getOrThrow() nodeBankOfCorda.nodeInfo.legalIdentity,
anonymous).returnValue.getOrThrow()
// Check Bank of Corda Vault Updates // Check Bank of Corda Vault Updates
vaultUpdatesBoc.expectEvents { vaultUpdatesBoc.expectEvents {
sequence( sequence(
// ISSUE // ISSUE
expect { update -> expect { update ->
require(update.consumed.isEmpty()) { update.consumed.size } require(update.consumed.isEmpty()) { "Expected 0 consumed states, actual: $update" }
require(update.produced.size == 1) { update.produced.size } require(update.produced.size == 1) { "Expected 1 produced states, actual: $update" }
}, },
// MOVE // MOVE
expect { update -> expect { update ->
require(update.consumed.size == 1) { update.consumed.size } require(update.consumed.size == 1) { "Expected 1 consumed states, actual: $update" }
require(update.produced.isEmpty()) { update.produced.size } require(update.produced.isEmpty()) { "Expected 0 produced states, actual: $update" }
} }
) )
} }

View File

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

View File

@ -44,7 +44,7 @@ class BankOfCordaClientApi(val hostAndPort: HostAndPort) {
val amount = Amount(params.amount, currency(params.currency)) val amount = Amount(params.amount, currency(params.currency))
val issuerToPartyRef = OpaqueBytes.of(params.issueToPartyRefAsString.toByte()) val issuerToPartyRef = OpaqueBytes.of(params.issueToPartyRefAsString.toByte())
return proxy.startFlow(::IssuanceRequester, amount, issueToParty, issuerToPartyRef, issuerBankParty).returnValue.getOrThrow() return proxy.startFlow(::IssuanceRequester, amount, issueToParty, issuerToPartyRef, issuerBankParty, params.anonymous).returnValue.getOrThrow().stx
} }
} }
} }

View File

@ -20,7 +20,8 @@ import javax.ws.rs.core.Response
class BankOfCordaWebApi(val rpc: CordaRPCOps) { class BankOfCordaWebApi(val rpc: CordaRPCOps) {
data class IssueRequestParams(val amount: Long, val currency: String, data class IssueRequestParams(val amount: Long, val currency: String,
val issueToPartyName: X500Name, val issueToPartyRefAsString: String, val issueToPartyName: X500Name, val issueToPartyRefAsString: String,
val issuerBankName: X500Name) val issuerBankName: X500Name,
val anonymous: Boolean)
private companion object { private companion object {
val logger = loggerFor<BankOfCordaWebApi>() val logger = loggerFor<BankOfCordaWebApi>()
@ -48,11 +49,12 @@ class BankOfCordaWebApi(val rpc: CordaRPCOps) {
val amount = Amount(params.amount, currency(params.currency)) val amount = Amount(params.amount, currency(params.currency))
val issuerToPartyRef = OpaqueBytes.of(params.issueToPartyRefAsString.toByte()) val issuerToPartyRef = OpaqueBytes.of(params.issueToPartyRefAsString.toByte())
val anonymous = params.anonymous
// invoke client side of Issuer Flow: IssuanceRequester // invoke client side of Issuer Flow: IssuanceRequester
// The line below blocks and waits for the future to resolve. // The line below blocks and waits for the future to resolve.
val status = try { val status = try {
rpc.startFlow(::IssuanceRequester, amount, issueToParty, issuerToPartyRef, issuerBankParty).returnValue.getOrThrow() rpc.startFlow(::IssuanceRequester, amount, issueToParty, issuerToPartyRef, issuerBankParty, anonymous).returnValue.getOrThrow()
logger.info("Issue request completed successfully: $params") logger.info("Issue request completed successfully: $params")
Response.Status.CREATED Response.Status.CREATED
} catch (e: FlowException) { } catch (e: FlowException) {

View File

@ -8,6 +8,7 @@ import net.corda.core.transactions.SignedTransaction
import net.corda.flows.AbstractStateReplacementFlow import net.corda.flows.AbstractStateReplacementFlow
import net.corda.flows.StateReplacementException import net.corda.flows.StateReplacementException
import net.corda.vega.contracts.RevisionedState import net.corda.vega.contracts.RevisionedState
import java.security.PublicKey
/** /**
* Flow that generates an update on a mutable deal state and commits the resulting transaction reaching consensus * Flow that generates an update on a mutable deal state and commits the resulting transaction reaching consensus
@ -16,13 +17,16 @@ import net.corda.vega.contracts.RevisionedState
object StateRevisionFlow { object StateRevisionFlow {
class Requester<T>(curStateRef: StateAndRef<RevisionedState<T>>, class Requester<T>(curStateRef: StateAndRef<RevisionedState<T>>,
updatedData: T) : AbstractStateReplacementFlow.Instigator<RevisionedState<T>, RevisionedState<T>, T>(curStateRef, updatedData) { updatedData: T) : AbstractStateReplacementFlow.Instigator<RevisionedState<T>, RevisionedState<T>, T>(curStateRef, updatedData) {
override fun assembleTx(): Pair<SignedTransaction, List<AbstractParty>> { override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx {
val state = originalState.state.data val state = originalState.state.data
val tx = state.generateRevision(originalState.state.notary, originalState, modification) val tx = state.generateRevision(originalState.state.notary, originalState, modification)
tx.addTimeWindow(serviceHub.clock.instant(), 30.seconds) tx.addTimeWindow(serviceHub.clock.instant(), 30.seconds)
val stx = serviceHub.signInitialTransaction(tx) val stx = serviceHub.signInitialTransaction(tx)
return Pair(stx, state.participants) val participantKeys = state.participants.map { it.owningKey }
// TODO: We need a much faster way of finding our key in the transaction
val myKey = serviceHub.keyManagementService.filterMyKeys(participantKeys).single()
return AbstractStateReplacementFlow.UpgradeTx(stx, participantKeys, myKey)
} }
} }

View File

@ -51,7 +51,8 @@ class TraderDemoTest : NodeBasedTest() {
val expectedBCash = clientB.cashCount + 1 val expectedBCash = clientB.cashCount + 1
val expectedPaper = listOf(clientA.commercialPaperCount + 1, clientB.commercialPaperCount) val expectedPaper = listOf(clientA.commercialPaperCount + 1, clientB.commercialPaperCount)
clientA.runBuyer(amount = 100.DOLLARS) // TODO: Enable anonymisation
clientA.runBuyer(amount = 100.DOLLARS, anonymous = false)
clientB.runSeller(counterparty = nodeA.info.legalIdentity.name, amount = 5.DOLLARS) clientB.runSeller(counterparty = nodeA.info.legalIdentity.name, amount = 5.DOLLARS)
assertThat(clientA.cashCount).isGreaterThan(originalACash) assertThat(clientA.cashCount).isGreaterThan(originalACash)

View File

@ -43,14 +43,14 @@ class TraderDemoClientApi(val rpc: CordaRPCOps) {
return vault.filterStatesOfType<CommercialPaper.State>().size return vault.filterStatesOfType<CommercialPaper.State>().size
} }
fun runBuyer(amount: Amount<Currency> = 30000.DOLLARS) { fun runBuyer(amount: Amount<Currency> = 30000.DOLLARS, anonymous: Boolean = true) {
val bankOfCordaParty = rpc.partyFromX500Name(BOC.name) val bankOfCordaParty = rpc.partyFromX500Name(BOC.name)
?: throw Exception("Unable to locate ${BOC.name} in Network Map Service") ?: throw Exception("Unable to locate ${BOC.name} in Network Map Service")
val me = rpc.nodeIdentity() val me = rpc.nodeIdentity()
val amounts = calculateRandomlySizedAmounts(amount, 3, 10, Random()) val amounts = calculateRandomlySizedAmounts(amount, 3, 10, Random())
// issuer random amounts of currency totaling 30000.DOLLARS in parallel // issuer random amounts of currency totaling 30000.DOLLARS in parallel
val resultFutures = amounts.map { pennies -> val resultFutures = amounts.map { pennies ->
rpc.startFlow(::IssuanceRequester, Amount(pennies, amount.token), me.legalIdentity, OpaqueBytes.of(1), bankOfCordaParty).returnValue rpc.startFlow(::IssuanceRequester, Amount(pennies, amount.token), me.legalIdentity, OpaqueBytes.of(1), bankOfCordaParty, anonymous).returnValue
} }
Futures.allAsList(resultFutures).getOrThrow() Futures.allAsList(resultFutures).getOrThrow()

View File

@ -5,6 +5,7 @@ import com.google.common.net.HostAndPort
import net.corda.core.crypto.entropyToKeyPair import net.corda.core.crypto.entropyToKeyPair
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.node.ServiceHub
import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.NetworkMapCache
import net.corda.core.utilities.getTestPartyAndCertificate import net.corda.core.utilities.getTestPartyAndCertificate
import net.corda.node.services.network.InMemoryNetworkMapCache import net.corda.node.services.network.InMemoryNetworkMapCache
@ -17,7 +18,7 @@ import java.math.BigInteger
/** /**
* Network map cache with no backing map service. * Network map cache with no backing map service.
*/ */
class MockNetworkMapCache : InMemoryNetworkMapCache() { class MockNetworkMapCache(serviceHub: ServiceHub) : InMemoryNetworkMapCache(serviceHub) {
private companion object { private companion object {
val BANK_C = getTestPartyAndCertificate(getTestX509Name("Bank C"), entropyToKeyPair(BigInteger.valueOf(1000)).public) val BANK_C = getTestPartyAndCertificate(getTestX509Name("Bank C"), entropyToKeyPair(BigInteger.valueOf(1000)).public)
val BANK_D = getTestPartyAndCertificate(getTestX509Name("Bank D"), entropyToKeyPair(BigInteger.valueOf(2000)).public) val BANK_D = getTestPartyAndCertificate(getTestX509Name("Bank D"), entropyToKeyPair(BigInteger.valueOf(2000)).public)

View File

@ -15,6 +15,7 @@ import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.DUMMY_CA import net.corda.core.utilities.DUMMY_CA
import net.corda.core.utilities.getTestPartyAndCertificate import net.corda.core.utilities.getTestPartyAndCertificate
import net.corda.flows.AnonymisedIdentity
import net.corda.node.services.database.HibernateConfiguration import net.corda.node.services.database.HibernateConfiguration
import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.identity.InMemoryIdentityService
import net.corda.node.services.keys.freshCertificate import net.corda.node.services.keys.freshCertificate
@ -104,7 +105,9 @@ class MockKeyManagementService(val identityService: IdentityService,
return k.public return k.public
} }
override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): Pair<X509CertificateHolder, CertPath> { override fun filterMyKeys(candidateKeys: Iterable<PublicKey>): Iterable<PublicKey> = candidateKeys.filter { it in this.keys }
override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): AnonymisedIdentity {
return freshCertificate(identityService, freshKey(), identity, getSigner(identity.owningKey), revocationEnabled) return freshCertificate(identityService, freshKey(), identity, getSigner(identity.owningKey), revocationEnabled)
} }

View File

@ -43,7 +43,8 @@ class SimpleNode(val config: NodeConfiguration, val address: HostAndPort = freeL
val identityService: IdentityService = InMemoryIdentityService(trustRoot = trustRoot) val identityService: IdentityService = InMemoryIdentityService(trustRoot = trustRoot)
val keyService: KeyManagementService = E2ETestKeyManagementService(identityService, setOf(identity)) val keyService: KeyManagementService = E2ETestKeyManagementService(identityService, setOf(identity))
val executor = ServiceAffinityExecutor(config.myLegalName.commonName, 1) val executor = ServiceAffinityExecutor(config.myLegalName.commonName, 1)
val broker = ArtemisMessagingServer(config, address.port, rpcAddress.port, InMemoryNetworkMapCache(), userService) // TODO: We should have a dummy service hub rather than change behaviour in tests
val broker = ArtemisMessagingServer(config, address.port, rpcAddress.port, InMemoryNetworkMapCache(serviceHub = null), userService)
val networkMapRegistrationFuture: SettableFuture<Unit> = SettableFuture.create<Unit>() val networkMapRegistrationFuture: SettableFuture<Unit> = SettableFuture.create<Unit>()
val network = database.transaction { val network = database.transaction {
NodeMessagingClient( NodeMessagingClient(

View File

@ -22,11 +22,7 @@ import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.ALICE import net.corda.core.utilities.ALICE
import net.corda.core.utilities.BOB import net.corda.core.utilities.BOB
import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.DUMMY_NOTARY
import net.corda.flows.CashExitFlow import net.corda.flows.*
import net.corda.flows.CashFlowCommand
import net.corda.flows.CashIssueFlow
import net.corda.flows.CashPaymentFlow
import net.corda.flows.IssuerFlow
import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.NodeHandle
import net.corda.testing.driver.PortAllocation import net.corda.testing.driver.PortAllocation
import net.corda.testing.driver.driver import net.corda.testing.driver.driver
@ -136,10 +132,11 @@ class ExplorerSimulation(val options: OptionSet) {
private fun startSimulation(eventGenerator: EventGenerator, maxIterations: Int) { private fun startSimulation(eventGenerator: EventGenerator, maxIterations: Int) {
// Log to logger when flow finish. // Log to logger when flow finish.
fun FlowHandle<SignedTransaction>.log(seq: Int, name: String) { fun FlowHandle<AbstractCashFlow.Result>.log(seq: Int, name: String) {
val out = "[$seq] $name $id :" val out = "[$seq] $name $id :"
returnValue.success { returnValue.success {
Main.log.info("$out ${it.id} ${(it.tx.outputs.first().data as Cash.State).amount}") val (stx, idenities) = it
Main.log.info("$out ${stx.id} ${(stx.tx.outputs.first().data as Cash.State).amount}")
}.failure { }.failure {
Main.log.info("$out ${it.message}") Main.log.info("$out ${it.message}")
} }
@ -179,11 +176,12 @@ class ExplorerSimulation(val options: OptionSet) {
currencies = listOf(GBP, USD) currencies = listOf(GBP, USD)
) )
val maxIterations = 100_000 val maxIterations = 100_000
val anonymous = true
// Pre allocate some money to each party. // Pre allocate some money to each party.
eventGenerator.parties.forEach { eventGenerator.parties.forEach {
for (ref in 0..1) { for (ref in 0..1) {
for ((currency, issuer) in issuers) { for ((currency, issuer) in issuers) {
CashFlowCommand.IssueCash(Amount(1_000_000, currency), OpaqueBytes(ByteArray(1, { ref.toByte() })), it, notaryNode.nodeInfo.notaryIdentity).startFlow(issuer) CashFlowCommand.IssueCash(Amount(1_000_000, currency), OpaqueBytes(ByteArray(1, { ref.toByte() })), it, notaryNode.nodeInfo.notaryIdentity, anonymous).startFlow(issuer)
} }
} }
} }

View File

@ -20,14 +20,16 @@ import net.corda.client.jfx.utils.unique
import net.corda.core.contracts.Amount import net.corda.core.contracts.Amount
import net.corda.core.contracts.sumOrNull import net.corda.core.contracts.sumOrNull
import net.corda.core.contracts.withoutIssuer import net.corda.core.contracts.withoutIssuer
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.flows.FlowException import net.corda.core.flows.FlowException
import net.corda.core.getOrThrow import net.corda.core.getOrThrow
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.messaging.FlowHandle
import net.corda.core.messaging.startFlow import net.corda.core.messaging.startFlow
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.OpaqueBytes
import net.corda.core.then import net.corda.core.then
import net.corda.core.transactions.SignedTransaction
import net.corda.explorer.formatters.PartyNameFormatter import net.corda.explorer.formatters.PartyNameFormatter
import net.corda.explorer.model.CashTransaction import net.corda.explorer.model.CashTransaction
import net.corda.explorer.model.IssuerModel import net.corda.explorer.model.IssuerModel
@ -35,6 +37,7 @@ import net.corda.explorer.model.ReportingCurrencyModel
import net.corda.explorer.views.bigDecimalFormatter import net.corda.explorer.views.bigDecimalFormatter
import net.corda.explorer.views.byteFormatter import net.corda.explorer.views.byteFormatter
import net.corda.explorer.views.stringConverter import net.corda.explorer.views.stringConverter
import net.corda.flows.AbstractCashFlow
import net.corda.flows.CashFlowCommand import net.corda.flows.CashFlowCommand
import net.corda.flows.IssuerFlow.IssuanceRequester import net.corda.flows.IssuerFlow.IssuanceRequester
import org.controlsfx.dialog.ExceptionDialog import org.controlsfx.dialog.ExceptionDialog
@ -92,18 +95,20 @@ class NewTransaction : Fragment() {
initOwner(window) initOwner(window)
show() show()
} }
val handle = if (command is CashFlowCommand.IssueCash) { val handle: FlowHandle<AbstractCashFlow.Result> = if (command is CashFlowCommand.IssueCash) {
rpcProxy.value!!.startFlow(::IssuanceRequester, rpcProxy.value!!.startFlow(::IssuanceRequester,
command.amount, command.amount,
command.recipient, command.recipient,
command.issueRef, command.issueRef,
myIdentity.value!!.legalIdentity) myIdentity.value!!.legalIdentity,
command.anonymous)
} else { } else {
command.startFlow(rpcProxy.value!!) command.startFlow(rpcProxy.value!!)
} }
runAsync { runAsync {
handle.returnValue.then { dialog.dialogPane.isDisable = false }.getOrThrow() handle.returnValue.then { dialog.dialogPane.isDisable = false }.getOrThrow()
}.ui { }.ui { it ->
val stx: SignedTransaction = it.stx
val type = when (command) { val type = when (command) {
is CashFlowCommand.IssueCash -> "Cash Issued" is CashFlowCommand.IssueCash -> "Cash Issued"
is CashFlowCommand.ExitCash -> "Cash Exited" is CashFlowCommand.ExitCash -> "Cash Exited"
@ -117,7 +122,7 @@ class NewTransaction : Fragment() {
row { label(type) { font = Font.font(font.family, FontWeight.EXTRA_BOLD, font.size + 2) } } row { label(type) { font = Font.font(font.family, FontWeight.EXTRA_BOLD, font.size + 2) } }
row { row {
label("Transaction ID :") { GridPane.setValignment(this, VPos.TOP) } label("Transaction ID :") { GridPane.setValignment(this, VPos.TOP) }
label { text = Splitter.fixedLength(16).split("${it.id}").joinToString("\n") } label { text = Splitter.fixedLength(16).split("${stx.id}").joinToString("\n") }
} }
} }
dialog.dialogPane.scene.window.sizeToScene() dialog.dialogPane.scene.window.sizeToScene()
@ -141,14 +146,16 @@ class NewTransaction : Fragment() {
dialogPane = root dialogPane = root
initOwner(window) initOwner(window)
setResultConverter { setResultConverter {
// TODO: Enable confidential identities
val anonymous = false
val defaultRef = OpaqueBytes.of(1) val defaultRef = OpaqueBytes.of(1)
val issueRef = if (issueRef.value != null) OpaqueBytes.of(issueRef.value) else defaultRef val issueRef = if (issueRef.value != null) OpaqueBytes.of(issueRef.value) else defaultRef
when (it) { when (it) {
executeButton -> when (transactionTypeCB.value) { executeButton -> when (transactionTypeCB.value) {
CashTransaction.Issue -> { CashTransaction.Issue -> {
CashFlowCommand.IssueCash(Amount.fromDecimal(amount.value, currencyChoiceBox.value), issueRef, partyBChoiceBox.value.legalIdentity, notaries.first().notaryIdentity) CashFlowCommand.IssueCash(Amount.fromDecimal(amount.value, currencyChoiceBox.value), issueRef, partyBChoiceBox.value.legalIdentity, notaries.first().notaryIdentity, anonymous)
} }
CashTransaction.Pay -> CashFlowCommand.PayCash(Amount.fromDecimal(amount.value, currencyChoiceBox.value), partyBChoiceBox.value.legalIdentity) CashTransaction.Pay -> CashFlowCommand.PayCash(Amount.fromDecimal(amount.value, currencyChoiceBox.value), partyBChoiceBox.value.legalIdentity, anonymous = anonymous)
CashTransaction.Exit -> CashFlowCommand.ExitCash(Amount.fromDecimal(amount.value, currencyChoiceBox.value), issueRef) CashTransaction.Exit -> CashFlowCommand.ExitCash(Amount.fromDecimal(amount.value, currencyChoiceBox.value), issueRef)
else -> null else -> null
} }

View File

@ -117,13 +117,14 @@ val crossCashTest = LoadTest<CrossCashCommand, CrossCashState>(
generate = { (nodeVaults), parallelism -> generate = { (nodeVaults), parallelism ->
val nodeMap = simpleNodes.associateBy { it.info.legalIdentity } val nodeMap = simpleNodes.associateBy { it.info.legalIdentity }
val anonymous = true
Generator.pickN(parallelism, simpleNodes).bind { nodes -> Generator.pickN(parallelism, simpleNodes).bind { nodes ->
Generator.sequence( Generator.sequence(
nodes.map { node -> nodes.map { node ->
val quantities = nodeVaults[node.info.legalIdentity] ?: mapOf() val quantities = nodeVaults[node.info.legalIdentity] ?: mapOf()
val possibleRecipients = nodeMap.keys.toList() val possibleRecipients = nodeMap.keys.toList()
val moves = quantities.map { val moves = quantities.map {
it.value.toDouble() / 1000 to generateMove(it.value, USD, node.info.legalIdentity, possibleRecipients) it.value.toDouble() / 1000 to generateMove(it.value, USD, node.info.legalIdentity, possibleRecipients, anonymous)
} }
val exits = quantities.mapNotNull { val exits = quantities.mapNotNull {
if (it.key == node.info.legalIdentity) { if (it.key == node.info.legalIdentity) {
@ -133,7 +134,7 @@ val crossCashTest = LoadTest<CrossCashCommand, CrossCashState>(
} }
} }
val command = Generator.frequency( val command = Generator.frequency(
listOf(1.0 to generateIssue(10000, USD, notary.info.notaryIdentity, possibleRecipients)) + moves + exits listOf(1.0 to generateIssue(10000, USD, notary.info.notaryIdentity, possibleRecipients, anonymous)) + moves + exits
) )
command.map { CrossCashCommand(it, nodeMap[node.info.legalIdentity]!!) } command.map { CrossCashCommand(it, nodeMap[node.info.legalIdentity]!!) }
} }

View File

@ -15,13 +15,14 @@ fun generateIssue(
max: Long, max: Long,
currency: Currency, currency: Currency,
notary: Party, notary: Party,
possibleRecipients: List<Party> possibleRecipients: List<Party>,
anonymous: Boolean
): Generator<CashFlowCommand.IssueCash> { ): Generator<CashFlowCommand.IssueCash> {
return generateAmount(1, max, Generator.pure(currency)).combine( return generateAmount(1, max, Generator.pure(currency)).combine(
Generator.pure(OpaqueBytes.of(0)), Generator.pure(OpaqueBytes.of(0)),
Generator.pickOne(possibleRecipients) Generator.pickOne(possibleRecipients)
) { amount, ref, recipient -> ) { amount, ref, recipient ->
CashFlowCommand.IssueCash(amount, ref, recipient, notary) CashFlowCommand.IssueCash(amount, ref, recipient, notary, anonymous)
} }
} }
@ -29,12 +30,13 @@ fun generateMove(
max: Long, max: Long,
currency: Currency, currency: Currency,
issuer: Party, issuer: Party,
possibleRecipients: List<Party> possibleRecipients: List<Party>,
anonymous: Boolean
): Generator<CashFlowCommand.PayCash> { ): Generator<CashFlowCommand.PayCash> {
return generateAmount(1, max, Generator.pure(Issued(PartyAndReference(issuer, OpaqueBytes.of(0)), currency))).combine( return generateAmount(1, max, Generator.pure(Issued(PartyAndReference(issuer, OpaqueBytes.of(0)), currency))).combine(
Generator.pickOne(possibleRecipients) Generator.pickOne(possibleRecipients)
) { amount, recipient -> ) { amount, recipient ->
CashFlowCommand.PayCash(amount.withoutIssuer(), recipient, issuer) CashFlowCommand.PayCash(amount.withoutIssuer(), recipient, issuer, anonymous)
} }
} }

View File

@ -38,7 +38,7 @@ val selfIssueTest = LoadTest<SelfIssueCommand, SelfIssueState>(
generate = { _, parallelism -> generate = { _, parallelism ->
val generateIssue = Generator.pickOne(simpleNodes).bind { node -> val generateIssue = Generator.pickOne(simpleNodes).bind { node ->
generateIssue(1000, USD, notary.info.notaryIdentity, listOf(node.info.legalIdentity)).map { generateIssue(1000, USD, notary.info.notaryIdentity, listOf(node.info.legalIdentity), anonymous = true).map {
SelfIssueCommand(it, node) SelfIssueCommand(it, node)
} }
} }

View File

@ -19,7 +19,7 @@ object StabilityTest {
val nodeMap = simpleNodes.associateBy { it.info.legalIdentity } val nodeMap = simpleNodes.associateBy { it.info.legalIdentity }
Generator.sequence(simpleNodes.map { node -> Generator.sequence(simpleNodes.map { node ->
val possibleRecipients = nodeMap.keys.toList() val possibleRecipients = nodeMap.keys.toList()
val moves = 0.5 to generateMove(1, USD, node.info.legalIdentity, possibleRecipients) val moves = 0.5 to generateMove(1, USD, node.info.legalIdentity, possibleRecipients, anonymous = true)
val exits = 0.5 to generateExit(1, USD) val exits = 0.5 to generateExit(1, USD)
val command = Generator.frequency(listOf(moves, exits)) val command = Generator.frequency(listOf(moves, exits))
command.map { CrossCashCommand(it, nodeMap[node.info.legalIdentity]!!) } command.map { CrossCashCommand(it, nodeMap[node.info.legalIdentity]!!) }
@ -42,7 +42,7 @@ object StabilityTest {
"Self issuing cash randomly", "Self issuing cash randomly",
generate = { _, parallelism -> generate = { _, parallelism ->
val generateIssue = Generator.pickOne(simpleNodes).bind { node -> val generateIssue = Generator.pickOne(simpleNodes).bind { node ->
generateIssue(1000, USD, notary.info.notaryIdentity, listOf(node.info.legalIdentity)).map { generateIssue(1000, USD, notary.info.notaryIdentity, listOf(node.info.legalIdentity), anonymous = true).map {
SelfIssueCommand(it, node) SelfIssueCommand(it, node)
} }
} }