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
fun `cash issue works end to end`() {
val anonymous = false
rpc.startFlow(::CashIssueFlow,
Amount(100, USD),
OpaqueBytes(ByteArray(1, { 1 })),
aliceNode.legalIdentity,
notaryNode.notaryIdentity
notaryNode.notaryIdentity,
anonymous
)
vaultUpdates.expectEvents(isStrict = false) {
@ -138,8 +140,9 @@ class NodeMonitorModelTest : DriverBasedTest() {
@Test
fun `cash issue and move`() {
rpc.startFlow(::CashIssueFlow, 100.DOLLARS, OpaqueBytes.of(1), aliceNode.legalIdentity, notaryNode.notaryIdentity).returnValue.getOrThrow()
rpc.startFlow(::CashPaymentFlow, 100.DOLLARS, bobNode.legalIdentity).returnValue.getOrThrow()
val anonymous = false
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 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 ->
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 ->
@ -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 ->
CashFlowCommand.PayCash(Amount(amountIssued, currency), recipient)
CashFlowCommand.PayCash(Amount(amountIssued, currency), recipient, anonymous = true)
}
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 ->
CashFlowCommand.PayCash(Amount(amountIssued, currency), recipient)
CashFlowCommand.PayCash(Amount(amountIssued, currency), recipient, anonymous = true)
}
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(

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
): 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.
*/

View File

@ -2,9 +2,11 @@ package net.corda.core.node.services
import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.contracts.Contract
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.messaging.DataFeed
import net.corda.core.node.NodeInfo
import net.corda.core.node.ServiceHub
import net.corda.core.randomOrNull
import net.corda.core.serialization.CordaSerializable
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()
/**
* 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. */
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.TransactionBuilder
import net.corda.core.transactions.WireTransaction
import net.corda.flows.AnonymisedIdentity
import org.bouncycastle.cert.X509CertificateHolder
import rx.Observable
import rx.subjects.PublishSubject
@ -486,9 +487,17 @@ interface KeyManagementService {
* @return X.509 certificate and path to the trust root.
*/
@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 publicKey The [PublicKey] partner to an internally held [PrivateKey], either derived from the node's primary identity,
* or previously generated via the [freshKey] method.

View File

@ -73,7 +73,7 @@ object DefaultKryoCustomizer {
noReferencesWithin<WireTransaction>()
register(sun.security.ec.ECPublicKeyImpl::class.java, PublicKeySerializer)
register(sun.security.ec.ECPublicKeyImpl::class.java, ECPublicKeyImplSerializer)
register(EdDSAPublicKey::class.java, Ed25519PublicKeySerializer)
register(EdDSAPrivateKey::class.java, Ed25519PrivateKeySerializer)
@ -113,6 +113,7 @@ object DefaultKryoCustomizer {
register(BCRSAPublicKey::class.java, PublicKeySerializer)
register(BCSphincs256PrivateKey::class.java, PrivateKeySerializer)
register(BCSphincs256PublicKey::class.java, PublicKeySerializer)
register(sun.security.ec.ECPublicKeyImpl::class.java, PublicKeySerializer)
val customization = KryoSerializationCustomization(this)
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.
@ThreadSafe
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.FlowLogic
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.SignedTransaction
@ -33,6 +34,15 @@ abstract class AbstractStateReplacementFlow {
@CordaSerializable
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
* ([Acceptor]) of that state. If participants agree to the proposed change, they each sign the transaction.
@ -57,17 +67,14 @@ abstract class AbstractStateReplacementFlow {
@Suspendable
@Throws(StateReplacementException::class)
override fun call(): StateAndRef<T> {
val (stx, participants) = assembleTx()
val (stx, participantKeys, myKey) = assembleTx()
progressTracker.currentStep = SIGNING
val myKey = serviceHub.myInfo.legalIdentity
val me = listOf(myKey)
val signatures = if (participants == me) {
val signatures = if (participantKeys.singleOrNull() == myKey) {
getNotarySignatures(stx)
} else {
collectSignatures((participants - me).map { it.owningKey }, stx)
collectSignatures(participantKeys - myKey, stx)
}
val finalTx = stx + signatures
@ -75,7 +82,13 @@ abstract class AbstractStateReplacementFlow {
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
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 stx = serviceHub.signInitialTransaction(baseTx)
return stx to originalState.state.data.participants
val participantKeys = originalState.state.data.participants.map { it.owningKey }.toSet()
// 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.TransactionBuilder
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
@ -24,24 +25,25 @@ class NotaryChangeFlow<out T : ContractState>(
progressTracker: ProgressTracker = tracker())
: 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 tx = TransactionType.NotaryChange.Builder(originalState.state.notary)
val participants: Iterable<AbstractParty>
if (state.encumbrance == null) {
val participants: Iterable<AbstractParty> = if (state.encumbrance == null) {
val modifiedState = TransactionState(state.data, modification)
tx.addInputState(originalState)
tx.addOutputState(modifiedState)
participants = state.data.participants
state.data.participants
} else {
participants = resolveEncumbrances(tx)
resolveEncumbrances(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.InitiatingFlow
import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.ProgressTracker
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.
@ -19,11 +16,11 @@ import java.security.cert.CertPath
*/
object TxKeyFlow {
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
if (theirCert.subject == otherSide.name) {
serviceHub.identityService.registerAnonymousIdentity(txIdentity, otherSide, certPath)
return AnonymousIdentity(certPath, theirCert, txIdentity)
return AnonymisedIdentity(certPath, theirCert, txIdentity)
} else
throw IllegalStateException("Expected certificate subject to be ${otherSide.name} but found ${theirCert.subject}")
}
@ -32,7 +29,7 @@ object TxKeyFlow {
@StartableByRPC
@InitiatingFlow
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())
companion object {
object AWAITING_KEY : ProgressTracker.Step("Awaiting key")
@ -41,14 +38,20 @@ object TxKeyFlow {
}
@Suspendable
override fun call(): Map<Party, AnonymousIdentity> {
override fun call(): TxIdentities {
progressTracker.currentStep = AWAITING_KEY
val myIdentityFragment = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, revocationEnabled)
val myIdentity = AnonymousIdentity(myIdentityFragment)
val theirIdentity = receive<AnonymousIdentity>(otherSide).unwrap { validateIdentity(it) }
send(otherSide, myIdentity)
return mapOf(Pair(otherSide, myIdentity),
Pair(serviceHub.myInfo.legalIdentity, theirIdentity))
val myIdentity = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, revocationEnabled)
serviceHub.identityService.registerAnonymousIdentity(myIdentity.identity, serviceHub.myInfo.legalIdentity, myIdentity.certPath)
// 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)
TxIdentities(Pair(otherSide, myIdentity),
Pair(serviceHub.myInfo.legalIdentity, theirIdentity))
}
}
}
@ -57,7 +60,7 @@ object TxKeyFlow {
* counterparty and as the result from the flow.
*/
@InitiatedBy(Requester::class)
class Provider(otherSide: Party) : AbstractIdentityFlow<Map<Party, AnonymousIdentity>>(otherSide, false) {
class Provider(otherSide: Party) : AbstractIdentityFlow<TxIdentities>(otherSide, false) {
companion object {
object SENDING_KEY : ProgressTracker.Step("Sending key")
}
@ -65,25 +68,24 @@ object TxKeyFlow {
override val progressTracker: ProgressTracker = ProgressTracker(SENDING_KEY)
@Suspendable
override fun call(): Map<Party, AnonymousIdentity> {
override fun call(): TxIdentities {
val revocationEnabled = false
progressTracker.currentStep = SENDING_KEY
val myIdentityFragment = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, revocationEnabled)
val myIdentity = AnonymousIdentity(myIdentityFragment)
val myIdentity = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, revocationEnabled)
send(otherSide, myIdentity)
val theirIdentity = receive<AnonymousIdentity>(otherSide).unwrap { validateIdentity(it) }
return mapOf(Pair(otherSide, myIdentity),
val theirIdentity = receive<AnonymisedIdentity>(otherSide).unwrap { validateIdentity(it) }
return TxIdentities(Pair(otherSide, myIdentity),
Pair(serviceHub.myInfo.legalIdentity, theirIdentity))
}
}
@CordaSerializable
data class AnonymousIdentity(
val certPath: CertPath,
val certificate: X509CertificateHolder,
val identity: AnonymousParty) {
constructor(myIdentity: Pair<X509CertificateHolder, CertPath>) : this(myIdentity.second,
myIdentity.first,
AnonymousParty(myIdentity.second.certificates.first().publicKey))
data class TxIdentities(val identities: List<Pair<Party, AnonymisedIdentity>>) {
constructor(vararg identities: Pair<Party, AnonymisedIdentity>) : this(identities.toList())
init {
require(identities.size == identities.map { it.first }.toSet().size) { "Identities must be unique: ${identities.map { it.first }}" }
}
fun forParty(party: Party): AnonymisedIdentity = identities.single { it.first == party }.second
fun toMap(): Map<Party, AnonymisedIdentity> = this.identities.toMap()
}
}

View File

@ -173,9 +173,11 @@ class ContractUpgradeFlowTest {
@Test
fun `upgrade Cash to v2`() {
// 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()
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() }
assertTrue(baseState.state.data is Cash.State, "Contract state is old version.")
// Starts contract upgrade flow.

View File

@ -40,7 +40,7 @@ class TxKeyFlowTests {
val requesterFlow = aliceNode.services.startFlow(TxKeyFlow.Requester(bob))
// 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)
// Verify that the generated anonymous identities do not match the well known identities
val aliceAnonymousIdentity = actual[alice] ?: throw IllegalStateException()

View File

@ -67,7 +67,8 @@ class IntegrationTestingTutorial {
i.DOLLARS,
issueRef,
bob.nodeInfo.legalIdentity,
notary.nodeInfo.notaryIdentity
notary.nodeInfo.notaryIdentity,
false // Not anonymised
).returnValue)
}
}.forEach(Thread::join) // Ensure the stack of futures is populated.
@ -90,7 +91,7 @@ class IntegrationTestingTutorial {
// START 5
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 {

View File

@ -13,7 +13,10 @@ import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.InitiatingFlow
import net.corda.core.identity.Party
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.vault.QueryCriteria
import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.unwrap
@ -39,14 +42,15 @@ private fun gatherOurInputs(serviceHub: ServiceHub,
amountRequired: Amount<Issued<Currency>>,
notary: Party?): Pair<List<StateAndRef<Cash.State>>, Long> {
// 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
val ourIdentity = serviceHub.myInfo.legalIdentity
val ourKeys = serviceHub.keyManagementService.keys
// Filter down to our own cash states with right currency and issuer
val suitableCashStates = cashStates.filter {
val state = it.state.data
(state.owner == ourIdentity)
&& (state.amount.token == amountRequired.token)
// TODO: We may want to have the list of our states pre-cached somewhere for performance
(state.owner.owningKey in ourKeys) && (state.amount.token == amountRequired.token)
}
require(!suitableCashStates.isEmpty()) { "Insufficient funds" }
var remaining = amountRequired.quantity
@ -132,9 +136,6 @@ class ForeignExchangeFlow(val tradeId: String,
require(it.inputs.all { it.state.notary == notary }) {
"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 }) {
"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
// so we can sign. Note the returned SignedTransaction is still not fully signed
// and would not pass full verification yet.
return serviceHub.signInitialTransaction(builder)
return serviceHub.signInitialTransaction(builder, ourSigners.single())
}
// DOCEND 3
}
@ -234,10 +235,11 @@ class ForeignExchangeRemoteFlow(val source: Party) : FlowLogic<Unit>() {
val ourResponse = prepareOurInputsAndOutputs(serviceHub, request)
// 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 wtx = it.tx
// 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
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
val ourSignature = serviceHub.createSignature(proposedTrade)
val ourSignature = serviceHub.createSignature(proposedTrade, ourKey)
// send the other side our signature.
send(source, ourSignature)

View File

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

View File

@ -429,7 +429,7 @@ class Obligation<P : Any> : Contract {
/**
* 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,
* 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 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.
* @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")
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
fun <S : FungibleAsset<T>, T: Any> generateIssue(tx: TransactionBuilder,
transactionState: TransactionState<S>,
issueCommand: CommandData) {
issueCommand: CommandData): Set<PublicKey> {
check(tx.inputStates().isEmpty())
check(tx.outputStates().map { it.data }.filterIsInstance(transactionState.javaClass).isEmpty())
require(transactionState.data.amount.quantity > 0)
val at = transactionState.data.amount.token.issuer
val commandSigner = at.party.owningKey
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 net.corda.core.flows.FlowException
import net.corda.core.flows.FlowLogic
import net.corda.core.identity.AnonymousParty
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.utilities.ProgressTracker
/**
* 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 {
object GENERATING_ID : ProgressTracker.Step("Generating anonymous identities")
object GENERATING_TX : ProgressTracker.Step("Generating transaction")
object SIGNING_TX : ProgressTracker.Step("Signing 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
internal fun finaliseTx(participants: Set<Party>, tx: SignedTransaction, message: String) {
protected fun finaliseTx(participants: Set<Party>, tx: SignedTransaction, message: String) {
try {
subFlow(FinalityFlow(tx, participants))
} catch (e: NotaryException) {
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)

View File

@ -9,7 +9,6 @@ import net.corda.core.contracts.issuedBy
import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.Party
import net.corda.core.serialization.OpaqueBytes
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.ProgressTracker
import java.util.*
@ -22,16 +21,20 @@ import java.util.*
* issuer.
*/
@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())
companion object {
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
@Throws(CashException::class)
override fun call(): SignedTransaction {
override fun call(): AbstractCashFlow.Result {
progressTracker.currentStep = GENERATING_TX
val builder: TransactionBuilder = TransactionType.General.Builder(notary = null as Party?)
val issuer = serviceHub.myInfo.legalIdentity.ref(issueRef)
@ -67,6 +70,6 @@ class CashExitFlow(val amount: Amount<Currency>, val issueRef: OpaqueBytes, prog
// Commit the transaction
progressTracker.currentStep = FINALISING_TX
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.startFlow
import net.corda.core.serialization.OpaqueBytes
import net.corda.core.transactions.SignedTransaction
import java.util.*
/**
* A command to initiate the cash flow with.
*/
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.
@ -21,8 +20,9 @@ sealed class CashFlowCommand {
data class IssueCash(val amount: Amount<Currency>,
val issueRef: OpaqueBytes,
val recipient: Party,
val notary: Party) : CashFlowCommand() {
override fun startFlow(proxy: CordaRPCOps) = proxy.startFlow(::CashIssueFlow, amount, issueRef, recipient, notary)
val notary: Party,
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 recipient the party to issue the cash to.
*/
data class PayCash(val amount: Amount<Currency>, val recipient: Party, val issuerConstraint: Party? = null) : CashFlowCommand() {
override fun startFlow(proxy: CordaRPCOps) = proxy.startFlow(::CashPaymentFlow, amount, recipient)
data class PayCash(val amount: Amount<Currency>, val recipient: Party, val issuerConstraint: Party? = null,
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.TransactionType
import net.corda.core.contracts.issuedBy
import net.corda.core.identity.Party
import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.Party
import net.corda.core.serialization.OpaqueBytes
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.ProgressTracker
import java.util.*
@ -26,23 +25,39 @@ class CashIssueFlow(val amount: Amount<Currency>,
val issueRef: OpaqueBytes,
val recipient: Party,
val notary: Party,
progressTracker: ProgressTracker) : AbstractCashFlow(progressTracker) {
val anonymous: Boolean,
progressTracker: ProgressTracker) : AbstractCashFlow<AbstractCashFlow.Result>(progressTracker) {
constructor(amount: Amount<Currency>,
issueRef: OpaqueBytes,
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
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
val builder: TransactionBuilder = TransactionType.General.Builder(notary = notary)
val issuer = serviceHub.myInfo.legalIdentity.ref(issueRef)
// TODO: Get a transaction key, don't just re-use the owning key
Cash().generateIssue(builder, amount.issuedBy(issuer), recipient, notary)
val signers = Cash().generateIssue(builder, amount.issuedBy(issuer), anonymousRecipient, notary)
progressTracker.currentStep = SIGNING_TX
val tx = serviceHub.signInitialTransaction(builder)
val tx = serviceHub.signInitialTransaction(builder, signers)
progressTracker.currentStep = FINALISING_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.flows.StartableByRPC
import net.corda.core.identity.Party
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.ProgressTracker
import java.util.*
@ -17,18 +16,34 @@ import java.util.*
* @param amount the amount of a currency to pay to the recipient.
* @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 anonymous whether to anonymous the recipient party. Should be true for normal usage, but may be false
* for testing purposes.
*/
@StartableByRPC
open class CashPaymentFlow(
val amount: Amount<Currency>,
val recipient: Party,
val anonymous: Boolean,
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. */
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
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
val builder: TransactionBuilder = TransactionType.General.Builder(null as Party?)
// TODO: Have some way of restricting this to states the caller controls
@ -36,8 +51,7 @@ open class CashPaymentFlow(
serviceHub.vaultService.generateSpend(
builder,
amount,
// TODO: Get a transaction key, don't just re-use the owning key
recipient,
anonymousRecipient,
issuerConstraint)
} catch (e: InsufficientBalanceException) {
throw CashException("Insufficient cash for spend: ${e.message}", e)
@ -48,6 +62,6 @@ open class CashPaymentFlow(
progressTracker.currentStep = FINALISING_TX
finaliseTx(setOf(recipient), tx, "Unable to notarise spend")
return tx
return Result(tx, txIdentities)
}
}

View File

@ -1,8 +1,10 @@
package net.corda.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.contracts.asset.Cash
import net.corda.core.contracts.*
import net.corda.core.flows.*
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.OpaqueBytes
@ -20,21 +22,45 @@ import java.util.*
*/
object IssuerFlow {
@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.
* 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
@StartableByRPC
class IssuanceRequester(val amount: Amount<Currency>, val issueToParty: Party, val issueToPartyRef: OpaqueBytes,
val issuerBankParty: Party) : FlowLogic<SignedTransaction>() {
class IssuanceRequester(val amount: Amount<Currency>,
val issueToParty: Party,
val issueToPartyRef: OpaqueBytes,
val issuerBankParty: Party,
val anonymous: Boolean) : FlowLogic<AbstractCashFlow.Result>() {
@Suspendable
@Throws(CashException::class)
override fun call(): SignedTransaction {
val issueRequest = IssuanceRequestState(amount, issueToParty, issueToPartyRef)
return sendAndReceive<SignedTransaction>(issuerBankParty, issueRequest).unwrap { it }
override fun call(): AbstractCashFlow.Result {
val issueRequest = IssuanceRequestState(amount, issueToParty, issueToPartyRef, anonymous)
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
}
// 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
send(otherParty, txn)
return txn
return txn.stx
}
@Suspendable
private fun issueCashTo(amount: Amount<Currency>,
issueTo: Party,
issuerPartyRef: OpaqueBytes): SignedTransaction {
issuerPartyRef: OpaqueBytes,
anonymous: Boolean): AbstractCashFlow.Result {
// TODO: pass notary in as request parameter
val notaryParty = serviceHub.networkMapCache.notaryNodes[0].notaryIdentity
// invoke Cash subflow to issue Asset
progressTracker.currentStep = ISSUING
val bankOfCordaParty = serviceHub.myInfo.legalIdentity
val issueCashFlow = CashIssueFlow(amount, issuerPartyRef, bankOfCordaParty, notaryParty)
val issueRecipient = serviceHub.myInfo.legalIdentity
val issueCashFlow = CashIssueFlow(amount, issuerPartyRef, issueRecipient, notaryParty, anonymous = false)
val issueTx = subFlow(issueCashFlow)
// NOTE: issueCashFlow performs a Broadcast (which stores a local copy of the txn to the ledger)
// short-circuit when issuing to self
@ -89,7 +116,7 @@ object IssuerFlow {
return issueTx
// now invoke Cash subflow to Move issued assetType to issue requester
progressTracker.currentStep = TRANSFERRING
val moveCashFlow = CashPaymentFlow(amount, issueTo)
val moveCashFlow = CashPaymentFlow(amount, issueTo, anonymous)
val moveTx = subFlow(moveCashFlow)
// NOTE: CashFlow PayCash calls FinalityFlow which performs a Broadcast (which stores a local copy of the txn to the ledger)
return moveTx

View File

@ -3,8 +3,8 @@ package net.corda.flows
import net.corda.contracts.asset.Cash
import net.corda.core.contracts.DOLLARS
import net.corda.core.contracts.`issued by`
import net.corda.core.identity.Party
import net.corda.core.getOrThrow
import net.corda.core.identity.Party
import net.corda.core.serialization.OpaqueBytes
import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin
import net.corda.testing.node.MockNetwork
@ -51,7 +51,7 @@ class CashExitFlowTests {
val future = bankOfCordaNode.services.startFlow(CashExitFlow(exitAmount,
ref)).resultFuture
mockNet.runNetwork()
val exitTx = future.getOrThrow().tx
val exitTx = future.getOrThrow().stx.tx
val expected = (initialBalance - exitAmount).`issued by`(bankOfCorda.ref(ref))
assertEquals(1, exitTx.inputs.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.core.contracts.DOLLARS
import net.corda.core.contracts.`issued by`
import net.corda.core.identity.Party
import net.corda.core.getOrThrow
import net.corda.core.identity.Party
import net.corda.core.serialization.OpaqueBytes
import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin
import net.corda.testing.node.MockNetwork
@ -46,7 +46,7 @@ class CashIssueFlowTests {
bankOfCorda,
notary)).resultFuture
mockNet.runNetwork()
val issueTx = future.getOrThrow()
val issueTx = future.getOrThrow().stx
val output = issueTx.tx.outputs.single().data as Cash.State
assertEquals(expected.`issued by`(bankOfCorda.ref(ref)), output.amount)
}

View File

@ -32,7 +32,9 @@ class CashPaymentFlowTests {
notary = notaryNode.info.notaryIdentity
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,
bankOfCorda,
notary)).resultFuture
@ -53,11 +55,11 @@ class CashPaymentFlowTests {
val future = bankOfCordaNode.services.startFlow(CashPaymentFlow(expectedPayment,
payTo)).resultFuture
mockNet.runNetwork()
val paymentTx = future.getOrThrow()
val (paymentTx, identities) = future.getOrThrow()
val states = paymentTx.tx.outputs.map { it.data }.filterIsInstance<Cash.State>()
val ourState = states.single { it.owner.owningKey != payTo.owningKey }
val paymentState = states.single { it.owner.owningKey == payTo.owningKey }
assertEquals(expectedChange.`issued by`(bankOfCorda.ref(ref)), ourState.amount)
val paymentState: Cash.State = states.single { it.owner == identities.forParty(payTo).identity }
val changeState: Cash.State = states.single { it != paymentState }
assertEquals(expectedChange.`issued by`(bankOfCorda.ref(ref)), changeState.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)
bankOfCordaNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.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
@ -51,7 +57,7 @@ class IssuerFlowTest {
// using default IssueTo Party Reference
val (issuer, issuerResult) = runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, 1000000.DOLLARS,
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
assertFailsWith<FlowException> {
@ -65,7 +71,7 @@ class IssuerFlowTest {
// using default IssueTo Party Reference
val (issuer, issuerResult) = runIssuerAndIssueRequester(bankOfCordaNode, bankOfCordaNode, 1000000.DOLLARS,
bankOfCordaNode.info.legalIdentity, OpaqueBytes.of(123))
assertEquals(issuerResult.get(), issuer.get().resultFuture.get())
assertEquals(issuerResult.get().stx, issuer.get().resultFuture.get())
}
@Test
@ -78,7 +84,7 @@ class IssuerFlowTest {
bankClientNode.info.legalIdentity, OpaqueBytes.of(123))
}
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 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
return IssuerFlowTest.RunResult(firstIssuerFiber, issueRequestResultFuture)
@ -99,6 +106,6 @@ class IssuerFlowTest {
private data class RunResult(
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()
}
// 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
registerInitiatedFlow(IssuerFlow.Issuer::class.java)
@ -459,7 +461,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
val storageServices = initialiseStorageService(configuration.baseDirectory)
storage = storageServices.first
checkpointStorage = storageServices.second
netMapCache = InMemoryNetworkMapCache()
netMapCache = InMemoryNetworkMapCache(services)
network = makeMessagingService()
schemas = makeSchemaService()
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.keys
import net.corda.core.crypto.sign
import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.node.services.IdentityService
import net.corda.core.node.services.KeyManagementService
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.flows.AnonymisedIdentity
import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.operator.ContentSigner
import java.security.KeyPair
@ -58,7 +58,7 @@ class E2ETestKeyManagementService(val identityService: IdentityService,
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)
}
@ -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 {
val keyPair = getSigningKeyPair(publicKey)
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.PartyAndCertificate
import net.corda.core.node.services.IdentityService
import net.corda.flows.AnonymisedIdentity
import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.operator.ContentSigner
import java.security.KeyPair
@ -30,7 +31,7 @@ fun freshCertificate(identityService: IdentityService,
subjectPublicKey: PublicKey,
issuer: PartyAndCertificate,
issuerSigner: ContentSigner,
revocationEnabled: Boolean = false): Pair<X509CertificateHolder, CertPath> {
revocationEnabled: Boolean = false): AnonymisedIdentity {
val issuerCertificate = issuer.certificate
val window = X509Utilities.getCertificateValidityWindow(Duration.ZERO, Duration.ofDays(10 * 365), issuerCertificate)
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),
issuer.party,
ourCertPath)
return Pair(issuerCertificate, ourCertPath)
return AnonymisedIdentity(ourCertPath, issuerCertificate, subjectPublicKey)
}
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.KeyManagementService
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.flows.AnonymisedIdentity
import net.corda.node.utilities.*
import org.bouncycastle.cert.X509CertificateHolder
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 fun filterMyKeys(candidateKeys: Iterable<PublicKey>): Iterable<PublicKey> {
return mutex.locked { candidateKeys.filter { it in this.keys } }
}
override fun freshKey(): PublicKey {
val keyPair = generateKeyPair()
mutex.locked {
@ -68,7 +73,7 @@ class PersistentKeyManagementService(val identityService: IdentityService,
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)
}

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.SettableFuture
import net.corda.core.bufferUntilSubscribed
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.map
import net.corda.core.messaging.DataFeed
import net.corda.core.messaging.SingleMessageRecipient
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.IdentityService
import net.corda.core.node.services.NetworkMapCache.MapChange
import net.corda.core.node.services.PartyInfo
import net.corda.core.serialization.SingletonSerializeAsToken
@ -35,9 +38,13 @@ import javax.annotation.concurrent.ThreadSafe
/**
* 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
open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCacheInternal {
open class InMemoryNetworkMapCache(private val serviceHub: ServiceHub?) : SingletonSerializeAsToken(), NetworkMapCacheInternal {
companion object {
val logger = loggerFor<InMemoryNetworkMapCache>()
}
@ -71,6 +78,17 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach
}
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> {
synchronized(_changed) {

View File

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

View File

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

View File

@ -60,7 +60,8 @@ class ArtemisMessagingTests {
var messagingClient: NodeMessagingClient? = 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 {
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.node.services.IdentityService
import net.corda.core.utilities.*
import net.corda.flows.AnonymisedIdentity
import net.corda.flows.TxKeyFlow
import net.corda.node.services.identity.InMemoryIdentityService
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 issuerKeyPair = generateKeyPair()
val issuer = getTestPartyAndCertificate(x500Name, issuerKeyPair.public, ca)
val txKey = Crypto.generateKeyPair()
val txCert = X509Utilities.createCertificate(CertificateType.IDENTITY, issuer.certificate, issuerKeyPair, x500Name, txKey.public)
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
import net.corda.core.getOrThrow
import net.corda.core.node.services.NetworkMapCache
import net.corda.core.node.services.ServiceInfo
import net.corda.core.utilities.ALICE
import net.corda.core.utilities.BOB
import net.corda.node.utilities.transaction
import net.corda.testing.node.MockNetwork
import org.junit.After
import org.junit.Test
import java.math.BigInteger
import kotlin.test.assertEquals
@ -13,6 +15,11 @@ import kotlin.test.assertEquals
class InMemoryNetworkMapCacheTest {
private val mockNet = MockNetwork()
@After
fun teardown() {
mockNet.stopNodes()
}
@Test
fun registerWithNetwork() {
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))
assertEquals(nodeA.info.legalIdentity, nodeB.info.legalIdentity)
mockNet.runNetwork()
// Node A currently knows only about itself, so this returns node A
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
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,
OpaqueBytes.of(0x01),
node1.info.legalIdentity,
notary1.info.notaryIdentity))
notary1.info.notaryIdentity,
anonymous = false))
// We pay a couple of times, the notary picking should go round robin
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()
}
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(BIGCORP_LEGAL_NAME)
).getOrThrow()
val anonymous = true
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)
}
}

View File

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

View File

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

View File

@ -8,6 +8,7 @@ import net.corda.core.transactions.SignedTransaction
import net.corda.flows.AbstractStateReplacementFlow
import net.corda.flows.StateReplacementException
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
@ -16,13 +17,16 @@ import net.corda.vega.contracts.RevisionedState
object StateRevisionFlow {
class Requester<T>(curStateRef: StateAndRef<RevisionedState<T>>,
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 tx = state.generateRevision(originalState.state.notary, originalState, modification)
tx.addTimeWindow(serviceHub.clock.instant(), 30.seconds)
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 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)
assertThat(clientA.cashCount).isGreaterThan(originalACash)

View File

@ -43,14 +43,14 @@ class TraderDemoClientApi(val rpc: CordaRPCOps) {
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)
?: throw Exception("Unable to locate ${BOC.name} in Network Map Service")
val me = rpc.nodeIdentity()
val amounts = calculateRandomlySizedAmounts(amount, 3, 10, Random())
// issuer random amounts of currency totaling 30000.DOLLARS in parallel
val resultFutures = amounts.map { pennies ->
rpc.startFlow(::IssuanceRequester, Amount(pennies, amount.token), me.legalIdentity, OpaqueBytes.of(1), bankOfCordaParty).returnValue
rpc.startFlow(::IssuanceRequester, Amount(pennies, amount.token), me.legalIdentity, OpaqueBytes.of(1), bankOfCordaParty, anonymous).returnValue
}
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.identity.Party
import net.corda.core.node.NodeInfo
import net.corda.core.node.ServiceHub
import net.corda.core.node.services.NetworkMapCache
import net.corda.core.utilities.getTestPartyAndCertificate
import net.corda.node.services.network.InMemoryNetworkMapCache
@ -17,7 +18,7 @@ import java.math.BigInteger
/**
* Network map cache with no backing map service.
*/
class MockNetworkMapCache : InMemoryNetworkMapCache() {
class MockNetworkMapCache(serviceHub: ServiceHub) : InMemoryNetworkMapCache(serviceHub) {
private companion object {
val BANK_C = getTestPartyAndCertificate(getTestX509Name("Bank C"), entropyToKeyPair(BigInteger.valueOf(1000)).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.utilities.DUMMY_CA
import net.corda.core.utilities.getTestPartyAndCertificate
import net.corda.flows.AnonymisedIdentity
import net.corda.node.services.database.HibernateConfiguration
import net.corda.node.services.identity.InMemoryIdentityService
import net.corda.node.services.keys.freshCertificate
@ -104,7 +105,9 @@ class MockKeyManagementService(val identityService: IdentityService,
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)
}

View File

@ -43,7 +43,8 @@ class SimpleNode(val config: NodeConfiguration, val address: HostAndPort = freeL
val identityService: IdentityService = InMemoryIdentityService(trustRoot = trustRoot)
val keyService: KeyManagementService = E2ETestKeyManagementService(identityService, setOf(identity))
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 network = database.transaction {
NodeMessagingClient(

View File

@ -22,11 +22,7 @@ import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.ALICE
import net.corda.core.utilities.BOB
import net.corda.core.utilities.DUMMY_NOTARY
import net.corda.flows.CashExitFlow
import net.corda.flows.CashFlowCommand
import net.corda.flows.CashIssueFlow
import net.corda.flows.CashPaymentFlow
import net.corda.flows.IssuerFlow
import net.corda.flows.*
import net.corda.testing.driver.NodeHandle
import net.corda.testing.driver.PortAllocation
import net.corda.testing.driver.driver
@ -136,10 +132,11 @@ class ExplorerSimulation(val options: OptionSet) {
private fun startSimulation(eventGenerator: EventGenerator, maxIterations: Int) {
// 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 :"
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 {
Main.log.info("$out ${it.message}")
}
@ -179,11 +176,12 @@ class ExplorerSimulation(val options: OptionSet) {
currencies = listOf(GBP, USD)
)
val maxIterations = 100_000
val anonymous = true
// Pre allocate some money to each party.
eventGenerator.parties.forEach {
for (ref in 0..1) {
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.sumOrNull
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.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.node.NodeInfo
import net.corda.core.serialization.OpaqueBytes
import net.corda.core.then
import net.corda.core.transactions.SignedTransaction
import net.corda.explorer.formatters.PartyNameFormatter
import net.corda.explorer.model.CashTransaction
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.byteFormatter
import net.corda.explorer.views.stringConverter
import net.corda.flows.AbstractCashFlow
import net.corda.flows.CashFlowCommand
import net.corda.flows.IssuerFlow.IssuanceRequester
import org.controlsfx.dialog.ExceptionDialog
@ -92,18 +95,20 @@ class NewTransaction : Fragment() {
initOwner(window)
show()
}
val handle = if (command is CashFlowCommand.IssueCash) {
val handle: FlowHandle<AbstractCashFlow.Result> = if (command is CashFlowCommand.IssueCash) {
rpcProxy.value!!.startFlow(::IssuanceRequester,
command.amount,
command.recipient,
command.issueRef,
myIdentity.value!!.legalIdentity)
myIdentity.value!!.legalIdentity,
command.anonymous)
} else {
command.startFlow(rpcProxy.value!!)
}
runAsync {
handle.returnValue.then { dialog.dialogPane.isDisable = false }.getOrThrow()
}.ui {
}.ui { it ->
val stx: SignedTransaction = it.stx
val type = when (command) {
is CashFlowCommand.IssueCash -> "Cash Issued"
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("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()
@ -141,14 +146,16 @@ class NewTransaction : Fragment() {
dialogPane = root
initOwner(window)
setResultConverter {
// TODO: Enable confidential identities
val anonymous = false
val defaultRef = OpaqueBytes.of(1)
val issueRef = if (issueRef.value != null) OpaqueBytes.of(issueRef.value) else defaultRef
when (it) {
executeButton -> when (transactionTypeCB.value) {
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)
else -> null
}

View File

@ -117,13 +117,14 @@ val crossCashTest = LoadTest<CrossCashCommand, CrossCashState>(
generate = { (nodeVaults), parallelism ->
val nodeMap = simpleNodes.associateBy { it.info.legalIdentity }
val anonymous = true
Generator.pickN(parallelism, simpleNodes).bind { nodes ->
Generator.sequence(
nodes.map { node ->
val quantities = nodeVaults[node.info.legalIdentity] ?: mapOf()
val possibleRecipients = nodeMap.keys.toList()
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 {
if (it.key == node.info.legalIdentity) {
@ -133,7 +134,7 @@ val crossCashTest = LoadTest<CrossCashCommand, CrossCashState>(
}
}
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]!!) }
}

View File

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

View File

@ -19,7 +19,7 @@ object StabilityTest {
val nodeMap = simpleNodes.associateBy { it.info.legalIdentity }
Generator.sequence(simpleNodes.map { node ->
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 command = Generator.frequency(listOf(moves, exits))
command.map { CrossCashCommand(it, nodeMap[node.info.legalIdentity]!!) }
@ -42,7 +42,7 @@ object StabilityTest {
"Self issuing cash randomly",
generate = { _, parallelism ->
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)
}
}