CORDA-1312: Network bootstrapper copies any CorDapp jars into each nodes' cordapps dir (#2974)

This commit is contained in:
Shams Asari
2018-04-17 15:38:25 +01:00
committed by GitHub
parent 1288f63998
commit 60323cca15
6 changed files with 49 additions and 35 deletions

View File

@ -1,4 +1,4 @@
gradlePluginsVersion=4.0.12 gradlePluginsVersion=4.0.13
kotlinVersion=1.2.20 kotlinVersion=1.2.20
platformVersion=4 platformVersion=4
guavaVersion=21.0 guavaVersion=21.0

View File

@ -19,8 +19,10 @@ Unreleased
* Shell (embedded available only in dev mode or via SSH) connects to the node via RPC instead of using the ``CordaRPCOps`` object directly. * Shell (embedded available only in dev mode or via SSH) connects to the node via RPC instead of using the ``CordaRPCOps`` object directly.
To enable RPC connectivity ensure nodes ``rpcSettings.address`` and ``rpcSettings.adminAddress`` settings are present. To enable RPC connectivity ensure nodes ``rpcSettings.address`` and ``rpcSettings.adminAddress`` settings are present.
* The network bootstrapper uses the existing network parameters file to update the current contracts whitelist, and no longer * Changes to the network bootstrapper:
needs the whitelist.txt file. * The whitelist.txt file is no longer needed. The existing network parameters file is used to update the current contracts
whitelist.
* The CorDapp jars are also copied to each nodes' `cordapps` directory.
* Errors thrown by a Corda node will now reported to a calling RPC client with attention to serialization and obfuscation of internal data. * Errors thrown by a Corda node will now reported to a calling RPC client with attention to serialization and obfuscation of internal data.

View File

@ -91,7 +91,7 @@ Whitelisting Contracts
If you want to create a *Zone whitelist* (see :doc:`api-contract-constraints`), you can pass in a list of CorDapp jars: If you want to create a *Zone whitelist* (see :doc:`api-contract-constraints`), you can pass in a list of CorDapp jars:
``java -jar network-bootstrapper.jar <nodes-root-dir> <path-to-first-corDapp> <path-to-second-corDapp> ..`` ``java -jar network-bootstrapper.jar <nodes-root-dir> <1st CorDapp jar> <2nd CorDapp jar> ..``
The CorDapp jars will be hashed and scanned for ``Contract`` classes. These contract class implementations will become part The CorDapp jars will be hashed and scanned for ``Contract`` classes. These contract class implementations will become part
of the whitelisted contracts in the network parameters (see ``NetworkParameters.whitelistedContractImplementations`` :doc:`network-map`). of the whitelisted contracts in the network parameters (see ``NetworkParameters.whitelistedContractImplementations`` :doc:`network-map`).
@ -111,6 +111,9 @@ For example:
net.corda.finance.contracts.asset.Cash net.corda.finance.contracts.asset.Cash
net.corda.finance.contracts.asset.CommercialPaper net.corda.finance.contracts.asset.CommercialPaper
In addition to using the CorDapp jars to update the whitelist, the bootstrapper will also copy them to all the nodes'
``cordapps`` directory.
Starting the nodes Starting the nodes
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~

View File

