Nodes part of a group identity (e.g. notary) now advertise the whole group Party on the network. When sending a message to a group, a representative node advertising the group identity is first chosen (at random), and its legal identity is used for communication. Currently we assume that a single legal identity can't be advertised by more than one node (the PublicKeyTree of an identity is used for Artemis queue names and we need to do more work to properly map a single queue to multiple nodes)

This commit is contained in:
Andrius Dagys 2016-11-14 10:51:13 +00:00
parent c33c55eb20
commit d855b10817
9 changed files with 96 additions and 21 deletions

View File

@ -7,8 +7,8 @@ import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.MoreExecutors
import com.google.common.util.concurrent.SettableFuture
import net.corda.core.crypto.newSecureRandom
import kotlinx.support.jdk7.use
import net.corda.core.crypto.newSecureRandom
import org.slf4j.Logger
import rx.Observable
import rx.subjects.UnicastSubject
@ -155,6 +155,16 @@ fun <T> Iterable<T>.noneOrSingle(): T? {
return single
}
/** Returns a random element in the list, or null if empty */
fun <T> List<T>.randomOrNull(): T? {
if (size <= 1) return firstOrNull()
val randomIndex = (Math.random() * size).toInt()
return get(randomIndex)
}
/** Returns a random element in the list matching the given predicate, or null if none found */
fun <T> List<T>.randomOrNull(predicate: (T) -> Boolean) = filter(predicate).randomOrNull()
// An alias that can sometimes make code clearer to read.
val RunOnCallerThread = MoreExecutors.directExecutor()

View File

