CORDA-1602: Added cmd line flags to the network bootstrapper (#3419)

The list of CorDapps jars is no longer passed in via the cmd line but is now expected to be placed in the bootstrapped directory.

Ended up being a bit of a refactor to cater for unit testing, and also tidied up the bootstrapper docs.
This commit is contained in:
Shams Asari
2018-06-23 11:36:10 +01:00
committed by GitHub
parent 366af50150
commit 3046843d40
27 changed files with 830 additions and 364 deletions

View File

@ -27,8 +27,8 @@ import net.corda.serialization.internal.CordaSerializationMagic
import net.corda.serialization.internal.SerializationFactoryImpl
import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme
import net.corda.serialization.internal.amqp.amqpMagic
import java.io.InputStream
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.StandardCopyOption.REPLACE_EXISTING
import java.time.Instant
import java.util.*
@ -43,7 +43,21 @@ import kotlin.streams.toList
/**
* Class to bootstrap a local network of Corda nodes on the same filesystem.
*/
class NetworkBootstrapper {
// TODO Move this to tools:bootstrapper
class NetworkBootstrapper
@VisibleForTesting
internal constructor(private val initSerEnv: Boolean,
private val embeddedCordaJar: () -> InputStream,
private val nodeInfosGenerator: (List<Path>) -> List<Path>,
private val contractsJarConverter: (Path) -> ContractsJar) {
constructor() : this(
initSerEnv = true,
embeddedCordaJar = Companion::extractEmbeddedCordaJar,
nodeInfosGenerator = Companion::generateNodeInfos,
contractsJarConverter = ::ContractsJarFile
)
companion object {
// TODO This will probably need to change once we start using a bundled JVM
private val nodeInfoGenCmd = listOf(
@ -55,11 +69,42 @@ class NetworkBootstrapper {
private const val LOGS_DIR_NAME = "logs"
@JvmStatic
fun main(args: Array<String>) {
val baseNodeDirectory = requireNotNull(args.firstOrNull()) { "Expecting first argument which is the nodes' parent directory" }
val cordappJars = if (args.size > 1) args.asList().drop(1).map { Paths.get(it) } else emptyList()
NetworkBootstrapper().bootstrap(Paths.get(baseNodeDirectory).toAbsolutePath().normalize(), cordappJars)
private fun extractEmbeddedCordaJar(): InputStream {
return Thread.currentThread().contextClassLoader.getResourceAsStream("corda.jar")
}
private fun generateNodeInfos(nodeDirs: List<Path>): List<Path> {
val numParallelProcesses = Runtime.getRuntime().availableProcessors()
val timePerNode = 40.seconds // On the test machine, generating the node info takes 7 seconds for a single node.
val tExpected = maxOf(timePerNode, timePerNode * nodeDirs.size.toLong() / numParallelProcesses.toLong())
val warningTimer = Timer("WarnOnSlowMachines", false).schedule(tExpected.toMillis()) {
println("... still waiting. If this is taking longer than usual, check the node logs.")
}
val executor = Executors.newFixedThreadPool(numParallelProcesses)
return try {
nodeDirs.map { executor.fork { generateNodeInfo(it) } }.transpose().getOrThrow()
} finally {
warningTimer.cancel()
executor.shutdownNow()
}
}
private fun generateNodeInfo(nodeDir: Path): Path {
val logsDir = (nodeDir / LOGS_DIR_NAME).createDirectories()
val process = ProcessBuilder(nodeInfoGenCmd)
.directory(nodeDir.toFile())
.redirectErrorStream(true)
.redirectOutput((logsDir / "node-info-gen.log").toFile())
.apply { environment()["CAPSULE_CACHE_DIR"] = "../.cache" }
.start()
if (!process.waitFor(3, TimeUnit.MINUTES)) {
process.destroyForcibly()
throw IllegalStateException("Error while generating node info file. Please check the logs in $logsDir.")
}
check(process.exitValue() == 0) { "Error while generating node info file. Please check the logs in $logsDir." }
return nodeDir.list { paths ->
paths.filter { it.fileName.toString().startsWith(NODE_INFO_FILE_NAME_PREFIX) }.findFirst().get()
}
}
}
@ -92,29 +137,61 @@ class NetworkBootstrapper {
private fun generateServiceIdentitiesForNotaryClusters(configs: Map<Path, Config>) {
notaryClusters(configs).forEach { (cluster, directories) ->
when (cluster) {
is NotaryCluster.BFT ->
DevIdentityGenerator.generateDistributedNotaryCompositeIdentity(directories, cluster.name, threshold = 1 + 2 * directories.size / 3)
is NotaryCluster.CFT ->
DevIdentityGenerator.generateDistributedNotarySingularIdentity(directories, cluster.name)
is NotaryCluster.BFT -> DevIdentityGenerator.generateDistributedNotaryCompositeIdentity(
directories,
cluster.name,
threshold = 1 + 2 * directories.size / 3
)
is NotaryCluster.CFT -> DevIdentityGenerator.generateDistributedNotarySingularIdentity(directories, cluster.name)
}
}
}
/** Entry point for Cordform */
fun bootstrap(directory: Path, cordappJars: List<Path>) {
bootstrap(directory, cordappJars, copyCordapps = true, fromCordform = true)
}
/** Entry point for the tool */
fun bootstrap(directory: Path, copyCordapps: Boolean) {
// Don't accidently include the bootstrapper jar as a CorDapp!
val bootstrapperJar = javaClass.location.toPath()
val cordappJars = directory.list { paths ->
paths.filter { it.toString().endsWith(".jar") && !it.isSameAs(bootstrapperJar) && it.fileName.toString() != "corda.jar" }.toList()
}
bootstrap(directory, cordappJars, copyCordapps, fromCordform = false)
}
private fun bootstrap(directory: Path, cordappJars: List<Path>, copyCordapps: Boolean, fromCordform: Boolean) {
directory.createDirectories()
println("Bootstrapping local network in $directory")
generateDirectoriesIfNeeded(directory, cordappJars)
val nodeDirs = directory.list { paths -> paths.filter { (it / "corda.jar").exists() }.toList() }
println("Bootstrapping local test network in $directory")
if (!fromCordform) {
println("Found the following CorDapps: ${cordappJars.map { it.fileName }}")
}
createNodeDirectoriesIfNeeded(directory, fromCordform)
val nodeDirs = gatherNodeDirectories(directory)
require(nodeDirs.isNotEmpty()) { "No nodes found" }
println("Nodes found in the following sub-directories: ${nodeDirs.map { it.fileName }}")
if (!fromCordform) {
println("Nodes found in the following sub-directories: ${nodeDirs.map { it.fileName }}")
}
val configs = nodeDirs.associateBy({ it }, { ConfigFactory.parseFile((it / "node.conf").toFile()) })
checkForDuplicateLegalNames(configs.values)
if (copyCordapps && cordappJars.isNotEmpty()) {
println("Copying CorDapp JARs into node directories")
for (nodeDir in nodeDirs) {
val cordappsDir = (nodeDir / "cordapps").createDirectories()
cordappJars.forEach { it.copyToDirectory(cordappsDir) }
}
}
generateServiceIdentitiesForNotaryClusters(configs)
initialiseSerialization()
if (initSerEnv) {
initialiseSerialization()
}
try {
println("Waiting for all nodes to generate their node-info files...")
val nodeInfoFiles = generateNodeInfos(nodeDirs)
println("Checking for duplicate nodes")
checkForDuplicateLegalNames(nodeInfoFiles)
val nodeInfoFiles = nodeInfosGenerator(nodeDirs)
println("Distributing all node-info files to all nodes")
distributeNodeInfos(nodeDirs, nodeInfoFiles)
print("Loading existing network parameters... ")
@ -123,72 +200,70 @@ class NetworkBootstrapper {
println("Gathering notary identities")
val notaryInfos = gatherNotaryInfos(nodeInfoFiles, configs)
println("Generating contract implementations whitelist")
val newWhitelist = generateWhitelist(existingNetParams, readExcludeWhitelist(directory), cordappJars.map(::ContractsJarFile))
val netParams = installNetworkParameters(notaryInfos, newWhitelist, existingNetParams, nodeDirs)
println("${if (existingNetParams == null) "New" else "Updated"} $netParams")
val newWhitelist = generateWhitelist(existingNetParams, readExcludeWhitelist(directory), cordappJars.map(contractsJarConverter))
val newNetParams = installNetworkParameters(notaryInfos, newWhitelist, existingNetParams, nodeDirs)
if (newNetParams != existingNetParams) {
println("${if (existingNetParams == null) "New" else "Updated"} $newNetParams")
} else {
println("Network parameters unchanged")
}
println("Bootstrapping complete!")
} finally {
_contextSerializationEnv.set(null)
if (initSerEnv) {
_contextSerializationEnv.set(null)
}
}
}
private fun generateNodeInfos(nodeDirs: List<Path>): List<Path> {
val numParallelProcesses = Runtime.getRuntime().availableProcessors()
val timePerNode = 40.seconds // On the test machine, generating the node info takes 7 seconds for a single node.
val tExpected = maxOf(timePerNode, timePerNode * nodeDirs.size.toLong() / numParallelProcesses.toLong())
val warningTimer = Timer("WarnOnSlowMachines", false).schedule(tExpected.toMillis()) {
println("...still waiting. If this is taking longer than usual, check the node logs.")
private fun createNodeDirectoriesIfNeeded(directory: Path, fromCordform: Boolean) {
val cordaJar = directory / "corda.jar"
var usingEmbedded = false
if (!cordaJar.exists()) {
embeddedCordaJar().use { it.copyTo(cordaJar) }
usingEmbedded = true
} else if (!fromCordform) {
println("Using corda.jar in root directory")
}
val executor = Executors.newFixedThreadPool(numParallelProcesses)
return try {
nodeDirs.map { executor.fork { generateNodeInfo(it) } }.transpose().getOrThrow()
} finally {
warningTimer.cancel()
executor.shutdownNow()
}
}
private fun generateNodeInfo(nodeDir: Path): Path {
val logsDir = (nodeDir / LOGS_DIR_NAME).createDirectories()
val process = ProcessBuilder(nodeInfoGenCmd)
.directory(nodeDir.toFile())
.redirectErrorStream(true)
.redirectOutput((logsDir / "node-info-gen.log").toFile())
.apply { environment()["CAPSULE_CACHE_DIR"] = "../.cache" }
.start()
if (!process.waitFor(3, TimeUnit.MINUTES)) {
process.destroyForcibly()
throw IllegalStateException("Error while generating node info file. Please check the logs in $logsDir.")
}
check(process.exitValue() == 0) { "Error while generating node info file. Please check the logs in $logsDir." }
return nodeDir.list { paths -> paths.filter { it.fileName.toString().startsWith(NODE_INFO_FILE_NAME_PREFIX) }.findFirst().get() }
}
private fun generateDirectoriesIfNeeded(directory: Path, cordappJars: List<Path>) {
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() }
if (confFiles.isEmpty()) return
println("Node config files found in the root directory - generating node directories and copying CorDapp jars into them")
val cordaJar = extractCordaJarTo(directory)
for (confFile in confFiles) {
val nodeName = confFile.fileName.toString().removeSuffix("_node.conf")
println("Generating directory for $nodeName")
println("Generating node directory for $nodeName")
val nodeDir = (directory / nodeName).createDirectories()
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)
confFile.copyTo(nodeDir / "node.conf", REPLACE_EXISTING)
webServerConfFiles.firstOrNull { directory.relativize(it).toString().removeSuffix("_web-server.conf") == nodeName }?.copyTo(nodeDir / "web-server.conf", REPLACE_EXISTING)
cordaJar.copyToDirectory(nodeDir, REPLACE_EXISTING)
val cordappsDir = (nodeDir / "cordapps").createDirectories()
cordappJars.forEach { it.copyToDirectory(cordappsDir) }
}
cordaJar.delete()
directory.list { paths ->
paths.filter { (it / "node.conf").exists() && !(it / "corda.jar").exists() }.forEach {
println("Copying corda.jar into node directory ${it.fileName}")
cordaJar.copyToDirectory(it)
}
}
if (fromCordform) {
confFiles.forEach(Path::delete)
webServerConfFiles.forEach(Path::delete)
}
if (fromCordform || usingEmbedded) {
cordaJar.delete()
}
}
private fun extractCordaJarTo(directory: Path): Path {
val cordaJarPath = directory / "corda.jar"
if (!cordaJarPath.exists()) {
Thread.currentThread().contextClassLoader.getResourceAsStream("corda.jar").use { it.copyTo(cordaJarPath) }
private fun gatherNodeDirectories(directory: Path): List<Path> {
return directory.list { paths ->
paths.filter {
val exists = (it / "corda.jar").exists()
if (exists) {
require((it / "node.conf").exists()) { "Missing node.conf in node directory ${it.fileName}" }
}
exists
}.toList()
}
return cordaJarPath
}
private fun distributeNodeInfos(nodeDirs: List<Path>, nodeInfoFiles: List<Path>) {
@ -200,20 +275,13 @@ class NetworkBootstrapper {
}
}
/*the function checks for duplicate myLegalName in the all the *_node.conf files
All the myLegalName values are added to a HashSet - this helps detect duplicate values.
If a duplicate name is found the process is aborted with an error message
*/
private fun checkForDuplicateLegalNames(nodeInfoFiles: List<Path>) {
val legalNames = HashSet<String>()
for (nodeInfoFile in nodeInfoFiles) {
val nodeConfig = ConfigFactory.parseFile((nodeInfoFile.parent / "node.conf").toFile())
val legalName = nodeConfig.getString("myLegalName")
if(!legalNames.add(legalName)){
println("Duplicate Node Found - ensure every node has a unique legal name");
throw IllegalArgumentException("Duplicate Node Found - $legalName");
private fun checkForDuplicateLegalNames(nodeConfigs: Collection<Config>) {
val duplicateLegalNames = nodeConfigs
.groupBy { it.getString("myLegalName") }
.mapNotNull { if (it.value.size > 1) it.key else null }
check(duplicateLegalNames.isEmpty()) {
"Nodes must have unique legal names. The following are used more than once: $duplicateLegalNames"
}
}
}
private fun gatherNotaryInfos(nodeInfoFiles: List<Path>, configs: Map<Path, Config>): List<NotaryInfo> {
@ -264,15 +332,19 @@ class NetworkBootstrapper {
whitelist: Map<String, List<AttachmentId>>,
existingNetParams: NetworkParameters?,
nodeDirs: List<Path>): NetworkParameters {
val networkParameters = if (existingNetParams != null) {
existingNetParams.copy(
notaries = notaryInfos,
modifiedTime = Instant.now(),
whitelistedContractImplementations = whitelist,
epoch = existingNetParams.epoch + 1
)
// TODO Add config for minimumPlatformVersion, maxMessageSize and maxTransactionSize
val netParams = if (existingNetParams != null) {
if (existingNetParams.whitelistedContractImplementations == whitelist && existingNetParams.notaries == notaryInfos) {
existingNetParams
} else {
existingNetParams.copy(
notaries = notaryInfos,
modifiedTime = Instant.now(),
whitelistedContractImplementations = whitelist,
epoch = existingNetParams.epoch + 1
)
}
} else {
// TODO Add config for minimumPlatformVersion, maxMessageSize and maxTransactionSize
NetworkParameters(
minimumPlatformVersion = 1,
notaries = notaryInfos,
@ -284,9 +356,9 @@ class NetworkBootstrapper {
eventHorizon = 30.days
)
}
val copier = NetworkParametersCopier(networkParameters, overwriteFile = true)
val copier = NetworkParametersCopier(netParams, overwriteFile = true)
nodeDirs.forEach(copier::install)
return networkParameters
return netParams
}
private fun NodeInfo.notaryIdentity(): Party {
@ -301,7 +373,6 @@ class NetworkBootstrapper {
}
// We need to to set serialization env, because generation of parameters is run from Cordform.
// KryoServerSerializationScheme is not accessible from nodeapi.
private fun initialiseSerialization() {
_contextSerializationEnv.set(SerializationEnvironmentImpl(
SerializationFactoryImpl().apply {

View File

@ -1,141 +1,289 @@
package net.corda.nodeapi.internal.network
import net.corda.core.contracts.ContractClassName
import net.corda.core.crypto.SecureHash
import net.corda.core.node.services.AttachmentId
import net.corda.nodeapi.internal.ContractsJar
import net.corda.testing.common.internal.testNetworkParameters
import com.typesafe.config.ConfigFactory
import net.corda.cordform.CordformNode.NODE_INFO_DIRECTORY
import net.corda.core.crypto.secureRandomBytes
import net.corda.core.crypto.sha256
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.*
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.serialize
import net.corda.node.services.config.NotaryConfig
import net.corda.nodeapi.internal.DEV_ROOT_CA
import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.config.parseAs
import net.corda.nodeapi.internal.config.toConfig
import net.corda.nodeapi.internal.network.NodeInfoFilesCopier.Companion.NODE_INFO_FILE_NAME_PREFIX
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.internal.createNodeInfoAndSigned
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatIllegalArgumentException
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.After
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.nio.file.Path
import kotlin.streams.toList
class NetworkBootstrapperTest {
@Test
fun `no jars against empty whitelist`() {
val whitelist = generateWhitelist(emptyMap(), emptyList(), emptyList())
assertThat(whitelist).isEmpty()
}
@Rule
@JvmField
val tempFolder = TemporaryFolder()
@Test
fun `no jars against single whitelist`() {
val existingWhitelist = mapOf("class1" to listOf(SecureHash.randomSHA256()))
val newWhitelist = generateWhitelist(existingWhitelist, emptyList(), emptyList())
assertThat(newWhitelist).isEqualTo(existingWhitelist)
}
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
@Test
fun `empty jar against empty whitelist`() {
val whitelist = generateWhitelist(emptyMap(), emptyList(), listOf(TestContractsJar(contractClassNames = emptyList())))
assertThat(whitelist).isEmpty()
}
private val fakeEmbeddedCordaJar = fakeFileBytes()
@Test
fun `empty jar against single whitelist`() {
val existingWhitelist = mapOf("class1" to listOf(SecureHash.randomSHA256()))
val newWhitelist = generateWhitelist(existingWhitelist, emptyList(), listOf(TestContractsJar(contractClassNames = emptyList())))
assertThat(newWhitelist).isEqualTo(existingWhitelist)
}
private val contractsJars = HashMap<Path, TestContractsJar>()
@Test
fun `jar with single contract against empty whitelist`() {
val jar = TestContractsJar(contractClassNames = listOf("class1"))
val whitelist = generateWhitelist(emptyMap(), emptyList(), listOf(jar))
assertThat(whitelist).isEqualTo(mapOf(
"class1" to listOf(jar.hash)
))
}
private val bootstrapper = NetworkBootstrapper(
initSerEnv = false,
embeddedCordaJar = fakeEmbeddedCordaJar::inputStream,
nodeInfosGenerator = { nodeDirs ->
nodeDirs.map { nodeDir ->
val name = nodeDir.fakeNodeConfig.myLegalName
val file = nodeDir / "$NODE_INFO_FILE_NAME_PREFIX${name.serialize().hash}"
if (!file.exists()) {
createNodeInfoAndSigned(name).signed.serialize().open().copyTo(file)
}
file
}
},
contractsJarConverter = { contractsJars[it]!! }
)
@Test
fun `single contract jar against single whitelist of different contract`() {
val class1JarHash = SecureHash.randomSHA256()
val existingWhitelist = mapOf("class1" to listOf(class1JarHash))
val jar = TestContractsJar(contractClassNames = listOf("class2"))
val whitelist = generateWhitelist(existingWhitelist, emptyList(), listOf(jar))
assertThat(whitelist).isEqualTo(mapOf(
"class1" to listOf(class1JarHash),
"class2" to listOf(jar.hash)
))
}
private val aliceConfig = FakeNodeConfig(ALICE_NAME)
private val bobConfig = FakeNodeConfig(BOB_NAME)
private val notaryConfig = FakeNodeConfig(DUMMY_NOTARY_NAME, NotaryConfig(validating = true))
@Test
fun `same jar with single contract`() {
val jarHash = SecureHash.randomSHA256()
val existingWhitelist = mapOf("class1" to listOf(jarHash))
val jar = TestContractsJar(hash = jarHash, contractClassNames = listOf("class1"))
val newWhitelist = generateWhitelist(existingWhitelist, emptyList(), listOf(jar))
assertThat(newWhitelist).isEqualTo(existingWhitelist)
}
private var providedCordaJar: ByteArray? = null
private val configFiles = HashMap<Path, String>()
@Test
fun `jar with updated contract`() {
val previousJarHash = SecureHash.randomSHA256()
val existingWhitelist = mapOf("class1" to listOf(previousJarHash))
val newContractsJar = TestContractsJar(contractClassNames = listOf("class1"))
val newWhitelist = generateWhitelist(existingWhitelist, emptyList(), listOf(newContractsJar))
assertThat(newWhitelist).isEqualTo(mapOf(
"class1" to listOf(previousJarHash, newContractsJar.hash)
))
}
@Test
fun `jar with one existing contract and one new one`() {
val previousJarHash = SecureHash.randomSHA256()
val existingWhitelist = mapOf("class1" to listOf(previousJarHash))
val newContractsJar = TestContractsJar(contractClassNames = listOf("class1", "class2"))
val newWhitelist = generateWhitelist(existingWhitelist, emptyList(), listOf(newContractsJar))
assertThat(newWhitelist).isEqualTo(mapOf(
"class1" to listOf(previousJarHash, newContractsJar.hash),
"class2" to listOf(newContractsJar.hash)
))
}
@Test
fun `two versions of the same contract`() {
val version1Jar = TestContractsJar(contractClassNames = listOf("class1"))
val version2Jar = TestContractsJar(contractClassNames = listOf("class1"))
val newWhitelist = generateWhitelist(emptyMap(), emptyList(), listOf(version1Jar, version2Jar))
assertThat(newWhitelist).isEqualTo(mapOf(
"class1" to listOf(version1Jar.hash, version2Jar.hash)
))
}
@Test
fun `jar with single new contract that's excluded`() {
val jar = TestContractsJar(contractClassNames = listOf("class1"))
val whitelist = generateWhitelist(emptyMap(), listOf("class1"), listOf(jar))
assertThat(whitelist).isEmpty()
}
@Test
fun `jar with two new contracts, one of which is excluded`() {
val jar = TestContractsJar(contractClassNames = listOf("class1", "class2"))
val whitelist = generateWhitelist(emptyMap(), listOf("class1"), listOf(jar))
assertThat(whitelist).isEqualTo(mapOf(
"class2" to listOf(jar.hash)
))
}
@Test
fun `jar with updated contract but it's excluded`() {
val existingWhitelist = mapOf("class1" to listOf(SecureHash.randomSHA256()))
val jar = TestContractsJar(contractClassNames = listOf("class1"))
assertThatIllegalArgumentException().isThrownBy {
generateWhitelist(existingWhitelist, listOf("class1"), listOf(jar))
@After
fun `check config files are preserved`() {
configFiles.forEach { file, text ->
assertThat(file).hasContent(text)
}
}
private fun generateWhitelist(existingWhitelist: Map<String, List<AttachmentId>>,
excludeContracts: List<ContractClassName>,
contractJars: List<TestContractsJar>): Map<String, List<AttachmentId>> {
return generateWhitelist(
testNetworkParameters(whitelistedContractImplementations = existingWhitelist),
excludeContracts,
contractJars
)
@After
fun `check provided corda jar is preserved`() {
if (providedCordaJar == null) {
// Make sure we clean up if we used the embedded jar
assertThat(rootDir / "corda.jar").doesNotExist()
} else {
// Make sure we don't delete it if it was provided by the user
assertThat(rootDir / "corda.jar").hasBinaryContent(providedCordaJar)
}
}
data class TestContractsJar(override val hash: SecureHash = SecureHash.randomSHA256(),
private val contractClassNames: List<ContractClassName>) : ContractsJar {
override fun scan(): List<ContractClassName> = contractClassNames
@Test
fun `empty dir`() {
assertThatThrownBy {
bootstrap()
}.hasMessage("No nodes found")
}
@Test
fun `single node conf file`() {
createNodeConfFile("node1", bobConfig)
bootstrap()
val networkParameters = assertBootstrappedNetwork(fakeEmbeddedCordaJar, "node1" to bobConfig)
networkParameters.run {
assertThat(epoch).isEqualTo(1)
assertThat(notaries).isEmpty()
assertThat(whitelistedContractImplementations).isEmpty()
}
}
@Test
fun `node conf file and corda jar`() {
createNodeConfFile("node1", bobConfig)
val fakeCordaJar = fakeFileBytes(rootDir / "corda.jar")
bootstrap()
assertBootstrappedNetwork(fakeCordaJar, "node1" to bobConfig)
}
@Test
fun `single node directory with just node conf file`() {
createNodeDir("bob", bobConfig)
bootstrap()
assertBootstrappedNetwork(fakeEmbeddedCordaJar, "bob" to bobConfig)
}
@Test
fun `single node directory with node conf file and corda jar`() {
val nodeDir = createNodeDir("bob", bobConfig)
val fakeCordaJar = fakeFileBytes(nodeDir / "corda.jar")
bootstrap()
assertBootstrappedNetwork(fakeCordaJar, "bob" to bobConfig)
}
@Test
fun `single node directory with just corda jar`() {
val nodeCordaJar = (rootDir / "alice").createDirectories() / "corda.jar"
val fakeCordaJar = fakeFileBytes(nodeCordaJar)
assertThatThrownBy {
bootstrap()
}.hasMessageStartingWith("Missing node.conf in node directory alice")
assertThat(nodeCordaJar).hasBinaryContent(fakeCordaJar) // Make sure the corda.jar is left untouched
}
@Test
fun `two node conf files, one of which is a notary`() {
createNodeConfFile("alice", aliceConfig)
createNodeConfFile("notary", notaryConfig)
bootstrap()
val networkParameters = assertBootstrappedNetwork(fakeEmbeddedCordaJar, "alice" to aliceConfig, "notary" to notaryConfig)
networkParameters.assertContainsNotary("notary")
}
@Test
fun `two node conf files with the same legal name`() {
createNodeConfFile("node1", aliceConfig)
createNodeConfFile("node2", aliceConfig)
assertThatThrownBy {
bootstrap()
}.hasMessageContaining("Nodes must have unique legal names")
}
@Test
fun `one node directory and one node conf file`() {
createNodeConfFile("alice", aliceConfig)
createNodeDir("bob", bobConfig)
bootstrap()
assertBootstrappedNetwork(fakeEmbeddedCordaJar, "alice" to aliceConfig, "bob" to bobConfig)
}
@Test
fun `node conf file and CorDapp jar`() {
createNodeConfFile("alice", aliceConfig)
val cordappBytes = createFakeCordappJar("sample-app", listOf("contract.class"))
bootstrap()
val networkParameters = assertBootstrappedNetwork(fakeEmbeddedCordaJar, "alice" to aliceConfig)
assertThat(rootDir / "alice" / "cordapps" / "sample-app.jar").hasBinaryContent(cordappBytes)
assertThat(networkParameters.whitelistedContractImplementations).isEqualTo(mapOf(
"contract.class" to listOf(cordappBytes.sha256())
))
}
@Test
fun `no copy CorDapps`() {
createNodeConfFile("alice", aliceConfig)
val cordappBytes = createFakeCordappJar("sample-app", listOf("contract.class"))
bootstrap(copyCordapps = false)
val networkParameters = assertBootstrappedNetwork(fakeEmbeddedCordaJar, "alice" to aliceConfig)
assertThat(rootDir / "alice" / "cordapps" / "sample-app.jar").doesNotExist()
assertThat(networkParameters.whitelistedContractImplementations).isEqualTo(mapOf(
"contract.class" to listOf(cordappBytes.sha256())
))
}
@Test
fun `add node to existing network`() {
createNodeConfFile("alice", aliceConfig)
bootstrap()
val networkParameters1 = (rootDir / "alice").networkParameters
createNodeConfFile("bob", bobConfig)
bootstrap()
val networkParameters2 = assertBootstrappedNetwork(fakeEmbeddedCordaJar, "alice" to aliceConfig, "bob" to bobConfig)
assertThat(networkParameters1).isEqualTo(networkParameters2)
}
@Test
fun `add notary to existing network`() {
createNodeConfFile("alice", aliceConfig)
bootstrap()
createNodeConfFile("notary", notaryConfig)
bootstrap()
val networkParameters = assertBootstrappedNetwork(fakeEmbeddedCordaJar, "alice" to aliceConfig, "notary" to notaryConfig)
networkParameters.assertContainsNotary("notary")
assertThat(networkParameters.epoch).isEqualTo(2)
}
private val rootDir get() = tempFolder.root.toPath()
private fun fakeFileBytes(writeToFile: Path? = null): ByteArray {
val bytes = secureRandomBytes(128)
writeToFile?.write(bytes)
return bytes
}
private fun bootstrap(copyCordapps: Boolean = true) {
providedCordaJar = (rootDir / "corda.jar").let { if (it.exists()) it.readAll() else null }
bootstrapper.bootstrap(rootDir, copyCordapps)
}
private fun createNodeConfFile(nodeDirName: String, config: FakeNodeConfig) {
writeNodeConfFile(rootDir / "${nodeDirName}_node.conf", config)
}
private fun createNodeDir(nodeDirName: String, config: FakeNodeConfig): Path {
val nodeDir = (rootDir / nodeDirName).createDirectories()
writeNodeConfFile(nodeDir / "node.conf", config)
return nodeDir
}
private fun writeNodeConfFile(file: Path, config: FakeNodeConfig) {
val configText = config.toConfig().root().render()
file.writeText(configText)
configFiles[file] = configText
}
private fun createFakeCordappJar(cordappName: String, contractClassNames: List<String>): ByteArray {
val cordappJarFile = rootDir / "$cordappName.jar"
val cordappBytes = fakeFileBytes(cordappJarFile)
contractsJars[cordappJarFile] = TestContractsJar(cordappBytes.sha256(), contractClassNames)
return cordappBytes
}
private val Path.networkParameters: NetworkParameters get() {
return (this / NETWORK_PARAMS_FILE_NAME).readObject<SignedNetworkParameters>().verifiedNetworkMapCert(DEV_ROOT_CA.certificate)
}
private val Path.nodeInfoFile: Path get() {
return list { it.filter { it.fileName.toString().startsWith(NODE_INFO_FILE_NAME_PREFIX) }.toList() }.single()
}
private val Path.nodeInfo: NodeInfo get() = nodeInfoFile.readObject<SignedNodeInfo>().verified()
private val Path.fakeNodeConfig: FakeNodeConfig get() {
return ConfigFactory.parseFile((this / "node.conf").toFile()).parseAs(FakeNodeConfig::class)
}
private fun assertBootstrappedNetwork(cordaJar: ByteArray, vararg nodes: Pair<String, FakeNodeConfig>): NetworkParameters {
val networkParameters = (rootDir / nodes[0].first).networkParameters
val allNodeInfoFiles = nodes.map { (rootDir / it.first).nodeInfoFile }.associateBy({ it }, { it.readAll() })
for ((nodeDirName, config) in nodes) {
val nodeDir = rootDir / nodeDirName
assertThat(nodeDir / "corda.jar").hasBinaryContent(cordaJar)
assertThat(nodeDir.fakeNodeConfig).isEqualTo(config)
assertThat(nodeDir.networkParameters).isEqualTo(networkParameters)
// Make sure all the nodes have all of each others' node-info files
allNodeInfoFiles.forEach { nodeInfoFile, bytes ->
assertThat(nodeDir / NODE_INFO_DIRECTORY / nodeInfoFile.fileName.toString()).hasBinaryContent(bytes)
}
}
return networkParameters
}
private fun NetworkParameters.assertContainsNotary(dirName: String) {
val notaryParty = (rootDir / dirName).nodeInfo.legalIdentities.single()
assertThat(notaries).hasSize(1)
notaries[0].run {
assertThat(validating).isTrue()
assertThat(identity.name).isEqualTo(notaryParty.name)
assertThat(identity.owningKey).isEqualTo(notaryParty.owningKey)
}
}
data class FakeNodeConfig(val myLegalName: CordaX500Name, val notary: NotaryConfig? = null)
}

View File

@ -0,0 +1,10 @@
package net.corda.nodeapi.internal.network
import net.corda.core.contracts.ContractClassName
import net.corda.core.crypto.SecureHash
import net.corda.nodeapi.internal.ContractsJar
data class TestContractsJar(override val hash: SecureHash = SecureHash.randomSHA256(),
private val contractClassNames: List<ContractClassName>) : ContractsJar {
override fun scan(): List<ContractClassName> = contractClassNames
}

View File

@ -1,41 +1,135 @@
package net.corda.nodeapi.internal.network
import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.verify
import net.corda.core.contracts.ContractClassName
import net.corda.core.crypto.SecureHash
import net.corda.nodeapi.internal.ContractsJar
import net.corda.core.node.services.AttachmentId
import net.corda.testing.common.internal.testNetworkParameters
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatIllegalArgumentException
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class WhitelistGeneratorTest {
@Test
fun `no jars against empty whitelist`() {
val whitelist = generateWhitelist(emptyMap(), emptyList(), emptyList())
assertThat(whitelist).isEmpty()
}
@Test
fun `whitelist generator builds the correct whitelist map`() {
// given
val jars = (0..9).map {
val index = it
mock<ContractsJar> {
val secureHash = SecureHash.randomSHA256()
on { scan() }.then {
listOf(index.toString())
}
on { hash }.then {
secureHash
}
}
}
fun `no jars against single whitelist`() {
val existingWhitelist = mapOf("class1" to listOf(SecureHash.randomSHA256()))
val newWhitelist = generateWhitelist(existingWhitelist, emptyList(), emptyList())
assertThat(newWhitelist).isEqualTo(existingWhitelist)
}
// when
val result = generateWhitelist(null, emptyList(), jars)
@Test
fun `empty jar against empty whitelist`() {
val whitelist = generateWhitelist(emptyMap(), emptyList(), listOf(TestContractsJar(contractClassNames = emptyList())))
assertThat(whitelist).isEmpty()
}
// then
jars.forEachIndexed { index, item ->
verify(item).scan()
val attachmentIds = requireNotNull(result[index.toString()])
assertEquals(1, attachmentIds.size)
assertTrue { attachmentIds.contains(item.hash) }
@Test
fun `empty jar against single whitelist`() {
val existingWhitelist = mapOf("class1" to listOf(SecureHash.randomSHA256()))
val newWhitelist = generateWhitelist(existingWhitelist, emptyList(), listOf(TestContractsJar(contractClassNames = emptyList())))
assertThat(newWhitelist).isEqualTo(existingWhitelist)
}
@Test
fun `jar with single contract against empty whitelist`() {
val jar = TestContractsJar(contractClassNames = listOf("class1"))
val whitelist = generateWhitelist(emptyMap(), emptyList(), listOf(jar))
assertThat(whitelist).isEqualTo(mapOf(
"class1" to listOf(jar.hash)
))
}
@Test
fun `single contract jar against single whitelist of different contract`() {
val class1JarHash = SecureHash.randomSHA256()
val existingWhitelist = mapOf("class1" to listOf(class1JarHash))
val jar = TestContractsJar(contractClassNames = listOf("class2"))
val whitelist = generateWhitelist(existingWhitelist, emptyList(), listOf(jar))
assertThat(whitelist).isEqualTo(mapOf(
"class1" to listOf(class1JarHash),
"class2" to listOf(jar.hash)
))
}
@Test
fun `same jar with single contract`() {
val jarHash = SecureHash.randomSHA256()
val existingWhitelist = mapOf("class1" to listOf(jarHash))
val jar = TestContractsJar(hash = jarHash, contractClassNames = listOf("class1"))
val newWhitelist = generateWhitelist(existingWhitelist, emptyList(), listOf(jar))
assertThat(newWhitelist).isEqualTo(existingWhitelist)
}
@Test
fun `jar with updated contract`() {
val previousJarHash = SecureHash.randomSHA256()
val existingWhitelist = mapOf("class1" to listOf(previousJarHash))
val newContractsJar = TestContractsJar(contractClassNames = listOf("class1"))
val newWhitelist = generateWhitelist(existingWhitelist, emptyList(), listOf(newContractsJar))
assertThat(newWhitelist).isEqualTo(mapOf(
"class1" to listOf(previousJarHash, newContractsJar.hash)
))
}
@Test
fun `jar with one existing contract and one new one`() {
val previousJarHash = SecureHash.randomSHA256()
val existingWhitelist = mapOf("class1" to listOf(previousJarHash))
val newContractsJar = TestContractsJar(contractClassNames = listOf("class1", "class2"))
val newWhitelist = generateWhitelist(existingWhitelist, emptyList(), listOf(newContractsJar))
assertThat(newWhitelist).isEqualTo(mapOf(
"class1" to listOf(previousJarHash, newContractsJar.hash),
"class2" to listOf(newContractsJar.hash)
))
}
@Test
fun `two versions of the same contract`() {
val version1Jar = TestContractsJar(contractClassNames = listOf("class1"))
val version2Jar = TestContractsJar(contractClassNames = listOf("class1"))
val newWhitelist = generateWhitelist(emptyMap(), emptyList(), listOf(version1Jar, version2Jar))
assertThat(newWhitelist).isEqualTo(mapOf(
"class1" to listOf(version1Jar.hash, version2Jar.hash)
))
}
@Test
fun `jar with single new contract that's excluded`() {
val jar = TestContractsJar(contractClassNames = listOf("class1"))
val whitelist = generateWhitelist(emptyMap(), listOf("class1"), listOf(jar))
assertThat(whitelist).isEmpty()
}
@Test
fun `jar with two new contracts, one of which is excluded`() {
val jar = TestContractsJar(contractClassNames = listOf("class1", "class2"))
val whitelist = generateWhitelist(emptyMap(), listOf("class1"), listOf(jar))
assertThat(whitelist).isEqualTo(mapOf(
"class2" to listOf(jar.hash)
))
}
@Test
fun `jar with updated contract but it's excluded`() {
val existingWhitelist = mapOf("class1" to listOf(SecureHash.randomSHA256()))
val jar = TestContractsJar(contractClassNames = listOf("class1"))
assertThatIllegalArgumentException().isThrownBy {
generateWhitelist(existingWhitelist, listOf("class1"), listOf(jar))
}
}
}
private fun generateWhitelist(existingWhitelist: Map<String, List<AttachmentId>>,
excludeContracts: List<ContractClassName>,
contractJars: List<TestContractsJar>): Map<String, List<AttachmentId>> {
return generateWhitelist(
testNetworkParameters(whitelistedContractImplementations = existingWhitelist),
excludeContracts,
contractJars
)
}
}