CORDA-2521: Checkpoint verifier no longer cares about the CorDapp jar name (#4669)

The check on the CorDapp hash is sufficient.
This commit is contained in:
Shams Asari 2019-01-29 16:32:07 +00:00 committed by GitHub
parent 262a7ad1b7
commit 88e4b85537
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 79 additions and 63 deletions

View File

@ -2,6 +2,7 @@ package net.corda.node.flows
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.core.flows.* import net.corda.core.flows.*
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.* import net.corda.core.internal.*
import net.corda.core.messaging.startFlow import net.corda.core.messaging.startFlow
@ -15,14 +16,12 @@ import net.corda.testing.driver.DriverDSL
import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.NodeParameters import net.corda.testing.driver.NodeParameters
import net.corda.testing.driver.driver import net.corda.testing.driver.driver
import net.corda.testing.node.internal.CustomCordapp
import net.corda.testing.node.internal.ListenProcessDeathException import net.corda.testing.node.internal.ListenProcessDeathException
import net.corda.testing.node.internal.assertCheckpoints import net.corda.testing.node.internal.assertCheckpoints
import net.corda.testing.node.internal.enclosedCordapp import net.corda.testing.node.internal.enclosedCordapp
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test import org.junit.Test
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.StandardCopyOption.REPLACE_EXISTING
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
// TraderDemoTest already has a test which checks the node can resume a flow from a checkpoint // TraderDemoTest already has a test which checks the node can resume a flow from a checkpoint
@ -32,27 +31,30 @@ class FlowCheckpointVersionNodeStartupCheckTest {
} }
@Test @Test
fun `restart node with incompatible version of suspended flow due to different jar name`() { fun `restart node with mismatch between suspended flow and installed CorDapps`() {
driver(parametersForRestartingNodes()) { driver(DriverParameters(
val defaultCordappJar = createSuspendedFlowInBob() startNodesInProcess = false,
defaultCordappJar.renameTo("renamed-${defaultCordappJar.fileName}") inMemoryDB = false, // Ensure database is persisted between node restarts so we can keep suspended flows
cordappsForAllNodes = emptyList(),
notarySpecs = emptyList()
)) {
createSuspendedFlowInBob()
val cordappsDir = baseDirectory(BOB_NAME) / "cordapps"
// Test the scenerio where the CorDapp no longer exists
cordappsDir.deleteRecursively()
cordappsDir.createDirectories()
assertBobFailsToStartWithLogMessage( assertBobFailsToStartWithLogMessage(
CheckpointIncompatibleException.FlowNotInstalledException(ReceiverFlow::class.java).message CheckpointIncompatibleException.CordappNotInstalledException(ReceiverFlow::class.java.name).message
) )
}
}
@Test // Clean-up
fun `restart node with incompatible version of suspended flow due to different jar hash`() { stdOutLogFile(BOB_NAME).let { it.renameTo("${it.fileName}-no-cordapp") }
driver(parametersForRestartingNodes()) {
val defaultCordappJar = createSuspendedFlowInBob()
// The name is part of the MANIFEST so changing it is sufficient to change the jar hash // Now test the scenerio where the CorDapp's hash is different but the flow exists within the jar
val modifiedCordapp = defaultCordapp.copy(name = "${defaultCordapp.name}-modified") val modifiedCordapp = defaultCordapp.copy(name = "${defaultCordapp.name}-modified")
val modifiedCordappJar = CustomCordapp.getJarFile(modifiedCordapp) assertThat(defaultCordapp.jarFile.hash).isNotEqualTo(modifiedCordapp.jarFile.hash) // Just double-check the hashes are different
modifiedCordappJar.moveTo(defaultCordappJar, REPLACE_EXISTING) modifiedCordapp.jarFile.copyToDirectory(cordappsDir)
assertBobFailsToStartWithLogMessage( assertBobFailsToStartWithLogMessage(
// The part of the log message generated by CheckpointIncompatibleException.FlowVersionIncompatibleException // The part of the log message generated by CheckpointIncompatibleException.FlowVersionIncompatibleException
"that is incompatible with the current installed version of" "that is incompatible with the current installed version of"
@ -60,7 +62,7 @@ class FlowCheckpointVersionNodeStartupCheckTest {
} }
} }
private fun DriverDSL.createSuspendedFlowInBob(): Path { private fun DriverDSL.createSuspendedFlowInBob() {
val (alice, bob) = listOf( val (alice, bob) = listOf(
startNode(providedName = ALICE_NAME), startNode(providedName = ALICE_NAME),
startNode(NodeParameters(providedName = BOB_NAME, additionalCordapps = listOf(defaultCordapp))) startNode(NodeParameters(providedName = BOB_NAME, additionalCordapps = listOf(defaultCordapp)))
@ -72,12 +74,11 @@ class FlowCheckpointVersionNodeStartupCheckTest {
// Wait until Bob's flow has started // Wait until Bob's flow has started
bob.rpc.stateMachinesFeed().let { it.updates.map { it.id }.startWith(it.snapshot.map { it.id }) }.toBlocking().first() bob.rpc.stateMachinesFeed().let { it.updates.map { it.id }.startWith(it.snapshot.map { it.id }) }.toBlocking().first()
bob.stop() bob.stop()
assertCheckpoints(BOB_NAME, 1)
return (bob.baseDirectory / "cordapps").list().single { it.toString().endsWith(".jar") }
} }
private fun DriverDSL.assertBobFailsToStartWithLogMessage(logMessage: String) { private fun DriverDSL.assertBobFailsToStartWithLogMessage(logMessage: String) {
assertCheckpoints(BOB_NAME, 1)
assertFailsWith(ListenProcessDeathException::class) { assertFailsWith(ListenProcessDeathException::class) {
startNode(NodeParameters( startNode(NodeParameters(
providedName = BOB_NAME, providedName = BOB_NAME,
@ -85,19 +86,11 @@ class FlowCheckpointVersionNodeStartupCheckTest {
)).getOrThrow() )).getOrThrow()
} }
val logDir = baseDirectory(BOB_NAME) assertThat(stdOutLogFile(BOB_NAME).readText()).contains(logMessage)
val logFile = logDir.list { it.filter { it.fileName.toString().endsWith("out.log") }.findAny().get() }
val matchingLineCount = logFile.readLines { it.filter { line -> logMessage in line }.count() }
assertEquals(1, matchingLineCount)
} }
private fun parametersForRestartingNodes(): DriverParameters { private fun DriverDSL.stdOutLogFile(name: CordaX500Name): Path {
return DriverParameters( return baseDirectory(name).list { it.filter { it.toString().endsWith("stdout.log") }.findAny().get() }
startNodesInProcess = false, // Start nodes in separate processes to ensure CordappLoader is not shared between restarts
inMemoryDB = false, // Ensure database is persisted between node restarts so we can keep suspended flows
cordappsForAllNodes = emptyList(),
notarySpecs = emptyList()
)
} }
@InitiatingFlow @InitiatingFlow

View File

@ -18,42 +18,59 @@ object CheckpointVerifier {
* Verifies that all Checkpoints stored in the db can be safely loaded with the currently installed version. * Verifies that all Checkpoints stored in the db can be safely loaded with the currently installed version.
* @throws CheckpointIncompatibleException if any offending checkpoint is found. * @throws CheckpointIncompatibleException if any offending checkpoint is found.
*/ */
fun verifyCheckpointsCompatible(checkpointStorage: CheckpointStorage, currentCordapps: List<Cordapp>, platformVersion: Int, serviceHub: ServiceHub, tokenizableServices: List<Any>) { fun verifyCheckpointsCompatible(
checkpointStorage: CheckpointStorage,
currentCordapps: List<Cordapp>,
platformVersion: Int,
serviceHub: ServiceHub,
tokenizableServices: List<Any>
) {
val checkpointSerializationContext = CheckpointSerializationDefaults.CHECKPOINT_CONTEXT.withTokenContext( val checkpointSerializationContext = CheckpointSerializationDefaults.CHECKPOINT_CONTEXT.withTokenContext(
CheckpointSerializeAsTokenContextImpl(tokenizableServices, CheckpointSerializationDefaults.CHECKPOINT_SERIALIZER, CheckpointSerializationDefaults.CHECKPOINT_CONTEXT, serviceHub) CheckpointSerializeAsTokenContextImpl(
tokenizableServices,
CheckpointSerializationDefaults.CHECKPOINT_SERIALIZER,
CheckpointSerializationDefaults.CHECKPOINT_CONTEXT,
serviceHub
)
) )
checkpointStorage.getAllCheckpoints().forEach { (_, serializedCheckpoint) ->
val cordappsByHash = currentCordapps.associateBy { it.jarHash }
checkpointStorage.getAllCheckpoints().forEach { (_, serializedCheckpoint) ->
val checkpoint = try { val checkpoint = try {
serializedCheckpoint.checkpointDeserialize(context = checkpointSerializationContext) serializedCheckpoint.checkpointDeserialize(context = checkpointSerializationContext)
} catch (e: ClassNotFoundException) {
val message = e.message
if (message != null) {
throw CheckpointIncompatibleException.CordappNotInstalledException(message)
} else {
throw CheckpointIncompatibleException.CannotBeDeserialisedException(e)
}
} catch (e: Exception) { } catch (e: Exception) {
throw CheckpointIncompatibleException.CannotBeDeserialisedException(e) throw CheckpointIncompatibleException.CannotBeDeserialisedException(e)
} }
// For each Subflow, compare the checkpointed version to the current version. // For each Subflow, compare the checkpointed version to the current version.
checkpoint.subFlowStack.forEach { checkFlowCompatible(it, currentCordapps, platformVersion) } checkpoint.subFlowStack.forEach { checkFlowCompatible(it, cordappsByHash, platformVersion) }
} }
} }
// Throws exception when the flow is incompatible // Throws exception when the flow is incompatible
private fun checkFlowCompatible(subFlow: SubFlow, currentCordapps: List<Cordapp>, platformVersion: Int) { private fun checkFlowCompatible(subFlow: SubFlow, currentCordappsByHash: Map<SecureHash.SHA256, Cordapp>, platformVersion: Int) {
val corDappInfo = subFlow.subFlowVersion val subFlowVersion = subFlow.subFlowVersion
if (corDappInfo.platformVersion != platformVersion) { if (subFlowVersion.platformVersion != platformVersion) {
throw CheckpointIncompatibleException.SubFlowCoreVersionIncompatibleException(subFlow.flowClass, corDappInfo.platformVersion) throw CheckpointIncompatibleException.SubFlowCoreVersionIncompatibleException(subFlow.flowClass, subFlowVersion.platformVersion)
} }
if (corDappInfo is SubFlowVersion.CorDappFlow) { // If the sub-flow is from a CorDapp then make sure we have that exact CorDapp jar loaded
val installedCordapps = currentCordapps.filter { it.name == corDappInfo.corDappName } if (subFlowVersion is SubFlowVersion.CorDappFlow && subFlowVersion.corDappHash !in currentCordappsByHash) {
when (installedCordapps.size) { // If we don't then see if the flow exists in any of the CorDapps so that we can give the user a more useful error message
0 -> throw CheckpointIncompatibleException.FlowNotInstalledException(subFlow.flowClass) val matchingCordapp = currentCordappsByHash.values.find { subFlow.flowClass in it.allFlows }
1 -> { if (matchingCordapp != null) {
val currenCordapp = installedCordapps.first() throw CheckpointIncompatibleException.FlowVersionIncompatibleException(subFlow.flowClass, matchingCordapp, subFlowVersion.corDappHash)
if (corDappInfo.corDappHash != currenCordapp.jarHash) { } else {
throw CheckpointIncompatibleException.FlowVersionIncompatibleException(subFlow.flowClass, currenCordapp, corDappInfo.corDappHash) throw CheckpointIncompatibleException.CordappNotInstalledException(subFlow.flowClass.name)
}
}
else -> throw IllegalStateException("Multiple Cordapps with name ${corDappInfo.corDappName} installed.") // This should not happen
} }
} }
} }
@ -64,15 +81,20 @@ object CheckpointVerifier {
*/ */
sealed class CheckpointIncompatibleException(override val message: String) : Exception() { sealed class CheckpointIncompatibleException(override val message: String) : Exception() {
class CannotBeDeserialisedException(val e: Exception) : CheckpointIncompatibleException( class CannotBeDeserialisedException(val e: Exception) : CheckpointIncompatibleException(
"Found checkpoint that cannot be deserialised using the current Corda version. Please revert to the previous version of Corda, drain your node (see https://docs.corda.net/upgrading-cordapps.html#flow-drains), and try again. Cause: ${e.message}") "Found checkpoint that cannot be deserialised using the current Corda version. Please revert to the previous version of Corda, " +
"drain your node (see https://docs.corda.net/upgrading-cordapps.html#flow-drains), and try again. Cause: ${e.message}")
class SubFlowCoreVersionIncompatibleException(val flowClass: Class<out FlowLogic<*>>, oldVersion: Int) : CheckpointIncompatibleException( class SubFlowCoreVersionIncompatibleException(val flowClass: Class<out FlowLogic<*>>, oldVersion: Int) : CheckpointIncompatibleException(
"Found checkpoint for flow: ${flowClass} that is incompatible with the current Corda platform. Please revert to the previous version of Corda (version ${oldVersion}), drain your node (see https://docs.corda.net/upgrading-cordapps.html#flow-drains), and try again.") "Found checkpoint for flow: $flowClass that is incompatible with the current Corda platform. Please revert to the previous " +
"version of Corda (version $oldVersion), drain your node (see https://docs.corda.net/upgrading-cordapps.html#flow-drains), and try again.")
class FlowVersionIncompatibleException(val flowClass: Class<out FlowLogic<*>>, val cordapp: Cordapp, oldHash: SecureHash) : CheckpointIncompatibleException( class FlowVersionIncompatibleException(val flowClass: Class<out FlowLogic<*>>, val cordapp: Cordapp, oldHash: SecureHash) : CheckpointIncompatibleException(
"Found checkpoint for flow: ${flowClass} that is incompatible with the current installed version of ${cordapp.name}. Please reinstall the previous version of the CorDapp (with hash: ${oldHash}), drain your node (see https://docs.corda.net/upgrading-cordapps.html#flow-drains), and try again.") "Found checkpoint for flow: $flowClass that is incompatible with the current installed version of ${cordapp.name}. " +
"Please reinstall the previous version of the CorDapp (with hash: $oldHash), drain your node " +
"(see https://docs.corda.net/upgrading-cordapps.html#flow-drains), and try again.")
class FlowNotInstalledException(val flowClass: Class<out FlowLogic<*>>) : CheckpointIncompatibleException( class CordappNotInstalledException(classNotFound: String) : CheckpointIncompatibleException(
"Found checkpoint for flow: ${flowClass} that is no longer installed. Please install the missing CorDapp, drain your node (see https://docs.corda.net/upgrading-cordapps.html#flow-drains), and try again.") "Found checkpoint for CorDapp that is no longer installed. Specifically, could not find class $classNotFound. Please install the " +
"missing CorDapp, drain your node (see https://docs.corda.net/upgrading-cordapps.html#flow-drains), and try again.")
} }

View File

@ -22,8 +22,8 @@ import net.corda.testing.internal.LogHelper
import net.corda.testing.internal.configureDatabase import net.corda.testing.internal.configureDatabase
import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import org.assertj.core.api.Assertions
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
@ -171,7 +171,7 @@ class DBCheckpointStorageTests {
checkpointStorage.addCheckpoint(id1, checkpoint1) checkpointStorage.addCheckpoint(id1, checkpoint1)
} }
Assertions.assertThatThrownBy { assertThatThrownBy {
database.transaction { database.transaction {
CheckpointVerifier.verifyCheckpointsCompatible(checkpointStorage, emptyList(), 1, mockServices, emptyList()) CheckpointVerifier.verifyCheckpointsCompatible(checkpointStorage, emptyList(), 1, mockServices, emptyList())
} }

View File

@ -6,7 +6,6 @@ import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigRenderOptions import com.typesafe.config.ConfigRenderOptions
import com.typesafe.config.ConfigValueFactory import com.typesafe.config.ConfigValueFactory
import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCClient
import net.corda.client.rpc.CordaRPCClientConfiguration
import net.corda.cliutils.CommonCliConstants.BASE_DIR import net.corda.cliutils.CommonCliConstants.BASE_DIR
import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.CordaFuture
import net.corda.core.concurrent.firstOf import net.corda.core.concurrent.firstOf
@ -579,7 +578,7 @@ class DriverDSLImpl(
extraCustomCordapps + (cordappsForAllNodes ?: emptySet()) extraCustomCordapps + (cordappsForAllNodes ?: emptySet())
) )
if (parameters.startInSameProcess ?: startNodesInProcess) { val nodeFuture = if (parameters.startInSameProcess ?: startNodesInProcess) {
val nodeAndThreadFuture = startInProcessNode(executorService, config) val nodeAndThreadFuture = startInProcessNode(executorService, config)
shutdownManager.registerShutdown( shutdownManager.registerShutdown(
nodeAndThreadFuture.map { (node, thread) -> nodeAndThreadFuture.map { (node, thread) ->
@ -603,7 +602,7 @@ class DriverDSLImpl(
} }
} }
} }
return nodeFuture nodeFuture
} else { } else {
val debugPort = if (isDebug) debugPortAllocation.nextPort() else null val debugPort = if (isDebug) debugPortAllocation.nextPort() else null
val process = startOutOfProcessNode(config, quasarJarPath, debugPort, systemProperties, parameters.maximumHeapSize) val process = startOutOfProcessNode(config, quasarJarPath, debugPort, systemProperties, parameters.maximumHeapSize)
@ -624,7 +623,7 @@ class DriverDSLImpl(
} }
val effectiveP2PAddress = config.corda.messagingServerAddress ?: config.corda.p2pAddress val effectiveP2PAddress = config.corda.messagingServerAddress ?: config.corda.p2pAddress
val p2pReadyFuture = addressMustBeBoundFuture(executorService, effectiveP2PAddress, process) val p2pReadyFuture = addressMustBeBoundFuture(executorService, effectiveP2PAddress, process)
return p2pReadyFuture.flatMap { p2pReadyFuture.flatMap {
val processDeathFuture = poll(executorService, "process death while waiting for RPC (${config.corda.myLegalName})") { val processDeathFuture = poll(executorService, "process death while waiting for RPC (${config.corda.myLegalName})") {
if (process.isAlive) null else process if (process.isAlive) null else process
} }
@ -644,6 +643,8 @@ class DriverDSLImpl(
} }
} }
} }
return nodeFuture.doOnError { onNodeExit() }
} }
override fun <A> pollUntilNonNull(pollName: String, pollInterval: Duration, warnCount: Int, check: () -> A?): CordaFuture<A> { override fun <A> pollUntilNonNull(pollName: String, pollInterval: Duration, warnCount: Int, check: () -> A?): CordaFuture<A> {