@ -72,9 +72,38 @@ interface NetworkMapCache {
fun getNodeByLegalName(name: String): NodeInfo?
/**
* Look up the node info for a public key.
* Look up the node info for a public key tree.
*/
fun getNodeByPublicKey(publicKey: PublicKeyTree): NodeInfo?
fun getNodeByPublicKeyTree(publicKeyTree: PublicKeyTree): NodeInfo?
/**
* Given a [party], returns a node advertising it as an identity. If more than one node found the result
* is chosen at random.
*
* In general, nodes can advertise multiple identities: a legal identity, and separate identities for each of
* the services it provides. In case of a distributed service run by multiple nodes each participant advertises
* the identity of the *whole group*. If the provided [party] is a group identity, multiple nodes advertising it
* will be found, and this method will return a randomly chosen one. If [party] is an individual (legal) identity,
* we currently assume that it will be advertised by one node only, which will be returned as the result.
*/
fun getRepresentativeNode(party: Party): NodeInfo?
/**
* Gets a notary identity by the given name.
*/
fun getNotary(name: String): Party?
/**
* Returns a notary identity advertised by any of the nodes on the network (chosen at random)
*
* @param type Limits the result to notaries of the specified type (optional)
*/
fun getAnyNotary(type: ServiceType? = null): Party?
/**
* Checks whether a given party is an advertised notary identity
*/
fun isNotary(party: Party): Boolean
/**
* Add a network map service; fetches a copy of the latest map from the service and subscribes to any further

View File

@ -20,10 +20,12 @@ abstract class BaseTransaction(
*/
val notary: Party?,
/**
* Keys that are required to have signed the wrapping [SignedTransaction], ordered to match the list of
* signatures. There is nothing that forces the list to be the _correct_ list of signers for this
* transaction until the transaction is verified by using [LedgerTransaction.verify]. It includes the
* notary key, if the notary field is set.
* Public key trees that need to be fulfilled by signatures in order for the transaction to be valid.
* In a [SignedTransaction] this list is used to check whether there are any missing signatures. Note that
* there is nothing that forces the list to be the _correct_ list of signers for this transaction until
* the transaction is verified by using [LedgerTransaction.verify].
*
* It includes the notary key, if the notary field is set.
*/
val mustSign: List<PublicKeyTree>,
/**

View File

@ -72,7 +72,7 @@ abstract class AbstractStateReplacementProtocol<T> {
@Suspendable
private fun collectSignatures(participants: List<PublicKeyTree>, stx: SignedTransaction): List<DigitalSignature.WithKey> {
val parties = participants.map {
val participantNode = serviceHub.networkMapCache.getNodeByPublicKey(it) ?:
val participantNode = serviceHub.networkMapCache.getNodeByPublicKeyTree(it) ?:
throw IllegalStateException("Participant $it to state $originalState not found on the network")
participantNode.legalIdentity
}

View File

@ -150,7 +150,7 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
if (queueName.startsWith(PEERS_PREFIX) && queueName != NETWORK_MAP_ADDRESS) {
try {
val identity = parseKeyFromQueueName(queueName.toString())
val nodeInfo = networkMapCache.getNodeByPublicKey(identity)
val nodeInfo = networkMapCache.getNodeByPublicKeyTree(identity)
if (nodeInfo != null) {
maybeDeployBridgeForAddress(queueName, nodeInfo.address)
} else {

View File

@ -18,6 +18,7 @@ import net.corda.core.node.services.NetworkMapCache
import net.corda.core.node.services.NetworkMapCache.MapChange
import net.corda.core.node.services.NetworkMapCache.MapChangeType
import net.corda.core.node.services.ServiceType
import net.corda.core.randomOrNull
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
@ -35,6 +36,8 @@ import javax.annotation.concurrent.ThreadSafe
/**
* Extremely simple in-memory cache of the network map.
*
* TODO: some method implementations can be moved up to [NetworkMapCache]
*/
@ThreadSafe
open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCache {
@ -65,18 +68,40 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach
override fun get(serviceType: ServiceType) = registeredNodes.filterValues { it.advertisedServices.any { it.info.type.isSubTypeOf(serviceType) } }.map { it.value }
override fun getRecommended(type: ServiceType, contract: Contract, vararg party: Party): NodeInfo? = get(type).firstOrNull()
override fun getNodeByLegalName(name: String) = get().singleOrNull { it.legalIdentity.name == name }
override fun getNodeByPublicKey(publicKey: PublicKeyTree): NodeInfo? {
override fun getNodeByPublicKeyTree(publicKeyTree: PublicKeyTree): NodeInfo? {
// Although we should never have more than one match, it is theoretically possible. Report an error if it happens.
val candidates = get().filter {
(it.legalIdentity.owningKey == publicKey)
|| it.advertisedServices.any { it.identity.owningKey == publicKey }
(it.legalIdentity.owningKey == publicKeyTree)
|| it.advertisedServices.any { it.identity.owningKey == publicKeyTree }
}
if (candidates.size > 1) {
throw IllegalStateException("Found more than one match for key $publicKey")
throw IllegalStateException("Found more than one match for key $publicKeyTree")
}
return candidates.singleOrNull()
}
override fun getRepresentativeNode(party: Party): NodeInfo? {
return partyNodes.randomOrNull { it.legalIdentity == party || it.advertisedServices.any { it.identity == party } }
}
override fun getNotary(name: String): Party? {
val notaryNode = notaryNodes.randomOrNull { it.advertisedServices.any { it.info.type.isSubTypeOf(ServiceType.notary) && it.info.name == name } }
return notaryNode?.notaryIdentity
}
override fun getAnyNotary(type: ServiceType?): Party? {
val nodes = if (type == null) {
notaryNodes
} else {
require(type != ServiceType.notary && type.isSubTypeOf(ServiceType.notary)) { "The provided type must be a specific notary sub-type" }
notaryNodes.filter { it.advertisedServices.any { it.info.type == type } }
}
return nodes.randomOrNull()?.notaryIdentity
}
override fun isNotary(party: Party) = notaryNodes.any { it.notaryIdentity == party }
override fun addMapService(net: MessagingService, networkMapAddress: SingleMessageRecipient, subscribe: Boolean,
ifChangedSinceVer: Int?): ListenableFuture<Unit> {
if (subscribe && !registeredForPush) {

View File

@ -182,11 +182,20 @@ class ProtocolStateMachineImpl<R>(override val id: StateMachineRunId,
}
}
/**
* Creates a new session. The provided [otherParty] can be an identity of any advertised service on the network,
* and might be advertised by more than one node. Therefore we first choose a single node that advertises it
* and use its *legal identity* for communication. At the moment a single node can compose its legal identity out of
* multiple public keys, but we **don't support multiple nodes advertising the same legal identity**.
*/
@Suspendable
private fun startNewSession(otherParty: Party, sessionProtocol: ProtocolLogic<*>, firstPayload: Any?) : ProtocolSession {
val session = ProtocolSession(sessionProtocol, otherParty, random63BitValue(), null)
openSessions[Pair(sessionProtocol, otherParty)] = session
val counterpartyProtocol = sessionProtocol.getCounterpartyMarker(otherParty).name
val node = serviceHub.networkMapCache.getRepresentativeNode(otherParty) ?: throw IllegalArgumentException("Don't know about party $otherParty")
val nodeIdentity = node.legalIdentity
logger.trace { "Initiating a new session with $nodeIdentity (representative of $otherParty)" }
val session = ProtocolSession(sessionProtocol, nodeIdentity, random63BitValue(), null)
openSessions[Pair(sessionProtocol, nodeIdentity)] = session
val counterpartyProtocol = sessionProtocol.getCounterpartyMarker(nodeIdentity).name
val sessionInit = SessionInit(session.ourSessionId, serviceHub.myInfo.legalIdentity, counterpartyProtocol, firstPayload)
val sessionInitResponse = sendAndReceiveInternal<SessionInitResponse>(session, sessionInit)
if (sessionInitResponse is SessionConfirm) {
@ -194,7 +203,7 @@ class ProtocolStateMachineImpl<R>(override val id: StateMachineRunId,
return session
} else {
sessionInitResponse as SessionReject
throw ProtocolSessionException("Party $otherParty rejected session attempt: ${sessionInitResponse.errorMessage}")
throw ProtocolSessionException("Party $nodeIdentity rejected session attempt: ${sessionInitResponse.errorMessage}")
}
}

View File

@ -7,6 +7,7 @@ import co.paralleluniverse.strands.Strand
import com.codahale.metrics.Gauge
import com.esotericsoftware.kryo.Kryo
import com.google.common.util.concurrent.ListenableFuture
import kotlinx.support.jdk8.collections.removeIf
import net.corda.core.ThreadBox
import net.corda.core.abbreviate
import net.corda.core.crypto.Party
@ -28,7 +29,6 @@ import net.corda.node.services.api.ServiceHubInternal
import net.corda.node.utilities.AddOrRemove
import net.corda.node.utilities.AffinityExecutor
import net.corda.node.utilities.isolatedTransaction
import kotlinx.support.jdk8.collections.removeIf
import org.apache.activemq.artemis.utils.ReusableLatch
import org.jetbrains.exposed.sql.Database
import rx.Observable
@ -432,7 +432,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
}
private fun sendSessionMessage(party: Party, message: SessionMessage, psm: ProtocolStateMachineImpl<*>?) {
val node = serviceHub.networkMapCache.getNodeByPublicKey(party.owningKey)
val node = serviceHub.networkMapCache.getNodeByPublicKeyTree(party.owningKey)
?: throw IllegalArgumentException("Don't know about party $party")
val logger = psm?.logger ?: logger
logger.trace { "Sending $message to party $party" }

View File

@ -34,12 +34,12 @@ class InMemoryNetworkMapCacheTest {
val nodeB = network.createNode(null, -1, MockNetwork.DefaultFactory, true, "Node B", keyPair, ServiceInfo(NetworkMapService.type))
// Node A currently knows only about itself, so this returns node A
assertEquals(nodeA.netMapCache.getNodeByPublicKey(keyPair.public.tree), nodeA.info)
assertEquals(nodeA.netMapCache.getNodeByPublicKeyTree(keyPair.public.tree), nodeA.info)
nodeA.netMapCache.addNode(nodeB.info)
// Now both nodes match, so it throws an error
expect<IllegalStateException> {
nodeA.netMapCache.getNodeByPublicKey(keyPair.public.tree)
nodeA.netMapCache.getNodeByPublicKeyTree(keyPair.public.tree)
}
}
}