Change CashIssueFlow to use anonymous identity

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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