ENT-1557 - Network Map returns 404 not found for current node info advertised in the network map (#511)

* * added is current and timestamp to the node info table
* getNodeInfoHashes returns all "current" node info hashes
* TODO: network map should return 404 if receive old node info request
* TODO: database migration integration test

* fix compilation error

* * removed unnecessary unique constraint

* rebase and tidy up liquid base xml

* address PR issues

* address PR issues

* address PR issues
This commit is contained in:
Patrick Kuo 2018-03-14 11:53:55 +00:00 committed by GitHub
parent 341e060424
commit a435c23e19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 162 additions and 84 deletions

View File

@ -48,6 +48,14 @@ The built file will appear in
network-management/capsule-hsm-cert-generator/build/libs/hsm-cert-generator-<VERSION>.jar network-management/capsule-hsm-cert-generator/build/libs/hsm-cert-generator-<VERSION>.jar
``` ```
# Logs
In order to set the desired logging level the system properties need to be used.
Appropriate system properties can be set at the execution time.
Example:
```
java -DdefaultLogLevel=TRACE -DconsoleLogLevel=TRACE -jar doorman-<version>.jar --config-file <config file>
```
#Configuring network management service #Configuring network management service
### Local signing ### Local signing
@ -98,7 +106,8 @@ The doorman service can use JIRA to manage the certificate signing request appro
signInterval = 10000 signInterval = 10000
} }
``` ```
`cacheTimeout`(ms) indicates how often the network map should poll the database for a newly signed network map. This is also added to the HTTP response header to set the node's network map update frequency. `cacheTimeout`(ms) determines how often the server should poll the database for a newly signed network map and also how often nodes should poll for a new network map (by including this value in the HTTP response header). **This is not how often changes to the network map are signed, which is a different process.**
`signInterval`(ms) this is only relevant when local signer is enabled. The signer poll the database according to the `signInterval`, and create a new network map if the collection of node info hashes is different from the current network map. `signInterval`(ms) this is only relevant when local signer is enabled. The signer poll the database according to the `signInterval`, and create a new network map if the collection of node info hashes is different from the current network map.
##Example config file ##Example config file
@ -212,10 +221,12 @@ We can now restart the network management server with both doorman and network m
java -jar doorman-<version>.jar --config-file <config file> --update-network-parameters network-parameters.conf java -jar doorman-<version>.jar --config-file <config file> --update-network-parameters network-parameters.conf
``` ```
### 7. Logs ### 7. Archive policy
In order to set the desired logging level the system properties need to be used. The ``node_info`` and ``network_map`` table are designed to retain all historical data for auditing purposes and will grow over time.
Appropriate system properties can be set at the execution time. **It is recommended to monitor the space usage and archive these tables according to the data retention policy.**
Example:
Run the following SQL script to archive the node info table (change the timestamp according to the archive policy):
``` ```
java -DdefaultLogLevel=TRACE -DconsoleLogLevel=TRACE -jar doorman-<version>.jar --config-file <config file> delect from node_info where is_current = false and published_at < '2018-03-12'
``` ```

View File

