Revert "Raft notaries can share a single key pair for the service identity (i… (#2269)"

This reverts commit 3e00676851.
This commit is contained in:
Andrius Dagys 2018-01-09 10:28:49 +00:00
parent 46d88ed740
commit 63e1bdaa94
10 changed files with 92 additions and 183 deletions

View File

@ -12,9 +12,6 @@ import net.corda.nodeapi.internal.config.NodeSSLConfiguration
import net.corda.nodeapi.internal.crypto.*
import org.slf4j.LoggerFactory
import java.nio.file.Path
import java.security.KeyPair
import java.security.PublicKey
import java.security.cert.X509Certificate
/**
* Contains utility methods for generating identities for a node.
@ -50,41 +47,19 @@ object DevIdentityGenerator {
return identity.party
}
fun generateDistributedNotaryCompositeIdentity(dirs: List<Path>, notaryName: CordaX500Name, threshold: Int = 1): Party {
fun generateDistributedNotaryIdentity(dirs: List<Path>, notaryName: CordaX500Name, threshold: Int = 1): Party {
require(dirs.isNotEmpty())
log.trace { "Generating composite identity \"$notaryName\" for nodes: ${dirs.joinToString()}" }
val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass")
val intermediateCa = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass")
val rootCert = caKeyStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA)
log.trace { "Generating identity \"$notaryName\" for nodes: ${dirs.joinToString()}" }
val keyPairs = (1..dirs.size).map { generateKeyPair() }
val notaryKey = CompositeKey.Builder().addKeys(keyPairs.map { it.public }).build(threshold)
keyPairs.zip(dirs) { keyPair, nodeDir ->
generateCertificates(keyPair, notaryKey, intermediateCa, notaryName, nodeDir, rootCert)
}
val compositeKey = CompositeKey.Builder().addKeys(keyPairs.map { it.public }).build(threshold)
return Party(notaryName, notaryKey)
}
fun generateDistributedNotarySingularIdentity(dirs: List<Path>, notaryName: CordaX500Name): Party {
require(dirs.isNotEmpty())
log.trace { "Generating singular identity \"$notaryName\" for nodes: ${dirs.joinToString()}" }
val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass")
val intermediateCa = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass")
val rootCert = caKeyStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA)
val rootCert = caKeyStore.getCertificate(X509Utilities.CORDA_ROOT_CA)
val keyPair = generateKeyPair()
val notaryKey = keyPair.public
dirs.forEach { dir ->
generateCertificates(keyPair, notaryKey, intermediateCa, notaryName, dir, rootCert)
}
return Party(notaryName, notaryKey)
}
private fun generateCertificates(keyPair: KeyPair, notaryKey: PublicKey, intermediateCa: CertificateAndKeyPair, notaryName: CordaX500Name, nodeDir: Path, rootCert: X509Certificate) {
val (serviceKeyCert, compositeKeyCert) = listOf(keyPair.public, notaryKey).map { publicKey ->
keyPairs.zip(dirs) { keyPair, nodeDir ->
val (serviceKeyCert, compositeKeyCert) = listOf(keyPair.public, compositeKey).map { publicKey ->
X509Utilities.createCertificate(
CertificateType.SERVICE_IDENTITY,
intermediateCa.certificate,
@ -102,4 +77,7 @@ object DevIdentityGenerator {
arrayOf(serviceKeyCert, intermediateCa.certificate, rootCert))
keystore.save(distServKeyStoreFile, "cordacadevpass")
}
return Party(notaryName, compositeKey)
}
}

View File

@ -60,7 +60,7 @@ class BFTNotaryServiceTests {
(Paths.get("config") / "currentView").deleteIfExists() // XXX: Make config object warn if this exists?
val replicaIds = (0 until clusterSize)
notary = DevIdentityGenerator.generateDistributedNotaryCompositeIdentity(
notary = DevIdentityGenerator.generateDistributedNotaryIdentity(
replicaIds.map { mockNet.baseDirectory(mockNet.nextNodeId + it) },
CordaX500Name("BFT", "Zurich", "CH"))

View File

@ -16,11 +16,11 @@ import net.corda.node.services.Permissions.Companion.startFlow
import net.corda.nodeapi.internal.config.User
import net.corda.testing.*
import net.corda.testing.driver.NodeHandle
import net.corda.testing.driver.PortAllocation
import net.corda.testing.driver.driver
import net.corda.testing.node.ClusterSpec
import net.corda.testing.node.NotarySpec
import net.corda.testing.node.internal.DummyClusterSpec
import org.assertj.core.api.Assertions.assertThat
import org.junit.Ignore
import org.junit.Test
import rx.Observable
import java.util.*
@ -32,23 +32,18 @@ class DistributedServiceTests {
private lateinit var raftNotaryIdentity: Party
private lateinit var notaryStateMachines: Observable<Pair<Party, StateMachineUpdate>>
private fun setup(compositeIdentity: Boolean = false, testBlock: () -> Unit) {
private fun setup(testBlock: () -> Unit) {
val testUser = User("test", "test", permissions = setOf(
startFlow<CashIssueFlow>(),
startFlow<CashPaymentFlow>(),
invokeRpc(CordaRPCOps::nodeInfo),
invokeRpc(CordaRPCOps::stateMachinesFeed))
)
driver(
extraCordappPackagesToScan = listOf("net.corda.finance.contracts"),
notarySpecs = listOf(
NotarySpec(
DUMMY_NOTARY_NAME,
rpcUsers = listOf(testUser),
cluster = DummyClusterSpec(clusterSize = 3, compositeServiceIdentity = compositeIdentity))
),
portAllocation = PortAllocation.RandomFree
) {
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, rpcUsers = listOf(testUser), cluster = ClusterSpec.Raft(clusterSize = 3))))
{
alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(testUser)).getOrThrow()
raftNotaryIdentity = defaultNotaryIdentity
notaryNodes = defaultNotaryHandle.nodeHandles.getOrThrow().map { it as NodeHandle.OutOfProcess }
@ -77,10 +72,40 @@ class DistributedServiceTests {
}
}
// TODO This should be in RaftNotaryServiceTests
// TODO Use a dummy distributed service rather than a Raft Notary Service as this test is only about Artemis' ability
// to handle distributed services
@Ignore("Test has undeterministic capacity to hang, ignore till fixed")
@Test
fun `cluster survives if a notary is killed`() {
setup {
fun `requests are distributed evenly amongst the nodes`() = setup {
// Issue 100 pounds, then pay ourselves 50x2 pounds
issueCash(100.POUNDS)
for (i in 1..50) {
paySelf(2.POUNDS)
}
// The state machines added in the notaries should map one-to-one to notarisation requests
val notarisationsPerNotary = HashMap<Party, Int>()
notaryStateMachines.expectEvents(isStrict = false) {
replicate<Pair<Party, StateMachineUpdate>>(50) {
expect(match = { it.second is StateMachineUpdate.Added }) { (notary, update) ->
update as StateMachineUpdate.Added
notarisationsPerNotary.compute(notary) { _, number -> number?.plus(1) ?: 1 }
}
}
}
// The distribution of requests should be very close to sg like 16/17/17 as by default artemis does round robin
println("Notarisation distribution: $notarisationsPerNotary")
require(notarisationsPerNotary.size == 3)
// We allow some leeway for artemis as it doesn't always produce perfect distribution
require(notarisationsPerNotary.values.all { it > 10 })
}
// TODO This should be in RaftNotaryServiceTests
@Ignore("Test has undeterministic capacity to hang, ignore till fixed")
@Test
fun `cluster survives if a notary is killed`() = setup {
// Issue 100 pounds, then pay ourselves 10x5 pounds
issueCash(100.POUNDS)
@ -112,49 +137,6 @@ class DistributedServiceTests {
println("Notarisation distribution: $notarisationsPerNotary")
require(notarisationsPerNotary.size == 3)
}
}
// TODO Use a dummy distributed service rather than a Raft Notary Service as this test is only about Artemis' ability
// to handle distributed services
@Test
fun `requests are distributed evenly amongst the nodes`() {
setup {
checkRequestsDistributedEvenly()
}
}
@Test
fun `requests are distributed evenly amongst the nodes with a composite public key`() {
setup(true) {
checkRequestsDistributedEvenly()
}
}
private fun checkRequestsDistributedEvenly() {
// Issue 100 pounds, then pay ourselves 50x2 pounds
issueCash(100.POUNDS)
for (i in 1..50) {
paySelf(2.POUNDS)
}
// The state machines added in the notaries should map one-to-one to notarisation requests
val notarisationsPerNotary = HashMap<Party, Int>()
notaryStateMachines.expectEvents(isStrict = false) {
replicate<Pair<Party, StateMachineUpdate>>(50) {
expect(match = { it.second is StateMachineUpdate.Added }) { (notary, update) ->
update as StateMachineUpdate.Added
notarisationsPerNotary.compute(notary) { _, number -> number?.plus(1) ?: 1 }
}
}
}
// The distribution of requests should be very close to sg like 16/17/17 as by default artemis does round robin
println("Notarisation distribution: $notarisationsPerNotary")
require(notarisationsPerNotary.size == 3)
// We allow some leeway for artemis as it doesn't always produce perfect distribution
require(notarisationsPerNotary.values.all { it > 10 })
}
private fun issueCash(amount: Amount<Currency>) {
aliceProxy.startFlow(::CashIssueFlow, amount, OpaqueBytes.of(0), raftNotaryIdentity).returnValue.getOrThrow()

View File

@ -173,11 +173,11 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
}
private inline fun signNodeInfo(nodeInfo: NodeInfo, sign: (PublicKey, SerializedBytes<NodeInfo>) -> DigitalSignature): SignedNodeInfo {
// For now we exclude any composite identities, see [SignedNodeInfo]
val owningKeys = nodeInfo.legalIdentities.map { it.owningKey }.filter { it !is CompositeKey }
// For now we assume the node has only one identity (excluding any composite ones)
val owningKey = nodeInfo.legalIdentities.single { it.owningKey !is CompositeKey }.owningKey
val serialised = nodeInfo.serialize()
val signatures = owningKeys.map { sign(it, serialised) }
return SignedNodeInfo(serialised, signatures)
val signature = sign(owningKey, serialised)
return SignedNodeInfo(serialised, listOf(signature))
}
open fun generateAndSaveNodeInfo(): NodeInfo {

View File

@ -62,7 +62,7 @@ class BFTNotaryCordform : CordformDefinition() {
}
override fun setup(context: CordformContext) {
DevIdentityGenerator.generateDistributedNotaryCompositeIdentity(
DevIdentityGenerator.generateDistributedNotaryIdentity(
notaryNames.map { context.baseDirectory(it.toString()) },
clusterName,
minCorrectReplicas(clusterSize)

View File

@ -1,7 +1,6 @@
package net.corda.notarydemo
import net.corda.client.rpc.CordaRPCClient
import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.toStringShort
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.messaging.CordaRPCOps
@ -39,8 +38,7 @@ private class NotaryDemoClientApi(val rpc: CordaRPCOps) {
/** Makes calls to the node rpc to start transaction notarisation. */
fun notarise(count: Int) {
val keyType = if (notary.owningKey is CompositeKey) "composite" else "public"
println("Notary: \"${notary.name}\", with $keyType key: ${notary.owningKey.toStringShort()}")
println("Notary: \"${notary.name}\", with composite key: ${notary.owningKey.toStringShort()}")
val transactions = buildTransactions(count)
println("Notarised ${transactions.size} transactions:")
transactions.zip(notariseTransactions(transactions)).forEach { (tx, signersFuture) ->

View File

@ -58,7 +58,7 @@ class RaftNotaryCordform : CordformDefinition() {
}
override fun setup(context: CordformContext) {
DevIdentityGenerator.generateDistributedNotarySingularIdentity(
DevIdentityGenerator.generateDistributedNotaryIdentity(
notaryNames.map { context.baseDirectory(it.toString()) },
clusterName
)

View File

@ -14,12 +14,10 @@ data class NotarySpec(
)
@DoNotImplement
abstract class ClusterSpec {
sealed class ClusterSpec {
abstract val clusterSize: Int
data class Raft(
override val clusterSize: Int
) : ClusterSpec() {
data class Raft(override val clusterSize: Int) : ClusterSpec() {
init {
require(clusterSize > 0)
}

View File

@ -267,7 +267,7 @@ class DriverDSLImpl(
if (cordform.notary == null) continue
val name = CordaX500Name.parse(cordform.name)
val notaryConfig = ConfigFactory.parseMap(cordform.notary).parseAs<NotaryConfig>()
// We need to first group the nodes that form part of a cluster. We assume for simplicity that nodes of the
// We need to first group the nodes that form part of a cluser. We assume for simplicity that nodes of the
// same cluster type and validating flag are part of the same cluster.
if (notaryConfig.raft != null) {
val key = if (notaryConfig.validating) VALIDATING_RAFT else NON_VALIDATING_RAFT
@ -282,17 +282,10 @@ class DriverDSLImpl(
}
clusterNodes.asMap().forEach { type, nodeNames ->
val identity = if (type == ClusterType.NON_VALIDATING_RAFT || type == ClusterType.VALIDATING_RAFT) {
DevIdentityGenerator.generateDistributedNotarySingularIdentity(
val identity = DevIdentityGenerator.generateDistributedNotaryIdentity(
dirs = nodeNames.map { baseDirectory(it) },
notaryName = type.clusterName
)
} else {
DevIdentityGenerator.generateDistributedNotaryCompositeIdentity(
dirs = nodeNames.map { baseDirectory(it) },
notaryName = type.clusterName
)
}
notaryInfos += NotaryInfo(identity, type.validating)
}
@ -389,31 +382,14 @@ class DriverDSLImpl(
private fun startNotaryIdentityGeneration(): CordaFuture<List<NotaryInfo>> {
return executorService.fork {
notarySpecs.map { spec ->
val identity = when (spec.cluster) {
null -> {
val identity = if (spec.cluster == null) {
DevIdentityGenerator.installKeyStoreWithNodeIdentity(baseDirectory(spec.name), spec.name)
}
is ClusterSpec.Raft -> {
DevIdentityGenerator.generateDistributedNotarySingularIdentity(
dirs = generateNodeNames(spec).map { baseDirectory(it) },
notaryName = spec.name
)
}
is DummyClusterSpec -> {
if (spec.cluster.compositeServiceIdentity) {
DevIdentityGenerator.generateDistributedNotarySingularIdentity(
dirs = generateNodeNames(spec).map { baseDirectory(it) },
notaryName = spec.name
)
} else {
DevIdentityGenerator.generateDistributedNotaryCompositeIdentity(
DevIdentityGenerator.generateDistributedNotaryIdentity(
dirs = generateNodeNames(spec).map { baseDirectory(it) },
notaryName = spec.name
)
}
}
else -> throw UnsupportedOperationException("Cluster spec ${spec.cluster} not supported by Driver")
}
NotaryInfo(identity, spec.validating)
}
}
@ -457,12 +433,9 @@ class DriverDSLImpl(
private fun startNotaries(localNetworkMap: LocalNetworkMap?): List<CordaFuture<List<NodeHandle>>> {
return notarySpecs.map {
when (it.cluster) {
null -> startSingleNotary(it, localNetworkMap)
is ClusterSpec.Raft,
// DummyCluster is used for testing the notary communication path, and it does not matter
// which underlying consensus algorithm is used, so we just stick to Raft
is DummyClusterSpec -> startRaftNotaryCluster(it, localNetworkMap)
when {
it.cluster == null -> startSingleNotary(it, localNetworkMap)
it.cluster is ClusterSpec.Raft -> startRaftNotaryCluster(it, localNetworkMap)
else -> throw IllegalArgumentException("BFT-SMaRt not supported")
}
}

View File

@ -1,20 +0,0 @@
package net.corda.testing.node.internal
import net.corda.testing.node.ClusterSpec
/**
* Only used for testing the notary communication path. Can be configured to act as a Raft (singular identity),
* or a BFT (composite key identity) notary service.
*/
data class DummyClusterSpec(
override val clusterSize: Int,
/**
* If *true*, the cluster will use a shared composite public key for the service identity, with individual
* private keys. If *false*, the same "singular" key pair will be shared by all replicas.
*/
val compositeServiceIdentity: Boolean = false
) : ClusterSpec() {
init {
require(clusterSize > 0)
}
}