ENT-1684: Remove nodes that didn't accept update from network map (#692)

On flag day all nodes that didn't accept new parameters update will be kicked out of the network map.
This commit is contained in:
Katarzyna Streich 2018-04-07 15:28:57 +01:00 committed by Shams Asari
parent c7826bd1eb
commit 00a22c91df
10 changed files with 107 additions and 14 deletions

View File

@ -94,6 +94,7 @@ class NetworkParametersUpdateTest : IntegrationTest() {
) { ) {
var (alice) = listOf( var (alice) = listOf(
startNode(providedName = ALICE_NAME), startNode(providedName = ALICE_NAME),
startNode(providedName = BOB_NAME),
defaultNotaryNode defaultNotaryNode
).transpose().getOrThrow() ).transpose().getOrThrow()
alice as NodeHandleInternal alice as NodeHandleInternal
@ -137,6 +138,8 @@ class NetworkParametersUpdateTest : IntegrationTest() {
.readObject<SignedNetworkParameters>().verified() .readObject<SignedNetworkParameters>().verified()
assertEquals(networkParameters, paramUpdateInfo.parameters) assertEquals(networkParameters, paramUpdateInfo.parameters)
assertThat(alice.rpc.networkParametersFeed().snapshot).isNull() // Check that NMS doesn't advertise updates anymore. assertThat(alice.rpc.networkParametersFeed().snapshot).isNull() // Check that NMS doesn't advertise updates anymore.
// Check that Bob is no longer on the network as it didn't accept the new parameteres.
assertThat(alice.rpc.networkMapSnapshot().map { it.legalIdentities[0].name }).doesNotContain(BOB_NAME)
} }
} }

View File

@ -36,6 +36,7 @@ interface NetworkMapStorage {
/** /**
* Retrieves node info hashes where [NodeInfoEntity.isCurrent] is true and the certificate status is [CertificateStatus.VALID] * Retrieves node info hashes where [NodeInfoEntity.isCurrent] is true and the certificate status is [CertificateStatus.VALID]
* Nodes should have declared that they are using correct set of parameters.
*/ */
// TODO "Active" is the wrong word here // TODO "Active" is the wrong word here
fun getActiveNodeInfoHashes(): List<SecureHash> fun getActiveNodeInfoHashes(): List<SecureHash>
@ -70,4 +71,11 @@ interface NetworkMapStorage {
fun getCurrentParametersUpdate(): ParametersUpdateEntity? fun getCurrentParametersUpdate(): ParametersUpdateEntity?
fun setParametersUpdateStatus(update: ParametersUpdateEntity, newStatus: UpdateStatus) fun setParametersUpdateStatus(update: ParametersUpdateEntity, newStatus: UpdateStatus)
/**
* Perform the switch of parameters on the flagDay.
* 1. Change status of ParametersUpdateEntity to [UpdateStatus.APPLIED]
* 2. Mark all the node infos that didn't accept the update as not current (so they won't be advertised in the network map)
*/
fun switchFlagDay(update: ParametersUpdateEntity)
} }

View File

@ -15,6 +15,7 @@ import net.corda.core.crypto.SecureHash
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.nodeapi.internal.NodeInfoAndSigned import net.corda.nodeapi.internal.NodeInfoAndSigned
import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.SignedNodeInfo
import java.security.PublicKey
import java.security.cert.CertPath import java.security.cert.CertPath
/** /**
@ -48,8 +49,8 @@ interface NodeInfoStorage {
/** /**
* Store information about latest accepted [NetworkParameters] hash. * Store information about latest accepted [NetworkParameters] hash.
* @param publicKeyHash Hash of public key that accepted network parameters. This public key should belong to [NodeInfo] * @param publicKey Public key that accepted network parameters. This public key should belong to [NodeInfo]
* @param acceptedParametersHash Hash of latest accepted network parameters. * @param acceptedParametersHash Hash of latest accepted network parameters.
*/ */
fun ackNodeInfoParametersUpdate(publicKeyHash: SecureHash, acceptedParametersHash: SecureHash) fun ackNodeInfoParametersUpdate(publicKey: PublicKey, acceptedParametersHash: SecureHash)
} }

