diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 0627ba4332..cc8b283997 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -24,7 +24,6 @@ import net.corda.core.node.services.* import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken -import net.corda.core.serialization.serialize import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.* import net.corda.node.CordaClock @@ -155,7 +154,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, /** Set to non-null once [start] has been successfully called. */ open val started get() = _started - @Volatile private var _started: StartedNode? = null + @Volatile + private var _started: StartedNode? = null /** The implementation of the [CordaRPCOps] interface used by this node. */ open fun makeRPCOps(flowStarter: FlowStarter, database: CordaPersistence, smm: StateMachineManager): CordaRPCOps { @@ -319,7 +319,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, serial = 0 ) - val nodeInfoFromDb = networkMapCache.getNodeByLegalName(identity.name) + val nodeInfoFromDb = getPreviousNodeInfoIfPresent(networkMapCache, identity) + val nodeInfo = if (potentialNodeInfo == nodeInfoFromDb?.copy(serial = 0)) { // The node info hasn't changed. We use the one from the database to preserve the serial. @@ -349,6 +350,19 @@ abstract class AbstractNode(val configuration: NodeConfiguration, return Pair(keyPairs, nodeInfo) } + private fun getPreviousNodeInfoIfPresent(networkMapCache: NetworkMapCacheBaseInternal, identity: PartyAndCertificate): NodeInfo? { + val nodeInfosFromDb = networkMapCache.getNodesByLegalName(identity.name) + + return when (nodeInfosFromDb.size) { + 0 -> null + 1 -> nodeInfosFromDb[0] + else -> { + log.warn("Found more than one node registration with our legal name, this is only expected if our keypair has been regenerated") + nodeInfosFromDb[0] + } + } + } + // Publish node info on startup and start task that sends every day a heartbeat - republishes node info. private fun tryPublishNodeInfoAsync(signedNodeInfo: SignedNodeInfo, networkMapClient: NetworkMapClient) { // By default heartbeat interval should be set to 1 day, but for testing we may change it. @@ -712,7 +726,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } private fun makeCoreNotaryService(notaryConfig: NotaryConfig, database: CordaPersistence): NotaryService { - val notaryKey = myNotaryIdentity?.owningKey ?: throw IllegalArgumentException("No notary identity initialized when creating a notary service") + val notaryKey = myNotaryIdentity?.owningKey + ?: throw IllegalArgumentException("No notary identity initialized when creating a notary service") return notaryConfig.run { if (raft != null) { val uniquenessProvider = RaftUniquenessProvider(configuration, database, services.monitoringService.metrics, raft) @@ -846,7 +861,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, override val networkMapUpdater: NetworkMapUpdater get() = this@AbstractNode.networkMapUpdater override fun cordaService(type: Class): T { require(type.isAnnotationPresent(CordaService::class.java)) { "${type.name} is not a Corda service" } - return cordappServices.getInstance(type) ?: throw IllegalArgumentException("Corda service ${type.name} does not exist") + return cordappServices.getInstance(type) + ?: throw IllegalArgumentException("Corda service ${type.name} does not exist") } override fun getFlowFactory(initiatingFlowClass: Class>): InitiatedFlowFactory<*>? { diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt index a9012dd514..d6c99a300e 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt @@ -162,7 +162,7 @@ open class PersistentNetworkMapCache( } } - override fun getNodesByLegalName(name: CordaX500Name): List = database.transaction { queryByLegalName(session, name) } + override fun getNodesByLegalName(name: CordaX500Name): List = database.transaction { queryByLegalName(session, name) }.sortedByDescending { it.serial } override fun getNodesByLegalIdentityKey(identityKey: PublicKey): List = nodesByKeyCache[identityKey] diff --git a/node/src/test/kotlin/net/corda/node/internal/NodeTest.kt b/node/src/test/kotlin/net/corda/node/internal/NodeTest.kt index 4c734bf17b..1841947415 100644 --- a/node/src/test/kotlin/net/corda/node/internal/NodeTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/NodeTest.kt @@ -6,13 +6,17 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.internal.readObject import net.corda.core.node.NodeInfo import net.corda.core.serialization.deserialize +import net.corda.core.serialization.serialize import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.VersionInfo +import net.corda.node.internal.schemas.NodeInfoSchemaV1 import net.corda.node.services.config.NodeConfiguration import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.network.NodeInfoFilesCopier.Companion.NODE_INFO_FILE_NAME_PREFIX import net.corda.nodeapi.internal.persistence.DatabaseConfig +import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.SerializationEnvironmentRule +import net.corda.testing.internal.createNodeInfoAndSigned import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import org.junit.Rule @@ -67,4 +71,67 @@ class NodeTest { assertEquals(node.generateNodeInfo(), node.generateNodeInfo()) // Node info doesn't change (including the serial) } } + + @Test + fun `Node can start with multiple keypairs for it's identity`() { + val configuration = createConfig(ALICE_NAME) + val (nodeInfo1, _) = createNodeInfoAndSigned(ALICE_NAME) + val (nodeInfo2, _) = createNodeInfoAndSigned(ALICE_NAME) + + + val persistentNodeInfo2 = NodeInfoSchemaV1.PersistentNodeInfo( + id = 0, + hash = nodeInfo2.serialize().hash.toString(), + addresses = nodeInfo2.addresses.map { NodeInfoSchemaV1.DBHostAndPort.fromHostAndPort(it) }, + legalIdentitiesAndCerts = nodeInfo2.legalIdentitiesAndCerts.mapIndexed { idx, elem -> + NodeInfoSchemaV1.DBPartyAndCertificate(elem, isMain = idx == 0) + }, + platformVersion = nodeInfo2.platformVersion, + serial = nodeInfo2.serial + ) + + val persistentNodeInfo1 = NodeInfoSchemaV1.PersistentNodeInfo( + id = 0, + hash = nodeInfo1.serialize().hash.toString(), + addresses = nodeInfo1.addresses.map { NodeInfoSchemaV1.DBHostAndPort.fromHostAndPort(it) }, + legalIdentitiesAndCerts = nodeInfo1.legalIdentitiesAndCerts.mapIndexed { idx, elem -> + NodeInfoSchemaV1.DBPartyAndCertificate(elem, isMain = idx == 0) + }, + platformVersion = nodeInfo1.platformVersion, + serial = nodeInfo1.serial + ) + + configureDatabase(configuration.dataSourceProperties, configuration.database, rigorousMock()).use { + it.transaction { + session.save(persistentNodeInfo1) + } + it.transaction { + session.save(persistentNodeInfo2) + } + + val node = Node(configuration, rigorousMock().also { + doReturn(10).whenever(it).platformVersion + }, initialiseSerialization = false) + + //this throws an exception with old behaviour + node.generateNodeInfo() + } + } + + private fun createConfig(nodeName: CordaX500Name): NodeConfiguration { + val dataSourceProperties = makeTestDataSourceProperties() + val databaseConfig = DatabaseConfig() + val nodeAddress = NetworkHostAndPort("0.1.2.3", 456) + return rigorousMock().also { + doReturn(nodeAddress).whenever(it).p2pAddress + doReturn(nodeName).whenever(it).myLegalName + doReturn(null).whenever(it).notary // Don't add notary identity. + doReturn(dataSourceProperties).whenever(it).dataSourceProperties + doReturn(databaseConfig).whenever(it).database + doReturn(temporaryFolder.root.toPath()).whenever(it).baseDirectory + doReturn(true).whenever(it).devMode // Needed for identity cert. + doReturn("tsp").whenever(it).trustStorePassword + doReturn("ksp").whenever(it).keyStorePassword + } + } }