@ -6,11 +6,11 @@ import net.corda.core.contracts.ContractClassName
import net.corda.core.internal.copyTo import net.corda.core.internal.copyTo
import net.corda.core.internal.deleteIfExists import net.corda.core.internal.deleteIfExists
import net.corda.core.internal.read import net.corda.core.internal.read
import java.io.File
import java.io.InputStream import java.io.InputStream
import java.lang.reflect.Modifier import java.lang.reflect.Modifier
import java.net.URLClassLoader import java.net.URLClassLoader
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths import java.nio.file.Paths
import java.nio.file.StandardCopyOption import java.nio.file.StandardCopyOption
@ -18,13 +18,16 @@ import java.nio.file.StandardCopyOption
* Scans the jar for contracts. * Scans the jar for contracts.
* @returns: found contract class names or null if none found * @returns: found contract class names or null if none found
*/ */
fun scanJarForContracts(cordappJarPath: String): List<ContractClassName> { fun scanJarForContracts(cordappJar: Path): List<ContractClassName> {
val currentClassLoader = Contract::class.java.classLoader val currentClassLoader = Contract::class.java.classLoader
val scanResult = FastClasspathScanner().addClassLoader(currentClassLoader).overrideClasspath(cordappJarPath, Paths.get(Contract::class.java.protectionDomain.codeSource.location.toURI()).toString()).scan() val scanResult = FastClasspathScanner()
.addClassLoader(currentClassLoader)
.overrideClasspath(cordappJar, Paths.get(Contract::class.java.protectionDomain.codeSource.location.toURI()))
.scan()
val contracts = (scanResult.getNamesOfClassesImplementing(Contract::class.qualifiedName) ).distinct() val contracts = (scanResult.getNamesOfClassesImplementing(Contract::class.qualifiedName) ).distinct()
// Only keep instantiable contracts // Only keep instantiable contracts
return URLClassLoader(arrayOf(File(cordappJarPath).toURL()), currentClassLoader).use { return URLClassLoader(arrayOf(cordappJar.toUri().toURL()), currentClassLoader).use {
contracts.map(it::loadClass).filter { !it.isInterface && !Modifier.isAbstract(it.modifiers) } contracts.map(it::loadClass).filter { !it.isInterface && !Modifier.isAbstract(it.modifiers) }
}.map { it.name } }.map { it.name }
} }
@ -33,7 +36,7 @@ fun <T> withContractsInJar(jarInputStream: InputStream, withContracts: (List<Con
val tempFile = Files.createTempFile("attachment", ".jar") val tempFile = Files.createTempFile("attachment", ".jar")
try { try {
jarInputStream.copyTo(tempFile, StandardCopyOption.REPLACE_EXISTING) jarInputStream.copyTo(tempFile, StandardCopyOption.REPLACE_EXISTING)
val contracts = scanJarForContracts(tempFile.toAbsolutePath().toString()) val contracts = scanJarForContracts(tempFile.toAbsolutePath())
return tempFile.read { withContracts(contracts, it) } return tempFile.read { withContracts(contracts, it) }
} finally { } finally {
tempFile.deleteIfExists() tempFile.deleteIfExists()

View File

@ -18,6 +18,7 @@ import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.seconds import net.corda.core.utilities.seconds
import net.corda.nodeapi.internal.DEV_ROOT_CA import net.corda.nodeapi.internal.DEV_ROOT_CA
import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.network.NodeInfoFilesCopier.Companion.NODE_INFO_FILE_NAME_PREFIX
import net.corda.nodeapi.internal.scanJarForContracts import net.corda.nodeapi.internal.scanJarForContracts
import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT
import net.corda.nodeapi.internal.serialization.CordaSerializationMagic import net.corda.nodeapi.internal.serialization.CordaSerializationMagic
@ -53,15 +54,15 @@ class NetworkBootstrapper {
@JvmStatic @JvmStatic
fun main(args: Array<String>) { fun main(args: Array<String>) {
val baseNodeDirectory = requireNotNull(args.firstOrNull()) { "Expecting first argument which is the nodes' parent directory" } val baseNodeDirectory = requireNotNull(args.firstOrNull()) { "Expecting first argument which is the nodes' parent directory" }
val cordapps = if (args.size > 1) args.toList().drop(1) else null val cordappJars = if (args.size > 1) args.asList().drop(1).map { Paths.get(it) } else emptyList()
NetworkBootstrapper().bootstrap(Paths.get(baseNodeDirectory).toAbsolutePath().normalize(), cordapps) NetworkBootstrapper().bootstrap(Paths.get(baseNodeDirectory).toAbsolutePath().normalize(), cordappJars)
} }
} }
fun bootstrap(directory: Path, cordapps: List<String>?) { fun bootstrap(directory: Path, cordappJars: List<Path>) {
directory.createDirectories() directory.createDirectories()
println("Bootstrapping local network in $directory") println("Bootstrapping local network in $directory")
generateDirectoriesIfNeeded(directory) generateDirectoriesIfNeeded(directory, cordappJars)
val nodeDirs = directory.list { paths -> paths.filter { (it / "corda.jar").exists() }.toList() } val nodeDirs = directory.list { paths -> paths.filter { (it / "corda.jar").exists() }.toList() }
require(nodeDirs.isNotEmpty()) { "No nodes found" } require(nodeDirs.isNotEmpty()) { "No nodes found" }
println("Nodes found in the following sub-directories: ${nodeDirs.map { it.fileName }}") println("Nodes found in the following sub-directories: ${nodeDirs.map { it.fileName }}")
@ -78,7 +79,7 @@ class NetworkBootstrapper {
println("Gathering notary identities") println("Gathering notary identities")
val notaryInfos = gatherNotaryInfos(nodeInfoFiles) val notaryInfos = gatherNotaryInfos(nodeInfoFiles)
println("Generating contract implementations whitelist") println("Generating contract implementations whitelist")
val newWhitelist = generateWhitelist(existingNetParams, directory / EXCLUDE_WHITELIST_FILE_NAME, cordapps?.distinct()) val newWhitelist = generateWhitelist(existingNetParams, directory / EXCLUDE_WHITELIST_FILE_NAME, cordappJars)
val netParams = installNetworkParameters(notaryInfos, newWhitelist, existingNetParams, nodeDirs) val netParams = installNetworkParameters(notaryInfos, newWhitelist, existingNetParams, nodeDirs)
println("${if (existingNetParams == null) "New" else "Updated"} $netParams") println("${if (existingNetParams == null) "New" else "Updated"} $netParams")
println("Bootstrapping complete!") println("Bootstrapping complete!")
@ -88,11 +89,11 @@ class NetworkBootstrapper {
} }
} }
private fun generateDirectoriesIfNeeded(directory: Path) { private fun generateDirectoriesIfNeeded(directory: Path, cordappJars: List<Path>) {
val confFiles = directory.list { it.filter { it.toString().endsWith("_node.conf") }.toList() } val confFiles = directory.list { it.filter { it.toString().endsWith("_node.conf") }.toList() }
val webServerConfFiles = directory.list { it.filter { it.toString().endsWith("_web-server.conf") }.toList() } val webServerConfFiles = directory.list { it.filter { it.toString().endsWith("_web-server.conf") }.toList() }
if (confFiles.isEmpty()) return if (confFiles.isEmpty()) return
println("Node config files found in the root directory - generating node directories") println("Node config files found in the root directory - generating node directories and copying CorDapp jars into them")
val cordaJar = extractCordaJarTo(directory) val cordaJar = extractCordaJarTo(directory)
for (confFile in confFiles) { for (confFile in confFiles) {
val nodeName = confFile.fileName.toString().removeSuffix("_node.conf") val nodeName = confFile.fileName.toString().removeSuffix("_node.conf")
@ -101,6 +102,8 @@ class NetworkBootstrapper {
confFile.moveTo(nodeDir / "node.conf", REPLACE_EXISTING) confFile.moveTo(nodeDir / "node.conf", REPLACE_EXISTING)
webServerConfFiles.firstOrNull { directory.relativize(it).toString().removeSuffix("_web-server.conf") == nodeName }?.moveTo(nodeDir / "web-server.conf", REPLACE_EXISTING) webServerConfFiles.firstOrNull { directory.relativize(it).toString().removeSuffix("_web-server.conf") == nodeName }?.moveTo(nodeDir / "web-server.conf", REPLACE_EXISTING)
cordaJar.copyToDirectory(nodeDir, REPLACE_EXISTING) cordaJar.copyToDirectory(nodeDir, REPLACE_EXISTING)
val cordappsDir = (nodeDir / "cordapps").createDirectories()
cordappJars.forEach { it.copyToDirectory(cordappsDir) }
} }
Files.delete(cordaJar) Files.delete(cordaJar)
} }
@ -108,7 +111,6 @@ class NetworkBootstrapper {
private fun extractCordaJarTo(directory: Path): Path { private fun extractCordaJarTo(directory: Path): Path {
val cordaJarPath = directory / "corda.jar" val cordaJarPath = directory / "corda.jar"
if (!cordaJarPath.exists()) { if (!cordaJarPath.exists()) {
println("No corda jar found in root directory. Extracting from jar")
Thread.currentThread().contextClassLoader.getResourceAsStream("corda.jar").copyTo(cordaJarPath) Thread.currentThread().contextClassLoader.getResourceAsStream("corda.jar").copyTo(cordaJarPath)
} }
return cordaJarPath return cordaJarPath
@ -134,7 +136,7 @@ class NetworkBootstrapper {
check(process.waitFor() == 0) { check(process.waitFor() == 0) {
"Node in ${nodeDir.fileName} exited with ${process.exitValue()} when generating its node-info - see logs in ${nodeDir / LOGS_DIR_NAME}" "Node in ${nodeDir.fileName} exited with ${process.exitValue()} when generating its node-info - see logs in ${nodeDir / LOGS_DIR_NAME}"
} }
nodeDir.list { paths -> paths.filter { it.fileName.toString().startsWith("nodeInfo-") }.findFirst().get() } nodeDir.list { paths -> paths.filter { it.fileName.toString().startsWith(NODE_INFO_FILE_NAME_PREFIX) }.findFirst().get() }
} }
} }
@ -182,7 +184,7 @@ class NetworkBootstrapper {
} }
val msg = StringBuilder("Differing sets of network parameters were found. Make sure all the nodes have the same " + val msg = StringBuilder("Differing sets of network parameters were found. Make sure all the nodes have the same " +
"network parameters by copying over the correct $NETWORK_PARAMS_FILE_NAME file.\n\n") "network parameters by copying the correct $NETWORK_PARAMS_FILE_NAME file across.\n\n")
netParamsFilesGrouped.forEach { bytes, netParamsFiles -> netParamsFilesGrouped.forEach { bytes, netParamsFiles ->
netParamsFiles.map { it.parent.fileName }.joinTo(msg, ", ") netParamsFiles.map { it.parent.fileName }.joinTo(msg, ", ")
@ -211,6 +213,7 @@ class NetworkBootstrapper {
epoch = existingNetParams.epoch + 1 epoch = existingNetParams.epoch + 1
) )
} else { } else {
// TODO Add config for minimumPlatformVersion, maxMessageSize and maxTransactionSize
NetworkParameters( NetworkParameters(
minimumPlatformVersion = 1, minimumPlatformVersion = 1,
notaries = notaryInfos, notaries = notaryInfos,
@ -221,28 +224,25 @@ class NetworkBootstrapper {
epoch = 1 epoch = 1
) )
} }
// TODO Add config for minimumPlatformVersion, maxMessageSize and maxTransactionSize
val copier = NetworkParametersCopier(networkParameters, overwriteFile = true) val copier = NetworkParametersCopier(networkParameters, overwriteFile = true)
nodeDirs.forEach { copier.install(it) } nodeDirs.forEach(copier::install)
return networkParameters return networkParameters
} }
private fun generateWhitelist(networkParameters: NetworkParameters?, private fun generateWhitelist(networkParameters: NetworkParameters?,
excludeWhitelistFile: Path, excludeWhitelistFile: Path,
cordapps: List<String>?): Map<String, List<AttachmentId>> { cordappJars: List<Path>): Map<String, List<AttachmentId>> {
val existingWhitelist = networkParameters?.whitelistedContractImplementations ?: emptyMap() val existingWhitelist = networkParameters?.whitelistedContractImplementations ?: emptyMap()
val excludeContracts = if (excludeWhitelistFile.exists()) readExcludeWhitelist(excludeWhitelistFile) else emptyList() val excludeContracts = readExcludeWhitelist(excludeWhitelistFile)
if (excludeContracts.isNotEmpty()) { if (excludeContracts.isNotEmpty()) {
println("Exclude contracts from whitelist: ${excludeContracts.joinToString()}}") println("Exclude contracts from whitelist: ${excludeContracts.joinToString()}")
} }
val newWhiteList = cordapps?.flatMap { cordappJarPath -> val newWhiteList = cordappJars.flatMap { cordappJar ->
val jarHash = Paths.get(cordappJarPath).hash val jarHash = cordappJar.hash
scanJarForContracts(cordappJarPath).map { contract -> scanJarForContracts(cordappJar).map { contract -> contract to jarHash }
contract to jarHash }.filter { (contractClassName, _) -> contractClassName !in excludeContracts }.toMap()
}
}?.filter { (contractClassName, _) -> contractClassName !in excludeContracts }?.toMap() ?: emptyMap()
return (newWhiteList.keys + existingWhitelist.keys).map { contractClassName -> return (newWhiteList.keys + existingWhitelist.keys).map { contractClassName ->
val existing = existingWhitelist[contractClassName] ?: emptyList() val existing = existingWhitelist[contractClassName] ?: emptyList()
@ -251,7 +251,9 @@ class NetworkBootstrapper {
}.toMap() }.toMap()
} }
private fun readExcludeWhitelist(file: Path): List<String> = file.readAllLines().map(String::trim) private fun readExcludeWhitelist(file: Path): List<String> {
return if (file.exists()) file.readAllLines().map(String::trim) else emptyList()
}
private fun NodeInfo.notaryIdentity(): Party { private fun NodeInfo.notaryIdentity(): Party {
return when (legalIdentities.size) { return when (legalIdentities.size) {

View File

@ -33,10 +33,14 @@ data class NetworkMap(
val parametersUpdate: ParametersUpdate? val parametersUpdate: ParametersUpdate?
) { ) {
override fun toString(): String { override fun toString(): String {
return """${NetworkMap::class.java.simpleName}(nodeInfoHashes= return """NetworkMap {
${nodeInfoHashes.joinToString("\n")} nodeInfoHashes {
networkParameterHash=$networkParameterHash ${nodeInfoHashes.asSequence().take(10).joinToString("\n ")}
parametersUpdate=$parametersUpdate)""" ${if (nodeInfoHashes.size > 10) "... ${nodeInfoHashes.size - 10} more" else ""}
}
networkParameterHash=$networkParameterHash
parametersUpdate=$parametersUpdate
}"""
} }
} }