View File

@ -156,6 +156,15 @@ class PersistentNetworkMapStorage(private val database: CordaPersistence) : Netw
session.merge(update.copy(status = newStatus)) session.merge(update.copy(status = newStatus))
} }
} }
override fun switchFlagDay(update: ParametersUpdateEntity) {
database.transaction {
setParametersUpdateStatus(update, UpdateStatus.APPLIED)
session.createQuery("update ${NodeInfoEntity::class.java.name} n set n.isCurrent = false " +
"where (n.acceptedParametersUpdate != :acceptedParamUp or n.acceptedParametersUpdate is null) and n.isCurrent = true")
.setParameter("acceptedParamUp", update).executeUpdate()
}
}
} }
internal fun DatabaseTransaction.getNetworkParametersEntity(hash: SecureHash): NetworkParametersEntity? { internal fun DatabaseTransaction.getNetworkParametersEntity(hash: SecureHash): NetworkParametersEntity? {

View File

@ -25,6 +25,7 @@ import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.crypto.x509Certificates import net.corda.nodeapi.internal.crypto.x509Certificates
import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseTransaction import net.corda.nodeapi.internal.persistence.DatabaseTransaction
import java.security.PublicKey
import java.security.cert.CertPath import java.security.cert.CertPath
/** /**
@ -39,7 +40,7 @@ class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeIn
database.transaction { database.transaction {
val count = session.createQuery( val count = session.createQuery(
"select count(*) from ${NodeInfoEntity::class.java.name} where nodeInfoHash = :nodeInfoHash", java.lang.Long::class.java) "select count(*) from ${NodeInfoEntity::class.java.name} where nodeInfoHash = :nodeInfoHash and isCurrent = true", java.lang.Long::class.java)
.setParameter("nodeInfoHash", nodeInfoHash.toString()) .setParameter("nodeInfoHash", nodeInfoHash.toString())
.singleResult .singleResult
.toLong() .toLong()
@ -64,7 +65,7 @@ class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeIn
// Update any [NodeInfoEntity] instance for this CSR as not current. // Update any [NodeInfoEntity] instance for this CSR as not current.
existingNodeInfos.forEach { session.merge(it.copy(isCurrent = false)) } existingNodeInfos.forEach { session.merge(it.copy(isCurrent = false)) }
session.save(NodeInfoEntity( session.saveOrUpdate(NodeInfoEntity(
nodeInfoHash = nodeInfoHash.toString(), nodeInfoHash = nodeInfoHash.toString(),
publicKeyHash = nodeInfo.legalIdentities[0].owningKey.hashString(), publicKeyHash = nodeInfo.legalIdentities[0].owningKey.hashString(),
certificateSigningRequest = request, certificateSigningRequest = request,
@ -96,11 +97,11 @@ class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeIn
} }
} }
override fun ackNodeInfoParametersUpdate(publicKeyHash: SecureHash, acceptedParametersHash: SecureHash) { override fun ackNodeInfoParametersUpdate(publicKey: PublicKey, acceptedParametersHash: SecureHash) {
return database.transaction { return database.transaction {
val nodeInfoEntity = session.fromQuery<NodeInfoEntity>( val nodeInfoEntity = session.fromQuery<NodeInfoEntity>(
"n where n.publicKeyHash = :publicKeyHash and isCurrent = true") "n where n.publicKeyHash = :publicKeyHash and isCurrent = true")
.setParameter("publicKeyHash", publicKeyHash.toString()) .setParameter("publicKeyHash", publicKey.hashString())
.singleResult .singleResult
val parametersUpdateEntity = session.fromQuery<ParametersUpdateEntity>( val parametersUpdateEntity = session.fromQuery<ParametersUpdateEntity>(
"u where u.networkParameters.hash = :acceptedParametersHash"). "u where u.networkParameters.hash = :acceptedParametersHash").

View File

@ -51,9 +51,6 @@ class NetworkMapSigner(private val networkMapStorage: NetworkMapStorage, private
val activeNetworkParameters = activeNetworkMap?.networkParameters val activeNetworkParameters = activeNetworkMap?.networkParameters
logger.debug { "Current network map parameters: ${activeNetworkParameters?.networkParameters}" } logger.debug { "Current network map parameters: ${activeNetworkParameters?.networkParameters}" }
val nodeInfoHashes = networkMapStorage.getActiveNodeInfoHashes()
logger.debug { "Retrieved node info hashes:\n${nodeInfoHashes.joinToString("\n")}" }
// We persist signed parameters only if they were not persisted before (they are not in currentSignedNetworkMap as // We persist signed parameters only if they were not persisted before (they are not in currentSignedNetworkMap as
// normal parameters or as an update) // normal parameters or as an update)
if (!latestNetworkParameters.isSigned) { if (!latestNetworkParameters.isSigned) {
@ -63,12 +60,15 @@ class NetworkMapSigner(private val networkMapStorage: NetworkMapStorage, private
} }
val parametersToNetworkMap = if (parametersUpdate?.status == FLAG_DAY || activeNetworkParameters == null) { val parametersToNetworkMap = if (parametersUpdate?.status == FLAG_DAY || activeNetworkParameters == null) {
parametersUpdate?.let { networkMapStorage.setParametersUpdateStatus(it, APPLIED) } parametersUpdate?.let { networkMapStorage.switchFlagDay(it) }
latestNetworkParameters latestNetworkParameters
} else { } else {
activeNetworkParameters activeNetworkParameters
} }
val nodeInfoHashes = networkMapStorage.getActiveNodeInfoHashes()
logger.debug { "Retrieved node info hashes:\n${nodeInfoHashes.joinToString("\n")}" }
val newNetworkMap = NetworkMap( val newNetworkMap = NetworkMap(
nodeInfoHashes, nodeInfoHashes,
SecureHash.parse(parametersToNetworkMap.hash), SecureHash.parse(parametersToNetworkMap.hash),

View File

@ -115,7 +115,7 @@ class NetworkMapWebService(private val nodeInfoStorage: NodeInfoStorage,
val hash = signedParametersHash.verified() val hash = signedParametersHash.verified()
networkMapStorage.getSignedNetworkParameters(hash) ?: throw IllegalArgumentException("No network parameters with hash $hash") networkMapStorage.getSignedNetworkParameters(hash) ?: throw IllegalArgumentException("No network parameters with hash $hash")
logger.debug { "Received ack-parameters with $hash from ${signedParametersHash.sig.by}" } logger.debug { "Received ack-parameters with $hash from ${signedParametersHash.sig.by}" }
nodeInfoStorage.ackNodeInfoParametersUpdate(signedParametersHash.sig.by.encoded.sha256(), hash) nodeInfoStorage.ackNodeInfoParametersUpdate(signedParametersHash.sig.by, hash)
ok() ok()
} catch (e: SignatureException) { } catch (e: SignatureException) {
status(Response.Status.FORBIDDEN).entity(e.message) status(Response.Status.FORBIDDEN).entity(e.message)

View File

@ -14,7 +14,10 @@ import com.r3.corda.networkmanage.TestBase
import com.r3.corda.networkmanage.common.persistence.entity.NodeInfoEntity import com.r3.corda.networkmanage.common.persistence.entity.NodeInfoEntity
import com.r3.corda.networkmanage.common.persistence.entity.ParametersUpdateEntity import com.r3.corda.networkmanage.common.persistence.entity.ParametersUpdateEntity
import com.r3.corda.networkmanage.common.persistence.entity.UpdateStatus import com.r3.corda.networkmanage.common.persistence.entity.UpdateStatus
import com.r3.corda.networkmanage.common.utils.hashString
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256
import net.corda.core.serialization.serialize
import net.corda.core.utilities.days import net.corda.core.utilities.days
import net.corda.nodeapi.internal.createDevNetworkMapCa import net.corda.nodeapi.internal.createDevNetworkMapCa
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
@ -149,4 +152,54 @@ class PersistentNetworkMapStorageTest : TestBase() {
assertThat(firstUpdate.status).isEqualTo(UpdateStatus.CANCELLED) assertThat(firstUpdate.status).isEqualTo(UpdateStatus.CANCELLED)
assertThat(networkMapStorage.getCurrentParametersUpdate()?.description).isEqualTo("Update of update") assertThat(networkMapStorage.getCurrentParametersUpdate()?.description).isEqualTo("Update of update")
} }
@Test
fun `switch ParametersUpdate on flag day`() {
// Update
val networkParameters1 = testNetworkParameters()
val updateDeadline = Instant.now() + 10.days
networkMapStorage.saveNewParametersUpdate(networkParameters1, "Update 1", updateDeadline)
// given
val (signedNodeInfoA) = createValidSignedNodeInfo("TestA", requestStorage) // null as acceptedParametersUpdate
val (signedNodeInfoB) = createValidSignedNodeInfo("TestB", requestStorage) // accepts update
// Put signed node info data
nodeInfoStorage.putNodeInfo(signedNodeInfoA)
val nodeInfoHashB = nodeInfoStorage.putNodeInfo(signedNodeInfoB)
nodeInfoStorage.ackNodeInfoParametersUpdate(signedNodeInfoB.nodeInfo.legalIdentities[0].owningKey, networkParameters1.serialize().hash)
val parameterUpdate = networkMapStorage.getCurrentParametersUpdate()!!
networkMapStorage.switchFlagDay(parameterUpdate)
// when
val validNodeInfoHashes = networkMapStorage.getActiveNodeInfoHashes()
// then
assertThat(validNodeInfoHashes).containsOnly(nodeInfoHashB)
}
@Test
fun `accept second set of parameters and switch on flag day`() {
// Update 1
val networkParameters1 = testNetworkParameters()
val updateDeadline = Instant.now() + 10.days
networkMapStorage.saveNewParametersUpdate(networkParameters1, "Update 1", updateDeadline)
// given
val (signedNodeInfoA) = createValidSignedNodeInfo("TestA", requestStorage) // Update 1 as acceptedParametersUpdate
val (signedNodeInfoB) = createValidSignedNodeInfo("TestB", requestStorage) // Update 2 as acceptedParametersUpdate
// Put signed node info data
nodeInfoStorage.putNodeInfo(signedNodeInfoA)
val nodeInfoHashB = nodeInfoStorage.putNodeInfo(signedNodeInfoB)
nodeInfoStorage.ackNodeInfoParametersUpdate(signedNodeInfoA.nodeInfo.legalIdentities[0].owningKey, networkParameters1.serialize().hash)
// Update 2
val networkParameters2 = testNetworkParameters(epoch = 2)
networkMapStorage.saveNewParametersUpdate(networkParameters2, "Update 2", updateDeadline + 10.days)
nodeInfoStorage.ackNodeInfoParametersUpdate(signedNodeInfoB.nodeInfo.legalIdentities[0].owningKey, networkParameters2.serialize().hash)
val parameterUpdate = networkMapStorage.getCurrentParametersUpdate()!!
networkMapStorage.switchFlagDay(parameterUpdate)
// when
val validNodeInfoHashes = networkMapStorage.getActiveNodeInfoHashes()
// then
assertThat(validNodeInfoHashes).containsOnly(nodeInfoHashB)
}
} }

View File

@ -164,6 +164,24 @@ class PersistentNodeInfoStorageTest : TestBase() {
assertThat(singleNodeInfo().isCurrent).isTrue() assertThat(singleNodeInfo().isCurrent).isTrue()
} }
@Test
fun `publish same node info twice after isCurrent change`() {
fun singleNodeInfo() = persistence.transaction { session.fromQuery<NodeInfoEntity>("").singleResult }
val (nodeInfoAndSigned) = createValidSignedNodeInfo("Test", requestStorage)
nodeInfoStorage.putNodeInfo(nodeInfoAndSigned)
// Change isCurrent to false (that happens on flagDay change)
persistence.transaction {
val ni = singleNodeInfo()
session.merge(ni.copy(isCurrent = false))
}
assertThat(singleNodeInfo().isCurrent).isFalse()
val nodeInfo = singleNodeInfo()
nodeInfoStorage.putNodeInfo(nodeInfoAndSigned)
assertThat(nodeInfo.publishedAt).isBeforeOrEqualTo(singleNodeInfo().publishedAt)
assertThat(singleNodeInfo().isCurrent).isTrue()
}
@Test @Test
fun `accept parameters updates node info correctly`() { fun `accept parameters updates node info correctly`() {
// given // given
@ -174,7 +192,7 @@ class PersistentNodeInfoStorageTest : TestBase() {
val netParamsHash = networkParameters.serialize().hash val netParamsHash = networkParameters.serialize().hash
networkMapStorage.saveNewParametersUpdate(networkParameters, "Update", Instant.now() + 1.days) networkMapStorage.saveNewParametersUpdate(networkParameters, "Update", Instant.now() + 1.days)
val nodeInfoHash = nodeInfoStorage.putNodeInfo(nodeInfoAndSigned) val nodeInfoHash = nodeInfoStorage.putNodeInfo(nodeInfoAndSigned)
nodeInfoStorage.ackNodeInfoParametersUpdate(nodeInfoAndSigned.nodeInfo.legalIdentities[0].owningKey.encoded.sha256(), netParamsHash) nodeInfoStorage.ackNodeInfoParametersUpdate(nodeInfoAndSigned.nodeInfo.legalIdentities[0].owningKey, netParamsHash)
// then // then
val acceptedUpdate = nodeInfoStorage.getAcceptedParametersUpdate(nodeInfoHash) val acceptedUpdate = nodeInfoStorage.getAcceptedParametersUpdate(nodeInfoHash)
@ -190,7 +208,7 @@ class PersistentNodeInfoStorageTest : TestBase() {
val (nodeInfoAndSigned, privateKey) = createValidSignedNodeInfo("Test", requestStorage) val (nodeInfoAndSigned, privateKey) = createValidSignedNodeInfo("Test", requestStorage)
nodeInfoStorage.putNodeInfo(nodeInfoAndSigned) nodeInfoStorage.putNodeInfo(nodeInfoAndSigned)
nodeInfoStorage.ackNodeInfoParametersUpdate(nodeInfoAndSigned.nodeInfo.legalIdentities[0].owningKey.encoded.sha256(), netParamsHash) nodeInfoStorage.ackNodeInfoParametersUpdate(nodeInfoAndSigned.nodeInfo.legalIdentities[0].owningKey, netParamsHash)
val nodeInfo2 = nodeInfoAndSigned.nodeInfo.copy(serial = 2) val nodeInfo2 = nodeInfoAndSigned.nodeInfo.copy(serial = 2)
val nodeInfoAndSigned2 = NodeInfoAndSigned(nodeInfo2.signWith(listOf(privateKey))) val nodeInfoAndSigned2 = NodeInfoAndSigned(nodeInfo2.signWith(listOf(privateKey)))

View File

@ -217,7 +217,7 @@ class NetworkMapWebServiceTest {
val keyPair = Crypto.generateKeyPair() val keyPair = Crypto.generateKeyPair()
val signedHash = hash.serialize().sign { keyPair.sign(it) } val signedHash = hash.serialize().sign { keyPair.sign(it) }
it.doPost("ack-parameters", signedHash.serialize()) it.doPost("ack-parameters", signedHash.serialize())
verify(nodeInfoStorage).ackNodeInfoParametersUpdate(keyPair.public.encoded.sha256(), hash) verify(nodeInfoStorage).ackNodeInfoParametersUpdate(keyPair.public, hash)
val randomSigned = SecureHash.randomSHA256().serialize().sign { keyPair.sign(it) } val randomSigned = SecureHash.randomSHA256().serialize().sign { keyPair.sign(it) }
assertThatThrownBy { it.doPost("ack-parameters", randomSigned.serialize()) } assertThatThrownBy { it.doPost("ack-parameters", randomSigned.serialize()) }
.hasMessageContaining("HTTP ERROR 500") .hasMessageContaining("HTTP ERROR 500")