Update node-driver to support testing nodes with DJVM support.

This commit is contained in:
Chris Rankin 2019-10-11 13:37:15 +01:00
parent ea6db636fb
commit 739ffda6c7
14 changed files with 245 additions and 57 deletions
node
build.gradle
capsule
src
integration-test/kotlin/net/corda
main/kotlin/net/corda/node
testing/node-driver/src/main/kotlin/net/corda/testing

@ -32,7 +32,6 @@ apply plugin: 'com.jfrog.artifactory'
description 'Corda node modules'
repositories {
mavenLocal()
// Extra repository for the deterministic-rt JAR.
maven {
url "$artifactory_contextUrl/corda-dev"
@ -51,16 +50,6 @@ configurations {
cacheChangingModulesFor 0, 'seconds'
}
deterministic
// This is for the latest deterministic Corda SNAPSHOT artifacts...
[ compileClasspath, runtimeClasspath ].forEach { cfg ->
cfg.resolutionStrategy {
dependencySubstitution {
substitute module("net.corda:corda-core") with project(':core')
substitute module("net.corda:corda-serialization") with project(':serialization')
}
}
}
}
sourceSets {
@ -181,7 +170,9 @@ dependencies {
// Sandbox for deterministic contract verification
compile "net.corda.djvm:corda-djvm:$djvm_version"
compile "net.corda.djvm:corda-djvm-serialization:$djvm_version"
compile("net.corda.djvm:corda-djvm-serialization:$djvm_version") {
exclude group: 'net.corda'
}
compile(project(':node:djvm')) {
transitive = false
}
@ -284,8 +275,11 @@ tasks.withType(JavaCompile) {
}
tasks.withType(Test) {
if (JavaVersion.current() == JavaVersion.VERSION_11)
if (JavaVersion.current() == JavaVersion.VERSION_11) {
jvmArgs '-Djdk.attach.allowAttachSelf=true'
}
systemProperty 'deterministic-rt.path', configurations.jdkRt.asPath
systemProperty 'deterministic-sources.path', configurations.deterministic.asPath
}
task integrationTest(type: Test) {

@ -108,8 +108,9 @@ task buildCordaJAR(type: FatCapsule, dependsOn: [
//
// If you change these flags, please also update Driver.kt
jvmArgs = ['-Xmx512m', '-XX:+UseG1GC']
if (JavaVersion.current() == JavaVersion.VERSION_11)
if (JavaVersion.current() == JavaVersion.VERSION_11) {
jvmArgs += ['-Djdk.attach.allowAttachSelf=true']
}
}
}

@ -1,2 +0,0 @@
quiet
exclude sandbox/*.*

@ -0,0 +1,20 @@
package net.corda.contracts.djvm
import net.corda.core.contracts.Contract
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.TypeOnlyCommandData
import net.corda.core.identity.AbstractParty
import net.corda.core.transactions.LedgerTransaction
import java.time.Instant
class NonDeterministicContract : Contract {
override fun verify(tx: LedgerTransaction) {
Instant.now().toString()
}
class State : ContractState {
override val participants: List<AbstractParty> get() = emptyList()
}
object Cmd : TypeOnlyCommandData()
}

@ -0,0 +1,31 @@
package net.corda.flows.djvm
import co.paralleluniverse.fibers.Suspendable
import net.corda.contracts.djvm.NonDeterministicContract
import net.corda.core.contracts.Command
import net.corda.core.flows.FinalityFlow
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.Party
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.unwrap
@InitiatingFlow
@StartableByRPC
class NonDeterministicFlow(private val otherSide: Party) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
val notary = serviceHub.networkMapCache.notaryIdentities[0]
val stx = serviceHub.signInitialTransaction(
TransactionBuilder(notary)
.addOutputState(NonDeterministicContract.State())
.addCommand(Command(NonDeterministicContract.Cmd, ourIdentity.owningKey))
)
stx.verify(serviceHub, checkSufficientSignatures = false)
val session = initiateFlow(otherSide)
subFlow(FinalityFlow(stx, session))
// It's important we wait on this dummy receive, as otherwise it's possible we miss any errors the other side throws
session.receive<String>().unwrap { require(it == "OK") { "Not OK: $it"} }
}
}

@ -0,0 +1,31 @@
package net.corda.node
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.test.fail
class DeterministicSourcesRule : TestRule {
private var deterministicRt: Path? = null
private var deterministicSources: List<Path>? = null
val bootstrap: Path get() = deterministicRt ?: fail("deterministic-rt.path property not set")
val corda: List<Path> get() = deterministicSources ?: fail("deterministic-sources.path property not set")
override fun apply(statement: Statement, description: Description?): Statement {
deterministicRt = System.getProperty("deterministic-rt.path")?.run { Paths.get(this) }
deterministicSources = System.getProperty("deterministic-sources.path")?.split(File.pathSeparator)
?.map { Paths.get(it) }
?.filter { Files.exists(it) }
return object : Statement() {
override fun evaluate() {
statement.evaluate()
}
}
}
}

@ -0,0 +1,50 @@
package net.corda.node.services
import net.corda.core.messaging.startFlow
import net.corda.core.utilities.getOrThrow
import net.corda.flows.djvm.NonDeterministicFlow
import net.corda.node.DeterministicSourcesRule
import net.corda.node.internal.djvm.DeterministicVerificationException
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.singleIdentity
import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.driver
import net.corda.testing.driver.internal.incrementalPortAllocation
import net.corda.testing.node.NotarySpec
import net.corda.testing.node.internal.cordappsForPackages
import org.assertj.core.api.AssertionsForInterfaceTypes.assertThat
import org.junit.ClassRule
import org.junit.Test
import org.junit.jupiter.api.assertThrows
class DeterministicContractVerifyTest {
companion object {
@ClassRule
@JvmField
val djvmSources = DeterministicSourcesRule()
}
@Test
fun `test DJVM rejects non-deterministic contract`() {
driver(DriverParameters(
portAllocation = incrementalPortAllocation(),
startNodesInProcess = false,
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = true)),
cordappsForAllNodes = cordappsForPackages(
"net.corda.contracts.djvm",
"net.corda.flows.djvm"
),
djvmBootstrapSource = djvmSources.bootstrap,
djvmCordaSource = djvmSources.corda
)) {
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
val ex = assertThrows<DeterministicVerificationException> {
alice.rpc.startFlow(::NonDeterministicFlow, alice.nodeInfo.singleIdentity()).returnValue.getOrThrow()
}
assertThat(ex)
.hasMessageStartingWith("NoSuchMethodError: ")
.hasMessageContaining(" sandbox.java.time.Instant.now()")
}
}
}

@ -104,7 +104,7 @@ open class Node(configuration: NodeConfiguration,
flowManager: FlowManager = NodeFlowManager(configuration.flowOverrides),
cacheFactoryPrototype: BindableNamedCacheFactory = DefaultNamedCacheFactory(),
djvmBootstrapSource: ApiSource = createBootstrapSource(configuration),
djvmCordaSource: UserSource? = createDeterministicClasspath(configuration)
djvmCordaSource: UserSource? = createCordaSource(configuration)
) : AbstractNode<NodeInfo>(
configuration,
createClock(configuration),
@ -185,7 +185,7 @@ open class Node(configuration: NodeConfiguration,
private fun manifestValue(attrName: String): String? = if (Manifests.exists(attrName)) Manifests.read(attrName) else null
fun createDeterministicClasspath(config: NodeConfiguration): UserSource? {
private fun createManifestCordaSource(config: NodeConfiguration): UserSource? {
val classpathSource = config.baseDirectory.resolve("djvm")
val djvmClasspath = manifestValue(CORDA_DETERMINISTIC_CLASSPATH_ATTR)
@ -207,7 +207,7 @@ open class Node(configuration: NodeConfiguration,
}
}
fun createBootstrapSource(config: NodeConfiguration): ApiSource {
private fun createManifestBootstrapSource(config: NodeConfiguration): ApiSource {
val deterministicRt = manifestValue(CORDA_DETERMINISTIC_RUNTIME_ATTR)
if (deterministicRt == null) {
staticLog.warn("{} missing from MANIFEST.MF - will use host JVM for deterministic runtime.",
@ -224,6 +224,28 @@ open class Node(configuration: NodeConfiguration,
EmptyApi
}
}
private fun createBootstrapSource(config: NodeConfiguration): ApiSource {
val djvm = config.devModeOptions?.djvm
return if (config.devMode && djvm != null) {
djvm.bootstrapSource?.let { BootstrapClassLoader(Paths.get(it)) } ?: EmptyApi
} else {
createManifestBootstrapSource(config)
}
}
private fun createCordaSource(config: NodeConfiguration): UserSource? {
val djvm = config.devModeOptions?.djvm
return if (config.devMode && djvm != null) {
if (djvm.cordaSource.isEmpty()) {
null
} else {
UserPathSource(djvm.cordaSource.map { Paths.get(it) })
}
} else {
createManifestCordaSource(config)
}
}
}
override val log: Logger get() = staticLog

@ -95,5 +95,5 @@ class DeterministicVerifier(
}
}
class DeterministicVerificationException(id: SecureHash, message: String, cause: Throwable)
: TransactionVerificationException(id, message, cause)
class DeterministicVerificationException(txId: SecureHash, message: String, cause: Throwable)
: TransactionVerificationException(txId, message, cause)

@ -116,15 +116,22 @@ enum class JmxReporterType {
JOLOKIA, NEW_RELIC
}
data class DevModeOptions(val disableCheckpointChecker: Boolean = Defaults.disableCheckpointChecker, val allowCompatibilityZone: Boolean = Defaults.disableCheckpointChecker) {
data class DevModeOptions(
val disableCheckpointChecker: Boolean = Defaults.disableCheckpointChecker,
val allowCompatibilityZone: Boolean = Defaults.allowCompatibilityZone,
val djvm: DJVMOptions? = null
) {
internal object Defaults {
val disableCheckpointChecker = false
val allowCompatibilityZone = false
}
}
data class DJVMOptions(
val bootstrapSource: String?,
val cordaSource: List<String>
)
fun NodeConfiguration.shouldCheckCheckpoints(): Boolean {
return this.devMode && this.devModeOptions?.disableCheckpointChecker != true
}

@ -14,20 +14,7 @@ import net.corda.common.validation.internal.Validated.Companion.invalid
import net.corda.common.validation.internal.Validated.Companion.valid
import net.corda.core.context.AuthServiceId
import net.corda.core.internal.notary.NotaryServiceFlow
import net.corda.node.services.config.AuthDataSourceType
import net.corda.node.services.config.CertChainPolicyConfig
import net.corda.node.services.config.CertChainPolicyType
import net.corda.node.services.config.DevModeOptions
import net.corda.node.services.config.FlowOverride
import net.corda.node.services.config.FlowOverrideConfig
import net.corda.node.services.config.FlowTimeoutConfiguration
import net.corda.node.services.config.NetworkParameterAcceptanceSettings
import net.corda.node.services.config.NetworkServicesConfig
import net.corda.node.services.config.NodeH2Settings
import net.corda.node.services.config.NodeRpcSettings
import net.corda.node.services.config.NotaryConfig
import net.corda.node.services.config.PasswordEncryption
import net.corda.node.services.config.SecurityConfiguration
import net.corda.node.services.config.*
import net.corda.node.services.config.SecurityConfiguration.AuthService.Companion.defaultAuthServiceId
import net.corda.node.services.config.Valid
import net.corda.node.services.config.schema.parsers.attempt
@ -127,9 +114,19 @@ internal object SecurityConfigurationSpec : Configuration.Specification<Security
internal object DevModeOptionsSpec : Configuration.Specification<DevModeOptions>("DevModeOptions") {
private val disableCheckpointChecker by boolean().optional().withDefaultValue(DevModeOptions.Defaults.disableCheckpointChecker)
private val allowCompatibilityZone by boolean().optional().withDefaultValue(DevModeOptions.Defaults.allowCompatibilityZone)
private val djvm by nested(DJVMOptionsSpec).optional()
private object DJVMOptionsSpec : Configuration.Specification<DJVMOptions>("DJVMOptions") {
private val bootstrapSource by string().optional()
private val cordaSource by string().list()
override fun parseValid(configuration: Config): Valid<DJVMOptions> {
return valid(DJVMOptions(configuration[bootstrapSource], configuration[cordaSource]))
}
}
override fun parseValid(configuration: Config): Valid<DevModeOptions> {
return valid(DevModeOptions(configuration[disableCheckpointChecker], configuration[allowCompatibilityZone]))
return valid(DevModeOptions(configuration[disableCheckpointChecker], configuration[allowCompatibilityZone], configuration[djvm]))
}
}

@ -200,7 +200,9 @@ fun <A> driver(defaultParameters: DriverParameters = DriverParameters(), dsl: Dr
notaryCustomOverrides = defaultParameters.notaryCustomOverrides,
inMemoryDB = defaultParameters.inMemoryDB,
cordappsForAllNodes = uncheckedCast(defaultParameters.cordappsForAllNodes),
environmentVariables = defaultParameters.environmentVariables
environmentVariables = defaultParameters.environmentVariables,
djvmBootstrapSource = defaultParameters.djvmBootstrapSource,
djvmCordaSource = defaultParameters.djvmCordaSource
),
coerce = { it },
dsl = dsl
@ -257,7 +259,9 @@ data class DriverParameters(
val notaryCustomOverrides: Map<String, Any?> = emptyMap(),
val inMemoryDB: Boolean = true,
val cordappsForAllNodes: Collection<TestCordapp>? = null,
val environmentVariables : Map<String, String> = emptyMap()
val environmentVariables : Map<String, String> = emptyMap(),
val djvmBootstrapSource: Path? = null,
val djvmCordaSource: List<Path> = emptyList()
) {
constructor(cordappsForAllNodes: Collection<TestCordapp>) : this(isDebug = false, cordappsForAllNodes = cordappsForAllNodes)

@ -2,10 +2,7 @@ package net.corda.testing.node.internal
import co.paralleluniverse.fibers.instrument.JavaAgent
import com.google.common.util.concurrent.ThreadFactoryBuilder
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigRenderOptions
import com.typesafe.config.ConfigValueFactory
import com.typesafe.config.*
import net.corda.client.rpc.CordaRPCClient
import net.corda.cliutils.CommonCliConstants.BASE_DIR
import net.corda.core.concurrent.CordaFuture
@ -92,7 +89,9 @@ class DriverDSLImpl(
val notaryCustomOverrides: Map<String, Any?>,
val inMemoryDB: Boolean,
val cordappsForAllNodes: Collection<TestCordappInternal>?,
val environmentVariables : Map<String, String>
val environmentVariables : Map<String, String>,
val djvmBootstrapSource: Path?,
val djvmCordaSource: List<Path>
) : InternalDriverDSL {
private var _executorService: ScheduledExecutorService? = null
@ -129,7 +128,7 @@ class DriverDSLImpl(
if (inMemoryDB && corda.dataSourceProperties.getProperty("dataSource.url").startsWith("jdbc:h2:")) {
val jdbcUrl = "jdbc:h2:mem:persistence${inMemoryCounter.getAndIncrement()};DB_CLOSE_ON_EXIT=FALSE;LOCK_TIMEOUT=10000;WRITE_DELAY=100"
corda.dataSourceProperties.setProperty("dataSource.url", jdbcUrl)
NodeConfig(typesafe = typesafe + mapOf("dataSourceProperties" to mapOf("dataSource.url" to jdbcUrl)))
NodeConfig(typesafe + mapOf("dataSourceProperties" to mapOf("dataSource.url" to jdbcUrl)))
} else {
this
}
@ -236,11 +235,13 @@ class DriverDSLImpl(
NodeConfiguration::flowOverrides.name to flowOverrideConfig.toConfig().root().unwrapped(),
NodeConfiguration::additionalNodeInfoPollingFrequencyMsec.name to 1000
) + czUrlConfig + jmxConfig + parameters.customOverrides
val config = NodeConfig(ConfigHelper.loadConfig(
val config = NodeConfig(
ConfigHelper.loadConfig(
baseDirectory = baseDirectory(name),
allowMissingConfig = true,
configOverrides = if (overrides.hasPath("devMode")) overrides else overrides + mapOf("devMode" to true)
)).checkAndOverrideForInMemoryDB()
).withDJVMConfig(djvmBootstrapSource, djvmCordaSource)
).checkAndOverrideForInMemoryDB()
return startNodeInternal(config, webAddress, localNetworkMap, parameters)
}
@ -261,11 +262,13 @@ class DriverDSLImpl(
),
"additionalNodeInfoPollingFrequencyMsec" to 1000,
"devMode" to false) + customOverrides
val config = NodeConfig(ConfigHelper.loadConfig(
val config = NodeConfig(
ConfigHelper.loadConfig(
baseDirectory = baseDirectory,
allowMissingConfig = true,
configOverrides = overrides
)).checkAndOverrideForInMemoryDB()
).withDJVMConfig(djvmBootstrapSource, djvmCordaSource)
).checkAndOverrideForInMemoryDB()
val versionInfo = VersionInfo(PLATFORM_VERSION, "1", "1", "1")
config.corda.certificatesDirectory.createDirectories()
@ -711,6 +714,25 @@ class DriverDSLImpl(
Permissions.invokeRpc(CordaRPCOps::killFlow)
)
/**
* Add the DJVM's sources to the node's configuration file.
* These will all be ignored unless devMode is also true.
*/
private fun Config.withDJVMConfig(bootstrapSource: Path?, cordaSource: List<Path>): Config {
return withOptionalValue("devModeOptions.djvm.bootstrapSource", bootstrapSource) { path -> valueFor(path.toString()) }
.withValue("devModeOptions.djvm.cordaSource", valueFor(cordaSource.map(Path::toString)))
}
private inline fun <T> Config.withOptionalValue(key: String, obj: T?, body: (T) -> ConfigValue): Config {
return if (obj == null) {
this
} else {
withValue(key, body(obj))
}
}
private fun <T> valueFor(any: T): ConfigValue = ConfigValueFactory.fromAnyRef(any)
private fun <A> oneOf(array: Array<A>) = array[Random().nextInt(array.size)]
private fun startInProcessNode(
@ -767,16 +789,17 @@ class DriverDSLImpl(
systemProperties += overriddenSystemProperties
// See experimental/quasar-hook/README.md for how to generate.
val excludePattern = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;" +
val excludePackagePattern = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;" +
"com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;" +
"com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;" +
"io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;" +
"io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.corda.djvm**;djvm.**;net.bytebuddy**;net.i2p**;org.apache**;" +
"org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;" +
"org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;" +
"org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**;org.jolokia**;" +
"com.lmax**;picocli**;liquibase**;com.github.benmanes**;org.json**;org.postgresql**;nonapi.io.github.classgraph**;)"
val excludeClassloaderPattern = "l(net.corda.djvm.**)"
val extraJvmArguments = systemProperties.removeResolvedClasspath().map { "-D${it.key}=${it.value}" } +
"-javaagent:$quasarJarPath=$excludePattern"
"-javaagent:$quasarJarPath=$excludePackagePattern$excludeClassloaderPattern"
val loggingLevel = when {
logLevelOverride != null -> logLevelOverride
@ -1030,7 +1053,9 @@ fun <DI : DriverDSL, D : InternalDriverDSL, A> genericDriver(
notaryCustomOverrides = defaultParameters.notaryCustomOverrides,
inMemoryDB = defaultParameters.inMemoryDB,
cordappsForAllNodes = uncheckedCast(defaultParameters.cordappsForAllNodes),
environmentVariables = defaultParameters.environmentVariables
environmentVariables = defaultParameters.environmentVariables,
djvmBootstrapSource = defaultParameters.djvmBootstrapSource,
djvmCordaSource = defaultParameters.djvmCordaSource
)
)
val shutdownHook = addShutdownHook(driverDsl::shutdown)
@ -1126,6 +1151,8 @@ fun <A> internalDriver(
inMemoryDB: Boolean = DriverParameters().inMemoryDB,
cordappsForAllNodes: Collection<TestCordappInternal>? = null,
environmentVariables: Map<String, String> = emptyMap(),
djvmBootstrapSource: Path? = null,
djvmCordaSource: List<Path> = emptyList(),
dsl: DriverDSLImpl.() -> A
): A {
return genericDriver(
@ -1146,7 +1173,9 @@ fun <A> internalDriver(
notaryCustomOverrides = notaryCustomOverrides,
inMemoryDB = inMemoryDB,
cordappsForAllNodes = cordappsForAllNodes,
environmentVariables = environmentVariables
environmentVariables = environmentVariables,
djvmBootstrapSource = djvmBootstrapSource,
djvmCordaSource = djvmCordaSource
),
coerce = { it },
dsl = dsl

@ -123,6 +123,8 @@ fun <A> rpcDriver(
inMemoryDB: Boolean = true,
cordappsForAllNodes: Collection<TestCordappInternal>? = null,
environmentVariables: Map<String, String> = emptyMap(),
djvmBootstrapSource: Path? = null,
djvmCordaSource: List<Path> = emptyList(),
dsl: RPCDriverDSL.() -> A
): A {
return genericDriver(
@ -144,7 +146,9 @@ fun <A> rpcDriver(
notaryCustomOverrides = notaryCustomOverrides,
inMemoryDB = inMemoryDB,
cordappsForAllNodes = cordappsForAllNodes,
environmentVariables = environmentVariables
environmentVariables = environmentVariables,
djvmBootstrapSource = djvmBootstrapSource,
djvmCordaSource = djvmCordaSource
), externalTrace
),
coerce = { it },