@ -27,13 +27,11 @@ interface NetworkMapStorage {
fun getCurrentNetworkMap(): SignedNetworkMap? fun getCurrentNetworkMap(): SignedNetworkMap?
/** /**
* Retrieves node info hashes where the certificate status matches [certificateStatus]. * Retrieves node info hashes where [isCurrent] is true and the certificate status is [CertificateStatus.VALID]
* *
* @param certificateStatus certificate status to be used in the node info filtering. Node info hash is returned * @return list of current and valid node info hashes.
* in the result collection if its certificate status matches [certificateStatus].
* @return list of node info hashes satisfying the filtering criteria given by [certificateStatus].
*/ */
fun getNodeInfoHashes(certificateStatus: CertificateStatus): List<SecureHash> fun getActiveNodeInfoHashes(): List<SecureHash>
/** /**
* Persists a new instance of the signed network map. * Persists a new instance of the signed network map.
@ -51,6 +49,7 @@ interface NetworkMapStorage {
/** /**
* Retrieve the network parameters of the current network map, or null if there's no network map. * Retrieve the network parameters of the current network map, or null if there's no network map.
*/ */
// TODO: Remove this method. We should get the "current" network parameter by using the the hash in the network map and use the [getSignedNetworkParameters] method.
fun getNetworkParametersOfNetworkMap(): SignedNetworkParameters? fun getNetworkParametersOfNetworkMap(): SignedNetworkParameters?
/** /**

View File

@ -24,19 +24,23 @@ import javax.persistence.criteria.CriteriaBuilder
import javax.persistence.criteria.Path import javax.persistence.criteria.Path
import javax.persistence.criteria.Predicate import javax.persistence.criteria.Predicate
fun <T> DatabaseTransaction.singleRequestWhere(clazz: Class<T>, predicate: (CriteriaBuilder, Path<T>) -> Predicate): T? { inline fun <reified T> DatabaseTransaction.singleEntityWhere(predicate: (CriteriaBuilder, Path<T>) -> Predicate): T? {
val builder = session.criteriaBuilder return getEntitiesWhere(predicate).firstOrNull()
val criteriaQuery = builder.createQuery(clazz)
val query = criteriaQuery.from(clazz).run {
criteriaQuery.where(predicate(builder, this))
}
return session.createQuery(query).setLockMode(LockModeType.PESSIMISTIC_WRITE).resultList.firstOrNull()
} }
fun <T> DatabaseTransaction.deleteRequest(clazz: Class<T>, predicate: (CriteriaBuilder, Path<T>) -> Predicate): Int { inline fun <reified T> DatabaseTransaction.getEntitiesWhere(predicate: (CriteriaBuilder, Path<T>) -> Predicate): List<T> {
val builder = session.criteriaBuilder val builder = session.criteriaBuilder
val criteriaDelete = builder.createCriteriaDelete(clazz) val criteriaQuery = builder.createQuery(T::class.java)
val delete = criteriaDelete.from(clazz).run { val query = criteriaQuery.from(T::class.java).run {
criteriaQuery.where(predicate(builder, this))
}
return session.createQuery(query).setLockMode(LockModeType.PESSIMISTIC_WRITE).resultList
}
inline fun <reified T> DatabaseTransaction.deleteEntity(predicate: (CriteriaBuilder, Path<T>) -> Predicate): Int {
val builder = session.criteriaBuilder
val criteriaDelete = builder.createCriteriaDelete(T::class.java)
val delete = criteriaDelete.from(T::class.java).run {
criteriaDelete.where(predicate(builder, this)) criteriaDelete.where(predicate(builder, this))
} }
return session.createQuery(delete).executeUpdate() return session.createQuery(delete).executeUpdate()

View File

@ -40,7 +40,7 @@ class PersistentCertificateRevocationListStorage(private val database: CordaPers
} }
private fun revokeCertificate(certificateSerialNumber: BigInteger, time: Instant, transaction: DatabaseTransaction) { private fun revokeCertificate(certificateSerialNumber: BigInteger, time: Instant, transaction: DatabaseTransaction) {
val revocation = transaction.singleRequestWhere(CertificateRevocationRequestEntity::class.java) { builder, path -> val revocation = transaction.singleEntityWhere<CertificateRevocationRequestEntity> { builder, path ->
builder.equal(path.get<BigInteger>(CertificateRevocationRequestEntity::certificateSerialNumber.name), certificateSerialNumber) builder.equal(path.get<BigInteger>(CertificateRevocationRequestEntity::certificateSerialNumber.name), certificateSerialNumber)
} }
revocation ?: throw IllegalStateException("The certificate revocation request for $certificateSerialNumber does not exist") revocation ?: throw IllegalStateException("The certificate revocation request for $certificateSerialNumber does not exist")

View File

@ -13,7 +13,7 @@ class PersistentCertificateRevocationRequestStorage(private val database: CordaP
override fun saveRevocationRequest(certificateSerialNumber: BigInteger, reason: CRLReason, reporter: String): String { override fun saveRevocationRequest(certificateSerialNumber: BigInteger, reason: CRLReason, reporter: String): String {
return database.transaction(TransactionIsolationLevel.SERIALIZABLE) { return database.transaction(TransactionIsolationLevel.SERIALIZABLE) {
// Check if there is an entry for the given certificate serial number // Check if there is an entry for the given certificate serial number
val revocation = singleRequestWhere(CertificateRevocationRequestEntity::class.java) { builder, path -> val revocation = singleEntityWhere<CertificateRevocationRequestEntity> { builder, path ->
val serialNumberEqual = builder.equal(path.get<BigInteger>(CertificateRevocationRequestEntity::certificateSerialNumber.name), certificateSerialNumber) val serialNumberEqual = builder.equal(path.get<BigInteger>(CertificateRevocationRequestEntity::certificateSerialNumber.name), certificateSerialNumber)
val statusNotEqualRejected = builder.notEqual(path.get<RequestStatus>(CertificateRevocationRequestEntity::status.name), RequestStatus.REJECTED) val statusNotEqualRejected = builder.notEqual(path.get<RequestStatus>(CertificateRevocationRequestEntity::status.name), RequestStatus.REJECTED)
builder.and(serialNumberEqual, statusNotEqualRejected) builder.and(serialNumberEqual, statusNotEqualRejected)
@ -21,7 +21,7 @@ class PersistentCertificateRevocationRequestStorage(private val database: CordaP
if (revocation != null) { if (revocation != null) {
revocation.requestId revocation.requestId
} else { } else {
val certificateData = singleRequestWhere(CertificateDataEntity::class.java) { builder, path -> val certificateData = singleEntityWhere<CertificateDataEntity> { builder, path ->
val serialNumberEqual = builder.equal(path.get<BigInteger>(CertificateDataEntity::certificateSerialNumber.name), certificateSerialNumber) val serialNumberEqual = builder.equal(path.get<BigInteger>(CertificateDataEntity::certificateSerialNumber.name), certificateSerialNumber)
val statusEqualValid = builder.equal(path.get<CertificateStatus>(CertificateDataEntity::certificateStatus.name), CertificateStatus.VALID) val statusEqualValid = builder.equal(path.get<CertificateStatus>(CertificateDataEntity::certificateStatus.name), CertificateStatus.VALID)
builder.and(serialNumberEqual, statusEqualValid) builder.and(serialNumberEqual, statusEqualValid)
@ -90,7 +90,7 @@ class PersistentCertificateRevocationRequestStorage(private val database: CordaP
} }
private fun getRevocationRequestEntity(requestId: String): CertificateRevocationRequestEntity? = database.transaction { private fun getRevocationRequestEntity(requestId: String): CertificateRevocationRequestEntity? = database.transaction {
singleRequestWhere(CertificateRevocationRequestEntity::class.java) { builder, path -> singleEntityWhere { builder, path ->
builder.equal(path.get<String>(CertificateRevocationRequestEntity::requestId.name), requestId) builder.equal(path.get<String>(CertificateRevocationRequestEntity::requestId.name), requestId)
} }
} }

View File

@ -38,7 +38,7 @@ class PersistentCertificateSigningRequestStorage(private val database: CordaPers
override fun putCertificatePath(requestId: String, certPath: CertPath, signedBy: String) { override fun putCertificatePath(requestId: String, certPath: CertPath, signedBy: String) {
return database.transaction(TransactionIsolationLevel.SERIALIZABLE) { return database.transaction(TransactionIsolationLevel.SERIALIZABLE) {
val request = singleRequestWhere(CertificateSigningRequestEntity::class.java) { builder, path -> val request = singleEntityWhere<CertificateSigningRequestEntity> { builder, path ->
val requestIdEq = builder.equal(path.get<String>(CertificateSigningRequestEntity::requestId.name), requestId) val requestIdEq = builder.equal(path.get<String>(CertificateSigningRequestEntity::requestId.name), requestId)
val statusEq = builder.equal(path.get<String>(CertificateSigningRequestEntity::status.name), RequestStatus.APPROVED) val statusEq = builder.equal(path.get<String>(CertificateSigningRequestEntity::status.name), RequestStatus.APPROVED)
builder.and(requestIdEq, statusEq) builder.and(requestIdEq, statusEq)
@ -89,7 +89,7 @@ class PersistentCertificateSigningRequestStorage(private val database: CordaPers
private fun DatabaseTransaction.findRequest(requestId: String, private fun DatabaseTransaction.findRequest(requestId: String,
requestStatus: RequestStatus? = null): CertificateSigningRequestEntity? { requestStatus: RequestStatus? = null): CertificateSigningRequestEntity? {
return singleRequestWhere(CertificateSigningRequestEntity::class.java) { builder, path -> return singleEntityWhere { builder, path ->
val idClause = builder.equal(path.get<String>(CertificateSigningRequestEntity::requestId.name), requestId) val idClause = builder.equal(path.get<String>(CertificateSigningRequestEntity::requestId.name), requestId)
if (requestStatus == null) { if (requestStatus == null) {
idClause idClause

View File

@ -57,15 +57,17 @@ class PersistentNetworkMapStorage(private val database: CordaPersistence) : Netw
} }
} }
override fun getNodeInfoHashes(certificateStatus: CertificateStatus): List<SecureHash> { override fun getActiveNodeInfoHashes(): List<SecureHash> {
return database.transaction { return database.transaction {
val builder = session.criteriaBuilder val builder = session.criteriaBuilder
val query = builder.createQuery(String::class.java).run { val query = builder.createQuery(String::class.java).run {
from(NodeInfoEntity::class.java).run { from(NodeInfoEntity::class.java).run {
select(get<String>(NodeInfoEntity::nodeInfoHash.name)) val certStatusExpression = get<CertificateSigningRequestEntity>(NodeInfoEntity::certificateSigningRequest.name)
.where(builder.equal(get<CertificateSigningRequestEntity>(NodeInfoEntity::certificateSigningRequest.name) .get<CertificateDataEntity>(CertificateSigningRequestEntity::certificateData.name)
.get<CertificateDataEntity>(CertificateSigningRequestEntity::certificateData.name) .get<CertificateStatus>(CertificateDataEntity::certificateStatus.name)
.get<CertificateStatus>(CertificateDataEntity::certificateStatus.name), certificateStatus)) val certStatusEq = builder.equal(certStatusExpression, CertificateStatus.VALID)
val isCurrentNodeInfo = builder.isTrue(get<Boolean>(NodeInfoEntity::isCurrent.name))
select(get<String>(NodeInfoEntity::nodeInfoHash.name)).where(builder.and(certStatusEq, isCurrentNodeInfo))
} }
} }
session.createQuery(query).resultList.map { SecureHash.parse(it) } session.createQuery(query).resultList.map { SecureHash.parse(it) }
@ -117,7 +119,7 @@ class PersistentNetworkMapStorage(private val database: CordaPersistence) : Netw
private fun getNetworkParametersEntity(parameterHash: String): NetworkParametersEntity? { private fun getNetworkParametersEntity(parameterHash: String): NetworkParametersEntity? {
return database.transaction { return database.transaction {
singleRequestWhere(NetworkParametersEntity::class.java) { builder, path -> singleEntityWhere { builder, path ->
builder.equal(path.get<String>(NetworkParametersEntity::parametersHash.name), parameterHash) builder.equal(path.get<String>(NetworkParametersEntity::parametersHash.name), parameterHash)
} }
} }

View File

@ -35,29 +35,27 @@ class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeIn
nodeCaCert ?: throw IllegalArgumentException("Missing Node CA") nodeCaCert ?: throw IllegalArgumentException("Missing Node CA")
return database.transaction { return database.transaction {
// TODO Move these checks out of data access layer // TODO Move these checks out of data access layer
val request = requireNotNull(getSignedRequestByPublicHash(nodeCaCert.publicKey.encoded.sha256(), this)) { val request = requireNotNull(getSignedRequestByPublicHash(nodeCaCert.publicKey.encoded.sha256())) {
"Node-info not registered with us" "Node-info not registered with us"
} }
request.certificateData?.certificateStatus.let { request.certificateData?.certificateStatus.let {
require(it == CertificateStatus.VALID) { "Certificate is no longer valid: $it" } require(it == CertificateStatus.VALID) { "Certificate is no longer valid: $it" }
} }
/* // Update any [NodeInfoEntity] instance for this CSR as not current.
* Delete any previous [NodeInfoEntity] instance for this CSR val existingNodeInfo = getEntitiesWhere<NodeInfoEntity> { builder, path ->
* Possibly it should be moved at the network signing process at the network signing process val requestEq = builder.equal(path.get<CertificateSigningRequestEntity>(NodeInfoEntity::certificateSigningRequest.name), request)
* as for a while the network map will have invalid entries (i.e. hashes for node info which have been val isCurrent = builder.isTrue(path.get<Boolean>(NodeInfoEntity::isCurrent.name))
* removed). Either way, there will be a period of time when the network map data will be invalid builder.and(requestEq, isCurrent)
* but it has been confirmed that this fact has been acknowledged at the design time and we are fine with it.
*/
deleteRequest(NodeInfoEntity::class.java) { builder, path ->
builder.equal(path.get<CertificateSigningRequestEntity>(NodeInfoEntity::certificateSigningRequest.name), request)
} }
val hash = signedNodeInfo.raw.hash existingNodeInfo.forEach { session.merge(it.copy(isCurrent = false)) }
val hash = signedNodeInfo.raw.hash
val hashedNodeInfo = NodeInfoEntity( val hashedNodeInfo = NodeInfoEntity(
nodeInfoHash = hash.toString(), nodeInfoHash = hash.toString(),
certificateSigningRequest = request, certificateSigningRequest = request,
signedNodeInfoBytes = signedNodeInfo.serialize().bytes) signedNodeInfoBytes = signedNodeInfo.serialize().bytes,
isCurrent = true)
session.save(hashedNodeInfo) session.save(hashedNodeInfo)
hash hash
} }
@ -71,13 +69,13 @@ class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeIn
override fun getCertificatePath(publicKeyHash: SecureHash): CertPath? { override fun getCertificatePath(publicKeyHash: SecureHash): CertPath? {
return database.transaction { return database.transaction {
val request = getSignedRequestByPublicHash(publicKeyHash, this) val request = getSignedRequestByPublicHash(publicKeyHash)
request?.let { buildCertPath(it.certificateData!!.certificatePathBytes) } request?.let { buildCertPath(it.certificateData!!.certificatePathBytes) }
} }
} }
private fun getSignedRequestByPublicHash(publicKeyHash: SecureHash, transaction: DatabaseTransaction): CertificateSigningRequestEntity? { private fun DatabaseTransaction.getSignedRequestByPublicHash(publicKeyHash: SecureHash): CertificateSigningRequestEntity? {
return transaction.singleRequestWhere(CertificateSigningRequestEntity::class.java) { builder, path -> return singleEntityWhere { builder, path ->
val publicKeyEq = builder.equal(path.get<String>(CertificateSigningRequestEntity::publicKeyHash.name), publicKeyHash.toString()) val publicKeyEq = builder.equal(path.get<String>(CertificateSigningRequestEntity::publicKeyHash.name), publicKeyHash.toString())
val statusEq = builder.equal(path.get<RequestStatus>(CertificateSigningRequestEntity::status.name), RequestStatus.DONE) val statusEq = builder.equal(path.get<RequestStatus>(CertificateSigningRequestEntity::status.name), RequestStatus.DONE)
builder.and(publicKeyEq, statusEq) builder.and(publicKeyEq, statusEq)

View File

@ -12,6 +12,8 @@ package com.r3.corda.networkmanage.common.persistence.entity
import net.corda.core.serialization.deserialize import net.corda.core.serialization.deserialize
import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.SignedNodeInfo
import org.hibernate.annotations.CreationTimestamp
import java.time.Instant
import javax.persistence.* import javax.persistence.*
@Entity @Entity
@ -22,13 +24,19 @@ class NodeInfoEntity(
@Column(name = "node_info_hash", length = 64) @Column(name = "node_info_hash", length = 64)
val nodeInfoHash: String = "", val nodeInfoHash: String = "",
@OneToOne(optional = false, fetch = FetchType.LAZY) @ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "certificate_signing_request", nullable = true) @JoinColumn(name = "certificate_signing_request")
val certificateSigningRequest: CertificateSigningRequestEntity? = null, val certificateSigningRequest: CertificateSigningRequestEntity,
@Lob @Lob
@Column(name = "signed_node_info_bytes") @Column(name = "signed_node_info_bytes")
val signedNodeInfoBytes: ByteArray val signedNodeInfoBytes: ByteArray,
@Column(name="is_current")
val isCurrent: Boolean,
@Column(name = "published_at")
val publishedAt: Instant = Instant.now()
) { ) {
/** /**
* Deserializes NodeInfoEntity.soignedNodeInfoBytes into the [SignedNodeInfo] instance * Deserializes NodeInfoEntity.soignedNodeInfoBytes into the [SignedNodeInfo] instance
@ -36,13 +44,17 @@ class NodeInfoEntity(
fun signedNodeInfo() = signedNodeInfoBytes.deserialize<SignedNodeInfo>() fun signedNodeInfo() = signedNodeInfoBytes.deserialize<SignedNodeInfo>()
fun copy(nodeInfoHash: String = this.nodeInfoHash, fun copy(nodeInfoHash: String = this.nodeInfoHash,
certificateSigningRequest: CertificateSigningRequestEntity? = this.certificateSigningRequest, certificateSigningRequest: CertificateSigningRequestEntity = this.certificateSigningRequest,
signedNodeInfoBytes: ByteArray = this.signedNodeInfoBytes signedNodeInfoBytes: ByteArray = this.signedNodeInfoBytes,
isCurrent: Boolean = this.isCurrent,
publishedAt: Instant = this.publishedAt
): NodeInfoEntity { ): NodeInfoEntity {
return NodeInfoEntity( return NodeInfoEntity(
nodeInfoHash = nodeInfoHash, nodeInfoHash = nodeInfoHash,
certificateSigningRequest = certificateSigningRequest, certificateSigningRequest = certificateSigningRequest,
signedNodeInfoBytes = signedNodeInfoBytes signedNodeInfoBytes = signedNodeInfoBytes,
isCurrent = isCurrent,
publishedAt = publishedAt
) )
} }
} }

View File

@ -10,7 +10,6 @@
package com.r3.corda.networkmanage.common.signer package com.r3.corda.networkmanage.common.signer
import com.r3.corda.networkmanage.common.persistence.CertificateStatus
import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
import net.corda.core.internal.SignedDataWithCert import net.corda.core.internal.SignedDataWithCert
import net.corda.core.node.NetworkParameters import net.corda.core.node.NetworkParameters
@ -45,7 +44,7 @@ class NetworkMapSigner(private val networkMapStorage: NetworkMapStorage, private
logger.debug("Fetching current network map...") logger.debug("Fetching current network map...")
val currentSignedNetworkMap = networkMapStorage.getCurrentNetworkMap() val currentSignedNetworkMap = networkMapStorage.getCurrentNetworkMap()
logger.debug("Fetching node info hashes with VALID certificates...") logger.debug("Fetching node info hashes with VALID certificates...")
val nodeInfoHashes = networkMapStorage.getNodeInfoHashes(CertificateStatus.VALID) val nodeInfoHashes = networkMapStorage.getActiveNodeInfoHashes()
logger.debug("Retrieved node info hashes: $nodeInfoHashes") logger.debug("Retrieved node info hashes: $nodeInfoHashes")
val newNetworkMap = NetworkMap(nodeInfoHashes, latestNetworkParameters.serialize().hash, null) val newNetworkMap = NetworkMap(nodeInfoHashes, latestNetworkParameters.serialize().hash, null)
val serialisedNetworkMap = newNetworkMap.serialize() val serialisedNetworkMap = newNetworkMap.serialize()

View File

@ -51,11 +51,23 @@ class NetworkMapWebService(private val nodeInfoStorage: NodeInfoStorage,
const val NETWORK_MAP_PATH = "network-map" const val NETWORK_MAP_PATH = "network-map"
} }
private val networkMapCache: LoadingCache<Boolean, Pair<SignedNetworkMap?, NetworkParameters?>> = CacheBuilder.newBuilder() private val networkMapCache: LoadingCache<Boolean, CachedData> = CacheBuilder.newBuilder()
.expireAfterWrite(config.cacheTimeout, TimeUnit.MILLISECONDS) .expireAfterWrite(config.cacheTimeout, TimeUnit.MILLISECONDS)
.build(CacheLoader.from { _ -> .build(CacheLoader.from { _ -> networkMapStorage.getCurrentNetworkMap()?.let {
Pair(networkMapStorage.getCurrentNetworkMap(), networkMapStorage.getNetworkParametersOfNetworkMap()?.verified()) } val networkMap = it.verified()
) CachedData(it, networkMap.nodeInfoHashes.toSet(), networkMapStorage.getSignedNetworkParameters(networkMap.networkParameterHash)?.verified()) }
})
private val nodeInfoCache: LoadingCache<SecureHash, SignedNodeInfo> = CacheBuilder.newBuilder()
// TODO: Define cache retention policy.
.softValues()
.build(CacheLoader.from { key ->
key?.let { nodeInfoStorage.getNodeInfo(it) }
})
private val currentSignedNetworkMap: SignedNetworkMap? get() = networkMapCache.getOrNull(true)?.signedNetworkMap
private val currentNodeInfoHashes: Set<SecureHash> get() = networkMapCache.getOrNull(true)?.nodeInfoHashes ?: emptySet()
private val currentNetworkParameters: NetworkParameters? get() = networkMapCache.getOrNull(true)?.currentNetworkParameter
@POST @POST
@Path("publish") @Path("publish")
@ -84,13 +96,19 @@ class NetworkMapWebService(private val nodeInfoStorage: NodeInfoStorage,
} }
@GET @GET
fun getNetworkMap(): Response = createResponse(networkMapCache.get(true).first, addCacheTimeout = true) fun getNetworkMap(): Response = createResponse(currentSignedNetworkMap, addCacheTimeout = true)
@GET @GET
@Path("node-info/{nodeInfoHash}") @Path("node-info/{nodeInfoHash}")
fun getNodeInfo(@PathParam("nodeInfoHash") nodeInfoHash: String): Response { fun getNodeInfo(@PathParam("nodeInfoHash") nodeInfoHash: String): Response {
val signedNodeInfo = nodeInfoStorage.getNodeInfo(SecureHash.parse(nodeInfoHash)) // Only serve node info if its in the current network map, otherwise return 404.
logger.trace { "Precessed node info request for hash: '$nodeInfoHash'" } logger.trace { "Processing node info request for hash: '$nodeInfoHash'" }
val signedNodeInfo = if (SecureHash.parse(nodeInfoHash) in currentNodeInfoHashes) {
nodeInfoCache.getOrNull(SecureHash.parse(nodeInfoHash))
} else {
logger.trace { "Requested node info is not current, returning null." }
null
}
logger.trace { "Node Info: ${signedNodeInfo?.verified()}" } logger.trace { "Node Info: ${signedNodeInfo?.verified()}" }
return createResponse(signedNodeInfo) return createResponse(signedNodeInfo)
} }
@ -113,7 +131,7 @@ class NetworkMapWebService(private val nodeInfoStorage: NodeInfoStorage,
} }
private fun verifyNodeInfo(nodeInfo: NodeInfo) { private fun verifyNodeInfo(nodeInfo: NodeInfo) {
val minimumPlatformVersion = networkMapCache.get(true).second?.minimumPlatformVersion val minimumPlatformVersion = currentNetworkParameters?.minimumPlatformVersion
?: throw NetworkMapNotInitialisedException("Network parameters have not been initialised") ?: throw NetworkMapNotInitialisedException("Network parameters have not been initialised")
if (nodeInfo.platformVersion < minimumPlatformVersion) { if (nodeInfo.platformVersion < minimumPlatformVersion) {
throw InvalidPlatformVersionException("Minimum platform version is $minimumPlatformVersion") throw InvalidPlatformVersionException("Minimum platform version is $minimumPlatformVersion")
@ -134,4 +152,16 @@ class NetworkMapWebService(private val nodeInfoStorage: NodeInfoStorage,
class NetworkMapNotInitialisedException(message: String?) : Exception(message) class NetworkMapNotInitialisedException(message: String?) : Exception(message)
class InvalidPlatformVersionException(message: String?) : Exception(message) class InvalidPlatformVersionException(message: String?) : Exception(message)
private data class CachedData(val signedNetworkMap: SignedNetworkMap, val nodeInfoHashes: Set<SecureHash>, val currentNetworkParameter: NetworkParameters?)
// Guava loading cache will throw if value is null, this helper method returns null instead.
// The loading cache will load the data from persistence again ignoring timeout if previous value was null.
private fun <K : Any, V : Any> LoadingCache<K, V>.getOrNull(key: K): V? {
return try {
get(key)
} catch (e: CacheLoader.InvalidCacheLoadException) {
null
}
}
} }

View File

@ -100,10 +100,18 @@
<column name="node_info_hash" type="NVARCHAR(64)"> <column name="node_info_hash" type="NVARCHAR(64)">
<constraints nullable="false"/> <constraints nullable="false"/>
</column> </column>
<column name="signed_node_info_bytes" type="BLOB"/> <column name="signed_node_info_bytes" type="BLOB">
<constraints nullable="false"/>
</column>
<column name="certificate_signing_request" type="NVARCHAR(64)"> <column name="certificate_signing_request" type="NVARCHAR(64)">
<constraints nullable="false"/> <constraints nullable="false"/>
</column> </column>
<column name="is_current" type="BOOLEAN">
<constraints nullable="false"/>
</column>
<column name="published_at" type="TIMESTAMP">
<constraints nullable="false"/>
</column>
</createTable> </createTable>
</changeSet> </changeSet>
<changeSet author="R3.Corda" id="1520338500424-8"> <changeSet author="R3.Corda" id="1520338500424-8">
@ -135,9 +143,6 @@
<changeSet author="R3.Corda" id="1520338500424-15"> <changeSet author="R3.Corda" id="1520338500424-15">
<addUniqueConstraint columnNames="certificate_signing_request" constraintName="UK_CD_CSR" tableName="certificate_data"/> <addUniqueConstraint columnNames="certificate_signing_request" constraintName="UK_CD_CSR" tableName="certificate_data"/>
</changeSet> </changeSet>
<changeSet author="R3.Corda" id="1520338500424-16">
<addUniqueConstraint columnNames="certificate_signing_request" constraintName="UK_NI_CSR" tableName="node_info"/>
</changeSet>
<changeSet author="R3.Corda" id="1520338500424-19"> <changeSet author="R3.Corda" id="1520338500424-19">
<createIndex indexName="IDX_CSRA_REV" tableName="certificate_signing_request_AUD"> <createIndex indexName="IDX_CSRA_REV" tableName="certificate_signing_request_AUD">
<column name="REV"/> <column name="REV"/>

View File

@ -141,7 +141,7 @@ class PersistentNetworkMapStorageTest : TestBase() {
networkMapStorage.saveNetworkMap(signedNetworkMap) networkMapStorage.saveNetworkMap(signedNetworkMap)
// when // when
val validNodeInfoHash = networkMapStorage.getNodeInfoHashes(CertificateStatus.VALID) val validNodeInfoHash = networkMapStorage.getActiveNodeInfoHashes()
// then // then
assertThat(validNodeInfoHash).containsOnly(nodeInfoHashA, nodeInfoHashB) assertThat(validNodeInfoHash).containsOnly(nodeInfoHashA, nodeInfoHashB)

View File

@ -37,10 +37,12 @@ import javax.security.auth.x500.X500Principal
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertNotNull import kotlin.test.assertNotNull
import kotlin.test.assertNull import kotlin.test.assertNull
import kotlin.test.assertTrue
class PersistentNodeInfoStorageTest : TestBase() { class PersistentNodeInfoStorageTest : TestBase() {
private lateinit var requestStorage: CertificateSigningRequestStorage private lateinit var requestStorage: CertificateSigningRequestStorage
private lateinit var nodeInfoStorage: PersistentNodeInfoStorage private lateinit var nodeInfoStorage: PersistentNodeInfoStorage
private lateinit var networkMapStorage: PersistentNetworkMapStorage
private lateinit var persistence: CordaPersistence private lateinit var persistence: CordaPersistence
private lateinit var rootCaCert: X509Certificate private lateinit var rootCaCert: X509Certificate
private lateinit var intermediateCa: CertificateAndKeyPair private lateinit var intermediateCa: CertificateAndKeyPair
@ -53,6 +55,7 @@ class PersistentNodeInfoStorageTest : TestBase() {
persistence = configureDatabase(MockServices.makeTestDataSourceProperties(), DatabaseConfig(runMigration = true)) persistence = configureDatabase(MockServices.makeTestDataSourceProperties(), DatabaseConfig(runMigration = true))
nodeInfoStorage = PersistentNodeInfoStorage(persistence) nodeInfoStorage = PersistentNodeInfoStorage(persistence)
requestStorage = PersistentCertificateSigningRequestStorage(persistence) requestStorage = PersistentCertificateSigningRequestStorage(persistence)
networkMapStorage = PersistentNetworkMapStorage(persistence)
} }
@After @After
@ -119,12 +122,15 @@ class PersistentNodeInfoStorageTest : TestBase() {
val nodeInfo1Hash = nodeInfoStorage.putNodeInfo(node1) val nodeInfo1Hash = nodeInfoStorage.putNodeInfo(node1)
assertEquals(node1.nodeInfo, nodeInfoStorage.getNodeInfo(nodeInfo1Hash)?.verified()) assertEquals(node1.nodeInfo, nodeInfoStorage.getNodeInfo(nodeInfo1Hash)?.verified())
assertTrue(networkMapStorage.getActiveNodeInfoHashes().contains(nodeInfo1Hash))
// This should replace the node info. // This should replace the node info.
nodeInfoStorage.putNodeInfo(node2) val nodeInfo2Hash = nodeInfoStorage.putNodeInfo(node2)
// Old node info should be removed. // Old node info should be removed from list of current node info hashes, but still accessible if required.
assertNull(nodeInfoStorage.getNodeInfo(nodeInfo1Hash)) assertThat(networkMapStorage.getActiveNodeInfoHashes()).doesNotContain(nodeInfo1Hash)
assertThat(networkMapStorage.getActiveNodeInfoHashes()).contains(nodeInfo2Hash)
assertNotNull(nodeInfoStorage.getNodeInfo(nodeInfo1Hash))
assertEquals(nodeInfo2, nodeInfoStorage.getNodeInfo(nodeInfo2.serialize().hash)?.verified()) assertEquals(nodeInfo2, nodeInfoStorage.getNodeInfo(nodeInfo2.serialize().hash)?.verified())
} }

View File

@ -27,10 +27,10 @@ import net.corda.nodeapi.internal.network.NetworkMap
import net.corda.nodeapi.internal.network.verifiedNetworkMapCert import net.corda.nodeapi.internal.network.verifiedNetworkMapCert
import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.internal.createDevIntermediateCaCertPath import net.corda.testing.internal.createDevIntermediateCaCertPath
import java.security.cert.X509Certificate
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import java.security.cert.X509Certificate
import kotlin.test.assertEquals import kotlin.test.assertEquals
class NetworkMapSignerTest : TestBase() { class NetworkMapSignerTest : TestBase() {
@ -60,7 +60,7 @@ class NetworkMapSignerTest : TestBase() {
val networkMap = NetworkMap(signedNodeInfoHashes, currentParameters.serialize().hash, null) val networkMap = NetworkMap(signedNodeInfoHashes, currentParameters.serialize().hash, null)
val signedNetworkMap = networkMap.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate) val signedNetworkMap = networkMap.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)
whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(signedNetworkMap) whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(signedNetworkMap)
whenever(networkMapStorage.getNodeInfoHashes(any())).thenReturn(signedNodeInfoHashes) whenever(networkMapStorage.getActiveNodeInfoHashes()).thenReturn(signedNodeInfoHashes)
whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(latestNetworkParameters) whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(latestNetworkParameters)
whenever(networkMapStorage.getNetworkParametersOfNetworkMap()).thenReturn(currentParameters.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)) whenever(networkMapStorage.getNetworkParametersOfNetworkMap()).thenReturn(currentParameters.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate))
whenever(signer.signBytes(any())).then { whenever(signer.signBytes(any())).then {
@ -76,7 +76,7 @@ class NetworkMapSignerTest : TestBase() {
// then // then
// Verify networkMapStorage calls // Verify networkMapStorage calls
verify(networkMapStorage).getNodeInfoHashes(any()) verify(networkMapStorage).getActiveNodeInfoHashes()
verify(networkMapStorage).getLatestNetworkParameters() verify(networkMapStorage).getLatestNetworkParameters()
verify(networkMapStorage).getNetworkParametersOfNetworkMap() verify(networkMapStorage).getNetworkParametersOfNetworkMap()
argumentCaptor<SignedNetworkMap>().apply { argumentCaptor<SignedNetworkMap>().apply {
@ -96,7 +96,7 @@ class NetworkMapSignerTest : TestBase() {
val networkMap = NetworkMap(emptyList(), networkMapParametersHash, null) val networkMap = NetworkMap(emptyList(), networkMapParametersHash, null)
val signedNetworkMap = networkMap.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate) val signedNetworkMap = networkMap.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)
whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(signedNetworkMap) whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(signedNetworkMap)
whenever(networkMapStorage.getNodeInfoHashes(any())).thenReturn(emptyList()) whenever(networkMapStorage.getActiveNodeInfoHashes()).thenReturn(emptyList())
whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(networkParameters) whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(networkParameters)
whenever(networkMapStorage.getNetworkParametersOfNetworkMap()).thenReturn(networkParameters.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)) whenever(networkMapStorage.getNetworkParametersOfNetworkMap()).thenReturn(networkParameters.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate))
@ -113,7 +113,7 @@ class NetworkMapSignerTest : TestBase() {
// given // given
val networkParameters = testNetworkParameters(emptyList()) val networkParameters = testNetworkParameters(emptyList())
whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(null) whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(null)
whenever(networkMapStorage.getNodeInfoHashes(any())).thenReturn(emptyList()) whenever(networkMapStorage.getActiveNodeInfoHashes()).thenReturn(emptyList())
whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(networkParameters) whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(networkParameters)
whenever(signer.signBytes(any())).then { whenever(signer.signBytes(any())).then {
DigitalSignatureWithCert(networkMapCa.certificate, Crypto.doSign(networkMapCa.keyPair.private, it.arguments[0] as ByteArray)) DigitalSignatureWithCert(networkMapCa.certificate, Crypto.doSign(networkMapCa.keyPair.private, it.arguments[0] as ByteArray))
@ -127,7 +127,7 @@ class NetworkMapSignerTest : TestBase() {
// then // then
// Verify networkMapStorage calls // Verify networkMapStorage calls
verify(networkMapStorage).getNodeInfoHashes(any()) verify(networkMapStorage).getActiveNodeInfoHashes()
verify(networkMapStorage).getLatestNetworkParameters() verify(networkMapStorage).getLatestNetworkParameters()
argumentCaptor<SignedNetworkMap>().apply { argumentCaptor<SignedNetworkMap>().apply {
verify(networkMapStorage).saveNetworkMap(capture()) verify(networkMapStorage).saveNetworkMap(capture())

View File

@ -10,6 +10,7 @@
package com.r3.corda.networkmanage.doorman.webservice package com.r3.corda.networkmanage.doorman.webservice
import com.nhaarman.mockito_kotlin.any
import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.times import com.nhaarman.mockito_kotlin.times
import com.nhaarman.mockito_kotlin.verify import com.nhaarman.mockito_kotlin.verify
@ -67,7 +68,9 @@ class NetworkMapWebServiceTest {
@Test @Test
fun `submit nodeInfo`() { fun `submit nodeInfo`() {
val networkMapStorage: NetworkMapStorage = mock { val networkMapStorage: NetworkMapStorage = mock {
on { getNetworkParametersOfNetworkMap() }.thenReturn(testNetworkParameters(emptyList()).signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)) val networkParameter = testNetworkParameters(emptyList()).signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)
on { getSignedNetworkParameters(any()) }.thenReturn(networkParameter)
on { getCurrentNetworkMap() }.thenReturn(NetworkMap(emptyList(), networkParameter.raw.hash, null).signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate))
} }
// Create node info. // Create node info.
val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB")) val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB"))
@ -83,7 +86,9 @@ class NetworkMapWebServiceTest {
@Test @Test
fun `submit old nodeInfo`() { fun `submit old nodeInfo`() {
val networkMapStorage: NetworkMapStorage = mock { val networkMapStorage: NetworkMapStorage = mock {
on { getNetworkParametersOfNetworkMap() }.thenReturn(testNetworkParameters(emptyList(), minimumPlatformVersion = 2).signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)) val networkParameter = testNetworkParameters(emptyList(), minimumPlatformVersion = 2).signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)
on { getSignedNetworkParameters(any()) }.thenReturn(networkParameter)
on { getCurrentNetworkMap() }.thenReturn(NetworkMap(emptyList(), networkParameter.raw.hash, null).signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate))
} }
// Create node info. // Create node info.
val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB"), platformVersion = 1) val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB"), platformVersion = 1)
@ -131,14 +136,21 @@ class NetworkMapWebServiceTest {
@Test @Test
fun `get node info`() { fun `get node info`() {
// Mock node info storage
val (nodeInfo, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB")) val (nodeInfo, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB"))
val nodeInfoHash = nodeInfo.serialize().hash val nodeInfoHash = nodeInfo.serialize().hash
val nodeInfoStorage: NodeInfoStorage = mock { val nodeInfoStorage: NodeInfoStorage = mock {
on { getNodeInfo(nodeInfoHash) }.thenReturn(signedNodeInfo) on { getNodeInfo(nodeInfoHash) }.thenReturn(signedNodeInfo)
} }
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(nodeInfoStorage, mock(), testNetworkMapConfig)).use { // Mock network map storage
val networkMap = NetworkMap(listOf(nodeInfoHash), randomSHA256(), null)
val signedNetworkMap = networkMap.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)
val networkMapStorage: NetworkMapStorage = mock {
on { getCurrentNetworkMap() }.thenReturn(signedNetworkMap)
}
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(nodeInfoStorage, networkMapStorage, testNetworkMapConfig)).use {
it.start() it.start()
val nodeInfoResponse = it.doGet<SignedNodeInfo>("node-info/$nodeInfoHash") val nodeInfoResponse = it.doGet<SignedNodeInfo>("node-info/$nodeInfoHash")
verify(nodeInfoStorage, times(1)).getNodeInfo(nodeInfoHash) verify(nodeInfoStorage, times(1)).getNodeInfo(nodeInfoHash)