mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
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:
parent
e5395fe1b7
commit
1a4965c294
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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))
|
||||
}
|
@ -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.
|
||||
*/
|
||||
|
@ -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 }
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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) }
|
||||
|
@ -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>() {
|
||||
|
@ -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> {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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()
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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>>>,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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)))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)!!
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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" }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -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 ...")
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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]!!) }
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user