Merge pull request #1505 from corda/anthony-os-merge-2018-10-24

O/S Merge 24/10/2018
This commit is contained in:
Anthony Keenan 2018-10-28 21:43:52 +00:00 committed by GitHub
commit 2ef1f1c7c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
130 changed files with 2802 additions and 1382 deletions

View File

@ -180,7 +180,7 @@ see changes to this list.
* Scott James
* Sean Zhang (Wells Fargo)
* Shams Asari (R3)
* Shivan Sawant (Persistent Systems Limited)
* Shivan Sawant
* Siddhartha Sengupta (Tradewind Markets)
* Simon Taylor (Barclays)
* Sofus Mortensen (Digital Asset Holdings)

View File

@ -77,7 +77,7 @@ buildscript {
ext.snappy_version = '0.4'
ext.class_graph_version = '4.2.12'
ext.jcabi_manifests_version = '1.1'
ext.picocli_version = '3.5.2'
ext.picocli_version = '3.6.1'
// Name of the IntelliJ SDK created for the deterministic Java rt.jar.
// ext.deterministic_idea_sdk = '1.8 (Deterministic)'

View File

@ -1,4 +1,4 @@
gradlePluginsVersion=4.0.32
gradlePluginsVersion=4.0.33
kotlinVersion=1.2.71
# ***************************************************************#
# When incrementing platformVersion make sure to update #

View File

@ -1,7 +1,6 @@
package net.corda.core.contracts
import net.corda.core.KeepForDJVM
import net.corda.core.identity.Party
import net.corda.core.internal.extractFile
import net.corda.core.serialization.CordaSerializable
import java.io.FileNotFoundException

View File

@ -28,6 +28,14 @@ object JarSignatureCollector {
fun collectSigningParties(jar: JarInputStream): List<Party> = getSigners(jar).toPartiesOrderedByName()
/**
* Returns an ordered list of every [X509Certificate] which has signed every signable item in the given [JarInputStream].
*
* @param jar The open [JarInputStream] to collect signing parties from.
* @throws InvalidJarSignersException If the signer sets for any two signable items are different from each other.
*/
fun collectCertificates(jar: JarInputStream): List<X509Certificate> = getSigners(jar).toCertificates()
private fun getSigners(jar: JarInputStream): Set<CodeSigner> {
val signerSets = jar.fileSignerSets
if (signerSets.isEmpty()) return emptySet()
@ -71,6 +79,10 @@ object JarSignatureCollector {
Party(it.signerCertPath.certificates[0] as X509Certificate)
}.sortedBy { it.name.toString() } // Sorted for determinism.
private fun Set<CodeSigner>.toCertificates(): List<X509Certificate> = map {
it.signerCertPath.certificates[0] as X509Certificate
}.sortedBy { it.toString() } // Sorted for determinism.
private val JarInputStream.entries get(): Sequence<JarEntry> = generateSequence(nextJarEntry) { nextJarEntry }
}

View File

@ -43,7 +43,7 @@ data class CordappImpl(
*/
override val cordappClasses: List<String> = run {
val classList = rpcFlows + initiatedFlows + services + serializationWhitelists.map { javaClass } + notaryService
classList.mapNotNull { it?.name } + contractClassNames
classList.mapNotNull { it?.name } + contractClassNames
}
// TODO Why a seperate Info class and not just have the fields directly in CordappImpl?

View File

@ -4,7 +4,11 @@ import co.paralleluniverse.strands.Strand
import net.corda.core.CordaInternal
import net.corda.core.DeleteForDJVM
import net.corda.core.contracts.*
import net.corda.core.crypto.*
import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SignableData
import net.corda.core.crypto.SignatureMetadata
import net.corda.core.crypto.keys
import net.corda.core.identity.Party
import net.corda.core.internal.AttachmentWithContext
import net.corda.core.internal.FlowStateMachine
@ -24,6 +28,38 @@ import java.time.Duration
import java.time.Instant
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.Collection
import kotlin.collections.List
import kotlin.collections.Map
import kotlin.collections.MutableList
import kotlin.collections.Set
import kotlin.collections.all
import kotlin.collections.any
import kotlin.collections.arrayListOf
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.emptyList
import kotlin.collections.filter
import kotlin.collections.filterNot
import kotlin.collections.find
import kotlin.collections.flatMap
import kotlin.collections.flatten
import kotlin.collections.forEach
import kotlin.collections.groupBy
import kotlin.collections.intersect
import kotlin.collections.isNotEmpty
import kotlin.collections.listOf
import kotlin.collections.map
import kotlin.collections.mapNotNull
import kotlin.collections.none
import kotlin.collections.plus
import kotlin.collections.setOf
import kotlin.collections.single
import kotlin.collections.singleOrNull
import kotlin.collections.toList
import kotlin.collections.toMap
import kotlin.collections.toSet
import kotlin.collections.toSortedSet
/**
* A TransactionBuilder is a transaction class that's mutable (unlike the others which are all immutable). It is

View File

@ -1,10 +1,13 @@
package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.concurrent.CordaFuture
import net.corda.core.toFuture
import net.corda.core.utilities.UntrustworthyData
import net.corda.core.utilities.unwrap
import net.corda.node.internal.InitiatedFlowFactory
import net.corda.testing.node.internal.TestStartedNode
import rx.Observable
import kotlin.reflect.KClass
/**
@ -34,20 +37,6 @@ class NoAnswer(private val closure: () -> Unit = {}) : FlowLogic<Unit>() {
override fun call() = closure()
}
/**
* Allows to register a flow of type [R] against an initiating flow of type [I].
*/
inline fun <I : FlowLogic<*>, reified R : FlowLogic<*>> TestStartedNode.registerInitiatedFlow(initiatingFlowType: KClass<I>, crossinline construct: (session: FlowSession) -> R) {
registerFlowFactory(initiatingFlowType.java, InitiatedFlowFactory.Core { session -> construct(session) }, R::class.javaObjectType, true)
}
/**
* Allows to register a flow of type [Answer] against an initiating flow of type [I], returning a valure of type [R].
*/
inline fun <I : FlowLogic<*>, reified R : Any> TestStartedNode.registerAnswer(initiatingFlowType: KClass<I>, value: R) {
registerFlowFactory(initiatingFlowType.java, InitiatedFlowFactory.Core { session -> Answer(session, value) }, Answer::class.javaObjectType, true)
}
/**
* Extracts data from a [Map[FlowSession, UntrustworthyData<Any>]] without performing checks and casting to [R].
*/
@ -112,4 +101,23 @@ inline fun <reified R : Any> FlowLogic<*>.receiveAll(session: FlowSession, varar
private fun Array<out Pair<FlowSession, Class<out Any>>>.enforceNoDuplicates() {
require(this.size == this.toSet().size) { "A flow session can only appear once as argument." }
}
inline fun <reified P : FlowLogic<*>> TestStartedNode.registerCordappFlowFactory(
initiatingFlowClass: KClass<out FlowLogic<*>>,
initiatedFlowVersion: Int = 1,
noinline flowFactory: (FlowSession) -> P): CordaFuture<P> {
val observable = internals.registerInitiatedFlowFactory(
initiatingFlowClass.java,
P::class.java,
InitiatedFlowFactory.CorDapp(initiatedFlowVersion, "", flowFactory),
track = true)
return observable.toFuture()
}
fun <T : FlowLogic<*>> TestStartedNode.registerCoreFlowFactory(initiatingFlowClass: Class<out FlowLogic<*>>,
initiatedFlowClass: Class<T>,
flowFactory: (FlowSession) -> T , track: Boolean): Observable<T> {
return this.internals.registerInitiatedFlowFactory(initiatingFlowClass, initiatedFlowClass, InitiatedFlowFactory.Core(flowFactory), track)
}

View File

@ -2,16 +2,18 @@ package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable
import com.natpryce.hamkrest.assertion.assert
import net.corda.testing.internal.matchers.flow.willReturn
import net.corda.core.flows.mixins.WithMockNet
import net.corda.core.identity.Party
import net.corda.core.utilities.UntrustworthyData
import net.corda.core.utilities.unwrap
import net.corda.testing.core.singleIdentity
import net.corda.testing.internal.matchers.flow.willReturn
import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.TestStartedNode
import org.assertj.core.api.Assertions.assertThat
import org.junit.AfterClass
import org.junit.Test
import kotlin.reflect.KClass
class ReceiveMultipleFlowTests : WithMockNet {
@ -43,7 +45,7 @@ class ReceiveMultipleFlowTests : WithMockNet {
}
}
nodes[1].registerInitiatedFlow(initiatingFlow::class) { session ->
nodes[1].registerCordappFlowFactory(initiatingFlow::class) { session ->
object : FlowLogic<Unit>() {
@Suspendable
override fun call() {
@ -123,4 +125,15 @@ class ReceiveMultipleFlowTests : WithMockNet {
return double * string.length
}
}
}
private inline fun <reified T> TestStartedNode.registerAnswer(kClass: KClass<out FlowLogic<Any>>, value1: T) {
this.registerCordappFlowFactory(kClass) { session ->
object : FlowLogic<Unit>() {
@Suspendable
override fun call() {
session.send(value1!!)
}
}
}
}

View File

@ -3,16 +3,12 @@ package net.corda.core.serialization
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.Attachment
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSession
import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.TestNoSecurityDataVendingFlow
import net.corda.core.flows.*
import net.corda.core.identity.Party
import net.corda.core.internal.FetchAttachmentsFlow
import net.corda.core.internal.FetchDataFlow
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.unwrap
import net.corda.node.internal.InitiatedFlowFactory
import net.corda.node.services.persistence.NodeAttachmentService
import net.corda.nodeapi.internal.persistence.currentDBSession
import net.corda.testing.core.ALICE_NAME
@ -151,11 +147,10 @@ class AttachmentSerializationTest {
}
private fun launchFlow(clientLogic: ClientLogic, rounds: Int, sendData: Boolean = false) {
server.registerFlowFactory(
ClientLogic::class.java,
InitiatedFlowFactory.Core { ServerLogic(it, sendData) },
ServerLogic::class.java,
track = false)
server.registerCordappFlowFactory(
ClientLogic::class,
1
) { ServerLogic(it, sendData) }
client.services.startFlow(clientLogic)
mockNet.runNetwork(rounds)
}

View File

@ -57,7 +57,8 @@ shadowJar {
// we will generate better versions from deterministic-rt.jar.
exclude 'sandbox/java/lang/Appendable.class'
exclude 'sandbox/java/lang/CharSequence.class'
exclude 'sandbox/java/lang/Character\$*.class'
exclude 'sandbox/java/lang/Character\$Subset.class'
exclude 'sandbox/java/lang/Character\$Unicode*.class'
exclude 'sandbox/java/lang/Comparable.class'
exclude 'sandbox/java/lang/Enum.class'
exclude 'sandbox/java/lang/Iterable.class'

View File

@ -191,12 +191,14 @@ abstract class ClassCommand : CommandBase() {
emitters = ignoreEmitters.emptyListIfTrueOtherwiseNull(),
definitionProviders = if (ignoreDefinitionProviders) { emptyList() } else { Discovery.find() },
enableTracing = !disableTracing,
analysisConfiguration = AnalysisConfiguration(
analysisConfiguration = AnalysisConfiguration.createRoot(
whitelist = whitelist,
minimumSeverityLevel = level,
classPath = getClasspath(),
analyzeAnnotations = analyzeAnnotations,
prefixFilters = prefixFilters.toList()
prefixFilters = prefixFilters.toList(),
sourceClassLoaderFactory = { classResolver, bootstrapClassLoader ->
SourceClassLoader(getClasspath(), classResolver, bootstrapClassLoader)
}
)
)
}

View File

@ -33,7 +33,7 @@ class WhitelistGenerateCommand : CommandBase() {
override fun validateArguments() = paths.isNotEmpty()
override fun handleCommand(): Boolean {
val entries = AnalysisConfiguration().use { configuration ->
val entries = AnalysisConfiguration.createRoot().use { configuration ->
val entries = mutableListOf<String>()
val visitor = object : ClassAndMemberVisitor(configuration, null) {
override fun visitClass(clazz: ClassRepresentation): ClassRepresentation {

View File

@ -45,8 +45,8 @@ public class Object {
private static java.lang.Object unwrap(java.lang.Object arg) {
if (arg instanceof Object) {
return ((Object) arg).fromDJVM();
} else if (Object[].class.isAssignableFrom(arg.getClass())) {
return fromDJVM((Object[]) arg);
} else if (java.lang.Object[].class.isAssignableFrom(arg.getClass())) {
return fromDJVM((java.lang.Object[]) arg);
} else {
return arg;
}

View File

@ -1,5 +1,6 @@
package sandbox.java.lang;
import net.corda.djvm.SandboxRuntimeContext;
import org.jetbrains.annotations.NotNull;
import sandbox.java.nio.charset.Charset;
import sandbox.java.util.Comparator;
@ -8,7 +9,6 @@ import sandbox.java.util.Locale;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Constructor;
import java.util.Map;
@SuppressWarnings("unused")
public final class String extends Object implements Comparable<String>, CharSequence, Serializable {
@ -24,7 +24,6 @@ public final class String extends Object implements Comparable<String>, CharSequ
private static final String TRUE = new String("true");
private static final String FALSE = new String("false");
private static final Map<java.lang.String, String> INTERNAL = new java.util.HashMap<>();
private static final Constructor SHARED;
static {
@ -335,7 +334,7 @@ public final class String extends Object implements Comparable<String>, CharSequ
return toDJVM(value.trim());
}
public String intern() { return INTERNAL.computeIfAbsent(value, s -> this); }
public String intern() { return (String) SandboxRuntimeContext.getInstance().intern(value, this); }
public char[] toCharArray() {
return value.toCharArray();

View File

@ -1,20 +1,15 @@
package sandbox.java.lang;
import net.corda.djvm.SandboxRuntimeContext;
@SuppressWarnings({"WeakerAccess", "unused"})
public final class System extends Object {
private System() {}
/*
* This class is duplicated into every sandbox, where everything is single-threaded.
*/
private static final java.util.Map<java.lang.Integer, java.lang.Integer> objectHashCodes = new java.util.LinkedHashMap<>();
private static int objectCounter = 0;
public static int identityHashCode(java.lang.Object obj) {
int nativeHashCode = java.lang.System.identityHashCode(obj);
// TODO Instead of using a magic offset below, one could take in a per-context seed
return objectHashCodes.computeIfAbsent(nativeHashCode, i -> ++objectCounter + 0xfed_c0de);
return SandboxRuntimeContext.getInstance().getHashCodeFor(nativeHashCode);
}
public static final String lineSeparator = String.toDJVM("\n");

View File

@ -5,6 +5,7 @@ import net.corda.djvm.code.DefinitionProvider
import net.corda.djvm.code.EMIT_TRACING
import net.corda.djvm.code.Emitter
import net.corda.djvm.execution.ExecutionProfile
import net.corda.djvm.rewiring.SandboxClassLoader
import net.corda.djvm.rules.Rule
import net.corda.djvm.utilities.Discovery
@ -16,24 +17,28 @@ import net.corda.djvm.utilities.Discovery
* @property definitionProviders The meta-data providers to apply to class and member definitions.
* @property executionProfile The execution profile to use in the sandbox.
* @property analysisConfiguration The configuration used in the analysis of classes.
* @property parentClassLoader The [SandboxClassLoader] that this sandbox will use as a parent.
*/
@Suppress("unused")
class SandboxConfiguration private constructor(
val rules: List<Rule>,
val emitters: List<Emitter>,
val definitionProviders: List<DefinitionProvider>,
val executionProfile: ExecutionProfile,
val analysisConfiguration: AnalysisConfiguration
val analysisConfiguration: AnalysisConfiguration,
val parentClassLoader: SandboxClassLoader?
) {
@Suppress("unused")
companion object {
/**
* Default configuration for the deterministic sandbox.
*/
@JvmField
val DEFAULT = SandboxConfiguration.of()
/**
* Configuration with no emitters, rules, meta-data providers or runtime thresholds.
*/
@JvmField
val EMPTY = SandboxConfiguration.of(
ExecutionProfile.UNLIMITED, emptyList(), emptyList(), emptyList()
)
@ -47,7 +52,8 @@ class SandboxConfiguration private constructor(
emitters: List<Emitter>? = null,
definitionProviders: List<DefinitionProvider> = Discovery.find(),
enableTracing: Boolean = true,
analysisConfiguration: AnalysisConfiguration = AnalysisConfiguration()
analysisConfiguration: AnalysisConfiguration = AnalysisConfiguration.createRoot(),
parentClassLoader: SandboxClassLoader? = null
) = SandboxConfiguration(
executionProfile = profile,
rules = rules,
@ -55,7 +61,8 @@ class SandboxConfiguration private constructor(
enableTracing || it.priority > EMIT_TRACING
},
definitionProviders = definitionProviders,
analysisConfiguration = analysisConfiguration
analysisConfiguration = analysisConfiguration,
parentClassLoader = parentClassLoader
)
}
}

View File

@ -1,6 +1,5 @@
package net.corda.djvm
import net.corda.djvm.analysis.AnalysisContext
import net.corda.djvm.costing.RuntimeCostSummary
import net.corda.djvm.rewiring.SandboxClassLoader
@ -14,16 +13,27 @@ class SandboxRuntimeContext(val configuration: SandboxConfiguration) {
/**
* The class loader to use inside the sandbox.
*/
val classLoader: SandboxClassLoader = SandboxClassLoader(
configuration,
AnalysisContext.fromConfiguration(configuration.analysisConfiguration)
)
val classLoader: SandboxClassLoader = SandboxClassLoader.createFor(configuration)
/**
* A summary of the currently accumulated runtime costs (for, e.g., memory allocations, invocations, etc.).
*/
val runtimeCosts = RuntimeCostSummary(configuration.executionProfile)
private val hashCodes: MutableMap<Int, Int> = mutableMapOf()
private var objectCounter: Int = 0
// TODO Instead of using a magic offset below, one could take in a per-context seed
fun getHashCodeFor(nativeHashCode: Int): Int {
return hashCodes.computeIfAbsent(nativeHashCode) { ++objectCounter + MAGIC_HASH_OFFSET }
}
private val internStrings: MutableMap<String, Any> = mutableMapOf()
fun intern(key: String, value: Any): Any {
return internStrings.computeIfAbsent(key) { value }
}
/**
* Run a set of actions within the provided sandbox context.
*/
@ -39,10 +49,12 @@ class SandboxRuntimeContext(val configuration: SandboxConfiguration) {
companion object {
private val threadLocalContext = ThreadLocal<SandboxRuntimeContext?>()
private const val MAGIC_HASH_OFFSET = 0xfed_c0de
/**
* When called from within a sandbox, this returns the context for the current sandbox thread.
*/
@JvmStatic
var instance: SandboxRuntimeContext
get() = threadLocalContext.get()
?: throw IllegalStateException("SandboxContext has not been initialized before use")

View File

@ -8,6 +8,7 @@ import net.corda.djvm.references.ClassModule
import net.corda.djvm.references.Member
import net.corda.djvm.references.MemberModule
import net.corda.djvm.references.MethodBody
import net.corda.djvm.source.AbstractSourceClassLoader
import net.corda.djvm.source.BootstrapClassLoader
import net.corda.djvm.source.SourceClassLoader
import org.objectweb.asm.Opcodes.*
@ -21,36 +22,36 @@ import java.nio.file.Path
* The configuration to use for an analysis.
*
* @property whitelist The whitelist of class names.
* @param additionalPinnedClasses Classes that have already been declared in the sandbox namespace and that should be
* made available inside the sandboxed environment.
* @property pinnedClasses Classes that have already been declared in the sandbox namespace and that should be
* made available inside the sandboxed environment. These classes belong to the application
* classloader and so are shared across all sandboxes.
* @property classResolver Functionality used to resolve the qualified name and relevant information about a class.
* @property exceptionResolver Resolves the internal names of synthetic exception classes.
* @property minimumSeverityLevel The minimum severity level to log and report.
* @param classPath The extended class path to use for the analysis.
* @param bootstrapJar The location of a jar containing the Java APIs.
* @property analyzeAnnotations Analyze annotations despite not being explicitly referenced.
* @property prefixFilters Only record messages where the originating class name matches one of the provided prefixes.
* If none are provided, all messages will be reported.
* @property classModule Module for handling evolution of a class hierarchy during analysis.
* @property memberModule Module for handling the specification and inspection of class members.
* @property bootstrapClassLoader Optional provider for the Java API classes.
* @property supportingClassLoader ClassLoader providing the classes to run inside the sandbox.
* @property isRootConfiguration Effectively, whether we are allowed to close [bootstrapClassLoader].
*/
class AnalysisConfiguration(
val whitelist: Whitelist = Whitelist.MINIMAL,
additionalPinnedClasses: Set<String> = emptySet(),
val minimumSeverityLevel: Severity = Severity.WARNING,
classPath: List<Path> = emptyList(),
bootstrapJar: Path? = null,
val analyzeAnnotations: Boolean = false,
val prefixFilters: List<String> = emptyList(),
val classModule: ClassModule = ClassModule(),
val memberModule: MemberModule = MemberModule()
class AnalysisConfiguration private constructor(
val whitelist: Whitelist,
val pinnedClasses: Set<String>,
val classResolver: ClassResolver,
val exceptionResolver: ExceptionResolver,
val minimumSeverityLevel: Severity,
val analyzeAnnotations: Boolean,
val prefixFilters: List<String>,
val classModule: ClassModule,
val memberModule: MemberModule,
private val bootstrapClassLoader: BootstrapClassLoader?,
val supportingClassLoader: AbstractSourceClassLoader,
private val isRootConfiguration: Boolean
) : Closeable {
/**
* Classes that have already been declared in the sandbox namespace and that should be made
* available inside the sandboxed environment. These classes belong to the application
* classloader and so are shared across all sandboxes.
*/
val pinnedClasses: Set<String> = MANDATORY_PINNED_CLASSES + additionalPinnedClasses
/**
* These interfaces are modified as they are mapped into the sandbox by
* having their unsandboxed version "stitched in" as a super-interface.
@ -63,26 +64,39 @@ class AnalysisConfiguration(
*/
val stitchedClasses: Map<String, List<Member>> get() = STITCHED_CLASSES
/**
* Functionality used to resolve the qualified name and relevant information about a class.
*/
val classResolver: ClassResolver = ClassResolver(pinnedClasses, TEMPLATE_CLASSES, whitelist, SANDBOX_PREFIX)
/**
* Resolves the internal names of synthetic exception classes.
*/
val exceptionResolver: ExceptionResolver = ExceptionResolver(JVM_EXCEPTIONS, pinnedClasses, SANDBOX_PREFIX)
private val bootstrapClassLoader = bootstrapJar?.let { BootstrapClassLoader(it, classResolver) }
val supportingClassLoader = SourceClassLoader(classPath, classResolver, bootstrapClassLoader)
@Throws(IOException::class)
override fun close() {
supportingClassLoader.use {
bootstrapClassLoader?.close()
if (isRootConfiguration) {
bootstrapClassLoader?.close()
}
}
}
/**
* Creates a child [AnalysisConfiguration] with this instance as its parent.
* The child inherits the same [whitelist], [pinnedClasses] and [bootstrapClassLoader].
*/
fun createChild(
classPaths: List<Path> = emptyList(),
newMinimumSeverityLevel: Severity?
): AnalysisConfiguration {
return AnalysisConfiguration(
whitelist = whitelist,
pinnedClasses = pinnedClasses,
classResolver = classResolver,
exceptionResolver = exceptionResolver,
minimumSeverityLevel = newMinimumSeverityLevel ?: minimumSeverityLevel,
analyzeAnnotations = analyzeAnnotations,
prefixFilters = prefixFilters,
classModule = classModule,
memberModule = memberModule,
bootstrapClassLoader = bootstrapClassLoader,
supportingClassLoader = SourceClassLoader(classPaths, classResolver, bootstrapClassLoader),
isRootConfiguration = false
)
}
fun isTemplateClass(className: String): Boolean = className in TEMPLATE_CLASSES
fun isPinnedClass(className: String): Boolean = className in pinnedClasses
@ -107,7 +121,7 @@ class AnalysisConfiguration(
/**
* These classes will be duplicated into every sandbox's
* classloader.
* parent classloader.
*/
private val TEMPLATE_CLASSES: Set<String> = setOf(
java.lang.Boolean::class.java,
@ -131,6 +145,7 @@ class AnalysisConfiguration(
).sandboxed() + setOf(
"sandbox/Task",
"sandbox/TaskTypes",
"sandbox/java/lang/Character\$Cache",
"sandbox/java/lang/DJVM",
"sandbox/java/lang/DJVMException",
"sandbox/java/lang/DJVMThrowableWrapper",
@ -139,8 +154,8 @@ class AnalysisConfiguration(
)
/**
* These are thrown by the JVM itself, and so
* we need to handle them without wrapping them.
* These exceptions are thrown by the JVM itself, and
* so we need to handle them without wrapping them.
*
* Note that this set is closed, i.e. every one
* of these exceptions' [Throwable] super classes
@ -271,6 +286,41 @@ class AnalysisConfiguration(
private fun Set<Class<*>>.sandboxed(): Set<String> = map(Companion::sandboxed).toSet()
private fun Iterable<Member>.mapByClassName(): Map<String, List<Member>>
= groupBy(Member::className).mapValues(Map.Entry<String, List<Member>>::value)
/**
* @see [AnalysisConfiguration]
*/
fun createRoot(
whitelist: Whitelist = Whitelist.MINIMAL,
additionalPinnedClasses: Set<String> = emptySet(),
minimumSeverityLevel: Severity = Severity.WARNING,
analyzeAnnotations: Boolean = false,
prefixFilters: List<String> = emptyList(),
classModule: ClassModule = ClassModule(),
memberModule: MemberModule = MemberModule(),
bootstrapClassLoader: BootstrapClassLoader? = null,
sourceClassLoaderFactory: (ClassResolver, BootstrapClassLoader?) -> AbstractSourceClassLoader = { classResolver, bootstrapCL ->
SourceClassLoader(emptyList(), classResolver, bootstrapCL)
}
): AnalysisConfiguration {
val pinnedClasses = MANDATORY_PINNED_CLASSES + additionalPinnedClasses
val classResolver = ClassResolver(pinnedClasses, TEMPLATE_CLASSES, whitelist, SANDBOX_PREFIX)
return AnalysisConfiguration(
whitelist = whitelist,
pinnedClasses = pinnedClasses,
classResolver = classResolver,
exceptionResolver = ExceptionResolver(JVM_EXCEPTIONS, pinnedClasses, SANDBOX_PREFIX),
minimumSeverityLevel = minimumSeverityLevel,
analyzeAnnotations = analyzeAnnotations,
prefixFilters = prefixFilters,
classModule = classModule,
memberModule = memberModule,
bootstrapClassLoader = bootstrapClassLoader,
supportingClassLoader = sourceClassLoaderFactory(classResolver, bootstrapClassLoader),
isRootConfiguration = true
)
}
}
private open class MethodBuilder(

View File

@ -71,14 +71,14 @@ open class SandboxExecutor<in TInput, out TOutput>(
// Load the "entry-point" task class into the sandbox. This task will marshall
// the input and outputs between Java types and sandbox wrapper types.
val taskClass = Class.forName("sandbox.Task", false, classLoader)
val taskClass = classLoader.loadClass("sandbox.Task")
// Create the user's task object inside the sandbox.
val runnable = classLoader.loadForSandbox(runnableClass, context).type.newInstance()
val runnable = classLoader.loadClassForSandbox(runnableClass).newInstance()
// Fetch this sandbox's instance of Class<Function> so we can retrieve Task(Function)
// and then instantiate the Task.
val functionClass = Class.forName("sandbox.java.util.function.Function", false, classLoader)
val functionClass = classLoader.loadClass("sandbox.java.util.function.Function")
val task = taskClass.getDeclaredConstructor(functionClass).newInstance(runnable)
// Execute the task...
@ -114,7 +114,7 @@ open class SandboxExecutor<in TInput, out TOutput>(
fun load(classSource: ClassSource): LoadedClass {
val context = AnalysisContext.fromConfiguration(configuration.analysisConfiguration)
val result = IsolatedTask("LoadClass", configuration).run {
classLoader.loadForSandbox(classSource, context)
classLoader.copyEmpty(context).loadForSandbox(classSource)
}
return result.output ?: throw ClassNotFoundException(classSource.qualifiedClassName)
}
@ -159,11 +159,13 @@ open class SandboxExecutor<in TInput, out TOutput>(
): ReferenceValidationSummary {
processClassQueue(*classSources.toTypedArray()) { classSource, className ->
val didLoad = try {
classLoader.loadForSandbox(classSource, context)
classLoader.copyEmpty(context).loadClassForSandbox(classSource)
true
} catch (exception: SandboxClassLoadingException) {
// Continue; all warnings and errors are captured in [context.messages]
false
} finally {
context.messages.acceptProvisional()
}
if (didLoad) {
context.classes[className]?.apply {

View File

@ -22,6 +22,7 @@ class MessageCollection(
private val memberMessages = mutableMapOf<String, MutableList<Message>>()
private val provisional = mutableListOf<Message>()
private var cachedEntries: List<Message>? = null
/**
@ -58,6 +59,28 @@ class MessageCollection(
}
}
/**
* Hold this message until we've decided whether or not it's real.
*/
fun provisionalAdd(message: Message) {
provisional.add(message)
}
/**
* Discard all provisional messages.
*/
fun clearProvisional() {
provisional.clear()
}
/**
* Accept all provisional messages.
*/
fun acceptProvisional() {
addAll(provisional)
clearProvisional()
}
/**
* Get all recorded messages for a given class.
*/

View File

@ -1,6 +1,7 @@
package net.corda.djvm.rewiring
import net.corda.djvm.SandboxConfiguration
import net.corda.djvm.analysis.AnalysisConfiguration
import net.corda.djvm.analysis.AnalysisContext
import net.corda.djvm.analysis.ClassAndMemberVisitor
import net.corda.djvm.analysis.ExceptionResolver.Companion.getDJVMExceptionOwner
@ -8,30 +9,32 @@ import net.corda.djvm.analysis.ExceptionResolver.Companion.isDJVMException
import net.corda.djvm.code.asPackagePath
import net.corda.djvm.code.asResourcePath
import net.corda.djvm.references.ClassReference
import net.corda.djvm.source.AbstractSourceClassLoader
import net.corda.djvm.source.ClassSource
import net.corda.djvm.utilities.loggerFor
import net.corda.djvm.validation.RuleValidator
import org.objectweb.asm.Type
/**
* Class loader that enables registration of rewired classes.
*
* @param configuration The configuration to use for the sandbox.
* @property analysisConfiguration The configuration to use for the analysis.
* @property ruleValidator The instance used to validate that any loaded class complies with the specified rules.
* @property supportingClassLoader The class loader used to find classes on the extended class path.
* @property rewriter The re-writer to use for registered classes.
* @property context The context in which analysis and processing is performed.
* @param throwableClass This sandbox's definition of [sandbox.java.lang.Throwable].
* @param parent This classloader's parent classloader.
*/
class SandboxClassLoader(
configuration: SandboxConfiguration,
private val context: AnalysisContext
) : ClassLoader() {
private val analysisConfiguration = configuration.analysisConfiguration
/**
* The instance used to validate that any loaded class complies with the specified rules.
*/
private val ruleValidator: RuleValidator = RuleValidator(
rules = configuration.rules,
configuration = analysisConfiguration
)
class SandboxClassLoader private constructor(
private val analysisConfiguration: AnalysisConfiguration,
private val ruleValidator: RuleValidator,
private val supportingClassLoader: AbstractSourceClassLoader,
private val rewriter: ClassRewriter,
private val context: AnalysisContext,
throwableClass: Class<*>?,
parent: ClassLoader?
) : ClassLoader(parent ?: getSystemClassLoader()) {
/**
* The analyzer used to traverse the class hierarchy.
@ -50,36 +53,65 @@ class SandboxClassLoader(
private val loadedClasses = mutableMapOf<String, LoadedClass>()
/**
* The class loader used to find classes on the extended class path.
* We need to load [sandbox.java.lang.Throwable] up front, so that we can
* identify sandboxed exception classes.
*/
private val supportingClassLoader = analysisConfiguration.supportingClassLoader
/**
* The re-writer to use for registered classes.
*/
private val rewriter: ClassRewriter = ClassRewriter(configuration, supportingClassLoader)
/**
* We need to load this class up front, so that we can identify sandboxed exception classes.
*/
private val throwableClass: Class<*>
init {
// Bootstrap the loading of the sandboxed Throwable class.
private val throwableClass: Class<*> = throwableClass ?: run {
loadClassAndBytes(ClassSource.fromClassName("sandbox.java.lang.Object"), context)
loadClassAndBytes(ClassSource.fromClassName("sandbox.java.lang.StackTraceElement"), context)
throwableClass = loadClassAndBytes(ClassSource.fromClassName("sandbox.java.lang.Throwable"), context).type
loadClassAndBytes(ClassSource.fromClassName("sandbox.java.lang.Throwable"), context).type
}
/**
* Creates an empty [SandboxClassLoader] with exactly the same
* configuration as this one, but with the given [AnalysisContext].
* @param newContext The [AnalysisContext] to use for the child classloader.
*/
fun copyEmpty(newContext: AnalysisContext) = SandboxClassLoader(
analysisConfiguration,
ruleValidator,
supportingClassLoader,
rewriter,
newContext,
throwableClass,
parent
)
/**
* Given a class name, provide its corresponding [LoadedClass] for the sandbox.
* This class may have been loaded by a parent classloader really.
*/
fun loadForSandbox(name: String, context: AnalysisContext): LoadedClass {
return loadClassAndBytes(ClassSource.fromClassName(analysisConfiguration.classResolver.resolveNormalized(name)), context)
@Throws(ClassNotFoundException::class)
fun loadForSandbox(className: String): LoadedClass {
val sandboxClass = loadClassForSandbox(className)
val sandboxName = Type.getInternalName(sandboxClass)
var loader = this
while(true) {
val loaded = loader.loadedClasses[sandboxName]
if (loaded != null) {
return loaded
}
loader = loader.parent as? SandboxClassLoader ?: return LoadedClass(sandboxClass, UNMODIFIED)
}
}
fun loadForSandbox(source: ClassSource, context: AnalysisContext): LoadedClass {
return loadForSandbox(source.qualifiedClassName, context)
@Throws(ClassNotFoundException::class)
fun loadForSandbox(source: ClassSource): LoadedClass {
return loadForSandbox(source.qualifiedClassName)
}
private fun loadClassForSandbox(className: String): Class<*> {
val sandboxName = analysisConfiguration.classResolver.resolveNormalized(className)
return try {
loadClass(sandboxName)
} finally {
context.messages.acceptProvisional()
}
}
@Throws(ClassNotFoundException::class)
fun loadClassForSandbox(source: ClassSource): Class<*> {
return loadClassForSandbox(source.qualifiedClassName)
}
/**
@ -95,10 +127,19 @@ class SandboxClassLoader(
var clazz = findLoadedClass(name)
if (clazz == null) {
val source = ClassSource.fromClassName(name)
clazz = if (analysisConfiguration.isSandboxClass(source.internalClassName)) {
loadSandboxClass(source, context).type
} else {
super.loadClass(name, resolve)
val isSandboxClass = analysisConfiguration.isSandboxClass(source.internalClassName)
if (!isSandboxClass || parent is SandboxClassLoader) {
try {
clazz = super.loadClass(name, resolve)
} catch (e: ClassNotFoundException) {
} catch (e: SandboxClassLoadingException) {
e.messages.clearProvisional()
}
}
if (clazz == null && isSandboxClass) {
clazz = loadSandboxClass(source, context).type
}
}
if (resolve) {
@ -107,15 +148,31 @@ class SandboxClassLoader(
return clazz
}
/**
* A sandboxed exception class cannot be thrown, and so we may also need to create a
* synthetic throwable wrapper for it. Or perhaps we've just been asked to load the
* synthetic wrapper class belonging to an exception that we haven't loaded yet?
* Either way, we need to load the sandboxed exception first so that we know what
* the synthetic wrapper's super-class needs to be.
*/
private fun loadSandboxClass(source: ClassSource, context: AnalysisContext): LoadedClass {
return if (isDJVMException(source.internalClassName)) {
/**
* We need to load a DJVMException's owner class before we can create
* its wrapper exception. And loading the owner should also create the
* wrapper class automatically.
* its wrapper exception. And loading the owner should then also create
* the wrapper class automatically.
*/
loadedClasses.getOrElse(source.internalClassName) {
loadSandboxClass(ClassSource.fromClassName(getDJVMExceptionOwner(source.qualifiedClassName)), context)
val exceptionOwner = ClassSource.fromClassName(getDJVMExceptionOwner(source.qualifiedClassName))
if (!analysisConfiguration.isJvmException(exceptionOwner.internalClassName)) {
/**
* JVM Exceptions belong to the parent classloader, and so will never
* be found inside a child classloader. Which means we must not try to
* create a duplicate inside any child classloaders either. Hence we
* re-invoke [loadClass] which will delegate back to the parent.
*/
loadClass(exceptionOwner.qualifiedClassName, false)
}
loadedClasses[source.internalClassName]
} ?: throw ClassNotFoundException(source.qualifiedClassName)
} else {
@ -171,6 +228,7 @@ class SandboxClassLoader(
}
// Check if any errors were found during analysis.
context.messages.acceptProvisional()
if (context.messages.errorCount > 0) {
logger.debug("Errors detected after analyzing class {}", request.qualifiedClassName)
throw SandboxClassLoadingException(context)
@ -214,6 +272,10 @@ class SandboxClassLoader(
}
}
/**
* Check whether the synthetic throwable wrapper already
* exists for this exception, and create it if it doesn't.
*/
private fun loadWrapperFor(throwable: Class<*>): LoadedClass {
val className = analysisConfiguration.exceptionResolver.getThrowableName(throwable)
return loadedClasses.getOrPut(className) {
@ -223,9 +285,30 @@ class SandboxClassLoader(
}
}
private companion object {
companion object {
private val logger = loggerFor<SandboxClassLoader>()
private val UNMODIFIED = ByteCode(ByteArray(0), false)
/**
* Factory function to create a [SandboxClassLoader].
* @param configuration The [SandboxConfiguration] containing the classloader's configuration parameters.
*/
fun createFor(configuration: SandboxConfiguration): SandboxClassLoader {
val analysisConfiguration = configuration.analysisConfiguration
val supportingClassLoader = analysisConfiguration.supportingClassLoader
val parentClassLoader = configuration.parentClassLoader
return SandboxClassLoader(
analysisConfiguration = analysisConfiguration,
supportingClassLoader = supportingClassLoader,
ruleValidator = RuleValidator(rules = configuration.rules,
configuration = analysisConfiguration),
rewriter = ClassRewriter(configuration, supportingClassLoader),
context = AnalysisContext.fromConfiguration(analysisConfiguration),
throwableClass = parentClassLoader?.throwableClass,
parent = parentClassLoader
)
}
}
}

View File

@ -19,7 +19,7 @@ class SandboxClassLoadingException(
val messages: MessageCollection = context.messages,
val classes: ClassHierarchy = context.classes,
val classOrigins: Map<String, Set<EntityReference>> = context.classOrigins
) : Exception("Failed to load class") {
) : RuntimeException("Failed to load class") {
/**
* The detailed description of the exception.
@ -28,7 +28,7 @@ class SandboxClassLoadingException(
get() = StringBuilder().apply {
appendln(super.message)
for (message in messages.sorted().map(Message::toString).distinct()) {
appendln(" - $message")
append(" - ").appendln(message)
}
}.toString().trimEnd('\r', '\n')

View File

@ -1,5 +1,7 @@
@file:JvmName("SourceClassLoaderTools")
package net.corda.djvm.source
import net.corda.djvm.analysis.AnalysisConfiguration.Companion.SANDBOX_PREFIX
import net.corda.djvm.analysis.AnalysisContext
import net.corda.djvm.analysis.ClassResolver
import net.corda.djvm.analysis.ExceptionResolver.Companion.getDJVMExceptionOwner
@ -33,19 +35,23 @@ abstract class AbstractSourceClassLoader(
className: String, context: AnalysisContext, origin: String? = null
): ClassReader {
val originalName = classResolver.reverse(className.asResourcePath)
fun throwClassLoadingError(): Nothing {
context.messages.provisionalAdd(Message(
message ="Class file not found; $originalName.class",
severity = Severity.ERROR,
location = SourceLocation(origin ?: "")
))
throw SandboxClassLoadingException(context)
}
return try {
logger.trace("Opening ClassReader for class {}...", originalName)
getResourceAsStream("$originalName.class").use {
getResourceAsStream("$originalName.class")?.use {
ClassReader(it)
}
} ?: run(::throwClassLoadingError)
} catch (exception: IOException) {
context.messages.add(Message(
message ="Class file not found; $originalName.class",
severity = Severity.ERROR,
location = SourceLocation(origin ?: "")
))
logger.error("Failed to open ClassReader for class", exception)
throw SandboxClassLoadingException(context)
throwClassLoadingError()
}
}
@ -78,50 +84,16 @@ abstract class AbstractSourceClassLoader(
protected companion object {
@JvmStatic
protected val logger = loggerFor<SourceClassLoader>()
private fun resolvePaths(paths: List<Path>): Array<URL> {
return paths.map(this::expandPath).flatMap {
when {
!Files.exists(it) -> throw FileNotFoundException("File not found; $it")
Files.isDirectory(it) -> {
listOf(it.toURL()) + Files.list(it).filter(::isJarFile).map { jar -> jar.toURL() }.toList()
}
Files.isReadable(it) && isJarFile(it) -> listOf(it.toURL())
else -> throw IllegalArgumentException("Expected JAR or class file, but found $it")
}
}.apply {
logger.trace("Resolved paths: {}", this)
}.toTypedArray()
}
private fun expandPath(path: Path): Path {
val pathString = path.toString()
if (pathString.startsWith("~/")) {
return homeDirectory.resolve(pathString.removePrefix("~/"))
}
return path
}
private fun isJarFile(path: Path) = path.toString().endsWith(".jar", true)
private fun Path.toURL(): URL = this.toUri().toURL()
private val homeDirectory: Path
get() = Paths.get(System.getProperty("user.home"))
}
}
/**
* Class loader to manage an optional JAR of replacement Java APIs.
* @param bootstrapJar The location of the JAR containing the Java APIs.
* @param classResolver The resolver to use to derive the original name of a requested class.
*/
class BootstrapClassLoader(
bootstrapJar: Path,
classResolver: ClassResolver
) : AbstractSourceClassLoader(listOf(bootstrapJar), classResolver, null) {
bootstrapJar: Path
) : URLClassLoader(resolvePaths(listOf(bootstrapJar)), null) {
/**
* Only search our own jars for the given resource.
@ -129,6 +101,37 @@ class BootstrapClassLoader(
override fun getResource(name: String): URL? = findResource(name)
}
/**
* Class loader that only provides our built-in sandbox classes.
* @param classResolver The resolver to use to derive the original name of a requested class.
*/
class SandboxSourceClassLoader(
classResolver: ClassResolver,
private val bootstrap: BootstrapClassLoader
) : AbstractSourceClassLoader(emptyList(), classResolver, SandboxSourceClassLoader::class.java.classLoader) {
/**
* Always check the bootstrap classloader first. If we're requesting
* built-in sandbox classes then delegate to our parent classloader,
* otherwise deny the request.
*/
override fun getResource(name: String): URL? {
val resource = bootstrap.findResource(name)
if (resource != null) {
return resource
} else if (isJvmInternal(name)) {
logger.error("Denying request for actual {}", name)
return null
}
return if (name.startsWith(SANDBOX_PREFIX)) {
parent.getResource(name)
} else {
null
}
}
}
/**
* Customizable class loader that allows the user to explicitly specify additional JARs and directories to scan.
*
@ -168,12 +171,41 @@ class SourceClassLoader(
return if (name.startsWith("net/corda/djvm/")) null else super.findResource(name)
}
/**
* Does [name] exist within any of the packages reserved for Java itself?
*/
private fun isJvmInternal(name: String): Boolean = name.startsWith("java/")
|| name.startsWith("javax/")
|| name.startsWith("com/sun/")
|| name.startsWith("sun/")
|| name.startsWith("jdk/")
}
}
private fun resolvePaths(paths: List<Path>): Array<URL> {
return paths.map(::expandPath).flatMap {
when {
!Files.exists(it) -> throw FileNotFoundException("File not found; $it")
Files.isDirectory(it) -> {
listOf(it.toURL()) + Files.list(it).filter(::isJarFile).map { jar -> jar.toURL() }.toList()
}
Files.isReadable(it) && isJarFile(it) -> listOf(it.toURL())
else -> throw IllegalArgumentException("Expected JAR or class file, but found $it")
}
}.toTypedArray()
}
private fun expandPath(path: Path): Path {
val pathString = path.toString()
if (pathString.startsWith("~/")) {
return homeDirectory.resolve(pathString.removePrefix("~/"))
}
return path
}
private fun isJarFile(path: Path) = path.toString().endsWith(".jar", true)
private fun Path.toURL(): URL = this.toUri().toURL()
private val homeDirectory: Path
get() = Paths.get(System.getProperty("user.home"))
/**
* Does [name] exist within any of the packages reserved for Java itself?
*/
private fun isJvmInternal(name: String): Boolean = name.startsWith("java/")
|| name.startsWith("javax/")
|| name.startsWith("com/sun/")
|| name.startsWith("sun/")
|| name.startsWith("jdk/")

View File

@ -2,6 +2,7 @@
@file:Suppress("unused")
package sandbox.java.lang
import net.corda.djvm.SandboxRuntimeContext
import net.corda.djvm.analysis.AnalysisConfiguration.Companion.JVM_EXCEPTIONS
import net.corda.djvm.analysis.ExceptionResolver.Companion.getDJVMException
import net.corda.djvm.rules.implementation.*
@ -42,14 +43,14 @@ fun Any.sandbox(): Any {
private fun Array<*>.fromDJVMArray(): Array<*> = Object.fromDJVM(this)
/**
* These functions use the "current" classloader, i.e. classloader
* that owns this DJVM class.
* Use the sandbox's classloader explicitly, because this class
* might belong to the shared parent classloader.
*/
@Throws(ClassNotFoundException::class)
internal fun Class<*>.toDJVMType(): Class<*> = Class.forName(name.toSandboxPackage())
internal fun Class<*>.toDJVMType(): Class<*> = SandboxRuntimeContext.instance.classLoader.loadClass(name.toSandboxPackage())
@Throws(ClassNotFoundException::class)
internal fun Class<*>.fromDJVMType(): Class<*> = Class.forName(name.fromSandboxPackage())
internal fun Class<*>.fromDJVMType(): Class<*> = SandboxRuntimeContext.instance.classLoader.loadClass(name.fromSandboxPackage())
private fun kotlin.String.toSandboxPackage(): kotlin.String {
return if (startsWith(SANDBOX_PREFIX)) {
@ -190,10 +191,11 @@ fun fromDJVM(t: Throwable?): kotlin.Throwable {
val sandboxedName = t!!.javaClass.name
if (Type.getInternalName(t.javaClass) in JVM_EXCEPTIONS) {
// We map these exceptions to their equivalent JVM classes.
Class.forName(sandboxedName.fromSandboxPackage()).createJavaThrowable(t)
SandboxRuntimeContext.instance.classLoader.loadClass(sandboxedName.fromSandboxPackage())
.createJavaThrowable(t)
} else {
// Whereas the sandbox creates a synthetic throwable wrapper for these.
Class.forName(getDJVMException(sandboxedName))
SandboxRuntimeContext.instance.classLoader.loadClass(getDJVMException(sandboxedName))
.getDeclaredConstructor(sandboxThrowable)
.newInstance(t) as kotlin.Throwable
}

View File

@ -12,13 +12,11 @@ import java.util.Map;
import java.util.function.Function;
import java.util.stream.Stream;
import static java.util.Collections.emptySet;
public class SandboxEnumJavaTest extends TestBase {
@Test
public void testEnumInsideSandbox() {
sandbox(new Object[]{ DEFAULT }, emptySet(), WARNING, true, ctx -> {
parentedSandbox(WARNING, true, ctx -> {
SandboxExecutor<Integer, String[]> executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration());
ExecutionSummaryWithResult<String[]> output = WithJava.run(executor, TransformEnum.class, 0);
assertThat(output.getResult())
@ -29,7 +27,7 @@ public class SandboxEnumJavaTest extends TestBase {
@Test
public void testReturnEnumFromSandbox() {
sandbox(new Object[]{ DEFAULT }, emptySet(), WARNING, true, ctx -> {
parentedSandbox(WARNING, true, ctx -> {
SandboxExecutor<String, ExampleEnum> executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration());
ExecutionSummaryWithResult<ExampleEnum> output = WithJava.run(executor, FetchEnum.class, "THREE");
assertThat(output.getResult())
@ -40,7 +38,7 @@ public class SandboxEnumJavaTest extends TestBase {
@Test
public void testWeCanIdentifyClassAsEnum() {
sandbox(new Object[]{ DEFAULT }, emptySet(), WARNING, true, ctx -> {
parentedSandbox(WARNING, true, ctx -> {
SandboxExecutor<ExampleEnum, Boolean> executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration());
ExecutionSummaryWithResult<Boolean> output = WithJava.run(executor, AssertEnum.class, ExampleEnum.THREE);
assertThat(output.getResult()).isTrue();
@ -50,7 +48,7 @@ public class SandboxEnumJavaTest extends TestBase {
@Test
public void testWeCanCreateEnumMap() {
sandbox(new Object[]{ DEFAULT }, emptySet(), WARNING, true, ctx -> {
parentedSandbox(WARNING, true, ctx -> {
SandboxExecutor<ExampleEnum, Integer> executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration());
ExecutionSummaryWithResult<Integer> output = WithJava.run(executor, UseEnumMap.class, ExampleEnum.TWO);
assertThat(output.getResult()).isEqualTo(1);
@ -60,7 +58,7 @@ public class SandboxEnumJavaTest extends TestBase {
@Test
public void testWeCanCreateEnumSet() {
sandbox(new Object[]{ DEFAULT }, emptySet(), WARNING, true, ctx -> {
parentedSandbox(WARNING, true, ctx -> {
SandboxExecutor<ExampleEnum, Boolean> executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration());
ExecutionSummaryWithResult<Boolean> output = WithJava.run(executor, UseEnumSet.class, ExampleEnum.ONE);
assertThat(output.getResult()).isTrue();

View File

@ -12,14 +12,13 @@ import java.util.LinkedList;
import java.util.List;
import java.util.function.Function;
import static java.util.Collections.emptySet;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
public class SandboxThrowableJavaTest extends TestBase {
@Test
public void testUserExceptionHandling() {
sandbox(new Object[]{ DEFAULT }, emptySet(), WARNING, true, ctx -> {
parentedSandbox(WARNING, true, ctx -> {
SandboxExecutor<String, String[]> executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration());
ExecutionSummaryWithResult<String[]> output = WithJava.run(executor, ThrowAndCatchJavaExample.class, "Hello World!");
assertThat(output.getResult())
@ -30,7 +29,7 @@ public class SandboxThrowableJavaTest extends TestBase {
@Test
public void testCheckedExceptions() {
sandbox(new Object[]{ DEFAULT }, emptySet(), WARNING, true, ctx -> {
parentedSandbox(WARNING, true, ctx -> {
SandboxExecutor<String, String> executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration());
ExecutionSummaryWithResult<String> success = WithJava.run(executor, JavaWithCheckedExceptions.class, "http://localhost:8080/hello/world");

View File

@ -1,15 +1,17 @@
package net.corda.djvm
import net.corda.djvm.assertions.AssertionExtensions.assertThatDJVM
import net.corda.djvm.rewiring.SandboxClassLoadingException
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.junit.Test
import sandbox.SandboxFunction
import sandbox.Task
import sandbox.java.lang.sandbox
import java.util.*
class DJVMExceptionTest {
class DJVMExceptionTest : TestBase() {
@Test
fun testSingleException() {
fun testSingleException() = parentedSandbox {
val result = Task(SingleExceptionTask()).apply("Hello World")
assertThat(result).isInstanceOf(Throwable::class.java)
result as Throwable
@ -22,7 +24,7 @@ class DJVMExceptionTest {
}
@Test
fun testMultipleExceptions() {
fun testMultipleExceptions() = parentedSandbox {
val result = Task(MultipleExceptionsTask()).apply("Hello World")
assertThat(result).isInstanceOf(Throwable::class.java)
result as Throwable
@ -56,24 +58,92 @@ class DJVMExceptionTest {
}
@Test
fun testJavaThrowableToSandbox() {
val result = Throwable("Hello World").sandbox()
assertThat(result).isInstanceOf(sandbox.java.lang.Throwable::class.java)
result as sandbox.java.lang.Throwable
fun testJavaThrowableToSandbox() = parentedSandbox {
val djvm = DJVM(classLoader)
val helloWorld = djvm.stringOf("Hello World")
assertThat(result.message).isEqualTo("Hello World".toDJVM())
assertThat(result.stackTrace).isNotEmpty()
assertThat(result.cause).isNull()
val result = djvm.sandbox(Throwable("Hello World"))
assertThatDJVM(result)
.hasClassName("sandbox.java.lang.Throwable")
.isAssignableFrom(djvm.throwableClass)
.hasGetterValue("getMessage", helloWorld)
.hasGetterNullValue("getCause")
assertThat(result.getArray("getStackTrace"))
.hasOnlyElementsOfType(djvm.stackTraceElementClass)
.isNotEmpty()
}
@Test
fun testWeTryToCreateCorrectSandboxExceptionsAtRuntime() {
fun testWeCreateCorrectJVMExceptionAtRuntime() = parentedSandbox {
val djvm = DJVM(classLoader)
val helloWorld = djvm.stringOf("Hello World")
val result = djvm.sandbox(RuntimeException("Hello World"))
assertThatDJVM(result)
.hasClassName("sandbox.java.lang.RuntimeException")
.isAssignableFrom(djvm.throwableClass)
.hasGetterValue("getMessage", helloWorld)
.hasGetterNullValue("getCause")
assertThat(result.getArray("getStackTrace"))
.hasOnlyElementsOfType(djvm.stackTraceElementClass)
.isNotEmpty()
assertThatExceptionOfType(ClassNotFoundException::class.java)
.isThrownBy { Exception("Hello World").sandbox() }
.withMessage("sandbox.java.lang.Exception")
.isThrownBy { djvm.classFor("sandbox.java.lang.RuntimeException\$1DJVM") }
.withMessage("sandbox.java.lang.RuntimeException\$1DJVM")
}
@Test
fun testWeCreateCorrectSyntheticExceptionAtRuntime() = parentedSandbox {
val djvm = DJVM(classLoader)
val result = djvm.sandbox(EmptyStackException())
assertThatDJVM(result)
.hasClassName("sandbox.java.util.EmptyStackException")
.isAssignableFrom(djvm.throwableClass)
.hasGetterNullValue("getMessage")
.hasGetterNullValue("getCause")
assertThat(result.getArray("getStackTrace"))
.hasOnlyElementsOfType(djvm.stackTraceElementClass)
.isNotEmpty()
assertThatDJVM(djvm.classFor("sandbox.java.util.EmptyStackException\$1DJVM"))
.isAssignableFrom(RuntimeException::class.java)
}
@Test
fun testWeCannotCreateSyntheticExceptionForNonException() = parentedSandbox {
val djvm = DJVM(classLoader)
assertThatExceptionOfType(ClassNotFoundException::class.java)
.isThrownBy { RuntimeException("Hello World").sandbox() }
.withMessage("sandbox.java.lang.RuntimeException")
.isThrownBy { djvm.classFor("sandbox.java.util.LinkedList\$1DJVM") }
.withMessage("sandbox.java.util.LinkedList\$1DJVM")
}
/**
* This scenario should never happen in practice. We just need to be sure
* that the classloader can handle it.
*/
@Test
fun testWeCannotCreateSyntheticExceptionForImaginaryJavaClass() = parentedSandbox {
val djvm = DJVM(classLoader)
assertThatExceptionOfType(SandboxClassLoadingException::class.java)
.isThrownBy { djvm.classFor("sandbox.java.util.DoesNotExist\$1DJVM") }
.withMessageContaining("Failed to load class")
}
/**
* This scenario should never happen in practice. We just need to be sure
* that the classloader can handle it.
*/
@Test
fun testWeCannotCreateSyntheticExceptionForImaginaryUserClass() = parentedSandbox {
val djvm = DJVM(classLoader)
assertThatExceptionOfType(SandboxClassLoadingException::class.java)
.isThrownBy { djvm.classFor("sandbox.com.example.DoesNotExist\$1DJVM") }
.withMessageContaining("Failed to load class")
}
}
@ -92,7 +162,7 @@ class MultipleExceptionsTask : SandboxFunction<Any?, sandbox.java.lang.Throwable
}
private infix operator fun sandbox.java.lang.String.plus(s: String): sandbox.java.lang.String {
return (toString() + s).toDJVM()
return sandbox.java.lang.String.valueOf(toString() + s)
}
private fun Array<StackTraceElement>.toLineNumbers(): IntArray {

View File

@ -4,9 +4,8 @@ import org.assertj.core.api.Assertions.*
import org.junit.Assert.*
import org.junit.Test
import sandbox.java.lang.sandbox
import sandbox.java.lang.unsandbox
class DJVMTest {
class DJVMTest : TestBase() {
@Test
fun testDJVMString() {
@ -16,39 +15,59 @@ class DJVMTest {
}
@Test
fun testSimpleIntegerFormats() {
val result = sandbox.java.lang.String.format("%d-%d-%d-%d".toDJVM(),
10.toDJVM(), 999999L.toDJVM(), 1234.toShort().toDJVM(), 108.toByte().toDJVM()).toString()
fun testSimpleIntegerFormats() = parentedSandbox {
val result = with(DJVM(classLoader)) {
stringClass.getMethod("format", stringClass, Array<Any>::class.java)
.invoke(null,
stringOf("%d-%d-%d-%d"),
arrayOf(intOf(10), longOf(999999L), shortOf(1234), byteOf(108))
).toString()
}
assertEquals("10-999999-1234-108", result)
}
@Test
fun testHexFormat() {
val result = sandbox.java.lang.String.format("%0#6x".toDJVM(), 768.toDJVM()).toString()
fun testHexFormat() = parentedSandbox {
val result = with(DJVM(classLoader)) {
stringClass.getMethod("format", stringClass, Array<Any>::class.java)
.invoke(null, stringOf("%0#6x"), arrayOf(intOf(768))).toString()
}
assertEquals("0x0300", result)
}
@Test
fun testDoubleFormat() {
val result = sandbox.java.lang.String.format("%9.4f".toDJVM(), 1234.5678.toDJVM()).toString()
fun testDoubleFormat() = parentedSandbox {
val result = with(DJVM(classLoader)) {
stringClass.getMethod("format", stringClass, Array<Any>::class.java)
.invoke(null, stringOf("%9.4f"), arrayOf(doubleOf(1234.5678))).toString()
}
assertEquals("1234.5678", result)
}
@Test
fun testFloatFormat() {
val result = sandbox.java.lang.String.format("%7.2f".toDJVM(), 1234.5678f.toDJVM()).toString()
fun testFloatFormat() = parentedSandbox {
val result = with(DJVM(classLoader)) {
stringClass.getMethod("format", stringClass, Array<Any>::class.java)
.invoke(null, stringOf("%7.2f"), arrayOf(floatOf(1234.5678f))).toString()
}
assertEquals("1234.57", result)
}
@Test
fun testCharFormat() {
val result = sandbox.java.lang.String.format("[%c]".toDJVM(), 'A'.toDJVM()).toString()
fun testCharFormat() = parentedSandbox {
val result = with(DJVM(classLoader)) {
stringClass.getMethod("format", stringClass, Array<Any>::class.java)
.invoke(null, stringOf("[%c]"), arrayOf(charOf('A'))).toString()
}
assertEquals("[A]", result)
}
@Test
fun testObjectFormat() {
val result = sandbox.java.lang.String.format("%s".toDJVM(), object : sandbox.java.lang.Object() {}).toString()
fun testObjectFormat() = parentedSandbox {
val result = with(DJVM(classLoader)) {
stringClass.getMethod("format", stringClass, Array<Any>::class.java)
.invoke(null, stringOf("%s"), arrayOf(object : sandbox.java.lang.Object() {})).toString()
}
assertThat(result).startsWith("sandbox.java.lang.Object@")
}
@ -59,48 +78,60 @@ class DJVMTest {
}
@Test
fun testSandboxingArrays() {
val result = arrayOf(1, 10L, "Hello World", '?', false, 1234.56).sandbox()
assertThat(result)
.isEqualTo(arrayOf(1.toDJVM(), 10L.toDJVM(), "Hello World".toDJVM(), '?'.toDJVM(), false.toDJVM(), 1234.56.toDJVM()))
fun testSandboxingArrays() = parentedSandbox {
with(DJVM(classLoader)) {
val result = sandbox(arrayOf(1, 10L, "Hello World", '?', false, 1234.56))
assertThat(result).isEqualTo(
arrayOf(intOf(1), longOf(10), stringOf("Hello World"), charOf('?'), booleanOf(false), doubleOf(1234.56)))
}
}
@Test
fun testUnsandboxingObjectArray() {
val result = arrayOf<sandbox.java.lang.Object>(1.toDJVM(), 10L.toDJVM(), "Hello World".toDJVM(), '?'.toDJVM(), false.toDJVM(), 1234.56.toDJVM()).unsandbox()
fun testUnsandboxingObjectArray() = parentedSandbox {
val result = with(DJVM(classLoader)) {
unsandbox(arrayOf(intOf(1), longOf(10L), stringOf("Hello World"), charOf('?'), booleanOf(false), doubleOf(1234.56)))
}
assertThat(result)
.isEqualTo(arrayOf(1, 10L, "Hello World", '?', false, 1234.56))
.isEqualTo(arrayOf(1, 10L, "Hello World", '?', false, 1234.56))
}
@Test
fun testSandboxingPrimitiveArray() {
val result = intArrayOf(1, 2, 3, 10).sandbox()
fun testSandboxingPrimitiveArray() = parentedSandbox {
val result = with(DJVM(classLoader)) {
sandbox(intArrayOf(1, 2, 3, 10))
}
assertThat(result).isEqualTo(intArrayOf(1, 2, 3, 10))
}
@Test
fun testSandboxingIntegersAsObjectArray() {
val result = arrayOf(1, 2, 3, 10).sandbox()
assertThat(result).isEqualTo(arrayOf(1.toDJVM(), 2.toDJVM(), 3.toDJVM(), 10.toDJVM()))
fun testSandboxingIntegersAsObjectArray() = parentedSandbox {
with(DJVM(classLoader)) {
val result = sandbox(arrayOf(1, 2, 3, 10))
assertThat(result).isEqualTo(
arrayOf(intOf(1), intOf(2), intOf(3), intOf(10))
)
}
}
@Test
fun testUnsandboxingArrays() {
val arr = arrayOf(
Array(1) { "Hello".toDJVM() },
Array(1) { 1234000L.toDJVM() },
Array(1) { 1234.toDJVM() },
Array(1) { 923.toShort().toDJVM() },
Array(1) { 27.toByte().toDJVM() },
Array(1) { 'X'.toDJVM() },
Array(1) { 987.65f.toDJVM() },
Array(1) { 343.282.toDJVM() },
Array(1) { true.toDJVM() },
ByteArray(1) { 127.toByte() },
CharArray(1) { '?'}
)
val result = arr.unsandbox() as Array<*>
assertEquals(arr.size, result.size)
fun testUnsandboxingArrays() = parentedSandbox {
val (array, result) = with(DJVM(classLoader)) {
val arr = arrayOf(
objectArrayOf(stringOf("Hello")),
objectArrayOf(longOf(1234000L)),
objectArrayOf(intOf(1234)),
objectArrayOf(shortOf(923)),
objectArrayOf(byteOf(27)),
objectArrayOf(charOf('X')),
objectArrayOf(floatOf(987.65f)),
objectArrayOf(doubleOf(343.282)),
objectArrayOf(booleanOf(true)),
ByteArray(1) { 127.toByte() },
CharArray(1) { '?' }
)
Pair(arr, unsandbox(arr) as Array<*>)
}
assertEquals(array.size, result.size)
assertArrayEquals(Array(1) { "Hello" }, result[0] as Array<*>)
assertArrayEquals(Array(1) { 1234000L }, result[1] as Array<*>)
assertArrayEquals(Array(1) { 1234 }, result[2] as Array<*>)

View File

@ -12,13 +12,18 @@ import net.corda.djvm.execution.ExecutionProfile
import net.corda.djvm.messages.Severity
import net.corda.djvm.references.ClassHierarchy
import net.corda.djvm.rewiring.LoadedClass
import net.corda.djvm.rewiring.SandboxClassLoader
import net.corda.djvm.rules.Rule
import net.corda.djvm.rules.implementation.*
import net.corda.djvm.source.BootstrapClassLoader
import net.corda.djvm.source.ClassSource
import net.corda.djvm.source.SandboxSourceClassLoader
import net.corda.djvm.utilities.Discovery
import net.corda.djvm.validation.RuleValidator
import org.junit.After
import org.junit.AfterClass
import org.junit.Assert.assertEquals
import org.junit.BeforeClass
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.Type
@ -39,6 +44,7 @@ abstract class TestBase {
// We need at least these emitters to handle the Java API classes.
@JvmField
val BASIC_EMITTERS: List<Emitter> = listOf(
AlwaysInheritFromSandboxedObject(),
ArgumentUnwrapper(),
HandleExceptionUnwrapper(),
ReturnTypeWrapper(),
@ -51,7 +57,10 @@ abstract class TestBase {
// We need at least these providers to handle the Java API classes.
@JvmField
val BASIC_DEFINITION_PROVIDERS: List<DefinitionProvider> = listOf(StaticConstantRemover())
val BASIC_DEFINITION_PROVIDERS: List<DefinitionProvider> = listOf(
AlwaysInheritFromSandboxedObject(),
StaticConstantRemover()
)
@JvmField
val BLANK = emptySet<Any>()
@ -63,17 +72,52 @@ abstract class TestBase {
val DETERMINISTIC_RT: Path = Paths.get(
System.getProperty("deterministic-rt.path") ?: throw AssertionError("deterministic-rt.path property not set"))
private lateinit var parentConfiguration: SandboxConfiguration
lateinit var parentClassLoader: SandboxClassLoader
/**
* Get the full name of type [T].
*/
inline fun <reified T> nameOf(prefix: String = "") = "$prefix${Type.getInternalName(T::class.java)}"
@BeforeClass
@JvmStatic
fun setupParentClassLoader() {
val rootConfiguration = AnalysisConfiguration.createRoot(
Whitelist.MINIMAL,
bootstrapClassLoader = BootstrapClassLoader(DETERMINISTIC_RT),
sourceClassLoaderFactory = { classResolver, bootstrapClassLoader ->
SandboxSourceClassLoader(classResolver, bootstrapClassLoader!!)
},
additionalPinnedClasses = setOf(
Utilities::class.java
).map(Type::getInternalName).toSet()
)
parentConfiguration = SandboxConfiguration.of(
ExecutionProfile.UNLIMITED,
ALL_RULES,
ALL_EMITTERS,
ALL_DEFINITION_PROVIDERS,
true,
rootConfiguration
)
parentClassLoader = SandboxClassLoader.createFor(parentConfiguration)
}
@AfterClass
@JvmStatic
fun destroyRootContext() {
parentConfiguration.analysisConfiguration.close()
}
}
/**
* Default analysis configuration.
*/
val configuration = AnalysisConfiguration(Whitelist.MINIMAL, bootstrapJar = DETERMINISTIC_RT)
val configuration = AnalysisConfiguration.createRoot(
Whitelist.MINIMAL,
bootstrapClassLoader = BootstrapClassLoader(DETERMINISTIC_RT)
)
/**
* Default analysis context
@ -94,9 +138,9 @@ abstract class TestBase {
noinline block: (RuleValidator.(AnalysisContext) -> Unit)
) {
val reader = ClassReader(T::class.java.name)
AnalysisConfiguration(
AnalysisConfiguration.createRoot(
minimumSeverityLevel = minimumSeverityLevel,
bootstrapJar = DETERMINISTIC_RT
bootstrapClassLoader = BootstrapClassLoader(DETERMINISTIC_RT)
).use { analysisConfiguration ->
val validator = RuleValidator(ALL_RULES, analysisConfiguration)
val context = AnalysisContext.fromConfiguration(analysisConfiguration)
@ -110,11 +154,11 @@ abstract class TestBase {
* the current thread, so this allows inspection of the cost summary object, etc. from within the provided delegate.
*/
fun sandbox(
vararg options: Any,
pinnedClasses: Set<java.lang.Class<*>> = emptySet(),
minimumSeverityLevel: Severity = Severity.WARNING,
enableTracing: Boolean = true,
action: SandboxRuntimeContext.() -> Unit
vararg options: Any,
pinnedClasses: Set<java.lang.Class<*>> = emptySet(),
minimumSeverityLevel: Severity = Severity.WARNING,
enableTracing: Boolean = true,
action: SandboxRuntimeContext.() -> Unit
) {
val rules = mutableListOf<Rule>()
val emitters = mutableListOf<Emitter>().apply { addAll(BASIC_EMITTERS) }
@ -141,11 +185,11 @@ abstract class TestBase {
thread {
try {
val pinnedTestClasses = pinnedClasses.map(Type::getInternalName).toSet()
AnalysisConfiguration(
AnalysisConfiguration.createRoot(
whitelist = whitelist,
bootstrapJar = DETERMINISTIC_RT,
additionalPinnedClasses = pinnedTestClasses,
minimumSeverityLevel = minimumSeverityLevel
minimumSeverityLevel = minimumSeverityLevel,
bootstrapClassLoader = BootstrapClassLoader(DETERMINISTIC_RT)
).use { analysisConfiguration ->
SandboxRuntimeContext(SandboxConfiguration.of(
executionProfile,
@ -166,6 +210,37 @@ abstract class TestBase {
throw thrownException ?: return
}
fun parentedSandbox(
minimumSeverityLevel: Severity = Severity.WARNING,
enableTracing: Boolean = true,
action: SandboxRuntimeContext.() -> Unit
) {
var thrownException: Throwable? = null
thread {
try {
parentConfiguration.analysisConfiguration.createChild(
newMinimumSeverityLevel = minimumSeverityLevel
).use { analysisConfiguration ->
SandboxRuntimeContext(SandboxConfiguration.of(
parentConfiguration.executionProfile,
parentConfiguration.rules,
parentConfiguration.emitters,
parentConfiguration.definitionProviders,
enableTracing,
analysisConfiguration,
parentClassLoader
)).use {
assertThat(runtimeCosts).areZero()
action(this)
}
}
} catch (exception: Throwable) {
thrownException = exception
}
}.join()
throw thrownException ?: return
}
/**
* Get a class reference from a class hierarchy based on [T].
*/
@ -178,8 +253,7 @@ abstract class TestBase {
inline fun <reified T : Any> SandboxRuntimeContext.loadClass(): LoadedClass = loadClass(T::class.jvmName)
fun SandboxRuntimeContext.loadClass(className: String): LoadedClass =
classLoader.loadForSandbox(className, context)
fun SandboxRuntimeContext.loadClass(className: String): LoadedClass = classLoader.loadForSandbox(className)
/**
* Run the entry-point of the loaded [Callable] class.
@ -203,4 +277,77 @@ abstract class TestBase {
}
}
@Suppress("MemberVisibilityCanBePrivate")
protected class DJVM(private val classLoader: ClassLoader) {
private val djvm: Class<*> = classFor("sandbox.java.lang.DJVM")
val objectClass: Class<*> by lazy { classFor("sandbox.java.lang.Object") }
val stringClass: Class<*> by lazy { classFor("sandbox.java.lang.String") }
val longClass: Class<*> by lazy { classFor("sandbox.java.lang.Long") }
val integerClass: Class<*> by lazy { classFor("sandbox.java.lang.Integer") }
val shortClass: Class<*> by lazy { classFor("sandbox.java.lang.Short") }
val byteClass: Class<*> by lazy { classFor("sandbox.java.lang.Byte") }
val characterClass: Class<*> by lazy { classFor("sandbox.java.lang.Character") }
val booleanClass: Class<*> by lazy { classFor("sandbox.java.lang.Boolean") }
val doubleClass: Class<*> by lazy { classFor("sandbox.java.lang.Double") }
val floatClass: Class<*> by lazy { classFor("sandbox.java.lang.Float") }
val throwableClass: Class<*> by lazy { classFor("sandbox.java.lang.Throwable") }
val stackTraceElementClass: Class<*> by lazy { classFor("sandbox.java.lang.StackTraceElement") }
fun classFor(className: String): Class<*> = Class.forName(className, false, classLoader)
fun sandbox(obj: Any): Any {
return djvm.getMethod("sandbox", Any::class.java).invoke(null, obj)
}
fun unsandbox(obj: Any): Any {
return djvm.getMethod("unsandbox", Any::class.java).invoke(null, obj)
}
fun stringOf(str: String): Any {
return stringClass.getMethod("toDJVM", String::class.java).invoke(null, str)
}
fun longOf(l: Long): Any {
return longClass.getMethod("toDJVM", Long::class.javaObjectType).invoke(null, l)
}
fun intOf(i: Int): Any {
return integerClass.getMethod("toDJVM", Int::class.javaObjectType).invoke(null, i)
}
fun shortOf(i: Int): Any {
return shortClass.getMethod("toDJVM", Short::class.javaObjectType).invoke(null, i.toShort())
}
fun byteOf(i: Int): Any {
return byteClass.getMethod("toDJVM", Byte::class.javaObjectType).invoke(null, i.toByte())
}
fun charOf(c: Char): Any {
return characterClass.getMethod("toDJVM", Char::class.javaObjectType).invoke(null, c)
}
fun booleanOf(bool: Boolean): Any {
return booleanClass.getMethod("toDJVM", Boolean::class.javaObjectType).invoke(null, bool)
}
fun doubleOf(d: Double): Any {
return doubleClass.getMethod("toDJVM", Double::class.javaObjectType).invoke(null, d)
}
fun floatOf(f: Float): Any {
return floatClass.getMethod("toDJVM", Float::class.javaObjectType).invoke(null, f)
}
fun objectArrayOf(vararg objs: Any): Array<in Any> {
@Suppress("unchecked_cast")
return (java.lang.reflect.Array.newInstance(objectClass, objs.size) as Array<in Any>).also {
for (i in 0 until objs.size) {
it[i] = objectClass.cast(objs[i])
}
}
}
}
fun Any.getArray(methodName: String): Array<*> = javaClass.getMethod(methodName).invoke(this) as Array<*>
}

View File

@ -12,13 +12,3 @@ object Utilities {
fun throwThresholdViolationError(): Nothing = throw ThresholdViolationError("Can't catch this!")
}
fun String.toDJVM(): sandbox.java.lang.String = sandbox.java.lang.String.toDJVM(this)
fun Long.toDJVM(): sandbox.java.lang.Long = sandbox.java.lang.Long.toDJVM(this)
fun Int.toDJVM(): sandbox.java.lang.Integer = sandbox.java.lang.Integer.toDJVM(this)
fun Short.toDJVM(): sandbox.java.lang.Short = sandbox.java.lang.Short.toDJVM(this)
fun Byte.toDJVM(): sandbox.java.lang.Byte = sandbox.java.lang.Byte.toDJVM(this)
fun Float.toDJVM(): sandbox.java.lang.Float = sandbox.java.lang.Float.toDJVM(this)
fun Double.toDJVM(): sandbox.java.lang.Double = sandbox.java.lang.Double.toDJVM(this)
fun Char.toDJVM(): sandbox.java.lang.Character = sandbox.java.lang.Character.toDJVM(this)
fun Boolean.toDJVM(): sandbox.java.lang.Boolean = sandbox.java.lang.Boolean.toDJVM(this)

View File

@ -36,6 +36,8 @@ object AssertionExtensions {
fun assertThat(references: ReferenceMap) =
AssertiveReferenceMap(references)
fun assertThatDJVM(obj: Any) = AssertiveDJVMObject(obj)
inline fun <reified T> IterableAssert<ClassRepresentation>.hasClass(): IterableAssert<ClassRepresentation> = this
.`as`("HasClass(${T::class.java.name})")
.anySatisfy {

View File

@ -24,6 +24,11 @@ class AssertiveClassWithByteCode(private val loadedClass: LoadedClass) {
return this
}
fun hasClassLoader(classLoader: ClassLoader): AssertiveClassWithByteCode {
assertThat(loadedClass.type.classLoader).isEqualTo(classLoader)
return this
}
fun hasClassName(className: String): AssertiveClassWithByteCode {
assertThat(loadedClass.type.name).isEqualTo(className)
return this

View File

@ -0,0 +1,26 @@
package net.corda.djvm.assertions
import org.assertj.core.api.Assertions.*
class AssertiveDJVMObject(private val djvmObj: Any) {
fun hasClassName(className: String): AssertiveDJVMObject {
assertThat(djvmObj.javaClass.name).isEqualTo(className)
return this
}
fun isAssignableFrom(clazz: Class<*>): AssertiveDJVMObject {
assertThat(djvmObj.javaClass.isAssignableFrom(clazz))
return this
}
fun hasGetterValue(methodName: String, value: Any): AssertiveDJVMObject {
assertThat(djvmObj.javaClass.getMethod(methodName).invoke(djvmObj)).isEqualTo(value)
return this
}
fun hasGetterNullValue(methodName: String): AssertiveDJVMObject {
assertThat(djvmObj.javaClass.getMethod(methodName).invoke(djvmObj)).isNull()
return this
}
}

View File

@ -8,7 +8,7 @@ import java.util.function.Function
class SandboxEnumTest : TestBase() {
@Test
fun `test enum inside sandbox`() = sandbox(DEFAULT) {
fun `test enum inside sandbox`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<Int, Array<String>>(configuration)
contractExecutor.run<TransformEnum>(0).apply {
assertThat(result).isEqualTo(arrayOf("ONE", "TWO", "THREE"))
@ -16,7 +16,7 @@ class SandboxEnumTest : TestBase() {
}
@Test
fun `return enum from sandbox`() = sandbox(DEFAULT) {
fun `return enum from sandbox`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<String, ExampleEnum>(configuration)
contractExecutor.run<FetchEnum>("THREE").apply {
assertThat(result).isEqualTo(ExampleEnum.THREE)
@ -24,7 +24,7 @@ class SandboxEnumTest : TestBase() {
}
@Test
fun `test we can identify class as Enum`() = sandbox(DEFAULT) {
fun `test we can identify class as Enum`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<ExampleEnum, Boolean>(configuration)
contractExecutor.run<AssertEnum>(ExampleEnum.THREE).apply {
assertThat(result).isTrue()
@ -32,7 +32,7 @@ class SandboxEnumTest : TestBase() {
}
@Test
fun `test we can create EnumMap`() = sandbox(DEFAULT) {
fun `test we can create EnumMap`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<ExampleEnum, Int>(configuration)
contractExecutor.run<UseEnumMap>(ExampleEnum.TWO).apply {
assertThat(result).isEqualTo(1)
@ -40,7 +40,7 @@ class SandboxEnumTest : TestBase() {
}
@Test
fun `test we can create EnumSet`() = sandbox(DEFAULT) {
fun `test we can create EnumSet`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<ExampleEnum, Boolean>(configuration)
contractExecutor.run<UseEnumSet>(ExampleEnum.ONE).apply {
assertThat(result).isTrue()

View File

@ -4,7 +4,7 @@ import foo.bar.sandbox.MyObject
import foo.bar.sandbox.testClock
import foo.bar.sandbox.toNumber
import net.corda.djvm.TestBase
import net.corda.djvm.analysis.Whitelist
import net.corda.djvm.analysis.Whitelist.Companion.MINIMAL
import net.corda.djvm.Utilities
import net.corda.djvm.Utilities.throwRuleViolationError
import net.corda.djvm.Utilities.throwThresholdViolationError
@ -22,7 +22,7 @@ import java.util.stream.Collectors.*
class SandboxExecutorTest : TestBase() {
@Test
fun `can load and execute runnable`() = sandbox(Whitelist.MINIMAL) {
fun `can load and execute runnable`() = sandbox(MINIMAL) {
val contractExecutor = DeterministicSandboxExecutor<Int, String>(configuration)
val summary = contractExecutor.run<TestSandboxedRunnable>(1)
val result = summary.result
@ -36,7 +36,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `can load and execute contract`() = sandbox(
fun `can load and execute contract`() = sandbox(DEFAULT,
pinnedClasses = setOf(Transaction::class.java, Utilities::class.java)
) {
val contractExecutor = DeterministicSandboxExecutor<Transaction, Unit>(configuration)
@ -56,7 +56,7 @@ class SandboxExecutorTest : TestBase() {
data class Transaction(val id: Int)
@Test
fun `can load and execute code that overrides object hash code`() = sandbox(DEFAULT) {
fun `can load and execute code that overrides object hash code`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration)
val summary = contractExecutor.run<TestObjectHashCode>(0)
val result = summary.result
@ -74,7 +74,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `can load and execute code that overrides object hash code when derived`() = sandbox(DEFAULT) {
fun `can load and execute code that overrides object hash code when derived`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration)
val summary = contractExecutor.run<TestObjectHashCodeWithHierarchy>(0)
val result = summary.result
@ -107,7 +107,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `can detect stack overflow`() = sandbox(DEFAULT) {
fun `can detect stack overflow`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration)
assertThatExceptionOfType(SandboxException::class.java)
.isThrownBy { contractExecutor.run<TestStackOverflow>(0) }
@ -141,7 +141,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `cannot execute runnable that references non-deterministic code`() = sandbox(DEFAULT) {
fun `cannot execute runnable that references non-deterministic code`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<Int, Long>(configuration)
assertThatExceptionOfType(SandboxException::class.java)
.isThrownBy { contractExecutor.run<TestNonDeterministicCode>(0) }
@ -156,7 +156,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `cannot execute runnable that catches ThreadDeath`() = sandbox(DEFAULT, pinnedClasses = setOf(Utilities::class.java)) {
fun `cannot execute runnable that catches ThreadDeath`() = parentedSandbox {
TestCatchThreadDeath().apply {
assertThat(apply(0)).isEqualTo(1)
}
@ -178,7 +178,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `cannot execute runnable that catches ThresholdViolationError`() = sandbox(DEFAULT, pinnedClasses = setOf(Utilities::class.java)) {
fun `cannot execute runnable that catches ThresholdViolationError`() = parentedSandbox {
TestCatchThresholdViolationError().apply {
assertThat(apply(0)).isEqualTo(1)
}
@ -201,7 +201,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `cannot execute runnable that catches RuleViolationError`() = sandbox(DEFAULT, pinnedClasses = setOf(Utilities::class.java)) {
fun `cannot execute runnable that catches RuleViolationError`() = parentedSandbox {
TestCatchRuleViolationError().apply {
assertThat(apply(0)).isEqualTo(1)
}
@ -224,7 +224,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `can catch Throwable`() = sandbox(DEFAULT, pinnedClasses = setOf(Utilities::class.java)) {
fun `can catch Throwable`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration)
contractExecutor.run<TestCatchThrowableAndError>(1).apply {
assertThat(result).isEqualTo(1)
@ -232,7 +232,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `can catch Error`() = sandbox(DEFAULT, pinnedClasses = setOf(Utilities::class.java)) {
fun `can catch Error`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration)
contractExecutor.run<TestCatchThrowableAndError>(2).apply {
assertThat(result).isEqualTo(2)
@ -240,7 +240,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `cannot catch ThreadDeath`() = sandbox(DEFAULT, pinnedClasses = setOf(Utilities::class.java)) {
fun `cannot catch ThreadDeath`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration)
assertThatExceptionOfType(SandboxException::class.java)
.isThrownBy { contractExecutor.run<TestCatchThrowableErrorsAndThreadDeath>(3) }
@ -295,7 +295,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `cannot catch stack-overflow error`() = sandbox(DEFAULT, pinnedClasses = setOf(Utilities::class.java)) {
fun `cannot catch stack-overflow error`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration)
assertThatExceptionOfType(SandboxException::class.java)
.isThrownBy { contractExecutor.run<TestCatchThrowableErrorsAndThreadDeath>(4) }
@ -304,7 +304,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `cannot catch out-of-memory error`() = sandbox(DEFAULT, pinnedClasses = setOf(Utilities::class.java)) {
fun `cannot catch out-of-memory error`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration)
assertThatExceptionOfType(SandboxException::class.java)
.isThrownBy { contractExecutor.run<TestCatchThrowableErrorsAndThreadDeath>(5) }
@ -313,7 +313,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `cannot persist state across sessions`() = sandbox(DEFAULT) {
fun `cannot persist state across sessions`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration)
val result1 = contractExecutor.run<TestStatePersistence>(0)
val result2 = contractExecutor.run<TestStatePersistence>(0)
@ -335,7 +335,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `can load and execute code that uses IO`() = sandbox(DEFAULT) {
fun `can load and execute code that uses IO`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration)
assertThatExceptionOfType(SandboxException::class.java)
.isThrownBy { contractExecutor.run<TestIO>(0) }
@ -354,7 +354,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `can load and execute code that uses reflection`() = sandbox(DEFAULT) {
fun `can load and execute code that uses reflection`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<Int, Int>(configuration)
assertThatExceptionOfType(SandboxException::class.java)
.isThrownBy { contractExecutor.run<TestReflection>(0) }
@ -373,7 +373,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `can load and execute code that uses notify()`() = sandbox(DEFAULT) {
fun `can load and execute code that uses notify()`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<Int, String?>(configuration)
assertThatExceptionOfType(SandboxException::class.java)
.isThrownBy { contractExecutor.run<TestMonitors>(1) }
@ -383,7 +383,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `can load and execute code that uses notifyAll()`() = sandbox(DEFAULT) {
fun `can load and execute code that uses notifyAll()`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<Int, String?>(configuration)
assertThatExceptionOfType(SandboxException::class.java)
.isThrownBy { contractExecutor.run<TestMonitors>(2) }
@ -393,7 +393,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `can load and execute code that uses wait()`() = sandbox(DEFAULT) {
fun `can load and execute code that uses wait()`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<Int, String?>(configuration)
assertThatExceptionOfType(SandboxException::class.java)
.isThrownBy { contractExecutor.run<TestMonitors>(3) }
@ -403,7 +403,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `can load and execute code that uses wait(long)`() = sandbox(DEFAULT) {
fun `can load and execute code that uses wait(long)`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<Int, String?>(configuration)
assertThatExceptionOfType(SandboxException::class.java)
.isThrownBy { contractExecutor.run<TestMonitors>(4) }
@ -413,7 +413,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `can load and execute code that uses wait(long,int)`() = sandbox(DEFAULT) {
fun `can load and execute code that uses wait(long,int)`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<Int, String?>(configuration)
assertThatExceptionOfType(SandboxException::class.java)
.isThrownBy { contractExecutor.run<TestMonitors>(5) }
@ -423,7 +423,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `code after forbidden APIs is intact`() = sandbox(DEFAULT) {
fun `code after forbidden APIs is intact`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<Int, String?>(configuration)
assertThat(contractExecutor.run<TestMonitors>(0).result)
.isEqualTo("unknown")
@ -462,7 +462,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `can load and execute code that has a native method`() = sandbox(DEFAULT) {
fun `can load and execute code that has a native method`() = parentedSandbox {
assertThatExceptionOfType(UnsatisfiedLinkError::class.java)
.isThrownBy { TestNativeMethod().apply(0) }
.withMessageContaining("TestNativeMethod.evilDeeds()I")
@ -483,7 +483,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `check arrays still work`() = sandbox(DEFAULT) {
fun `check arrays still work`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<Int, Array<Int>>(configuration)
contractExecutor.run<TestArray>(100).apply {
assertThat(result).isEqualTo(arrayOf(100))
@ -497,7 +497,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `check building a string`() = sandbox(DEFAULT) {
fun `check building a string`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<String?, String?>(configuration)
contractExecutor.run<TestStringBuilding>("Hello Sandbox!").apply {
assertThat(result)
@ -522,7 +522,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `check System-arraycopy still works with Objects`() = sandbox(DEFAULT) {
fun `check System-arraycopy still works with Objects`() = parentedSandbox {
val source = arrayOf("one", "two", "three")
assertThat(TestArrayCopy().apply(source))
.isEqualTo(source)
@ -545,7 +545,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `test System-arraycopy still works with CharArray`() = sandbox(DEFAULT) {
fun `test System-arraycopy still works with CharArray`() = parentedSandbox {
val source = CharArray(10) { '?' }
val contractExecutor = DeterministicSandboxExecutor<CharArray, CharArray>(configuration)
contractExecutor.run<TestCharArrayCopy>(source).apply {
@ -564,7 +564,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `can load and execute class that has finalize`() = sandbox(DEFAULT) {
fun `can load and execute class that has finalize`() = parentedSandbox {
assertThatExceptionOfType(UnsupportedOperationException::class.java)
.isThrownBy { TestFinalizeMethod().apply(100) }
.withMessageContaining("Very Bad Thing")
@ -587,7 +587,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `can execute parallel stream`() = sandbox(DEFAULT) {
fun `can execute parallel stream`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<String, String>(configuration)
contractExecutor.run<TestParallelStream>("Pebble").apply {
assertThat(result).isEqualTo("Five,Four,One,Pebble,Three,Two")
@ -605,7 +605,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `users cannot load our sandboxed classes`() = sandbox(DEFAULT) {
fun `users cannot load our sandboxed classes`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<String, Class<*>>(configuration)
assertThatExceptionOfType(SandboxException::class.java)
.isThrownBy { contractExecutor.run<TestClassForName>("java.lang.DJVM") }
@ -614,7 +614,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `users can load sandboxed classes`() = sandbox(DEFAULT) {
fun `users can load sandboxed classes`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<String, Class<*>>(configuration)
contractExecutor.run<TestClassForName>("java.util.List").apply {
assertThat(result?.name).isEqualTo("sandbox.java.util.List")
@ -628,7 +628,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `test case-insensitive string sorting`() = sandbox(DEFAULT) {
fun `test case-insensitive string sorting`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<Array<String>, Array<String>>(configuration)
contractExecutor.run<CaseInsensitiveSort>(arrayOf("Zelda", "angela", "BOB", "betsy", "ALBERT")).apply {
assertThat(result).isEqualTo(arrayOf("ALBERT", "angela", "betsy", "BOB", "Zelda"))
@ -642,7 +642,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `test unicode characters`() = sandbox(DEFAULT) {
fun `test unicode characters`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<Int, String>(configuration)
contractExecutor.run<ExamineUnicodeBlock>(0x01f600).apply {
assertThat(result).isEqualTo("EMOTICONS")
@ -656,7 +656,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `test unicode scripts`() = sandbox(DEFAULT) {
fun `test unicode scripts`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<String, Character.UnicodeScript?>(configuration)
contractExecutor.run<ExamineUnicodeScript>("COMMON").apply {
assertThat(result).isEqualTo(Character.UnicodeScript.COMMON)
@ -671,7 +671,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `test users cannot define new classes`() = sandbox(DEFAULT) {
fun `test users cannot define new classes`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<String, Class<*>>(configuration)
assertThatExceptionOfType(SandboxException::class.java)
.isThrownBy { contractExecutor.run<DefineNewClass>("sandbox.java.lang.DJVM") }
@ -693,7 +693,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `test users cannot load new classes`() = sandbox(DEFAULT) {
fun `test users cannot load new classes`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<String, Class<*>>(configuration)
assertThatExceptionOfType(SandboxException::class.java)
.isThrownBy { contractExecutor.run<LoadNewClass>("sandbox.java.lang.DJVM") }
@ -714,7 +714,7 @@ class SandboxExecutorTest : TestBase() {
}
@Test
fun `test users cannot lookup classes`() = sandbox(DEFAULT) {
fun `test users cannot lookup classes`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<String, Class<*>>(configuration)
assertThatExceptionOfType(SandboxException::class.java)
.isThrownBy { contractExecutor.run<FindClass>("sandbox.java.lang.DJVM") }

View File

@ -8,7 +8,7 @@ import java.util.function.Function
class SandboxThrowableTest : TestBase() {
@Test
fun `test user exception handling`() = sandbox(DEFAULT) {
fun `test user exception handling`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<String, Array<String>>(configuration)
contractExecutor.run<ThrowAndCatchExample>("Hello World").apply {
assertThat(result)
@ -17,7 +17,7 @@ class SandboxThrowableTest : TestBase() {
}
@Test
fun `test rethrowing an exception`() = sandbox(DEFAULT) {
fun `test rethrowing an exception`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<String, Array<String>>(configuration)
contractExecutor.run<ThrowAndRethrowExample>("Hello World").apply {
assertThat(result)
@ -26,7 +26,7 @@ class SandboxThrowableTest : TestBase() {
}
@Test
fun `test JVM exceptions still propagate`() = sandbox(DEFAULT) {
fun `test JVM exceptions still propagate`() = parentedSandbox {
val contractExecutor = DeterministicSandboxExecutor<Int, String>(configuration)
contractExecutor.run<TriggerJVMException>(-1).apply {
assertThat(result)

View File

@ -15,7 +15,7 @@ class ClassRewriterTest : TestBase() {
@Test
fun `empty transformer does nothing`() = sandbox(BLANK) {
val callable = newCallable<Empty>()
assertThat(callable).hasNotBeenModified()
assertThat(callable).isSandboxed()
callable.createAndInvoke()
assertThat(runtimeCosts).areZero()
}
@ -130,6 +130,32 @@ class ClassRewriterTest : TestBase() {
.hasInterface("sandbox.java.lang.CharSequence")
.hasBeenModified()
}
@Test
fun `test Java class is owned by parent classloader`() = parentedSandbox {
val stringBuilderClass = loadClass<StringBuilder>().type
assertThat(stringBuilderClass.classLoader).isEqualTo(parentClassLoader)
}
@Test
fun `test user class is owned by new classloader`() = parentedSandbox {
assertThat(loadClass<Empty>())
.hasClassLoader(classLoader)
.hasBeenModified()
}
@Test
fun `test template class is owned by parent classloader`() = parentedSandbox {
assertThat(classLoader.loadForSandbox("sandbox.java.lang.DJVM"))
.hasClassLoader(parentClassLoader)
.hasNotBeenModified()
}
@Test
fun `test pinned class is owned by application classloader`() = parentedSandbox {
val violationClass = loadClass<ThresholdViolationError>().type
assertThat(violationClass).isEqualTo(ThresholdViolationError::class.java)
}
}
@Suppress("unused")

View File

@ -105,9 +105,9 @@ The blob inspector can be started with the following command-line options:
.. code-block:: shell
blob-inspector [-hvV] [--full-parties] [--install-shell-extensions] [--schema]
[--format=type] [--input-format=type]
[--logging-level=<loggingLevel>] [SOURCE]
blob-inspector [-hvV] [--full-parties] [--schema] [--format=type]
[--input-format=type] [--logging-level=<loggingLevel>] SOURCE
[COMMAND]
* ``--format=type``: Output format. Possible values: [YAML, JSON]. Default: YAML.
* ``--input-format=type``: Input format. If the file can't be decoded with the given value it's auto-detected, so you should
@ -116,6 +116,10 @@ The blob inspector can be started with the following command-line options:
* ``--schema``: Print the blob's schema first.
* ``--verbose``, ``--log-to-console``, ``-v``: If set, prints logging to the console as well as to a file.
* ``--logging-level=<loggingLevel>``: Enable logging at this level and higher. Possible values: ERROR, WARN, INFO, DEBUG, TRACE. Default: INFO.
* ``--install-shell-extensions``: Install ``blob-inspector`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.
* ``--help``, ``-h``: Show this help message and exit.
* ``--version``, ``-V``: Print version information and exit.
* ``--version``, ``-V``: Print version information and exit.
Sub-commands
^^^^^^^^^^^^
``install-shell-extensions``: Install ``blob-inspector`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.

View File

@ -1548,9 +1548,9 @@ New features in this release:
* Testnet
* Permissioning infrastructure phase one is built out. The node now has a notion of developer mode vs normal
mode. In developer mode it works like M3 and the SSL certificates used by nodes running on your local
machine all self-sign using a developer key included in the source tree. When developer mode is not active,
* Permissioning infrastructure phase one is built out. The node now has a notion of development mode vs normal
mode. In development mode it works like M3 and the SSL certificates used by nodes running on your local
machine all self-sign using a developer key included in the source tree. When development mode is not active,
the node won't start until it has a signed certificate. Such a certificate can be obtained by simply running
an included command line utility which generates a CSR and submits it to a permissioning service, then waits
for the signed certificate to be returned. Note that currently there is no public Corda testnet, so we are

View File

@ -10,7 +10,7 @@ Users of ``bash`` or ``zsh`` can install an alias and auto-completion for Corda
.. code-block:: shell
java -jar <name-of-JAR>.jar --install-shell-extensions
java -jar <name-of-JAR>.jar install-shell-extensions
Then, either restart your shell, or for ``bash`` users run:
@ -34,7 +34,7 @@ For example, for the Corda node, install the shell extensions using
.. code-block:: shell
java -jar corda-<version>.jar --install-shell-extensions
java -jar corda-<version>.jar install-shell-extensions
And then run the node by running:

View File

@ -144,7 +144,7 @@ absolute path to the node's base directory.
.. note:: The RPC SSL certificate is used by RPC clients to authenticate the connection.
The Node operator must provide RPC clients with a truststore containing the certificate they can trust.
We advise Node operators to not use the P2P keystore for RPC.
The node ships with a command line argument "--just-generate-rpc-ssl-settings", which generates a secure keystore
The node can be run with the "generate-rpc-ssl-settings" command, which generates a secure keystore
and truststore that can be used to secure the RPC connection. You can use this if you have no special requirements.

View File

@ -46,8 +46,8 @@ threats is the [STRIDE](https://en.wikipedia.org/wiki/STRIDE_(security)) framewo
- Spoofing
- Tampering
- Information Disclosure
- Repudiation
- Information Disclosure
- Denial of Service
- Elevation of Privilege

View File

@ -0,0 +1,141 @@
Configuring Responder Flows
===========================
A flow can be a fairly complex thing that interacts with many backend systems, and so it is likely that different users
of a specific CordApp will require differences in how flows interact with their specific infrastructure.
Corda supports this functionality by providing two mechanisms to modify the behaviour of apps in your node.
Subclassing a Flow
------------------
If you have a workflow which is mostly common, but also requires slight alterations in specific situations, most developers would be familiar
with refactoring into `Base` and `Sub` classes. A simple example is shown below.
java
~~~~
.. code-block:: java
@InitiatingFlow
public class Initiator extends FlowLogic<String> {
private final Party otherSide;
public Initiator(Party otherSide) {
this.otherSide = otherSide;
}
@Override
public String call() throws FlowException {
return initiateFlow(otherSide).receive(String.class).unwrap((it) -> it);
}
}
@InitiatedBy(Initiator.class)
public class BaseResponder extends FlowLogic<Void> {
private FlowSession counterpartySession;
public BaseResponder(FlowSession counterpartySession) {
super();
this.counterpartySession = counterpartySession;
}
@Override
public Void call() throws FlowException {
counterpartySession.send(getMessage());
return Void;
}
protected String getMessage() {
return "This Is the Legacy Responder";
}
}
public class SubResponder extends BaseResponder {
public SubResponder(FlowSession counterpartySession) {
super(counterpartySession);
}
@Override
protected String getMessage() {
return "This is the sub responder";
}
}
kotlin
~~~~~~
.. code-block:: kotlin
@InitiatedBy(Initiator::class)
open class BaseResponder(internal val otherSideSession: FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
otherSideSession.send(getMessage())
}
protected open fun getMessage() = "This Is the Legacy Responder"
}
@InitiatedBy(Initiator::class)
class SubResponder(otherSideSession: FlowSession) : BaseResponder(otherSideSession) {
override fun getMessage(): String {
return "This is the sub responder"
}
}
Corda would detect that both ``BaseResponder`` and ``SubResponder`` are configured for responding to ``Initiator``.
Corda will then calculate the hops to ``FlowLogic`` and select the implementation which is furthest distance, ie: the most subclassed implementation.
In the above example, ``SubResponder`` would be selected as the default responder for ``Initiator``
.. note:: The flows do not need to be within the same CordApp, or package, therefore to customise a shared app you obtained from a third party, you'd write your own CorDapp that subclasses the first."
Overriding a flow via node configuration
----------------------------------------
Whilst the subclassing approach is likely to be useful for most applications, there is another mechanism to override this behaviour.
This would be useful if for example, a specific CordApp user requires such a different responder that subclassing an existing flow
would not be a good solution. In this case, it's possible to specify a hardcoded flow via the node configuration.
The configuration section is named ``flowOverrides`` and it accepts an array of ``overrides``
.. container:: codeset
.. code-block:: json
flowOverrides {
overrides=[
{
initiator="net.corda.Initiator"
responder="net.corda.BaseResponder"
}
]
}
The cordform plugin also provides a ``flowOverride`` method within the ``deployNodes`` block which can be used to override a flow. In the below example, we will override
the ``SubResponder`` with ``BaseResponder``
.. container:: codeset
.. code-block:: groovy
node {
name "O=Bank,L=London,C=GB"
p2pPort 10025
rpcUsers = ext.rpcUsers
rpcSettings {
address "localhost:10026"
adminAddress "localhost:10027"
}
extraConfig = ['h2Settings.address' : 'localhost:10035']
flowOverride("net.corda.Initiator", "net.corda.BaseResponder")
}
This will generate the corresponding ``flowOverrides`` section and place it in the configuration for that node.

View File

@ -255,14 +255,19 @@ The network bootstrapper can be started with the following command-line options:
.. code-block:: shell
bootstrapper [-hvV] [--install-shell-extensions] [--no-copy] [--dir=<dir>]
[--logging-level=<loggingLevel>]
bootstrapper [-hvV] [--no-copy] [--dir=<dir>] [--logging-level=<loggingLevel>]
[--minimum-platform-version=<minimumPlatformVersion>] [COMMAND]
* ``--dir=<dir>``: Root directory containing the node configuration files and CorDapp JARs that will form the test network.
It may also contain existing node directories. Defaults to the current directory.
* ``--no-copy``: Don't copy the CorDapp JARs into the nodes' "cordapps" directories.
* ``--verbose``, ``--log-to-console``, ``-v``: If set, prints logging to the console as well as to a file.
* ``--logging-level=<loggingLevel>``: Enable logging at this level and higher. Possible values: ERROR, WARN, INFO, DEBUG, TRACE. Default: INFO.
* ``--install-shell-extensions``: Install ``bootstrapper`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.
* ``--help``, ``-h``: Show this help message and exit.
* ``--version``, ``-V``: Print version information and exit.
* ``--version``, ``-V``: Print version information and exit.
* ``--minimum-platform-version``: The minimum platform version to use in the generated network-parameters.
Sub-commands
^^^^^^^^^^^^
``install-shell-extensions``: Install ``bootstrapper`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.

View File

@ -63,7 +63,7 @@ be used to supplement or replace the HTTP network map. If the same node is adver
latest one is taken.
On startup the node generates its own signed node info file, filename of the format ``nodeInfo-${hash}``. It can also be
generated using the ``--just-generate-node-info`` command line flag without starting the node. To create a simple network
generated using the ``generate-node-info`` sub-command without starting the node. To create a simple network
without the HTTP network map service simply place this file in the ``additional-node-infos`` directory of every node that's
part of this network. For example, a simple way to do this is to use rsync.
@ -210,7 +210,7 @@ you either need to run from the command line:
.. code-block:: shell
java -jar corda.jar --clear-network-map-cache
java -jar corda.jar clear-network-cache
or call RPC method `clearNetworkMapCache` (it can be invoked through the node's shell as `run clearNetworkMapCache`, for more information on
how to log into node's shell see :doc:`shell`). As we are testing and hardening the implementation this step shouldn't be required.

View File

@ -48,25 +48,35 @@ Command-line options
The node can optionally be started with the following command-line options:
* ``--base-directory``, ``-b``: The node working directory where all the files are kept (default: ``.``).
* ``--clear-network-map-cache``, ``-c``: Clears local copy of network map, on node startup it will be restored from server or file system.
* ``--config-file``, ``-f``: The path to the config file. Defaults to ``node.conf``.
* ``--dev-mode``, ``-d``: Runs the node in developer mode. Unsafe in production. Defaults to true on MacOS and desktop versions of Windows. False otherwise.
* ``--initial-registration``: Start initial node registration with the compatibility zone to obtain a certificate from the Doorman.
* ``--just-generate-node-info``: Perform the node start-up task necessary to generate its nodeInfo, save it to disk, then
quit.
* ``--just-generate-rpc-ssl-settings``: Generate the ssl keystore and truststore for a secure RPC connection.
* ``--network-root-truststore``, ``-t``: Network root trust store obtained from network operator.
* ``--network-root-truststore-password``, ``-p``: Network root trust store password obtained from network operator.
* ``--dev-mode``, ``-d``: Runs the node in development mode. Unsafe in production. Defaults to true on MacOS and desktop versions of Windows. False otherwise.
* ``--no-local-shell``, ``-n``: Do not start the embedded shell locally.
* ``--on-unknown-config-keys <[FAIL,WARN,INFO]>``: How to behave on unknown node configuration. Defaults to FAIL.
* ``--sshd``: Enables SSH server for node administration.
* ``--sshd-port``: Sets the port for the SSH server. If not supplied and SSH server is enabled, the port defaults to 2222.
* ``--verbose``, ``--log-to-console``, ``-v``: If set, prints logging to the console as well as to a file.
* ``--logging-level=<loggingLevel>``: Enable logging at this level and higher. Possible values: ERROR, WARN, INFO, DEBUG, TRACE. Default: INFO.
* ``--install-shell-extensions``: Install ``corda`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.
* ``--help``, ``-h``: Show this help message and exit.
* ``--version``, ``-V``: Print version information and exit.
Sub-commands
^^^^^^^^^^^^
``clear-network-cache``: Clears local copy of network map, on node startup it will be restored from server or file system.
``initial-registration``: Starts initial node registration with the compatibility zone to obtain a certificate from the Doorman.
Parameters:
* ``--network-root-truststore``, ``-t`` **required**: Network root trust store obtained from network operator.
* ``--network-root-truststore-password``, ``-p``: Network root trust store password obtained from network operator.
``generate-node-info``: Performs the node start-up tasks necessary to generate the nodeInfo file, saves it to disk, then exits.
``generate-rpc-ssl-settings``: Generates the SSL keystore and truststore for a secure RPC connection.
``install-shell-extensions``: Install ``corda`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.
.. _enabling-remote-debugging:
Enabling remote debugging

View File

@ -107,28 +107,15 @@ Starting the standalone shell
Run the following command from the terminal:
Linux and MacOS
^^^^^^^^^^^^^^^
.. code:: bash
java -jar corda-tools-shell-cli-VERSION_NUMBER.jar [--config-file PATH | --cordpass-directory PATH --commands-directory PATH --host HOST --port PORT
--user USER --password PASSWORD --sshd-port PORT --sshd-hostkey-directory PATH --keystore-password PASSWORD
--keystore-file FILE --truststore-password PASSWORD --truststore-file FILE | --help]
Windows
^^^^^^^
.. code:: bash
corda-shell [-hvV] [--install-shell-extensions]
[--logging-level=<loggingLevel>] [--password=<password>]
corda-shell [-hvV] [--logging-level=<loggingLevel>] [--password=<password>]
[--sshd-hostkey-directory=<sshdHostKeyDirectory>]
[--sshd-port=<sshdPort>] [--truststore-file=<trustStoreFile>]
[--truststore-password=<trustStorePassword>]
[--truststore-type=<trustStoreType>] [--user=<user>] [-a=<host>]
[-c=<cordappDirectory>] [-f=<configFile>] [-o=<commandsDirectory>]
[-p=<port>]
[-p=<port>] [COMMAND]
Where:
@ -146,10 +133,11 @@ Where:
* ``--truststore-type=<trustStoreType>``: The type of the TrustStore (e.g. JKS).
* ``--verbose``, ``--log-to-console``, ``-v``: If set, prints logging to the console as well as to a file.
* ``--logging-level=<loggingLevel>``: Enable logging at this level and higher. Possible values: ERROR, WARN, INFO, DEBUG, TRACE. Default: INFO.
* ``--install-shell-extensions``: Install ``corda-shell`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.
* ``--help``, ``-h``: Show this help message and exit.
* ``--version``, ``-V``: Print version information and exit.
Additionally, the ``install-shell-extensions`` subcommand can be used to install the ``corda-shell`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.
The format of ``config-file``:
.. code:: bash

View File

@ -26,10 +26,10 @@ Get the testing tools
To run the tests and make sure your node is connecting correctly to the network you will need to download and install a
couple of resources.
1. Log into your Cloud VM via SSH.
#. Log into your Cloud VM via SSH.
2. Stop the Corda node(s) running on your cloud instance.
#. Stop the Corda node(s) running on your cloud instance.
.. code:: bash
@ -38,7 +38,7 @@ couple of resources.
.. warning:: If this is an HA node, make sure to stop both the hot and cold nodes before proceeding. Any database migration should be performed whilst both nodes are offline.
3. Download the Resources:
#. Download the Resources:
Download the finance CorDapp and database manager to your VM instance:
@ -50,34 +50,33 @@ couple of resources.
Copy the downloads from ``/home/<USER>/`` to ``/opt/corda/cordapps/``.
This is required to run some flows to check your connections, and to issue/transfer cash to counterparties.
This is required to run some flows to check your connections, and to issue/transfer cash to counterparties. Copy it to
the Corda installation location:
.. code:: bash
sudo cp /home/<USER>/corda-finance-*.jar /opt/corda/cordapps/
4. Create a symbolic link to the shared database driver folder
#. Create a symbolic link to the shared database driver folder
.. code:: bash
sudo ln -s /opt/corda/drivers /opt/corda/plugins
5. Execute the database migration. This is required so that the node database has the right schema for finance transactions defined in the installed CorDapp.
#. Execute the database migration. This is required so that the node database has the right schema for finance transactions defined in the installed CorDapp.
.. code:: bash
cd /opt/corda
sudo java -jar /home/<USER>/corda-tools-database-manager-3.0.jar --base-directory /opt/corda --execute-migration
6. Add the following line to the bottom of your ``node.conf``:
#. Run the following to create a config file for the finance CorDapp:
.. code:: bash
custom : { issuableCurrencies : [ USD ] }
.. note:: Make sure that the config file is in the correct format, e.g., by ensuring that there's a comma at the end of the line prior to the added config.
7. Restart the Corda node:
echo "issuableCurrencies : [ USD ]" > /opt/corda/cordapps/config/corda-finance-<VERSION>-corda.conf
#. Restart the Corda node:
.. code:: bash
@ -85,17 +84,19 @@ couple of resources.
Your node is now running the Finance Cordapp.
.. note:: You can double-check that the CorDapp is loaded in the log file ``/opt/corda/logs/node-<VM-NAME>.log``. This file will list installed apps at startup. Search for ``Loaded CorDapps`` in the logs.
.. note:: You can double-check that the CorDapp is loaded in the log file ``/opt/corda/logs/node-<VM-NAME>.log``. This
file will list installed apps at startup. Search for ``Loaded CorDapps`` in the logs.
8. Now download the Node Explorer to your **LOCAL** machine:
#. Now download the Node Explorer to your **LOCAL** machine:
.. code:: bash
wget http://downloads.corda.net/tools/explorer/ENT-3.1/corda-tools-explorer-3.1.jar
.. warning:: The Enterprise Node Explorer is incompatible with open source versions of Corda and vice versa as they currently use different serialisation schemes (Kryo vs AMQP).
.. warning:: The Enterprise Node Explorer is incompatible with open source versions of Corda and vice versa as they currently
use different serialisation schemes (Kryo vs AMQP).
9. Run the Node Explorer tool on your **LOCAL** machine.
#. Run the Node Explorer tool on your **LOCAL** machine.
.. code:: bash
@ -110,8 +111,10 @@ Connect to the node
To connect to the node you will need:
* The IP address of your node (the public IP of your cloud instance). You can find this in the instance page of your cloud console.
* The port number of the RPC interface to the node, specified in ``/opt/corda/node.conf`` in the ``rpcSettings`` section, (by default this is 10003 on Testnet).
* The username and password of the RPC interface of the node, also in the ``node.conf`` in the ``rpcUsers`` section, (by default the username is ``cordazoneservice`` on Testnet).
* The port number of the RPC interface to the node, specified in ``/opt/corda/node.conf`` in the ``rpcSettings`` section,
(by default this is 10003 on Testnet).
* The username and password of the RPC interface of the node, also in the ``node.conf`` in the ``rpcUsers`` section,
(by default the username is ``cordazoneservice`` on Testnet).
Click on ``Connect`` to log into the node.
@ -122,7 +125,8 @@ Once Explorer has logged in to your node over RPC click on the ``Network`` tab i
.. image:: resources/explorer-network.png
If your Enterprise node is correctly configured and connected to the Testnet then you should be able to see the identities of your node, the Testnet notary and the network map listing all the counterparties currently on the network.
If your Enterprise node is correctly configured and connected to the Testnet then you should be able to see the identities of
your node, the Testnet notary and the network map listing all the counterparties currently on the network.
Test issuance transaction
@ -140,7 +144,8 @@ Click ``Execute`` and the transaction will start.
.. image:: resources/explorer-cash-issue3.png
Click on the red X to close the notification window and click on ``Transactions`` tab to see the transaction in progress, or wait for a success message to be displayed:
Click on the red X to close the notification window and click on ``Transactions`` tab to see the transaction in progress,
or wait for a success message to be displayed:
.. image:: resources/explorer-transactions.png

View File

@ -21,6 +21,7 @@ class Configuration(
nodeInterface.dbPort,
password = DEFAULT_PASSWORD
),
// TODO This is not being used when it could be. The call-site is using configElements instead.
val notary: NotaryConfiguration = NotaryConfiguration(),
val cordapps: CordappConfiguration = CordappConfiguration(),
vararg configElements: ConfigurationTemplate
@ -28,9 +29,7 @@ class Configuration(
private val developerMode = System.getProperty("USE_NETWORK_SERVICES") == null
val cordaX500Name: CordaX500Name by lazy({
CordaX500Name(name, location, country)
})
val cordaX500Name: CordaX500Name = CordaX500Name(name, location, country)
private val basicConfig = """
|myLegalName="C=$country,L=$location,O=$name"

View File

@ -1,5 +1,7 @@
package net.corda.behave.node.configuration
// TODO This is a ConfigurationTemplate but is never used as one. Therefore the private "applications" list is never used
// and thus includeFinance isn't necessary either. Something is amiss.
class CordappConfiguration(var apps: List<String> = emptyList(), val includeFinance: Boolean = false) : ConfigurationTemplate() {
private val applications = apps + if (includeFinance) {

View File

@ -7,6 +7,10 @@ class CurrencyConfiguration(private val issuableCurrencies: List<String>) : Conf
if (issuableCurrencies.isEmpty()) {
""
} else {
// TODO This is no longer correct. issuableCurrencies is a config of the finance app and belongs
// in a separate .conf file for the app (in the config sub-directory, with a filename matching the CorDapp
// jar filename). It is no longer read in from the node conf file. There seem to be pieces missing in the
// behave framework to allow one to do this.
"""
|custom : {
| issuableCurrencies : [

View File

@ -7,9 +7,9 @@ import net.corda.core.messaging.startFlow
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.finance.flows.CashConfigDataFlow
import net.corda.finance.flows.CashIssueFlow
import net.corda.finance.flows.CashPaymentFlow
import net.corda.finance.internal.CashConfigDataFlow
import java.util.*
import java.util.concurrent.TimeUnit

View File

@ -188,7 +188,7 @@ class FlowWorkerStartStopTest {
rpcUsers = listOf(), verifierType = VerifierType.InMemory, flowTimeout = FlowTimeoutConfiguration(5.seconds, 3, 1.0),
p2pAddress = NetworkHostAndPort("localhost", 1), rpcSettings = NodeRpcSettings(NetworkHostAndPort("localhost", 1), null, ssl = null),
relay = null, messagingServerAddress = null, enterpriseConfiguration = EnterpriseConfiguration(mutualExclusionConfiguration = MutualExclusionConfiguration(updateInterval = 0, waitInterval = 0)),
notary = null)
notary = null, flowOverrides = FlowOverrideConfig(listOf()))
}
private fun createFlowWorkerBroker(config: NodeConfiguration, maxMessageSize: Int): ArtemisBroker {

View File

@ -230,7 +230,7 @@ class FlowWorkerTest {
rpcUsers = listOf(), verifierType = VerifierType.InMemory, flowTimeout = FlowTimeoutConfiguration(5.seconds, 3, 1.0),
p2pAddress = NetworkHostAndPort("localhost", 1), rpcSettings = NodeRpcSettings(NetworkHostAndPort("localhost", 1), null, ssl = null),
relay = null, messagingServerAddress = null, enterpriseConfiguration = EnterpriseConfiguration(mutualExclusionConfiguration = MutualExclusionConfiguration(updateInterval = 0, waitInterval = 0)),
notary = null)
notary = null, flowOverrides = FlowOverrideConfig(listOf()))
}
private fun createFlowWorkerBroker(config: NodeConfiguration, maxMessageSize: Int): ArtemisBroker {

View File

@ -137,7 +137,7 @@ class FlowWorkerServiceHub(override val configuration: NodeConfiguration, overri
@Suppress("LeakingThis")
override val validatedTransactions: WritableTransactionStorage = DBTransactionStorage(database, cacheFactory).tokenize()
override val attachments = NodeAttachmentService(metricRegistry, cacheFactory, database).tokenize()
override val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(), attachments).tokenize()
override val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(emptyList()), attachments).tokenize()
@Suppress("LeakingThis")
override val keyManagementService = PersistentKeyManagementService(cacheFactory, identityService, database).tokenize()
private val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, validatedTransactions)

View File

@ -218,7 +218,7 @@ data class RpcFlowWorkerDriverDSL(private val driverDSL: DriverDSLImpl) : Intern
rpcUsers = listOf(), verifierType = VerifierType.InMemory, flowTimeout = FlowTimeoutConfiguration(5.seconds, 3, 1.0),
p2pAddress = NetworkHostAndPort("localhost", 1), rpcSettings = NodeRpcSettings(NetworkHostAndPort("localhost", 1), null, ssl = null),
relay = null, messagingServerAddress = null, enterpriseConfiguration = EnterpriseConfiguration(mutualExclusionConfiguration = MutualExclusionConfiguration(updateInterval = 0, waitInterval = 0), externalBridge = true),
notary = null)
notary = null, flowOverrides = FlowOverrideConfig(listOf()))
}
private fun createRpcWorkerBroker(config: NodeConfiguration): ArtemisBroker {

View File

@ -98,7 +98,7 @@ class RpcWorkerServiceHub(override val configuration: NodeConfiguration, overrid
private val networkMapClient: NetworkMapClient? = configuration.networkServices?.let { NetworkMapClient(it.networkMapURL, versionInfo) }
override val attachments = NodeAttachmentService(metricRegistry, cacheFactory, database)
override val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(), attachments)
override val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(emptyList()), attachments)
@Suppress("LeakingThis")
override val keyManagementService = PersistentKeyManagementService(cacheFactory, identityService, database)

View File

@ -1,38 +0,0 @@
package net.corda.finance.flows.test
import net.corda.core.messaging.startFlow
import net.corda.core.utilities.getOrThrow
import net.corda.finance.EUR
import net.corda.finance.USD
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.DUMMY_BANK_A_NAME
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.finance.flows.CashConfigDataFlow
import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.driver
import net.corda.testing.internal.IntegrationTest
import net.corda.testing.internal.IntegrationTestSchemas
import net.corda.testing.internal.toDatabaseSchemaName
import org.assertj.core.api.Assertions.assertThat
import org.junit.ClassRule
import org.junit.Test
class CashConfigDataFlowTest : IntegrationTest() {
companion object {
@ClassRule
@JvmField
val databaseSchemas = IntegrationTestSchemas(ALICE_NAME.toDatabaseSchemaName(), BOB_NAME.toDatabaseSchemaName(),
DUMMY_BANK_A_NAME.toDatabaseSchemaName(), DUMMY_NOTARY_NAME.toDatabaseSchemaName())
}
@Test
fun `issuable currencies are read in from node config`() {
driver(DriverParameters(
extraCordappPackagesToScan = listOf("net.corda.finance.flows"),
notarySpecs = emptyList())) {
val node = startNode(customOverrides = mapOf("custom" to mapOf("issuableCurrencies" to listOf("EUR", "USD")))).getOrThrow()
val config = node.rpc.startFlow(::CashConfigDataFlow).returnValue.getOrThrow()
assertThat(config.issuableCurrencies).containsExactly(EUR, USD)
}
}
}

View File

@ -1,77 +0,0 @@
package net.corda.finance.flows
import co.paralleluniverse.fibers.Suspendable
import com.typesafe.config.ConfigFactory
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByRPC
import net.corda.core.internal.declaredField
import net.corda.core.node.AppServiceHub
import net.corda.core.node.services.CordaService
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.finance.CHF
import net.corda.finance.EUR
import net.corda.finance.GBP
import net.corda.finance.USD
import net.corda.finance.flows.ConfigHolder.Companion.supportedCurrencies
import java.io.IOException
import java.io.InputStream
import java.nio.file.Files
import java.nio.file.OpenOption
import java.nio.file.Path
import java.nio.file.Paths
import java.util.*
// TODO Until apps have access to their own config, we'll hack things by first getting the baseDirectory, read the node.conf
// again to get our config and store it here for access by our flow
@CordaService
class ConfigHolder(services: AppServiceHub) : SingletonSerializeAsToken() {
companion object {
val supportedCurrencies = listOf(USD, GBP, CHF, EUR)
// TODO: ENT-2040 - After GA, the Finance app should be fully decoupled from use of internal APIs on both OS and ENT!
private operator fun Path.div(other: String): Path = resolve(other)
private operator fun String.div(other: String): Path = Paths.get(this) / other
private fun Path.inputStream(vararg options: OpenOption): InputStream = Files.newInputStream(this, *options)
private inline fun <R> Path.read(vararg options: OpenOption, block: (InputStream) -> R): R = inputStream(*options).use(block)
}
val issuableCurrencies: List<Currency>
init {
// Warning!! You are about to see a major hack!
val baseDirectory = services.declaredField<Any>("serviceHub").value
.let { it.javaClass.getMethod("getConfiguration").apply { isAccessible = true }.invoke(it) }
.let { it.javaClass.getMethod("getBaseDirectory").apply { isAccessible = true }.invoke(it) }
.let { it.javaClass.getMethod("toString").apply { isAccessible = true }.invoke(it) as String }
var issuableCurrenciesValue: List<Currency>
try {
val config = (baseDirectory / "node.conf").read { ConfigFactory.parseReader(it.reader()) }
if (config.hasPath("custom.issuableCurrencies")) {
issuableCurrenciesValue = config.getStringList("custom.issuableCurrencies").map { Currency.getInstance(it) }
require(supportedCurrencies.containsAll(issuableCurrenciesValue))
} else {
issuableCurrenciesValue = emptyList()
}
} catch (e: IOException) {
issuableCurrenciesValue = emptyList()
}
issuableCurrencies = issuableCurrenciesValue
}
}
/**
* Flow to obtain cash cordapp app configuration.
*/
@StartableByRPC
class CashConfigDataFlow : FlowLogic<CashConfiguration>() {
@Suspendable
override fun call(): CashConfiguration {
val configHolder = serviceHub.cordaService(ConfigHolder::class.java)
return CashConfiguration(configHolder.issuableCurrencies, supportedCurrencies)
}
}
@CordaSerializable
data class CashConfiguration(val issuableCurrencies: List<Currency>, val supportedCurrencies: List<Currency>)

View File

@ -0,0 +1,48 @@
package net.corda.finance.internal
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByRPC
import net.corda.core.internal.uncheckedCast
import net.corda.core.node.AppServiceHub
import net.corda.core.node.services.CordaService
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.finance.CHF
import net.corda.finance.EUR
import net.corda.finance.GBP
import net.corda.finance.USD
import net.corda.finance.internal.ConfigHolder.Companion.supportedCurrencies
import java.util.*
@CordaService
class ConfigHolder(services: AppServiceHub) : SingletonSerializeAsToken() {
companion object {
val supportedCurrencies = listOf(USD, GBP, CHF, EUR)
}
val issuableCurrencies: List<Currency>
init {
val issuableCurrenciesStringList: List<String> = uncheckedCast(services.getAppContext().config.get("issuableCurrencies"))
issuableCurrencies = issuableCurrenciesStringList.map(Currency::getInstance)
(issuableCurrencies - supportedCurrencies).let {
require(it.isEmpty()) { "$it are not supported currencies" }
}
}
}
/**
* Flow to obtain cash cordapp app configuration.
*/
@StartableByRPC
class CashConfigDataFlow : FlowLogic<CashConfiguration>() {
@Suspendable
override fun call(): CashConfiguration {
val configHolder = serviceHub.cordaService(ConfigHolder::class.java)
return CashConfiguration(configHolder.issuableCurrencies, supportedCurrencies)
}
}
@CordaSerializable
data class CashConfiguration(val issuableCurrencies: List<Currency>, val supportedCurrencies: List<Currency>)

View File

@ -0,0 +1,29 @@
package net.corda.finance.internal
import net.corda.core.internal.packageName
import net.corda.core.utilities.getOrThrow
import net.corda.finance.EUR
import net.corda.finance.USD
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.MockNetworkParameters
import net.corda.testing.node.MockNodeParameters
import net.corda.testing.node.internal.cordappForPackages
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Test
class CashConfigDataFlowTest {
private val mockNet = MockNetwork(emptyList(), MockNetworkParameters(threadPerNode = true))
@After
fun cleanUp() = mockNet.stopNodes()
@Test
fun `issuable currencies read in from cordapp config`() {
val node = mockNet.createNode(MockNodeParameters(additionalCordapps = listOf(
cordappForPackages(javaClass.packageName).withConfig(mapOf("issuableCurrencies" to listOf("EUR", "USD")))
)))
val config = node.startFlow(CashConfigDataFlow()).getOrThrow()
assertThat(config.issuableCurrencies).containsExactly(EUR, USD)
}
}

View File

@ -99,6 +99,8 @@ const val DEV_CA_TRUST_STORE_FILE: String = "cordatruststore.jks"
const val DEV_CA_TRUST_STORE_PASS: String = "trustpass"
const val DEV_CA_TRUST_STORE_PRIVATE_KEY_PASS: String = "trustpasskeypass"
val DEV_CERTIFICATES: List<X509Certificate> get() = listOf(DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate)
// We need a class so that we can get hold of the class loader
internal object DevCaHelper {
fun loadDevCa(alias: String): CertificateAndKeyPair {

View File

@ -65,7 +65,7 @@ internal constructor(private val initSerEnv: Boolean,
"java",
"-jar",
"corda.jar",
"--just-generate-node-info"
"generate-node-info"
)
private const val LOGS_DIR_NAME = "logs"

View File

@ -82,12 +82,11 @@ class FlowCheckpointVersionNodeStartupCheckTest: IntegrationTest() {
// Create the CorDapp jar file manually first to get hold of the directory that will contain it so that we can
// rename the filename later. The cordappDir, which acts as pointer to the jar file, does not get renamed.
val cordappDir = TestCordappDirectories.getJarDirectory(cordapp)
val cordappJar = cordappDir.list().single()
val cordappJar = cordappDir.list().single { it.toString().endsWith(".jar") }
createSuspendedFlowInBob(setOf(cordapp))
// Rename the jar file. TestCordappDirectories caches the location of the jar file but the use of the random
// UUID in the name means there's zero chance of contaminating another test.
// Rename the jar file.
cordappJar.moveTo(cordappDir / "renamed-${cordappJar.fileName}")
assertBobFailsToStartWithLogMessage(
@ -101,13 +100,13 @@ class FlowCheckpointVersionNodeStartupCheckTest: IntegrationTest() {
fun `restart node with incompatible version of suspended flow due to different jar hash`() {
driver(parametersForRestartingNodes()) {
val originalCordapp = defaultCordapp.withName("different-jar-hash-test-${UUID.randomUUID()}")
val originalCordappJar = TestCordappDirectories.getJarDirectory(originalCordapp).list().single()
val originalCordappJar = TestCordappDirectories.getJarDirectory(originalCordapp).list().single { it.toString().endsWith(".jar") }
createSuspendedFlowInBob(setOf(originalCordapp))
// The vendor is part of the MANIFEST so changing it is sufficient to change the jar hash
val modifiedCordapp = originalCordapp.withVendor("${originalCordapp.vendor}-modified")
val modifiedCordappJar = TestCordappDirectories.getJarDirectory(modifiedCordapp).list().single()
val modifiedCordappJar = TestCordappDirectories.getJarDirectory(modifiedCordapp).list().single { it.toString().endsWith(".jar") }
modifiedCordappJar.moveTo(originalCordappJar, REPLACE_EXISTING)
assertBobFailsToStartWithLogMessage(

View File

@ -0,0 +1,85 @@
package net.corda.node.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.flows.*
import net.corda.core.identity.Party
import net.corda.core.messaging.startFlow
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.unwrap
import net.corda.testing.core.singleIdentity
import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.driver
import net.corda.testing.node.internal.cordappForClasses
import org.hamcrest.CoreMatchers.`is`
import org.junit.Assert
import org.junit.Test
class FlowOverrideTests {
@StartableByRPC
@InitiatingFlow
class Ping(private val pongParty: Party) : FlowLogic<String>() {
@Suspendable
override fun call(): String {
val pongSession = initiateFlow(pongParty)
return pongSession.sendAndReceive<String>("PING").unwrap { it }
}
}
@InitiatedBy(Ping::class)
open class Pong(private val pingSession: FlowSession) : FlowLogic<Unit>() {
companion object {
val PONG = "PONG"
}
@Suspendable
override fun call() {
pingSession.send(PONG)
}
}
@InitiatedBy(Ping::class)
class Pong2(private val pingSession: FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
pingSession.send("PONGPONG")
}
}
@InitiatedBy(Ping::class)
class Pongiest(private val pingSession: FlowSession) : Pong(pingSession) {
companion object {
val GORGONZOLA = "Gorgonzola"
}
@Suspendable
override fun call() {
pingSession.send(GORGONZOLA)
}
}
private val nodeAClasses = setOf(Ping::class.java,
Pong::class.java, Pongiest::class.java)
private val nodeBClasses = setOf(Ping::class.java, Pong::class.java)
@Test
fun `should use the most specific implementation of a responding flow`() {
driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = emptySet())) {
val nodeA = startNode(additionalCordapps = setOf(cordappForClasses(*nodeAClasses.toTypedArray()))).getOrThrow()
val nodeB = startNode(additionalCordapps = setOf(cordappForClasses(*nodeBClasses.toTypedArray()))).getOrThrow()
Assert.assertThat(nodeB.rpc.startFlow(::Ping, nodeA.nodeInfo.singleIdentity()).returnValue.getOrThrow(), `is`(net.corda.node.flows.FlowOverrideTests.Pongiest.GORGONZOLA))
}
}
@Test
fun `should use the overriden implementation of a responding flow`() {
val flowOverrides = mapOf(Ping::class.java to Pong::class.java)
driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = emptySet())) {
val nodeA = startNode(additionalCordapps = setOf(cordappForClasses(*nodeAClasses.toTypedArray())), flowOverrides = flowOverrides).getOrThrow()
val nodeB = startNode(additionalCordapps = setOf(cordappForClasses(*nodeBClasses.toTypedArray()))).getOrThrow()
Assert.assertThat(nodeB.rpc.startFlow(::Ping, nodeA.nodeInfo.singleIdentity()).returnValue.getOrThrow(), `is`(net.corda.node.flows.FlowOverrideTests.Pong.PONG))
}
}
}

View File

@ -7,12 +7,13 @@ import net.corda.core.flows.InitiatingFlow
import net.corda.core.identity.Party
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.unwrap
import net.corda.testing.internal.IntegrationTestSchemas
import net.corda.testing.internal.toDatabaseSchemaName
import net.corda.testing.core.singleIdentity
import net.corda.testing.node.internal.NodeBasedTest
import net.corda.node.internal.NodeFlowManager
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.singleIdentity
import net.corda.testing.internal.IntegrationTestSchemas
import net.corda.testing.internal.toDatabaseSchemaName
import net.corda.testing.node.internal.NodeBasedTest
import net.corda.testing.node.internal.startFlow
import org.assertj.core.api.Assertions.assertThat
import org.junit.ClassRule
@ -27,9 +28,10 @@ class FlowVersioningTest : NodeBasedTest() {
@Test
fun `getFlowContext returns the platform version for core flows`() {
val bobFlowManager = NodeFlowManager()
val alice = startNode(ALICE_NAME, platformVersion = 2)
val bob = startNode(BOB_NAME, platformVersion = 3)
bob.node.installCoreFlow(PretendInitiatingCoreFlow::class, ::PretendInitiatedCoreFlow)
val bob = startNode(BOB_NAME, platformVersion = 3, flowManager = bobFlowManager)
bobFlowManager.registerInitiatedCoreFlowFactory(PretendInitiatingCoreFlow::class, ::PretendInitiatedCoreFlow)
val (alicePlatformVersionAccordingToBob, bobPlatformVersionAccordingToAlice) = alice.services.startFlow(
PretendInitiatingCoreFlow(bob.info.singleIdentity())).resultFuture.getOrThrow()
assertThat(alicePlatformVersionAccordingToBob).isEqualTo(2)
@ -54,4 +56,5 @@ class FlowVersioningTest : NodeBasedTest() {
@Suspendable
override fun call() = otherSideSession.send(otherSideSession.getCounterpartyFlowInfo().flowVersion)
}
}

View File

@ -10,5 +10,5 @@ fun main(args: Array<String>) {
// Pass the arguments to the Node factory. In the Enterprise edition, this line is modified to point to a subclass.
// It will exit the process in case of startup failure and is not intended to be used by embedders. If you want
// to embed Node in your own container, instantiate it directly and set up the configuration objects yourself.
EnterpriseNode.Startup().start(args)
EnterpriseNode.NodeCli().start(args)
}

View File

@ -2,18 +2,18 @@ package net.corda.node
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigRenderOptions
import net.corda.core.internal.div
import net.corda.core.internal.exists
import net.corda.core.utilities.Try
import net.corda.node.services.config.ConfigHelper
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.NodeConfigurationImpl
import net.corda.node.services.config.parseAsNodeConfiguration
import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy
import picocli.CommandLine.Option
import java.nio.file.Path
import java.nio.file.Paths
class NodeCmdLineOptions {
open class SharedNodeCmdLineOptions {
@Option(
names = ["-b", "--base-directory"],
description = ["The node working directory where all the files are kept."]
@ -27,6 +27,53 @@ class NodeCmdLineOptions {
private var _configFile: Path? = null
val configFile: Path get() = _configFile ?: (baseDirectory / "node.conf")
@Option(
names = ["--on-unknown-config-keys"],
description = ["How to behave on unknown node configuration. \${COMPLETION-CANDIDATES}"]
)
var unknownConfigKeysPolicy: UnknownConfigKeysPolicy = UnknownConfigKeysPolicy.FAIL
@Option(
names = ["-d", "--dev-mode"],
description = ["Runs the node in development mode. Unsafe for production."]
)
var devMode: Boolean? = null
open fun loadConfig(): NodeConfiguration {
return getRawConfig().parseAsNodeConfiguration(unknownConfigKeysPolicy::handle)
}
protected fun getRawConfig(): Config {
val rawConfig = ConfigHelper.loadConfig(
baseDirectory,
configFile
)
if (devMode == true) {
println("Config:\n${rawConfig.root().render(ConfigRenderOptions.defaults())}")
}
return rawConfig
}
fun copyFrom(other: SharedNodeCmdLineOptions) {
baseDirectory = other.baseDirectory
_configFile = other._configFile
unknownConfigKeysPolicy= other.unknownConfigKeysPolicy
devMode = other.devMode
}
}
class InitialRegistrationCmdLineOptions : SharedNodeCmdLineOptions() {
override fun loadConfig(): NodeConfiguration {
return getRawConfig().parseAsNodeConfiguration(unknownConfigKeysPolicy::handle).also { config ->
require(!config.devMode) { "Registration cannot occur in development mode" }
require(config.compatibilityZoneURL != null || config.networkServices != null) {
"compatibilityZoneURL or networkServices must be present in the node configuration file in registration mode."
}
}
}
}
open class NodeCmdLineOptions : SharedNodeCmdLineOptions() {
@Option(
names = ["--sshd"],
description = ["If set, enables SSH server for node administration."]
@ -45,84 +92,66 @@ class NodeCmdLineOptions {
)
var noLocalShell: Boolean = false
@Option(
names = ["--initial-registration"],
description = ["Start initial node registration with Corda network to obtain certificate from the permissioning server."]
)
var isRegistration: Boolean = false
@Option(
names = ["-t", "--network-root-truststore"],
description = ["Network root trust store obtained from network operator."]
)
private var _networkRootTrustStorePath: Path? = null
val networkRootTrustStorePath: Path get() = _networkRootTrustStorePath ?: baseDirectory / "certificates" / "network-root-truststore.jks"
@Option(
names = ["-p", "--network-root-truststore-password"],
description = ["Network root trust store password obtained from network operator."]
)
var networkRootTrustStorePassword: String? = null
@Option(
names = ["--on-unknown-config-keys"],
description = ["How to behave on unknown node configuration. \${COMPLETION-CANDIDATES}"]
)
var unknownConfigKeysPolicy: UnknownConfigKeysPolicy = UnknownConfigKeysPolicy.FAIL
@Option(
names = ["-d", "--dev-mode"],
description = ["Run the node in developer mode. Unsafe for production."]
)
var devMode: Boolean? = null
@Option(
names = ["--just-generate-node-info"],
description = ["Perform the node start-up task necessary to generate its node info, save it to disk, then quit"]
description = ["DEPRECATED. Performs the node start-up tasks necessary to generate the nodeInfo file, saves it to disk, then exits."],
hidden = true
)
var justGenerateNodeInfo: Boolean = false
@Option(
names = ["--just-generate-rpc-ssl-settings"],
description = ["Generate the SSL key and trust stores for a secure RPC connection."]
description = ["DEPRECATED. Generates the SSL key and trust stores for a secure RPC connection."],
hidden = true
)
var justGenerateRpcSslCerts: Boolean = false
@Option(
names = ["-c", "--clear-network-map-cache"],
description = ["Clears local copy of network map, on node startup it will be restored from server or file system."]
names = ["--clear-network-map-cache"],
description = ["DEPRECATED. Clears local copy of network map, on node startup it will be restored from server or file system."],
hidden = true
)
var clearNetworkMapCache: Boolean = false
val nodeRegistrationOption: NodeRegistrationOption? by lazy {
if (isRegistration) {
requireNotNull(networkRootTrustStorePassword) { "Network root trust store password must be provided in registration mode using --network-root-truststore-password." }
require(networkRootTrustStorePath.exists()) { "Network root trust store path: '$networkRootTrustStorePath' doesn't exist" }
NodeRegistrationOption(networkRootTrustStorePath, networkRootTrustStorePassword!!)
} else {
null
}
}
@Option(
names = ["--initial-registration"],
description = ["DEPRECATED. Starts initial node registration with Corda network to obtain certificate from the permissioning server."],
hidden = true
)
var isRegistration: Boolean = false
fun loadConfig(): Pair<Config, Try<NodeConfiguration>> {
@Option(
names = ["-t", "--network-root-truststore"],
description = ["DEPRECATED. Network root trust store obtained from network operator."],
hidden = true
)
var networkRootTrustStorePathParameter: Path? = null
@Option(
names = ["-p", "--network-root-truststore-password"],
description = ["DEPRECATED. Network root trust store password obtained from network operator."],
hidden = true
)
var networkRootTrustStorePassword: String? = null
override fun loadConfig(): NodeConfiguration {
val rawConfig = ConfigHelper.loadConfig(
baseDirectory,
configFile,
configOverrides = ConfigFactory.parseMap(mapOf("noLocalShell" to this.noLocalShell) +
if (sshdServer) mapOf("sshd" to mapOf("port" to sshdServerPort.toString())) else emptyMap<String, Any>() +
if (devMode != null) mapOf("devMode" to this.devMode) else emptyMap())
if (devMode != null) mapOf("devMode" to this.devMode) else emptyMap())
)
return rawConfig to Try.on {
rawConfig.parseAsNodeConfiguration(unknownConfigKeysPolicy::handle).also { config ->
if (nodeRegistrationOption != null) {
require(!config.devMode) { "Registration cannot occur in devMode" }
require(config.compatibilityZoneURL != null || config.networkServices != null) {
"compatibilityZoneURL or networkServices must be present in the node configuration file in registration mode."
}
return rawConfig.parseAsNodeConfiguration(unknownConfigKeysPolicy::handle).also { config ->
if (isRegistration) {
require(!config.devMode) { "Registration cannot occur in development mode" }
require(config.compatibilityZoneURL != null || config.networkServices != null) {
"compatibilityZoneURL or networkServices must be present in the node configuration file in registration mode."
}
}
}
}
}
data class NodeRegistrationOption(val networkRootTrustStorePath: Path, val networkRootTrustStorePassword: String)

View File

@ -46,7 +46,6 @@ import net.corda.node.services.FinalityHandler
import net.corda.node.services.NotaryChangeHandler
import net.corda.node.services.api.*
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.NotaryConfig
import net.corda.node.services.config.configureWithDevSSLCertificate
import net.corda.node.services.config.rpc.NodeRpcOptions
import net.corda.node.services.config.shell.toShellConfig
@ -70,6 +69,7 @@ import net.corda.node.services.transactions.SimpleNotaryService
import net.corda.node.services.upgrade.ContractUpgradeServiceImpl
import net.corda.node.services.vault.NodeVaultService
import net.corda.node.utilities.*
import net.corda.nodeapi.internal.DEV_CERTIFICATES
import net.corda.nodeapi.internal.NodeInfoAndSigned
import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.config.CertificateStore
@ -88,7 +88,6 @@ import org.slf4j.Logger
import rx.Observable
import rx.Scheduler
import java.io.IOException
import java.lang.UnsupportedOperationException
import java.lang.management.ManagementFactory
import java.lang.reflect.InvocationTargetException
import java.nio.file.Paths
@ -102,13 +101,10 @@ import java.time.Clock
import java.time.Duration
import java.time.format.DateTimeParseException
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit.MINUTES
import java.util.concurrent.TimeUnit.SECONDS
import kotlin.collections.set
import kotlin.reflect.KClass
import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair
/**
@ -123,11 +119,11 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
val platformClock: CordaClock,
cacheFactoryPrototype: BindableNamedCacheFactory,
protected val versionInfo: VersionInfo,
protected val flowManager: FlowManager,
protected val serverThread: AffinityExecutor.ServiceAffinityExecutor,
protected val busyNodeLatch: ReusableLatch = ReusableLatch()) : SingletonSerializeAsToken() {
protected abstract val log: Logger
@Suppress("LeakingThis")
private var tokenizableServices: MutableList<Any>? = mutableListOf(platformClock, this)
@ -169,7 +165,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
val transactionStorage = makeTransactionStorage(configuration.transactionCacheSizeBytes).tokenize()
val networkMapClient: NetworkMapClient? = configuration.networkServices?.let { NetworkMapClient(it.networkMapURL, versionInfo) }
val attachments = NodeAttachmentService(metricRegistry, cacheFactory, database).tokenize()
val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(), attachments).tokenize()
val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(configuration.cordappDirectories), attachments).tokenize()
@Suppress("LeakingThis")
val keyManagementService = makeKeyManagementService(identityService).tokenize()
val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, transactionStorage).also {
@ -213,7 +209,6 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
).tokenize().closeOnStop()
private val cordappServices = MutableClassToInstanceMap.create<SerializeAsToken>()
private val flowFactories = ConcurrentHashMap<Class<out FlowLogic<*>>, InitiatedFlowFactory<*>>()
private val shutdownExecutor = Executors.newSingleThreadExecutor()
protected abstract val transactionVerifierWorkerCount: Int
@ -239,7 +234,8 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
private var _started: S? = null
private fun <T : Any> T.tokenize(): T {
tokenizableServices?.add(this) ?: throw IllegalStateException("The tokenisable services list has already been finalised")
tokenizableServices?.add(this)
?: throw IllegalStateException("The tokenisable services list has already been finalised")
return this
}
@ -530,10 +526,12 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
// CorDapp will be generated.
generatedCordapps += VirtualCordapp.generateSimpleNotaryCordapp(versionInfo)
}
val blacklistedCerts = if (configuration.devMode) emptyList() else DEV_CERTIFICATES
return JarScanningCordappLoader.fromDirectories(
configuration.cordappDirectories,
versionInfo,
extraCordapps = generatedCordapps
extraCordapps = generatedCordapps,
blacklistedCerts = blacklistedCerts
)
}
@ -624,91 +622,27 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
}
private fun registerCordappFlows() {
cordappLoader.cordapps.flatMap { it.initiatedFlows }
.forEach {
cordappLoader.cordapps.forEach { cordapp ->
cordapp.initiatedFlows.groupBy { it.requireAnnotation<InitiatedBy>().value.java }.forEach { initiator, responders ->
responders.forEach { responder ->
try {
registerInitiatedFlowInternal(smm, it, track = false)
flowManager.registerInitiatedFlow(initiator, responder)
} catch (e: NoSuchMethodException) {
log.error("${it.name}, as an initiated flow, must have a constructor with a single parameter " +
log.error("${responder.name}, as an initiated flow, must have a constructor with a single parameter " +
"of type ${Party::class.java.name}")
} catch (e: Exception) {
log.error("Unable to register initiated flow ${it.name}", e)
throw e
}
}
}
fun <T : FlowLogic<*>> registerInitiatedFlow(smm: StateMachineManager, initiatedFlowClass: Class<T>): Observable<T> {
return registerInitiatedFlowInternal(smm, initiatedFlowClass, track = true)
}
// TODO remove once not needed
private fun deprecatedFlowConstructorMessage(flowClass: Class<*>): String {
return "Installing flow factory for $flowClass accepting a ${Party::class.java.simpleName}, which is deprecated. " +
"It should accept a ${FlowSession::class.java.simpleName} instead"
}
private fun <F : FlowLogic<*>> registerInitiatedFlowInternal(smm: StateMachineManager, initiatedFlow: Class<F>, track: Boolean): Observable<F> {
val constructors = initiatedFlow.declaredConstructors.associateBy { it.parameterTypes.toList() }
val flowSessionCtor = constructors[listOf(FlowSession::class.java)]?.apply { isAccessible = true }
val ctor: (FlowSession) -> F = if (flowSessionCtor == null) {
// Try to fallback to a Party constructor
val partyCtor = constructors[listOf(Party::class.java)]?.apply { isAccessible = true }
if (partyCtor == null) {
throw IllegalArgumentException("$initiatedFlow must have a constructor accepting a ${FlowSession::class.java.name}")
} else {
log.warn(deprecatedFlowConstructorMessage(initiatedFlow))
}
{ flowSession: FlowSession -> uncheckedCast(partyCtor.newInstance(flowSession.counterparty)) }
} else {
{ flowSession: FlowSession -> uncheckedCast(flowSessionCtor.newInstance(flowSession)) }
}
val initiatingFlow = initiatedFlow.requireAnnotation<InitiatedBy>().value.java
val (version, classWithAnnotation) = initiatingFlow.flowVersionAndInitiatingClass
require(classWithAnnotation == initiatingFlow) {
"${InitiatedBy::class.java.name} must point to ${classWithAnnotation.name} and not ${initiatingFlow.name}"
}
val flowFactory = InitiatedFlowFactory.CorDapp(version, initiatedFlow.appName, ctor)
val observable = internalRegisterFlowFactory(smm, initiatingFlow, flowFactory, initiatedFlow, track)
log.info("Registered ${initiatingFlow.name} to initiate ${initiatedFlow.name} (version $version)")
return observable
}
protected fun <F : FlowLogic<*>> internalRegisterFlowFactory(smm: StateMachineManager,
initiatingFlowClass: Class<out FlowLogic<*>>,
flowFactory: InitiatedFlowFactory<F>,
initiatedFlowClass: Class<F>,
track: Boolean): Observable<F> {
val observable = if (track) {
smm.changes.filter { it is StateMachineManager.Change.Add }.map { it.logic }.ofType(initiatedFlowClass)
} else {
Observable.empty()
}
check(initiatingFlowClass !in flowFactories.keys) {
"$initiatingFlowClass is attempting to register multiple initiated flows"
}
flowFactories[initiatingFlowClass] = flowFactory
return observable
}
/**
* Installs a flow that's core to the Corda platform. Unlike CorDapp flows which are versioned individually using
* [InitiatingFlow.version], core flows have the same version as the node's platform version. To cater for backwards
* compatibility [flowFactory] provides a second parameter which is the platform version of the initiating party.
*/
@VisibleForTesting
fun installCoreFlow(clientFlowClass: KClass<out FlowLogic<*>>, flowFactory: (FlowSession) -> FlowLogic<*>) {
require(clientFlowClass.java.flowVersionAndInitiatingClass.first == 1) {
"${InitiatingFlow::class.java.name}.version not applicable for core flows; their version is the node's platform version"
}
flowFactories[clientFlowClass.java] = InitiatedFlowFactory.Core(flowFactory)
log.debug { "Installed core flow ${clientFlowClass.java.name}" }
flowManager.validateRegistrations()
}
private fun installCoreFlows() {
installCoreFlow(FinalityFlow::class, ::FinalityHandler)
installCoreFlow(NotaryChangeFlow::class, ::NotaryChangeHandler)
installCoreFlow(ContractUpgradeFlow.Initiate::class, ::ContractUpgradeHandler)
installCoreFlow(SwapIdentitiesFlow::class, ::SwapIdentitiesHandler)
flowManager.registerInitiatedCoreFlowFactory(FinalityFlow::class, FinalityHandler::class, ::FinalityHandler)
flowManager.registerInitiatedCoreFlowFactory(NotaryChangeFlow::class, NotaryChangeHandler::class, ::NotaryChangeHandler)
flowManager.registerInitiatedCoreFlowFactory(ContractUpgradeFlow.Initiate::class, NotaryChangeHandler::class, ::ContractUpgradeHandler)
flowManager.registerInitiatedCoreFlowFactory(SwapIdentitiesFlow::class, SwapIdentitiesHandler::class, ::SwapIdentitiesHandler)
}
protected open fun makeTransactionStorage(transactionCacheSizeBytes: Long): WritableTransactionStorage {
@ -739,7 +673,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
requireNotNull(getCertificateStores()) {
"One or more keyStores (identity or TLS) or trustStore not found. " +
"Please either copy your existing keys and certificates from another node, " +
"or if you don't have one yet, fill out the config file and run corda.jar --initial-registration. " +
"or if you don't have one yet, fill out the config file and run corda.jar initial-registration. " +
"Read more at: https://docs.corda.net/permissioning.html"
}
} catch (e: KeyStoreException) {
@ -811,7 +745,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
service.run {
tokenize()
runOnStop += ::stop
installCoreFlow(NotaryFlow.Client::class, ::createServiceFlow)
flowManager.registerInitiatedCoreFlowFactory(NotaryFlow.Client::class, ::createServiceFlow)
start()
}
return service
@ -998,7 +932,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
}
override fun getFlowFactory(initiatingFlowClass: Class<out FlowLogic<*>>): InitiatedFlowFactory<*>? {
return flowFactories[initiatingFlowClass]
return flowManager.getFlowFactoryForInitiatingFlow(initiatingFlowClass)
}
override fun jdbcSession(): Connection = database.createSession()
@ -1103,4 +1037,4 @@ fun clientSslOptionsCompatibleWith(nodeRpcOptions: NodeRpcOptions): ClientRpcSsl
}
// Here we're using the node's RPC key store as the RPC client's trust store.
return ClientRpcSslOptions(trustStorePath = nodeRpcOptions.sslConfig!!.keyStorePath, trustStorePassword = nodeRpcOptions.sslConfig!!.keyStorePassword)
}
}

View File

@ -29,8 +29,9 @@ import java.util.concurrent.TimeUnit
open class EnterpriseNode(configuration: NodeConfiguration,
versionInfo: VersionInfo,
initialiseSerialization: Boolean = true
) : Node(configuration, versionInfo, initialiseSerialization, cacheFactoryPrototype = EnterpriseNamedCacheFactory(configuration.enterpriseConfiguration.getTracingConfig())) {
initialiseSerialization: Boolean = true,
flowManager: FlowManager = NodeFlowManager(configuration.flowOverrides)
) : Node(configuration, versionInfo, initialiseSerialization, flowManager, cacheFactoryPrototype = EnterpriseNamedCacheFactory(configuration.enterpriseConfiguration.getTracingConfig())) {
companion object {
private val logger by lazy { loggerFor<EnterpriseNode>() }
@ -50,6 +51,10 @@ open class EnterpriseNode(configuration: NodeConfiguration,
}
}
class NodeCli : NodeStartupCli() {
override val startup = Startup()
}
class Startup : NodeStartup() {
override fun preNetworkRegistration(conf: NodeConfiguration) {
super.preNetworkRegistration(conf)

View File

@ -0,0 +1,222 @@
package net.corda.node.internal
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSession
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.InitiatingFlow
import net.corda.core.identity.Party
import net.corda.core.internal.uncheckedCast
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import net.corda.node.internal.classloading.requireAnnotation
import net.corda.node.services.config.FlowOverrideConfig
import net.corda.node.services.statemachine.appName
import net.corda.node.services.statemachine.flowVersionAndInitiatingClass
import javax.annotation.concurrent.ThreadSafe
import kotlin.reflect.KClass
/**
*
* This class is responsible for organising which flow should respond to a specific @InitiatingFlow
*
* There are two main ways to modify the behaviour of a cordapp with regards to responding with a different flow
*
* 1.) implementing a new subclass. For example, if we have a ResponderFlow similar to @InitiatedBy(Sender) MyBaseResponder : FlowLogic
* If we subclassed a new Flow with specific logic for DB2, it would be similar to IBMB2Responder() : MyBaseResponder
* When these two flows are encountered by the classpath scan for @InitiatedBy, they will both be selected for responding to Sender
* This implementation will sort them for responding in order of their "depth" from FlowLogic - see: FlowWeightComparator
* So IBMB2Responder would win and it would be selected for responding
*
* 2.) It is possible to specify a flowOverride key in the node configuration. Say we configure a node to have
* flowOverrides{
* "Sender" = "MyBaseResponder"
* }
* In this case, FlowWeightComparator would detect that there is an override in action, and it will assign MyBaseResponder a maximum weight
* This will result in MyBaseResponder being selected for responding to Sender
*
*
*/
interface FlowManager {
fun registerInitiatedCoreFlowFactory(initiatingFlowClass: KClass<out FlowLogic<*>>, flowFactory: (FlowSession) -> FlowLogic<*>)
fun registerInitiatedCoreFlowFactory(initiatingFlowClass: KClass<out FlowLogic<*>>, initiatedFlowClass: KClass<out FlowLogic<*>>?, flowFactory: (FlowSession) -> FlowLogic<*>)
fun registerInitiatedCoreFlowFactory(initiatingFlowClass: KClass<out FlowLogic<*>>, initiatedFlowClass: KClass<out FlowLogic<*>>?, flowFactory: InitiatedFlowFactory.Core<FlowLogic<*>>)
fun <F : FlowLogic<*>> registerInitiatedFlow(initiator: Class<out FlowLogic<*>>, responder: Class<F>)
fun <F : FlowLogic<*>> registerInitiatedFlow(responder: Class<F>)
fun getFlowFactoryForInitiatingFlow(initiatedFlowClass: Class<out FlowLogic<*>>): InitiatedFlowFactory<*>?
fun validateRegistrations()
}
@ThreadSafe
open class NodeFlowManager(flowOverrides: FlowOverrideConfig? = null) : FlowManager {
private val flowFactories = HashMap<Class<out FlowLogic<*>>, MutableList<RegisteredFlowContainer>>()
private val flowOverrides = (flowOverrides
?: FlowOverrideConfig()).overrides.map { it.initiator to it.responder }.toMutableMap()
companion object {
private val log = contextLogger()
}
@Synchronized
override fun getFlowFactoryForInitiatingFlow(initiatedFlowClass: Class<out FlowLogic<*>>): InitiatedFlowFactory<*>? {
return flowFactories[initiatedFlowClass]?.firstOrNull()?.flowFactory
}
@Synchronized
override fun <F : FlowLogic<*>> registerInitiatedFlow(responder: Class<F>) {
return registerInitiatedFlow(responder.requireAnnotation<InitiatedBy>().value.java, responder)
}
@Synchronized
override fun <F : FlowLogic<*>> registerInitiatedFlow(initiator: Class<out FlowLogic<*>>, responder: Class<F>) {
val constructors = responder.declaredConstructors.associateBy { it.parameterTypes.toList() }
val flowSessionCtor = constructors[listOf(FlowSession::class.java)]?.apply { isAccessible = true }
val ctor: (FlowSession) -> F = if (flowSessionCtor == null) {
// Try to fallback to a Party constructor
val partyCtor = constructors[listOf(Party::class.java)]?.apply { isAccessible = true }
if (partyCtor == null) {
throw IllegalArgumentException("$responder must have a constructor accepting a ${FlowSession::class.java.name}")
} else {
log.warn("Installing flow factory for $responder accepting a ${Party::class.java.simpleName}, which is deprecated. " +
"It should accept a ${FlowSession::class.java.simpleName} instead")
}
{ flowSession: FlowSession -> uncheckedCast(partyCtor.newInstance(flowSession.counterparty)) }
} else {
{ flowSession: FlowSession -> uncheckedCast(flowSessionCtor.newInstance(flowSession)) }
}
val (version, classWithAnnotation) = initiator.flowVersionAndInitiatingClass
require(classWithAnnotation == initiator) {
"${InitiatedBy::class.java.name} must point to ${classWithAnnotation.name} and not ${initiator.name}"
}
val flowFactory = InitiatedFlowFactory.CorDapp(version, responder.appName, ctor)
registerInitiatedFlowFactory(initiator, flowFactory, responder)
log.info("Registered ${initiator.name} to initiate ${responder.name} (version $version)")
}
private fun <F : FlowLogic<*>> registerInitiatedFlowFactory(initiatingFlowClass: Class<out FlowLogic<*>>,
flowFactory: InitiatedFlowFactory<F>,
initiatedFlowClass: Class<F>?) {
check(flowFactory !is InitiatedFlowFactory.Core) { "This should only be used for Cordapp flows" }
val listOfFlowsForInitiator = flowFactories.computeIfAbsent(initiatingFlowClass) { mutableListOf() }
if (listOfFlowsForInitiator.isNotEmpty() && listOfFlowsForInitiator.first().type == FlowType.CORE) {
throw IllegalStateException("Attempting to register over an existing platform flow: $initiatingFlowClass")
}
synchronized(listOfFlowsForInitiator) {
val flowToAdd = RegisteredFlowContainer(initiatingFlowClass, initiatedFlowClass, flowFactory, FlowType.CORDAPP)
val flowWeightComparator = FlowWeightComparator(initiatingFlowClass, flowOverrides)
listOfFlowsForInitiator.add(flowToAdd)
listOfFlowsForInitiator.sortWith(flowWeightComparator)
if (listOfFlowsForInitiator.size > 1) {
log.warn("Multiple flows are registered for InitiatingFlow: $initiatingFlowClass, currently using: ${listOfFlowsForInitiator.first().initiatedFlowClass}")
}
}
}
// TODO Harmonise use of these methods - 99% of invocations come from tests.
@Synchronized
override fun registerInitiatedCoreFlowFactory(initiatingFlowClass: KClass<out FlowLogic<*>>, initiatedFlowClass: KClass<out FlowLogic<*>>?, flowFactory: (FlowSession) -> FlowLogic<*>) {
registerInitiatedCoreFlowFactory(initiatingFlowClass, initiatedFlowClass, InitiatedFlowFactory.Core(flowFactory))
}
@Synchronized
override fun registerInitiatedCoreFlowFactory(initiatingFlowClass: KClass<out FlowLogic<*>>, flowFactory: (FlowSession) -> FlowLogic<*>) {
registerInitiatedCoreFlowFactory(initiatingFlowClass, null, InitiatedFlowFactory.Core(flowFactory))
}
@Synchronized
override fun registerInitiatedCoreFlowFactory(initiatingFlowClass: KClass<out FlowLogic<*>>, initiatedFlowClass: KClass<out FlowLogic<*>>?, flowFactory: InitiatedFlowFactory.Core<FlowLogic<*>>) {
require(initiatingFlowClass.java.flowVersionAndInitiatingClass.first == 1) {
"${InitiatingFlow::class.java.name}.version not applicable for core flows; their version is the node's platform version"
}
flowFactories.computeIfAbsent(initiatingFlowClass.java) { mutableListOf() }.add(
RegisteredFlowContainer(
initiatingFlowClass.java,
initiatedFlowClass?.java,
flowFactory,
FlowType.CORE)
)
log.debug { "Installed core flow ${initiatingFlowClass.java.name}" }
}
// To verify the integrity of the current state, it is important that the tip of the responders is a unique weight
// if there are multiple flows with the same weight as the tip, it means that it is impossible to reliably pick one as the responder
private fun validateInvariants(toValidate: List<RegisteredFlowContainer>) {
val currentTip = toValidate.first()
val flowWeightComparator = FlowWeightComparator(currentTip.initiatingFlowClass, flowOverrides)
val equalWeightAsCurrentTip = toValidate.map { flowWeightComparator.compare(currentTip, it) to it }.filter { it.first == 0 }.map { it.second }
if (equalWeightAsCurrentTip.size > 1) {
val message = "Unable to determine which flow to use when responding to: ${currentTip.initiatingFlowClass.canonicalName}. ${equalWeightAsCurrentTip.map { it.initiatedFlowClass!!.canonicalName }} are all registered with equal weight."
throw IllegalStateException(message)
}
}
@Synchronized
override fun validateRegistrations() {
flowFactories.values.forEach {
validateInvariants(it)
}
}
private enum class FlowType {
CORE, CORDAPP
}
private data class RegisteredFlowContainer(val initiatingFlowClass: Class<out FlowLogic<*>>,
val initiatedFlowClass: Class<out FlowLogic<*>>?,
val flowFactory: InitiatedFlowFactory<FlowLogic<*>>,
val type: FlowType)
// this is used to sort the responding flows in order of "importance"
// the logic is as follows
// IF responder is a specific lambda (like for notary implementations / testing code) always return that responder
// ELSE IF responder is present in the overrides list, always return that responder
// ELSE compare responding flows by their depth from FlowLogic, always return the flow which is most specific (IE, has the most hops to FlowLogic)
private open class FlowWeightComparator(val initiatingFlowClass: Class<out FlowLogic<*>>, val flowOverrides: Map<String, String>) : Comparator<NodeFlowManager.RegisteredFlowContainer> {
override fun compare(o1: NodeFlowManager.RegisteredFlowContainer, o2: NodeFlowManager.RegisteredFlowContainer): Int {
if (o1.initiatedFlowClass == null && o2.initiatedFlowClass != null) {
return Int.MIN_VALUE
}
if (o1.initiatedFlowClass != null && o2.initiatedFlowClass == null) {
return Int.MAX_VALUE
}
if (o1.initiatedFlowClass == null && o2.initiatedFlowClass == null) {
return 0
}
val hopsTo1 = calculateHopsToFlowLogic(initiatingFlowClass, o1.initiatedFlowClass!!)
val hopsTo2 = calculateHopsToFlowLogic(initiatingFlowClass, o2.initiatedFlowClass!!)
return hopsTo1.compareTo(hopsTo2) * -1
}
private fun calculateHopsToFlowLogic(initiatingFlowClass: Class<out FlowLogic<*>>,
initiatedFlowClass: Class<out FlowLogic<*>>): Int {
val overriddenClassName = flowOverrides[initiatingFlowClass.canonicalName]
return if (overriddenClassName == initiatedFlowClass.canonicalName) {
Int.MAX_VALUE
} else {
var currentClass: Class<*> = initiatedFlowClass
var count = 0
while (currentClass != FlowLogic::class.java) {
currentClass = currentClass.superclass
count++
}
count;
}
}
}
}
private fun <X, Y> Iterable<Pair<X, Y>>.toMutableMap(): MutableMap<X, Y> {
return this.toMap(HashMap())
}

View File

@ -4,10 +4,12 @@ import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSession
sealed class InitiatedFlowFactory<out F : FlowLogic<*>> {
protected abstract val factory: (FlowSession) -> F
fun createFlow(initiatingFlowSession: FlowSession): F = factory(initiatingFlowSession)
data class Core<out F : FlowLogic<*>>(override val factory: (FlowSession) -> F) : InitiatedFlowFactory<F>()
data class CorDapp<out F : FlowLogic<*>>(val flowVersion: Int,
val appName: String,
override val factory: (FlowSession) -> F) : InitiatedFlowFactory<F>()

View File

@ -6,6 +6,7 @@ import com.codahale.metrics.MetricRegistry
import com.palominolabs.metrics.newrelic.AllEnabledMetricAttributeFilter
import com.palominolabs.metrics.newrelic.NewRelicReporter
import net.corda.client.rpc.internal.serialization.amqp.AMQPClientSerializationScheme
import net.corda.cliutils.ShellConstants
import net.corda.core.concurrent.CordaFuture
import net.corda.core.flows.FlowLogic
import net.corda.core.identity.CordaX500Name
@ -43,6 +44,7 @@ import net.corda.node.services.api.StartedNodeServices
import net.corda.node.services.config.*
import net.corda.node.services.messaging.*
import net.corda.node.services.rpc.ArtemisRpcBroker
import net.corda.node.services.statemachine.StateMachineManager
import net.corda.node.utilities.*
import net.corda.node.utilities.profiling.getTracingConfig
import net.corda.nodeapi.internal.ArtemisMessagingClient
@ -58,7 +60,6 @@ import org.apache.commons.lang.SystemUtils
import org.h2.jdbc.JdbcSQLException
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import rx.Observable
import rx.Scheduler
import rx.schedulers.Schedulers
import java.net.BindException
@ -74,8 +75,7 @@ import kotlin.system.exitProcess
class NodeWithInfo(val node: Node, val info: NodeInfo) {
val services: StartedNodeServices = object : StartedNodeServices, ServiceHubInternal by node.services, FlowStarter by node.flowStarter {}
fun dispose() = node.stop()
fun <T : FlowLogic<*>> registerInitiatedFlow(initiatedFlowClass: Class<T>): Observable<T> =
node.registerInitiatedFlow(node.smm, initiatedFlowClass)
fun <T : FlowLogic<*>> registerInitiatedFlow(initiatedFlowClass: Class<T>) = node.registerInitiatedFlow(node.smm, initiatedFlowClass)
}
/**
@ -87,12 +87,14 @@ class NodeWithInfo(val node: Node, val info: NodeInfo) {
open class Node(configuration: NodeConfiguration,
versionInfo: VersionInfo,
private val initialiseSerialization: Boolean = true,
flowManager: FlowManager = NodeFlowManager(configuration.flowOverrides),
cacheFactoryPrototype: BindableNamedCacheFactory = EnterpriseNamedCacheFactory(configuration.enterpriseConfiguration.getTracingConfig())
) : AbstractNode<NodeInfo>(
configuration,
createClock(configuration),
cacheFactoryPrototype,
versionInfo,
flowManager,
// Under normal (non-test execution) it will always be "1"
AffinityExecutor.ServiceAffinityExecutor("Node thread-${sameVmNodeCounter.incrementAndGet()}", 1)
) {
@ -111,9 +113,13 @@ open class Node(configuration: NodeConfiguration,
LoggerFactory.getLogger(loggerName).info(msg)
}
fun printInRed(message: String) {
println("${ShellConstants.RED}$message${ShellConstants.RESET}")
}
fun printWarning(message: String) {
Emoji.renderIfSupported {
println("${Emoji.warningSign} ATTENTION: $message")
printInRed("${Emoji.warningSign} ATTENTION: $message")
}
staticLog.warn(message)
}
@ -133,13 +139,13 @@ open class Node(configuration: NodeConfiguration,
// TODO: make this configurable.
const val MAX_RPC_MESSAGE_SIZE = 10485760
fun isValidJavaVersion(): Boolean {
fun isInvalidJavaVersion(): Boolean {
if (!hasMinimumJavaVersion()) {
println("You are using a version of Java that is not supported (${SystemUtils.JAVA_VERSION}). Please upgrade to the latest version of Java 8.")
println("Corda will now exit...")
return false
return true
}
return true
return false
}
private fun hasMinimumJavaVersion(): Boolean {
@ -204,7 +210,8 @@ open class Node(configuration: NodeConfiguration,
return P2PMessagingClient(
config = configuration,
versionInfo = versionInfo,
serverAddress = configuration.messagingServerAddress ?: NetworkHostAndPort("localhost", configuration.p2pAddress.port),
serverAddress = configuration.messagingServerAddress
?: NetworkHostAndPort("localhost", configuration.p2pAddress.port),
nodeExecutor = serverThread,
database = database,
networkMap = networkMapCache,
@ -230,7 +237,8 @@ open class Node(configuration: NodeConfiguration,
}
val messageBroker = if (!configuration.messagingServerExternal) {
val brokerBindAddress = configuration.messagingServerAddress ?: NetworkHostAndPort("0.0.0.0", configuration.p2pAddress.port)
val brokerBindAddress = configuration.messagingServerAddress
?: NetworkHostAndPort("0.0.0.0", configuration.p2pAddress.port)
ArtemisMessagingServer(configuration, brokerBindAddress, networkParameters.maxMessageSize)
} else {
null
@ -473,7 +481,7 @@ open class Node(configuration: NodeConfiguration,
}.build().start()
}
private fun registerNewRelicReporter (registry: MetricRegistry) {
private fun registerNewRelicReporter(registry: MetricRegistry) {
log.info("Registering New Relic JMX Reporter:")
val reporter = NewRelicReporter.forRegistry(registry)
.name("New Relic Reporter")
@ -535,4 +543,8 @@ open class Node(configuration: NodeConfiguration,
log.info("Shutdown complete")
}
fun <T : FlowLogic<*>> registerInitiatedFlow(smm: StateMachineManager, initiatedFlowClass: Class<T>) {
this.flowManager.registerInitiatedFlow(initiatedFlowClass)
}
}

View File

@ -1,31 +1,24 @@
package net.corda.node.internal
import com.typesafe.config.Config
import com.typesafe.config.ConfigException
import com.typesafe.config.ConfigRenderOptions
import io.netty.channel.unix.Errors
import net.corda.cliutils.CordaCliWrapper
import net.corda.cliutils.CordaVersionProvider
import net.corda.cliutils.ExitCodes
import net.corda.cliutils.*
import net.corda.core.crypto.Crypto
import net.corda.core.internal.*
import net.corda.core.internal.concurrent.thenMatch
import net.corda.core.internal.cordapp.CordappImpl
import net.corda.core.internal.errors.AddressBindingException
import net.corda.core.utilities.Try
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.loggerFor
import net.corda.node.*
import net.corda.node.internal.Node.Companion.isValidJavaVersion
import net.corda.node.internal.Node.Companion.isInvalidJavaVersion
import net.corda.node.internal.cordapp.MultipleCordappsForFlowException
import net.corda.node.internal.subcommands.*
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.shouldStartLocalShell
import net.corda.node.services.config.shouldStartSSHDaemon
import net.corda.node.utilities.createKeyPairAndSelfSignedTLSCertificate
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
import net.corda.node.utilities.registration.NodeRegistrationException
import net.corda.node.utilities.registration.NodeRegistrationHelper
import net.corda.node.utilities.saveToKeyStore
import net.corda.node.utilities.saveToTrustStore
import net.corda.nodeapi.internal.addShutdownHook
import net.corda.nodeapi.internal.config.UnknownConfigurationKeysException
import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException
@ -35,10 +28,8 @@ import net.corda.nodeapi.internal.persistence.oracleJdbcDriverSerialFilter
import net.corda.tools.shell.InteractiveShell
import org.fusesource.jansi.Ansi
import org.slf4j.bridge.SLF4JBridgeHandler
import picocli.CommandLine.Mixin
import picocli.CommandLine.*
import sun.misc.VMSupport
import java.io.Console
import java.io.File
import java.io.IOException
import java.io.RandomAccessFile
import java.lang.management.ManagementFactory
@ -47,201 +38,146 @@ import java.nio.file.Path
import java.time.DayOfWeek
import java.time.ZonedDateTime
import java.util.*
import kotlin.system.exitProcess
/** This class is responsible for starting a Node from command line arguments. */
open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
/** An interface that can be implemented to tell the node what to do once it's intitiated. */
interface RunAfterNodeInitialisation {
fun run(node: Node)
}
/** Base class for subcommands to derive from that initialises the logs and provides standard options. */
abstract class NodeCliCommand(alias: String, description: String, val startup: NodeStartup) : CliWrapperBase(alias, description), NodeStartupLogging {
companion object {
private val logger by lazy { loggerFor<Node>() } // I guess this is lazy to allow for logging init, but why Node?
const val LOGS_DIRECTORY_NAME = "logs"
const val LOGS_CAN_BE_FOUND_IN_STRING = "Logs can be found in"
private const val INITIAL_REGISTRATION_MARKER = ".initialregistration"
}
override fun initLogging() = this.initLogging(cmdLineOptions.baseDirectory)
@Mixin
val cmdLineOptions = SharedNodeCmdLineOptions()
}
/** Main corda entry point. */
open class NodeStartupCli : CordaCliWrapper("corda", "Runs a Corda Node") {
open val startup = NodeStartup()
private val networkCacheCli by lazy { ClearNetworkCacheCli(startup) }
private val justGenerateNodeInfoCli by lazy { GenerateNodeInfoCli(startup) }
private val justGenerateRpcSslCertsCli by lazy { GenerateRpcSslCertsCli(startup) }
private val initialRegistrationCli by lazy { InitialRegistrationCli(startup) }
override fun initLogging() = this.initLogging(cmdLineOptions.baseDirectory)
override fun additionalSubCommands() = setOf(networkCacheCli, justGenerateNodeInfoCli, justGenerateRpcSslCertsCli, initialRegistrationCli)
override fun runProgram(): Int {
return when {
InitialRegistration.checkRegistrationMode(cmdLineOptions.baseDirectory) -> {
println("Node was started before in `initial-registration` mode, but the registration was not completed.\nResuming registration.")
initialRegistrationCli.cmdLineOptions.copyFrom(cmdLineOptions)
initialRegistrationCli.runProgram()
}
//deal with legacy flags and redirect to subcommands
cmdLineOptions.isRegistration -> {
Node.printWarning("The --initial-registration flag has been deprecated and will be removed in a future version. Use the initial-registration command instead.")
requireNotNull(cmdLineOptions.networkRootTrustStorePassword) { "Network root trust store password must be provided in registration mode using --network-root-truststore-password." }
initialRegistrationCli.networkRootTrustStorePassword = cmdLineOptions.networkRootTrustStorePassword!!
initialRegistrationCli.networkRootTrustStorePathParameter = cmdLineOptions.networkRootTrustStorePathParameter
initialRegistrationCli.cmdLineOptions.copyFrom(cmdLineOptions)
initialRegistrationCli.runProgram()
}
cmdLineOptions.clearNetworkMapCache -> {
Node.printWarning("The --clear-network-map-cache flag has been deprecated and will be removed in a future version. Use the clear-network-cache command instead.")
networkCacheCli.cmdLineOptions.copyFrom(cmdLineOptions)
networkCacheCli.runProgram()
}
cmdLineOptions.justGenerateNodeInfo -> {
Node.printWarning("The --just-generate-node-info flag has been deprecated and will be removed in a future version. Use the generate-node-info command instead.")
justGenerateNodeInfoCli.cmdLineOptions.copyFrom(cmdLineOptions)
justGenerateNodeInfoCli.runProgram()
}
cmdLineOptions.justGenerateRpcSslCerts -> {
Node.printWarning("The --just-generate-rpc-ssl-settings flag has been deprecated and will be removed in a future version. Use the generate-rpc-ssl-settings command instead.")
justGenerateRpcSslCertsCli.cmdLineOptions.copyFrom(cmdLineOptions)
justGenerateRpcSslCertsCli.runProgram()
}
else -> startup.initialiseAndRun(cmdLineOptions, object : RunAfterNodeInitialisation {
val startupTime = System.currentTimeMillis()
override fun run(node: Node) = startup.startNode(node, startupTime)
})
}
}
@Mixin
val cmdLineOptions = NodeCmdLineOptions()
}
/** This class provides a common set of functionality for starting a Node from command line arguments. */
open class NodeStartup : NodeStartupLogging {
companion object {
private val logger by lazy { loggerFor<Node>() } // I guess this is lazy to allow for logging init, but why Node?
const val LOGS_DIRECTORY_NAME = "logs"
const val LOGS_CAN_BE_FOUND_IN_STRING = "Logs can be found in"
}
lateinit var cmdLineOptions: SharedNodeCmdLineOptions
fun initialiseAndRun(cmdLineOptions: SharedNodeCmdLineOptions, afterNodeInitialisation: RunAfterNodeInitialisation): Int {
this.cmdLineOptions = cmdLineOptions
/**
* @return exit code based on the success of the node startup. This value is intended to be the exit code of the process.
*/
override fun runProgram(): Int {
val startTime = System.currentTimeMillis()
// Step 1. Check for supported Java version.
if (!isValidJavaVersion()) return ExitCodes.FAILURE
if (isInvalidJavaVersion()) return ExitCodes.FAILURE
// Step 2. We do the single node check before we initialise logging so that in case of a double-node start it
// doesn't mess with the running node's logs.
enforceSingleNodeIsRunning(cmdLineOptions.baseDirectory)
// Step 3. Initialise logging.
initLogging()
// Step 4. Register all cryptography [Provider]s.
// Step 3. Register all cryptography [Provider]s.
// Required to install our [SecureRandom] before e.g., UUID asks for one.
// This needs to go after initLogging(netty clashes with our logging).
// This needs to go after initLogging(netty clashes with our logging)
Crypto.registerProviders()
// Step 5. Print banner and basic node info.
// Step 4. Print banner and basic node info.
val versionInfo = getVersionInfo()
drawBanner(versionInfo)
Node.printBasicNodeInfo(LOGS_CAN_BE_FOUND_IN_STRING, System.getProperty("log-path"))
// Step 6. Load and validate node configuration.
val configuration = (attempt { loadConfiguration() }.doOnException(handleConfigurationLoadingError(cmdLineOptions.configFile)) as? Try.Success)?.let(Try.Success<NodeConfiguration>::value) ?: return ExitCodes.FAILURE
// Step 5. Load and validate node configuration.
val configuration = (attempt { cmdLineOptions.loadConfig() }.doOnException(handleConfigurationLoadingError(cmdLineOptions.configFile)) as? Try.Success)?.let(Try.Success<NodeConfiguration>::value)
?: return ExitCodes.FAILURE
val errors = configuration.validate()
if (errors.isNotEmpty()) {
logger.error("Invalid node configuration. Errors were:${System.lineSeparator()}${errors.joinToString(System.lineSeparator())}")
return ExitCodes.FAILURE
}
// Step 7. Configuring special serialisation requirements, i.e., bft-smart relies on Java serialization.
attempt { banJavaSerialisation(configuration) }.doOnException { error -> error.logAsUnexpected("Exception while configuring serialisation") } as? Try.Success ?: return ExitCodes.FAILURE
// Step 6. Configuring special serialisation requirements, i.e., bft-smart relies on Java serialization.
attempt { banJavaSerialisation(configuration) }.doOnException { error -> error.logAsUnexpected("Exception while configuring serialisation") } as? Try.Success
?: return ExitCodes.FAILURE
// Step 8. Any actions required before starting up the Corda network layer.
attempt { preNetworkRegistration(configuration) }.doOnException(handleRegistrationError) as? Try.Success ?: return ExitCodes.FAILURE
// Step 7. Any actions required before starting up the Corda network layer.
attempt { preNetworkRegistration(configuration) }.doOnException(::handleRegistrationError) as? Try.Success
?: return ExitCodes.FAILURE
// Step 9. Check if in registration mode.
checkAndRunRegistrationMode(configuration, versionInfo)?.let {
return if (it) ExitCodes.SUCCESS
else ExitCodes.FAILURE
}
// Step 10. Log startup info.
// Step 8. Log startup info.
logStartupInfo(versionInfo, configuration)
// Step 11. Start node: create the node, check for other command-line options, add extra logging etc.
attempt { startNode(configuration, versionInfo, startTime) }.doOnSuccess { logger.info("Node exiting successfully") }.doOnException(handleStartError) as? Try.Success ?: return ExitCodes.FAILURE
// Step 9. Start node: create the node, check for other command-line options, add extra logging etc.
attempt {
cmdLineOptions.baseDirectory.createDirectories()
afterNodeInitialisation.run(createNode(configuration, versionInfo))
}.doOnException(::handleStartError) as? Try.Success ?: return ExitCodes.FAILURE
return ExitCodes.SUCCESS
}
private fun checkAndRunRegistrationMode(configuration: NodeConfiguration, versionInfo: VersionInfo): Boolean? {
checkUnfinishedRegistration()
cmdLineOptions.nodeRegistrationOption?.let {
// Null checks for [compatibilityZoneURL], [rootTruststorePath] and [rootTruststorePassword] has been done in [CmdLineOptions.loadConfig]
attempt { registerWithNetwork(configuration, versionInfo, it) }.doOnException(handleRegistrationError) as? Try.Success
?: return false
// At this point the node registration was successful. We can delete the marker file.
deleteNodeRegistrationMarker(cmdLineOptions.baseDirectory)
return true
}
return null
}
// TODO: Reconsider if automatic re-registration should be applied when something failed during initial registration.
// There might be cases where the node user should investigate what went wrong before registering again.
private fun checkUnfinishedRegistration() {
if (checkRegistrationMode() && !cmdLineOptions.isRegistration) {
println("Node was started before with `--initial-registration`, but the registration was not completed.\nResuming registration.")
// Pretend that the node was started with `--initial-registration` to help prevent user error.
cmdLineOptions.isRegistration = true
}
}
private fun <RESULT> attempt(action: () -> RESULT): Try<RESULT> = Try.on(action)
private fun Exception.isExpectedWhenStartingNode() = startNodeExpectedErrors.any { error -> error.isInstance(this) }
private val startNodeExpectedErrors = setOf(DatabaseMigrationException::class, MultipleCordappsForFlowException::class, CheckpointIncompatibleException::class, AddressBindingException::class, NetworkParametersReader::class, DatabaseIncompatibleException::class)
private fun Exception.logAsExpected(message: String? = this.message, print: (String?) -> Unit = logger::error) = print(message)
private fun Exception.logAsUnexpected(message: String? = this.message, error: Exception = this, print: (String?, Throwable) -> Unit = logger::error) = print("$message${this.message?.let { ": $it" } ?: ""}", error)
private fun Exception.isOpenJdkKnownIssue() = message?.startsWith("Unknown named curve:") == true
private val handleRegistrationError = { error: Exception ->
when (error) {
is NodeRegistrationException -> error.logAsExpected("Issue with Node registration: ${error.message}")
else -> error.logAsUnexpected("Exception during node registration")
}
}
private val handleStartError = { error: Exception ->
when {
error.isExpectedWhenStartingNode() -> error.logAsExpected()
error is CouldNotCreateDataSourceException -> error.logAsUnexpected()
error is Errors.NativeIoException && error.message?.contains("Address already in use") == true -> error.logAsExpected("One of the ports required by the Corda node is already in use.")
error.isOpenJdkKnownIssue() -> error.logAsExpected("Exception during node startup - ${error.message}. This is a known OpenJDK issue on some Linux distributions, please use OpenJDK from zulu.org or Oracle JDK.")
else -> error.logAsUnexpected("Exception during node startup")
}
}
private fun handleConfigurationLoadingError(configFile: Path) = { error: Exception ->
when (error) {
is UnknownConfigurationKeysException -> error.logAsExpected()
is ConfigException.IO -> error.logAsExpected(configFileNotFoundMessage(configFile), ::println)
else -> error.logAsUnexpected("Unexpected error whilst reading node configuration")
}
}
private fun configFileNotFoundMessage(configFile: Path): String {
return """
Unable to load the node config file from '$configFile'.
Try setting the --base-directory flag to change which directory the node
is looking in, or use the --config-file flag to specify it explicitly.
""".trimIndent()
}
private fun loadConfiguration(): NodeConfiguration {
val (rawConfig, configurationResult) = loadConfigFile()
if (cmdLineOptions.devMode == true) {
println("Config:\n${rawConfig.root().render(ConfigRenderOptions.defaults())}")
}
return configurationResult.getOrThrow()
}
private fun checkRegistrationMode(): Boolean {
// If the node was started with `--initial-registration`, create marker file.
// We do this here to ensure the marker is created even if parsing the args with NodeArgsParser fails.
val marker = cmdLineOptions.baseDirectory / INITIAL_REGISTRATION_MARKER
if (!cmdLineOptions.isRegistration && !marker.exists()) {
return false
}
try {
marker.createFile()
} catch (e: Exception) {
logger.warn("Could not create marker file for `--initial-registration`.", e)
}
return true
}
private fun deleteNodeRegistrationMarker(baseDir: Path) {
try {
val marker = File((baseDir / INITIAL_REGISTRATION_MARKER).toUri())
if (marker.exists()) {
marker.delete()
}
} catch (e: Exception) {
e.logAsUnexpected("Could not delete the marker file that was created for `--initial-registration`.", print = logger::warn)
}
}
protected open fun preNetworkRegistration(conf: NodeConfiguration) = Unit
protected open fun createNode(conf: NodeConfiguration, versionInfo: VersionInfo): Node = Node(conf, versionInfo)
open fun createNode(conf: NodeConfiguration, versionInfo: VersionInfo): Node = Node(conf, versionInfo)
protected open fun startNode(conf: NodeConfiguration, versionInfo: VersionInfo, startTime: Long) {
cmdLineOptions.baseDirectory.createDirectories()
val node = createNode(conf, versionInfo)
if (cmdLineOptions.clearNetworkMapCache) {
node.clearNetworkMapCache()
return
}
if (cmdLineOptions.justGenerateNodeInfo) {
// Perform the minimum required start-up logic to be able to write a nodeInfo to disk
node.generateAndSaveNodeInfo()
return
}
if (cmdLineOptions.justGenerateRpcSslCerts) {
generateRpcSslCertificates(conf)
return
}
if (conf.devMode) {
fun startNode(node: Node, startTime: Long) {
if (node.configuration.devMode) {
Emoji.renderIfSupported {
Node.printWarning("This node is running in developer mode! ${Emoji.developer} This is not safe for production deployment.")
Node.printWarning("This node is running in development mode! ${Emoji.developer} This is not safe for production deployment.")
}
} else {
logger.info("The Corda node is running in production mode. If this is a developer environment you can set 'devMode=true' in the node.conf file.")
@ -258,7 +194,7 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
Node.printBasicNodeInfo("Node for \"$name\" started up and registered in $elapsed sec")
// Don't start the shell if there's no console attached.
if (conf.shouldStartLocalShell()) {
if (node.configuration.shouldStartLocalShell()) {
node.startupComplete.then {
try {
InteractiveShell.runLocalShell(node::stop)
@ -267,8 +203,8 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
}
}
}
if (conf.shouldStartSSHDaemon()) {
Node.printBasicNodeInfo("SSH server listening on port", conf.sshd!!.port.toString())
if (node.configuration.shouldStartSSHDaemon()) {
Node.printBasicNodeInfo("SSH server listening on port", node.configuration.sshd!!.port.toString())
}
},
{ th ->
@ -277,82 +213,6 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
node.run()
}
private fun generateRpcSslCertificates(conf: NodeConfiguration) {
val (keyPair, cert) = createKeyPairAndSelfSignedTLSCertificate(conf.myLegalName.x500Principal)
val keyStorePath = conf.baseDirectory / "certificates" / "rpcsslkeystore.jks"
val trustStorePath = conf.baseDirectory / "certificates" / "export" / "rpcssltruststore.jks"
if (keyStorePath.exists() || trustStorePath.exists()) {
println("Found existing RPC SSL keystores. Command was already run. Exiting..")
exitProcess(0)
}
val console: Console? = System.console()
when (console) {
// In this case, the JVM is not connected to the console so we need to exit.
null -> {
println("Not connected to console. Exiting")
exitProcess(1)
}
// Otherwise we can proceed normally.
else -> {
while (true) {
val keystorePassword1 = console.readPassword("Enter the RPC keystore password => ")
// TODO: consider adding a password strength policy.
if (keystorePassword1.isEmpty()) {
println("The RPC keystore password cannot be an empty String.")
continue
}
val keystorePassword2 = console.readPassword("Re-enter the RPC keystore password => ")
if (!keystorePassword1.contentEquals(keystorePassword2)) {
println("The RPC keystore passwords don't match.")
continue
}
saveToKeyStore(keyStorePath, keyPair, cert, String(keystorePassword1), "rpcssl")
println("The RPC keystore was saved to: $keyStorePath .")
break
}
while (true) {
val trustStorePassword1 = console.readPassword("Enter the RPC truststore password => ")
// TODO: consider adding a password strength policy.
if (trustStorePassword1.isEmpty()) {
println("The RPC truststore password cannot be an empty String.")
continue
}
val trustStorePassword2 = console.readPassword("Re-enter the RPC truststore password => ")
if (!trustStorePassword1.contentEquals(trustStorePassword2)) {
println("The RPC truststore passwords don't match.")
continue
}
saveToTrustStore(trustStorePath, cert, String(trustStorePassword1), "rpcssl")
println("The RPC truststore was saved to: $trustStorePath .")
println("You need to distribute this file along with the password in a secure way to all RPC clients.")
break
}
val dollar = '$'
println("""
|
|The SSL certificates for RPC were generated successfully.
|
|Add this snippet to the "rpcSettings" section of your node.conf:
| useSsl=true
| ssl {
| keyStorePath=$dollar{baseDirectory}/certificates/rpcsslkeystore.jks
| keyStorePassword=the_above_password
| }
|""".trimMargin())
}
}
}
protected open fun logStartupInfo(versionInfo: VersionInfo, conf: NodeConfiguration) {
logger.info("Vendor: ${versionInfo.vendor}")
logger.info("Release: ${versionInfo.releaseVersion}")
@ -378,34 +238,6 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
logger.info(nodeStartedMessage)
}
protected open fun registerWithNetwork(
conf: NodeConfiguration,
versionInfo: VersionInfo,
nodeRegistrationConfig: NodeRegistrationOption
) {
println("\n" +
"******************************************************************\n" +
"* *\n" +
"* Registering as a new participant with a Corda network *\n" +
"* *\n" +
"******************************************************************\n")
NodeRegistrationHelper(conf,
HTTPNetworkRegistrationService(
requireNotNull(conf.networkServices),
versionInfo),
nodeRegistrationConfig).buildKeystore()
// Minimal changes to make registration tool create node identity.
// TODO: Move node identity generation logic from node to registration helper.
createNode(conf, getVersionInfo()).generateAndSaveNodeInfo()
println("Successfully registered Corda node with compatibility zone, node identity keys and certificates are stored in '${conf.certificatesDirectory}', it is advised to backup the private keys and certificates.")
println("Corda node will now terminate.")
}
protected open fun loadConfigFile(): Pair<Config, Try<NodeConfiguration>> = cmdLineOptions.loadConfig()
protected open fun banJavaSerialisation(conf: NodeConfiguration) {
// Enterprise only - Oracle database requires additional serialization
val isOracleDbDriver = conf.dataSourceProperties.getProperty("dataSource.url", "").startsWith("jdbc:oracle:")
@ -419,7 +251,7 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
SerialFilter.install(serialFilter)
}
protected open fun getVersionInfo(): VersionInfo {
open fun getVersionInfo(): VersionInfo {
return VersionInfo(
PLATFORM_VERSION,
CordaVersionProvider.releaseVersion,
@ -472,18 +304,6 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
}
}
override fun initLogging() {
val loggingLevel = loggingLevel.name.toLowerCase(Locale.ENGLISH)
System.setProperty("defaultLogLevel", loggingLevel) // These properties are referenced from the XML config file.
if (verbose) {
System.setProperty("consoleLogLevel", loggingLevel)
Node.renderBasicInfoToConsole = false
}
System.setProperty("log-path", (cmdLineOptions.baseDirectory / LOGS_DIRECTORY_NAME).toString())
SLF4JBridgeHandler.removeHandlersForRootLogger() // The default j.u.l config adds a ConsoleHandler.
SLF4JBridgeHandler.install()
}
private fun lookupMachineNameAndMaybeWarn(): String {
val start = System.currentTimeMillis()
val hostName: String = InetAddress.getLocalHost().hostName
@ -587,3 +407,67 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
}
}
/** Provide some common logging methods for node startup commands. */
interface NodeStartupLogging {
companion object {
val logger by lazy { contextLogger() }
val startupErrors = setOf(MultipleCordappsForFlowException::class, CheckpointIncompatibleException::class, AddressBindingException::class, NetworkParametersReader::class, DatabaseIncompatibleException::class)
}
fun <RESULT> attempt(action: () -> RESULT): Try<RESULT> = Try.on(action)
fun Exception.logAsExpected(message: String? = this.message, print: (String?) -> Unit = logger::error) = print(message)
fun Exception.logAsUnexpected(message: String? = this.message, error: Exception = this, print: (String?, Throwable) -> Unit = logger::error) = print("$message${this.message?.let { ": $it" }
?: ""}", error)
fun handleRegistrationError(error: Exception) {
when (error) {
is NodeRegistrationException -> error.logAsExpected("Issue with Node registration: ${error.message}")
else -> error.logAsUnexpected("Exception during node registration")
}
}
fun Exception.isOpenJdkKnownIssue() = message?.startsWith("Unknown named curve:") == true
fun Exception.isExpectedWhenStartingNode() = startupErrors.any { error -> error.isInstance(this) }
fun handleStartError(error: Exception) {
when {
error.isExpectedWhenStartingNode() -> error.logAsExpected()
error is CouldNotCreateDataSourceException -> error.logAsUnexpected()
error is Errors.NativeIoException && error.message?.contains("Address already in use") == true -> error.logAsExpected("One of the ports required by the Corda node is already in use.")
error.isOpenJdkKnownIssue() -> error.logAsExpected("Exception during node startup - ${error.message}. This is a known OpenJDK issue on some Linux distributions, please use OpenJDK from zulu.org or Oracle JDK.")
else -> error.logAsUnexpected("Exception during node startup")
}
}
fun handleConfigurationLoadingError(configFile: Path) = { error: Exception ->
when (error) {
is UnknownConfigurationKeysException -> error.logAsExpected()
is ConfigException.IO -> error.logAsExpected(configFileNotFoundMessage(configFile), ::println)
else -> error.logAsUnexpected("Unexpected error whilst reading node configuration")
}
}
private fun configFileNotFoundMessage(configFile: Path): String {
return """
Unable to load the node config file from '$configFile'.
Try setting the --base-directory flag to change which directory the node
is looking in, or use the --config-file flag to specify it explicitly.
""".trimIndent()
}
}
fun CliWrapperBase.initLogging(baseDirectory: Path) {
val loggingLevel = loggingLevel.name.toLowerCase(Locale.ENGLISH)
System.setProperty("defaultLogLevel", loggingLevel) // These properties are referenced from the XML config file.
if (verbose) {
System.setProperty("consoleLogLevel", loggingLevel)
Node.renderBasicInfoToConsole = false
}
System.setProperty("log-path", (baseDirectory / NodeCliCommand.LOGS_DIRECTORY_NAME).toString())
SLF4JBridgeHandler.removeHandlersForRootLogger() // The default j.u.l config adds a ConsoleHandler.
SLF4JBridgeHandler.install()
}

View File

@ -5,30 +5,26 @@ import com.typesafe.config.ConfigFactory
import net.corda.core.internal.createDirectories
import net.corda.core.internal.div
import net.corda.core.internal.exists
import net.corda.core.internal.isDirectory
import net.corda.core.internal.noneOrSingle
import net.corda.core.utilities.contextLogger
import java.nio.file.Path
import java.nio.file.Paths
class CordappConfigFileProvider(private val configDir: Path = DEFAULT_CORDAPP_CONFIG_DIR) : CordappConfigProvider {
class CordappConfigFileProvider(cordappDirectories: List<Path>) : CordappConfigProvider {
companion object {
val DEFAULT_CORDAPP_CONFIG_DIR = Paths.get("cordapps") / "config"
const val CONFIG_EXT = ".conf"
val logger = contextLogger()
private val logger = contextLogger()
}
init {
configDir.createDirectories()
}
private val configDirectories = cordappDirectories.map { (it / "config").createDirectories() }
override fun getConfigByName(name: String): Config {
val configFile = configDir / "$name$CONFIG_EXT"
return if (configFile.exists()) {
check(!configFile.isDirectory()) { "${configFile.toAbsolutePath()} is a directory, expected a config file" }
logger.info("Found config for cordapp $name in ${configFile.toAbsolutePath()}")
// TODO There's nothing stopping the same CorDapp jar from occuring in different directories and thus causing
// conflicts. The cordappDirectories list config option should just be a single cordappDirectory
val configFile = configDirectories.map { it / "$name.conf" }.noneOrSingle { it.exists() }
return if (configFile != null) {
logger.info("Found config for cordapp $name in $configFile")
ConfigFactory.parseFile(configFile.toFile())
} else {
logger.info("No config found for cordapp $name in ${configFile.toAbsolutePath()}")
logger.info("No config found for cordapp $name in $configDirectories")
ConfigFactory.empty()
}
}

View File

@ -21,7 +21,6 @@ import net.corda.core.serialization.SerializeAsToken
import net.corda.core.utilities.contextLogger
import net.corda.node.VersionInfo
import net.corda.node.cordapp.CordappLoader
import net.corda.node.internal.classloading.requireAnnotation
import net.corda.nodeapi.internal.coreContractClasses
import net.corda.serialization.internal.DefaultWhitelist
import org.apache.commons.collections4.map.LRUMap
@ -29,6 +28,7 @@ import java.lang.reflect.Modifier
import java.net.URL
import java.net.URLClassLoader
import java.nio.file.Path
import java.security.cert.X509Certificate
import java.util.*
import java.util.jar.JarInputStream
import kotlin.reflect.KClass
@ -41,7 +41,8 @@ import kotlin.streams.toList
*/
class JarScanningCordappLoader private constructor(private val cordappJarPaths: List<RestrictedURL>,
private val versionInfo: VersionInfo = VersionInfo.UNKNOWN,
extraCordapps: List<CordappImpl>) : CordappLoaderTemplate() {
extraCordapps: List<CordappImpl>,
private val blacklistedCordappSigners: List<X509Certificate> = emptyList()) : CordappLoaderTemplate() {
override val cordapps: List<CordappImpl> by lazy {
loadCordapps() + extraCordapps
@ -67,10 +68,11 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
*/
fun fromDirectories(cordappDirs: Collection<Path>,
versionInfo: VersionInfo = VersionInfo.UNKNOWN,
extraCordapps: List<CordappImpl> = emptyList()): JarScanningCordappLoader {
extraCordapps: List<CordappImpl> = emptyList(),
blacklistedCerts: List<X509Certificate> = emptyList()): JarScanningCordappLoader {
logger.info("Looking for CorDapps in ${cordappDirs.distinct().joinToString(", ", "[", "]")}")
val paths = cordappDirs.distinct().flatMap(this::jarUrlsInDirectory).map { it.restricted() }
return JarScanningCordappLoader(paths, versionInfo, extraCordapps)
return JarScanningCordappLoader(paths, versionInfo, extraCordapps, blacklistedCerts)
}
/**
@ -78,9 +80,9 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
*
* @param scanJars Uses the JAR URLs provided for classpath scanning and Cordapp detection.
*/
fun fromJarUrls(scanJars: List<URL>, versionInfo: VersionInfo = VersionInfo.UNKNOWN, extraCordapps: List<CordappImpl> = emptyList()): JarScanningCordappLoader {
fun fromJarUrls(scanJars: List<URL>, versionInfo: VersionInfo = VersionInfo.UNKNOWN, extraCordapps: List<CordappImpl> = emptyList(), blacklistedCerts: List<X509Certificate> = emptyList()): JarScanningCordappLoader {
val paths = scanJars.map { it.restricted() }
return JarScanningCordappLoader(paths, versionInfo, extraCordapps)
return JarScanningCordappLoader(paths, versionInfo, extraCordapps, blacklistedCerts)
}
private fun URL.restricted(rootPackageName: String? = null) = RestrictedURL(this, rootPackageName)
@ -109,6 +111,20 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
true
}
}
.filter {
if (blacklistedCordappSigners.isEmpty()) {
true //Nothing blacklisted, no need to check
} else {
val certificates = it.jarPath.openStream().let(::JarInputStream).use(JarSignatureCollector::collectCertificates)
if (certificates.isEmpty() || (certificates - blacklistedCordappSigners).isNotEmpty())
true // Cordapp is not signed or it is signed by at least one non-blacklisted certificate
else {
logger.warn("Not loading CorDapp ${it.info.shortName} (${it.info.vendor}) as it is signed by development key(s) only: " +
"${certificates.intersect(blacklistedCordappSigners).map { it.publicKey }}.")
false
}
}
}
cordapps.forEach { CordappInfoResolver.register(it.cordappClasses, it.info) }
return cordapps
}
@ -151,17 +167,6 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
private fun findInitiatedFlows(scanResult: RestrictedScanResult): List<Class<out FlowLogic<*>>> {
return scanResult.getClassesWithAnnotation(FlowLogic::class, InitiatedBy::class)
// First group by the initiating flow class in case there are multiple mappings
.groupBy { it.requireAnnotation<InitiatedBy>().value.java }
.map { (initiatingFlow, initiatedFlows) ->
val sorted = initiatedFlows.sortedWith(FlowTypeHierarchyComparator(initiatingFlow))
if (sorted.size > 1) {
logger.warn("${initiatingFlow.name} has been specified as the inititating flow by multiple flows " +
"in the same type hierarchy: ${sorted.joinToString { it.name }}. Choosing the most " +
"specific sub-type for registration: ${sorted[0].name}.")
}
sorted[0]
}
}
private fun Class<out FlowLogic<*>>.isUserInvokable(): Boolean {
@ -216,17 +221,7 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
}
}
private class FlowTypeHierarchyComparator(val initiatingFlow: Class<out FlowLogic<*>>) : Comparator<Class<out FlowLogic<*>>> {
override fun compare(o1: Class<out FlowLogic<*>>, o2: Class<out FlowLogic<*>>): Int {
return when {
o1 == o2 -> 0
o1.isAssignableFrom(o2) -> 1
o2.isAssignableFrom(o1) -> -1
else -> throw IllegalArgumentException("${initiatingFlow.name} has been specified as the initiating flow by " +
"both ${o1.name} and ${o2.name}")
}
}
}
private fun <T : Any> loadClass(className: String, type: KClass<T>): Class<out T>? {
return try {

View File

@ -0,0 +1,14 @@
package net.corda.node.internal.subcommands
import net.corda.node.internal.Node
import net.corda.node.internal.NodeCliCommand
import net.corda.node.internal.NodeStartup
import net.corda.node.internal.RunAfterNodeInitialisation
class ClearNetworkCacheCli(startup: NodeStartup): NodeCliCommand("clear-network-cache", "Clears local copy of network map, on node startup it will be restored from server or file system.", startup) {
override fun runProgram(): Int {
return startup.initialiseAndRun(cmdLineOptions, object: RunAfterNodeInitialisation {
override fun run(node: Node) = node.clearNetworkMapCache()
})
}
}

View File

@ -0,0 +1,16 @@
package net.corda.node.internal.subcommands
import net.corda.node.internal.Node
import net.corda.node.internal.NodeCliCommand
import net.corda.node.internal.NodeStartup
import net.corda.node.internal.RunAfterNodeInitialisation
class GenerateNodeInfoCli(startup: NodeStartup): NodeCliCommand("generate-node-info", "Performs the node start-up tasks necessary to generate the nodeInfo file, saves it to disk, then exits.", startup) {
override fun runProgram(): Int {
return startup.initialiseAndRun(cmdLineOptions, object : RunAfterNodeInitialisation {
override fun run(node: Node) {
node.generateAndSaveNodeInfo()
}
})
}
}

View File

@ -0,0 +1,103 @@
package net.corda.node.internal.subcommands
import net.corda.core.internal.div
import net.corda.core.internal.exists
import net.corda.node.internal.Node
import net.corda.node.internal.NodeCliCommand
import net.corda.node.internal.NodeStartup
import net.corda.node.internal.RunAfterNodeInitialisation
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.utilities.createKeyPairAndSelfSignedTLSCertificate
import net.corda.node.utilities.saveToKeyStore
import net.corda.node.utilities.saveToTrustStore
import java.io.Console
import kotlin.system.exitProcess
class GenerateRpcSslCertsCli(startup: NodeStartup): NodeCliCommand("generate-rpc-ssl-settings", "Generates the SSL key and trust stores for a secure RPC connection.", startup) {
override fun runProgram(): Int {
return startup.initialiseAndRun(cmdLineOptions, GenerateRpcSslCerts())
}
}
class GenerateRpcSslCerts: RunAfterNodeInitialisation {
override fun run(node: Node) {
generateRpcSslCertificates(node.configuration)
}
private fun generateRpcSslCertificates(conf: NodeConfiguration) {
val (keyPair, cert) = createKeyPairAndSelfSignedTLSCertificate(conf.myLegalName.x500Principal)
val keyStorePath = conf.baseDirectory / "certificates" / "rpcsslkeystore.jks"
val trustStorePath = conf.baseDirectory / "certificates" / "export" / "rpcssltruststore.jks"
if (keyStorePath.exists() || trustStorePath.exists()) {
println("Found existing RPC SSL keystores. Command was already run. Exiting.")
exitProcess(0)
}
val console: Console? = System.console()
when (console) {
// In this case, the JVM is not connected to the console so we need to exit.
null -> {
println("Not connected to console. Exiting.")
exitProcess(1)
}
// Otherwise we can proceed normally.
else -> {
while (true) {
val keystorePassword1 = console.readPassword("Enter the RPC keystore password:")
// TODO: consider adding a password strength policy.
if (keystorePassword1.isEmpty()) {
println("The RPC keystore password cannot be an empty String.")
continue
}
val keystorePassword2 = console.readPassword("Re-enter the RPC keystore password:")
if (!keystorePassword1.contentEquals(keystorePassword2)) {
println("The RPC keystore passwords don't match.")
continue
}
saveToKeyStore(keyStorePath, keyPair, cert, String(keystorePassword1), "rpcssl")
println("The RPC keystore was saved to: $keyStorePath .")
break
}
while (true) {
val trustStorePassword1 = console.readPassword("Enter the RPC truststore password:")
// TODO: consider adding a password strength policy.
if (trustStorePassword1.isEmpty()) {
println("The RPC truststore password cannot be an empty string.")
continue
}
val trustStorePassword2 = console.readPassword("Re-enter the RPC truststore password:")
if (!trustStorePassword1.contentEquals(trustStorePassword2)) {
println("The RPC truststore passwords don't match.")
continue
}
saveToTrustStore(trustStorePath, cert, String(trustStorePassword1), "rpcssl")
println("The RPC truststore was saved to: $trustStorePath.")
println("You need to distribute this file along with the password in a secure way to all RPC clients.")
break
}
val dollar = '$'
println("""
|
|The SSL certificates for RPC were generated successfully.
|
|Add this snippet to the "rpcSettings" section of your node.conf:
| useSsl=true
| ssl {
| keyStorePath=$dollar{baseDirectory}/certificates/rpcsslkeystore.jks
| keyStorePassword=the_above_password
| }
|""".trimMargin())
}
}
}
}

View File

@ -0,0 +1,110 @@
package net.corda.node.internal.subcommands
import net.corda.cliutils.CliWrapperBase
import net.corda.core.internal.createFile
import net.corda.core.internal.div
import net.corda.core.internal.exists
import net.corda.core.utilities.Try
import net.corda.node.InitialRegistrationCmdLineOptions
import net.corda.node.NodeRegistrationOption
import net.corda.node.internal.*
import net.corda.node.internal.NodeStartupLogging.Companion.logger
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
import net.corda.node.utilities.registration.NodeRegistrationHelper
import picocli.CommandLine.Mixin
import picocli.CommandLine.Option
import java.io.File
import java.nio.file.Path
class InitialRegistrationCli(val startup: NodeStartup): CliWrapperBase("initial-registration", "Starts initial node registration with Corda network to obtain certificate from the permissioning server.") {
@Option(names = ["-t", "--network-root-truststore"], description = ["Network root trust store obtained from network operator."])
var networkRootTrustStorePathParameter: Path? = null
@Option(names = ["-p", "--network-root-truststore-password"], description = ["Network root trust store password obtained from network operator."], required = true)
var networkRootTrustStorePassword: String = ""
override fun runProgram() : Int {
val networkRootTrustStorePath: Path = networkRootTrustStorePathParameter ?: cmdLineOptions.baseDirectory / "certificates" / "network-root-truststore.jks"
return startup.initialiseAndRun(cmdLineOptions, InitialRegistration(cmdLineOptions.baseDirectory, networkRootTrustStorePath, networkRootTrustStorePassword, startup))
}
override fun initLogging() = this.initLogging(cmdLineOptions.baseDirectory)
@Mixin
val cmdLineOptions = InitialRegistrationCmdLineOptions()
}
class InitialRegistration(val baseDirectory: Path, private val networkRootTrustStorePath: Path, networkRootTrustStorePassword: String, private val startup: NodeStartup) : RunAfterNodeInitialisation, NodeStartupLogging {
companion object {
private const val INITIAL_REGISTRATION_MARKER = ".initialregistration"
fun checkRegistrationMode(baseDirectory: Path): Boolean {
// If the node was started with `--initial-registration`, create marker file.
// We do this here to ensure the marker is created even if parsing the args with NodeArgsParser fails.
val marker = baseDirectory / INITIAL_REGISTRATION_MARKER
if (!marker.exists()) {
return false
}
try {
marker.createFile()
} catch (e: Exception) {
logger.warn("Could not create marker file for `initial-registration`.", e)
}
return true
}
}
private val nodeRegistration = NodeRegistrationOption(networkRootTrustStorePath, networkRootTrustStorePassword)
private fun registerWithNetwork(conf: NodeConfiguration) {
val versionInfo = startup.getVersionInfo()
println("\n" +
"******************************************************************\n" +
"* *\n" +
"* Registering as a new participant with a Corda network *\n" +
"* *\n" +
"******************************************************************\n")
NodeRegistrationHelper(conf,
HTTPNetworkRegistrationService(
requireNotNull(conf.networkServices),
versionInfo),
nodeRegistration).buildKeystore()
// Minimal changes to make registration tool create node identity.
// TODO: Move node identity generation logic from node to registration helper.
startup.createNode(conf, versionInfo).generateAndSaveNodeInfo()
println("Successfully registered Corda node with compatibility zone, node identity keys and certificates are stored in '${conf.certificatesDirectory}', it is advised to backup the private keys and certificates.")
println("Corda node will now terminate.")
}
private fun initialRegistration(config: NodeConfiguration) {
// Null checks for [compatibilityZoneURL], [rootTruststorePath] and [rootTruststorePassword] has been done in [CmdLineOptions.loadConfig]
attempt { registerWithNetwork(config) }.doOnException(this::handleRegistrationError) as? Try.Success
// At this point the node registration was successful. We can delete the marker file.
deleteNodeRegistrationMarker(baseDirectory)
}
private fun deleteNodeRegistrationMarker(baseDir: Path) {
try {
val marker = File((baseDir / INITIAL_REGISTRATION_MARKER).toUri())
if (marker.exists()) {
marker.delete()
}
} catch (e: Exception) {
e.logAsUnexpected( "Could not delete the marker file that was created for `initial-registration`.", print = logger::warn)
}
}
override fun run(node: Node) {
require(networkRootTrustStorePath.exists()) { "Network root trust store path: '$networkRootTrustStorePath' doesn't exist" }
if (checkRegistrationMode(baseDirectory)) {
println("Node was started before with `--initial-registration`, but the registration was not completed.\nResuming registration.")
}
initialRegistration(node.configuration)
}
}

View File

@ -82,6 +82,7 @@ interface NodeConfiguration {
val p2pSslOptions: MutualSslConfiguration
val cordappDirectories: List<Path>
val flowOverrides: FlowOverrideConfig?
fun validate(): List<String>
@ -103,6 +104,9 @@ interface NodeConfiguration {
}
}
data class FlowOverrideConfig(val overrides: List<FlowOverride> = listOf())
data class FlowOverride(val initiator: String, val responder: String)
/**
* Currently registered JMX Reporters
*/
@ -229,7 +233,8 @@ data class NodeConfigurationImpl(
override val flowMonitorSuspensionLoggingThresholdMillis: Duration = DEFAULT_FLOW_MONITOR_SUSPENSION_LOGGING_THRESHOLD_MILLIS,
override val cordappDirectories: List<Path> = listOf(baseDirectory / CORDAPPS_DIR_NAME_DEFAULT),
override val jmxReporterType: JmxReporterType? = JmxReporterType.JOLOKIA,
private val useOpenSsl: Boolean = false
private val useOpenSsl: Boolean = false,
override val flowOverrides: FlowOverrideConfig?
) : NodeConfiguration {
companion object {
private val logger = loggerFor<NodeConfigurationImpl>()

View File

@ -26,11 +26,6 @@
required: false
multiParam: false
acceptableValues: []
- parameterName: "--install-shell-extensions"
parameterType: "boolean"
required: false
multiParam: false
acceptableValues: []
- parameterName: "--just-generate-node-info"
parameterType: "boolean"
required: false
@ -99,11 +94,6 @@
required: false
multiParam: true
acceptableValues: []
- parameterName: "-c"
parameterType: "boolean"
required: false
multiParam: false
acceptableValues: []
- parameterName: "-d"
parameterType: "java.lang.Boolean"
required: false

View File

@ -12,7 +12,6 @@ import net.corda.testing.core.singleIdentity
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.MockNodeParameters
import net.corda.testing.node.StartedMockNode
import org.assertj.core.api.Assertions.assertThatIllegalStateException
import org.junit.After
import org.junit.Before
import org.junit.Test
@ -39,15 +38,15 @@ class FlowRegistrationTest {
}
@Test
fun `startup fails when two flows initiated by the same flow are registered`() {
fun `succeeds when a subclass of a flow initiated by the same flow is registered`() {
// register the same flow twice to invoke the error without causing errors in other tests
responder.registerInitiatedFlow(Responder::class.java)
assertThatIllegalStateException().isThrownBy { responder.registerInitiatedFlow(Responder::class.java) }
responder.registerInitiatedFlow(Responder1::class.java)
responder.registerInitiatedFlow(Responder1Subclassed::class.java)
}
@Test
fun `a single initiated flow can be registered without error`() {
responder.registerInitiatedFlow(Responder::class.java)
responder.registerInitiatedFlow(Responder1::class.java)
val result = initiator.startFlow(Initiator(responder.info.singleIdentity()))
mockNetwork.runNetwork()
assertNotNull(result.get())
@ -63,7 +62,38 @@ class Initiator(val party: Party) : FlowLogic<String>() {
}
@InitiatedBy(Initiator::class)
private class Responder(val session: FlowSession) : FlowLogic<Unit>() {
private open class Responder1(val session: FlowSession) : FlowLogic<Unit>() {
open fun getPayload(): String {
return "whats up"
}
@Suspendable
override fun call() {
session.receive<String>().unwrap { it }
session.send("What's up")
}
}
@InitiatedBy(Initiator::class)
private open class Responder2(val session: FlowSession) : FlowLogic<Unit>() {
open fun getPayload(): String {
return "whats up"
}
@Suspendable
override fun call() {
session.receive<String>().unwrap { it }
session.send("What's up")
}
}
@InitiatedBy(Initiator::class)
private class Responder1Subclassed(session: FlowSession) : Responder1(session) {
override fun getPayload(): String {
return "im subclassed! that's what's up!"
}
@Suspendable
override fun call() {
session.receive<String>().unwrap { it }

View File

@ -0,0 +1,110 @@
package net.corda.node.internal
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSession
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.InitiatingFlow
import net.corda.node.services.config.FlowOverride
import net.corda.node.services.config.FlowOverrideConfig
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.CoreMatchers.instanceOf
import org.junit.Assert
import org.junit.Test
import org.mockito.Mockito
import java.lang.IllegalStateException
private val marker = "This is a special marker"
class NodeFlowManagerTest {
@InitiatingFlow
class Init : FlowLogic<Unit>() {
override fun call() {
TODO("not implemented")
}
}
@InitiatedBy(Init::class)
open class Resp(val otherSesh: FlowSession) : FlowLogic<Unit>() {
override fun call() {
TODO("not implemented")
}
}
@InitiatedBy(Init::class)
class Resp2(val otherSesh: FlowSession) : FlowLogic<Unit>() {
override fun call() {
TODO("not implemented")
}
}
@InitiatedBy(Init::class)
open class RespSub(sesh: FlowSession) : Resp(sesh) {
override fun call() {
TODO("not implemented")
}
}
@InitiatedBy(Init::class)
class RespSubSub(sesh: FlowSession) : RespSub(sesh) {
override fun call() {
TODO("not implemented")
}
}
@Test(expected = IllegalStateException::class)
fun `should fail to validate if more than one registration with equal weight`() {
val nodeFlowManager = NodeFlowManager()
nodeFlowManager.registerInitiatedFlow(Init::class.java, Resp::class.java)
nodeFlowManager.registerInitiatedFlow(Init::class.java, Resp2::class.java)
nodeFlowManager.validateRegistrations()
}
@Test()
fun `should allow registration of flows with different weights`() {
val nodeFlowManager = NodeFlowManager()
nodeFlowManager.registerInitiatedFlow(Init::class.java, Resp::class.java)
nodeFlowManager.registerInitiatedFlow(Init::class.java, RespSub::class.java)
nodeFlowManager.validateRegistrations()
val factory = nodeFlowManager.getFlowFactoryForInitiatingFlow(Init::class.java)!!
val flow = factory.createFlow(Mockito.mock(FlowSession::class.java))
Assert.assertThat(flow, `is`(instanceOf(RespSub::class.java)))
}
@Test()
fun `should allow updating of registered responder at runtime`() {
val nodeFlowManager = NodeFlowManager()
nodeFlowManager.registerInitiatedFlow(Init::class.java, Resp::class.java)
nodeFlowManager.registerInitiatedFlow(Init::class.java, RespSub::class.java)
nodeFlowManager.validateRegistrations()
var factory = nodeFlowManager.getFlowFactoryForInitiatingFlow(Init::class.java)!!
var flow = factory.createFlow(Mockito.mock(FlowSession::class.java))
Assert.assertThat(flow, `is`(instanceOf(RespSub::class.java)))
// update
nodeFlowManager.registerInitiatedFlow(Init::class.java, RespSubSub::class.java)
nodeFlowManager.validateRegistrations()
factory = nodeFlowManager.getFlowFactoryForInitiatingFlow(Init::class.java)!!
flow = factory.createFlow(Mockito.mock(FlowSession::class.java))
Assert.assertThat(flow, `is`(instanceOf(RespSubSub::class.java)))
}
@Test
fun `should allow an override to be specified`() {
val nodeFlowManager = NodeFlowManager(FlowOverrideConfig(listOf(FlowOverride(Init::class.qualifiedName!!, Resp::class.qualifiedName!!))))
nodeFlowManager.registerInitiatedFlow(Init::class.java, Resp::class.java)
nodeFlowManager.registerInitiatedFlow(Init::class.java, Resp2::class.java)
nodeFlowManager.registerInitiatedFlow(Init::class.java, RespSubSub::class.java)
nodeFlowManager.validateRegistrations()
val factory = nodeFlowManager.getFlowFactoryForInitiatingFlow(Init::class.java)!!
val flow = factory.createFlow(Mockito.mock(FlowSession::class.java))
Assert.assertThat(flow, `is`(instanceOf(Resp::class.java)))
}
}

View File

@ -2,4 +2,4 @@ package net.corda.node.internal
import net.corda.testing.CliBackwardsCompatibleTest
class NodeStartupCompatibilityTest : CliBackwardsCompatibleTest(NodeStartup::class.java)
class NodeStartupCompatibilityTest : CliBackwardsCompatibleTest(NodeStartupCli::class.java)

View File

@ -1,6 +1,8 @@
package net.corda.node.internal
import net.corda.core.internal.div
import net.corda.node.InitialRegistrationCmdLineOptions
import net.corda.node.internal.subcommands.InitialRegistrationCli
import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy
import org.assertj.core.api.Assertions.assertThat
import org.junit.BeforeClass
@ -11,7 +13,7 @@ import java.nio.file.Path
import java.nio.file.Paths
class NodeStartupTest {
private val startup = NodeStartup()
private val startup = NodeStartupCli()
companion object {
private lateinit var workingDirectory: Path
@ -30,7 +32,6 @@ class NodeStartupTest {
assertThat(startup.cmdLineOptions.configFile).isEqualTo(workingDirectory / "node.conf")
assertThat(startup.verbose).isEqualTo(false)
assertThat(startup.loggingLevel).isEqualTo(Level.INFO)
assertThat(startup.cmdLineOptions.nodeRegistrationOption).isEqualTo(null)
assertThat(startup.cmdLineOptions.noLocalShell).isEqualTo(false)
assertThat(startup.cmdLineOptions.sshdServer).isEqualTo(false)
assertThat(startup.cmdLineOptions.justGenerateNodeInfo).isEqualTo(false)
@ -38,7 +39,7 @@ class NodeStartupTest {
assertThat(startup.cmdLineOptions.unknownConfigKeysPolicy).isEqualTo(UnknownConfigKeysPolicy.FAIL)
assertThat(startup.cmdLineOptions.devMode).isEqualTo(null)
assertThat(startup.cmdLineOptions.clearNetworkMapCache).isEqualTo(false)
assertThat(startup.cmdLineOptions.networkRootTrustStorePath).isEqualTo(workingDirectory / "certificates" / "network-root-truststore.jks")
assertThat(startup.cmdLineOptions.networkRootTrustStorePathParameter).isEqualTo(null)
}
@Test
@ -46,6 +47,6 @@ class NodeStartupTest {
CommandLine.populateCommand(startup, "--base-directory", (workingDirectory / "another-base-dir").toString())
assertThat(startup.cmdLineOptions.baseDirectory).isEqualTo(workingDirectory / "another-base-dir")
assertThat(startup.cmdLineOptions.configFile).isEqualTo(workingDirectory / "another-base-dir" / "node.conf")
assertThat(startup.cmdLineOptions.networkRootTrustStorePath).isEqualTo(workingDirectory / "another-base-dir" / "certificates" / "network-root-truststore.jks")
assertThat(startup.cmdLineOptions.networkRootTrustStorePathParameter).isEqualTo(null)
}
}

View File

@ -149,7 +149,7 @@ class NodeTest {
}
}
private fun createConfig(nodeName: CordaX500Name): NodeConfiguration {
private fun createConfig(nodeName: CordaX500Name): NodeConfigurationImpl {
val fakeAddress = NetworkHostAndPort("0.1.2.3", 456)
return NodeConfigurationImpl(
baseDirectory = temporaryFolder.root.toPath(),
@ -171,7 +171,8 @@ class NodeTest {
enterpriseConfiguration = EnterpriseConfiguration(
mutualExclusionConfiguration = MutualExclusionConfiguration(updateInterval = 0, waitInterval = 0)
),
relay = null
relay = null,
flowOverrides = FlowOverrideConfig(listOf())
)
}
}

View File

@ -12,16 +12,16 @@ import java.nio.file.Paths
class CordappConfigFileProviderTests {
private companion object {
val cordappConfDir = Paths.get("build") / "tmp" / "cordapps" / "config"
val cordappDir = Paths.get("build") / "tmp" / "cordapps"
const val cordappName = "test"
val cordappConfFile = cordappConfDir / "$cordappName.conf"
val cordappConfFile = cordappDir / "config" / "$cordappName.conf"
val validConfig: Config = ConfigFactory.parseString("key=value")
val alternateValidConfig: Config = ConfigFactory.parseString("key=alternateValue")
const val invalidConfig = "Invalid"
}
private val provider = CordappConfigFileProvider(cordappConfDir)
private val provider = CordappConfigFileProvider(listOf(cordappDir))
@Test
fun `test that config can be loaded`() {

View File

@ -6,6 +6,7 @@ import net.corda.core.internal.packageName
import net.corda.node.VersionInfo
import net.corda.testing.node.internal.TestCordappDirectories
import net.corda.testing.node.internal.cordappForPackages
import net.corda.nodeapi.internal.DEV_CERTIFICATES
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import java.nio.file.Paths
@ -56,7 +57,7 @@ class JarScanningCordappLoaderTest {
val actualCordapp = loader.cordapps.single()
assertThat(actualCordapp.contractClassNames).isEqualTo(listOf(isolatedContractId))
assertThat(actualCordapp.initiatedFlows.single().name).isEqualTo("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Acceptor")
assertThat(actualCordapp.initiatedFlows.first().name).isEqualTo("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Acceptor")
assertThat(actualCordapp.rpcFlows).isEmpty()
assertThat(actualCordapp.schedulableFlows).isEmpty()
assertThat(actualCordapp.services).isEmpty()
@ -74,7 +75,7 @@ class JarScanningCordappLoaderTest {
assertThat(loader.cordapps).isNotEmpty
val actualCordapp = loader.cordapps.single { !it.initiatedFlows.isEmpty() }
assertThat(actualCordapp.initiatedFlows).first().hasSameClassAs(DummyFlow::class.java)
assertThat(actualCordapp.initiatedFlows.first()).hasSameClassAs(DummyFlow::class.java)
assertThat(actualCordapp.rpcFlows).first().hasSameClassAs(DummyRPCFlow::class.java)
assertThat(actualCordapp.schedulableFlows).first().hasSameClassAs(DummySchedulableFlow::class.java)
}
@ -142,4 +143,25 @@ class JarScanningCordappLoaderTest {
val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 2))
assertThat(loader.cordapps).hasSize(1)
}
@Test
fun `cordapp classloader loads app signed by allowed certificate`() {
val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-dev-key.jar")!!
val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), blacklistedCerts = emptyList())
assertThat(loader.cordapps).hasSize(1)
}
@Test
fun `cordapp classloader does not load app signed by blacklisted certificate`() {
val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-dev-key.jar")!!
val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), blacklistedCerts = DEV_CERTIFICATES)
assertThat(loader.cordapps).hasSize(0)
}
@Test
fun `cordapp classloader loads app signed by both allowed and non-blacklisted certificate`() {
val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-two-keys.jar")!!
val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), blacklistedCerts = DEV_CERTIFICATES)
assertThat(loader.cordapps).hasSize(1)
}
}

View File

@ -209,8 +209,8 @@ class NodeConfigurationImplTest {
val errors = configuration.validate()
assertThat(errors).hasOnlyOneElementSatisfying {
error -> error.contains("Cannot configure both compatibilityZoneUrl and networkServices simultaneously")
assertThat(errors).hasOnlyOneElementSatisfying { error ->
error.contains("Cannot configure both compatibilityZoneUrl and networkServices simultaneously")
}
}
@ -311,7 +311,8 @@ class NodeConfigurationImplTest {
relay = null,
enterpriseConfiguration = EnterpriseConfiguration((MutualExclusionConfiguration(false, "", 20000, 40000))),
crlCheckSoftFail = true,
tlsCertCrlDistPoint = null
tlsCertCrlDistPoint = null,
flowOverrides = FlowOverrideConfig(listOf())
)
}
}

View File

@ -26,7 +26,6 @@ import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.ProgressTracker.Change
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.unwrap
import net.corda.node.internal.InitiatedFlowFactory
import net.corda.node.services.persistence.checkpoints
import net.corda.testing.contracts.DummyContract
import net.corda.testing.contracts.DummyState
@ -116,7 +115,7 @@ class FlowFrameworkTests {
@Test
fun `exception while fiber suspended`() {
bobNode.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) }
bobNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) }
val flow = ReceiveFlow(bob)
val fiber = aliceNode.services.startFlow(flow) as FlowStateMachineImpl
// Before the flow runs change the suspend action to throw an exception
@ -134,7 +133,7 @@ class FlowFrameworkTests {
@Test
fun `both sides do a send as their first IO request`() {
bobNode.registerFlowFactory(PingPongFlow::class) { PingPongFlow(it, 20L) }
bobNode.registerCordappFlowFactory(PingPongFlow::class) { PingPongFlow(it, 20L) }
aliceNode.services.startFlow(PingPongFlow(bob, 10L))
mockNet.runNetwork()
@ -151,7 +150,7 @@ class FlowFrameworkTests {
@Test
fun `other side ends before doing expected send`() {
bobNode.registerFlowFactory(ReceiveFlow::class) { NoOpFlow() }
bobNode.registerCordappFlowFactory(ReceiveFlow::class) { NoOpFlow() }
val resultFuture = aliceNode.services.startFlow(ReceiveFlow(bob)).resultFuture
mockNet.runNetwork()
assertThatExceptionOfType(UnexpectedFlowEndException::class.java).isThrownBy {
@ -161,7 +160,7 @@ class FlowFrameworkTests {
@Test
fun `receiving unexpected session end before entering sendAndReceive`() {
bobNode.registerFlowFactory(WaitForOtherSideEndBeforeSendAndReceive::class) { NoOpFlow() }
bobNode.registerCordappFlowFactory(WaitForOtherSideEndBeforeSendAndReceive::class) { NoOpFlow() }
val sessionEndReceived = Semaphore(0)
receivedSessionMessagesObservable().filter {
it.message is ExistingSessionMessage && it.message.payload === EndSessionMessage
@ -176,7 +175,7 @@ class FlowFrameworkTests {
@Test
fun `FlowException thrown on other side`() {
val erroringFlow = bobNode.registerFlowFactory(ReceiveFlow::class) {
val erroringFlow = bobNode.registerCordappFlowFactory(ReceiveFlow::class) {
ExceptionFlow { MyFlowException("Nothing useful") }
}
val erroringFlowSteps = erroringFlow.flatMap { it.progressSteps }
@ -240,7 +239,7 @@ class FlowFrameworkTests {
}
}
bobNode.registerFlowFactory(AskForExceptionFlow::class) { ConditionalExceptionFlow(it, "Hello") }
bobNode.registerCordappFlowFactory(AskForExceptionFlow::class) { ConditionalExceptionFlow(it, "Hello") }
val resultFuture = aliceNode.services.startFlow(RetryOnExceptionFlow(bob)).resultFuture
mockNet.runNetwork()
assertThat(resultFuture.getOrThrow()).isEqualTo("Hello")
@ -248,7 +247,7 @@ class FlowFrameworkTests {
@Test
fun `serialisation issue in counterparty`() {
bobNode.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(NonSerialisableData(1), it) }
bobNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(NonSerialisableData(1), it) }
val result = aliceNode.services.startFlow(ReceiveFlow(bob)).resultFuture
mockNet.runNetwork()
assertThatExceptionOfType(UnexpectedFlowEndException::class.java).isThrownBy {
@ -258,7 +257,7 @@ class FlowFrameworkTests {
@Test
fun `FlowException has non-serialisable object`() {
bobNode.registerFlowFactory(ReceiveFlow::class) {
bobNode.registerCordappFlowFactory(ReceiveFlow::class) {
ExceptionFlow { NonSerialisableFlowException(NonSerialisableData(1)) }
}
val result = aliceNode.services.startFlow(ReceiveFlow(bob)).resultFuture
@ -275,7 +274,7 @@ class FlowFrameworkTests {
.addCommand(dummyCommand(alice.owningKey))
val stx = aliceNode.services.signInitialTransaction(ptx)
val committerFiber = aliceNode.registerFlowFactory(WaitingFlows.Waiter::class) {
val committerFiber = aliceNode.registerCordappFlowFactory(WaitingFlows.Waiter::class) {
WaitingFlows.Committer(it)
}.map { it.stateMachine }.map { uncheckedCast<FlowStateMachine<*>, FlowStateMachine<Any>>(it) }
val waiterStx = bobNode.services.startFlow(WaitingFlows.Waiter(stx, alice)).resultFuture
@ -290,7 +289,7 @@ class FlowFrameworkTests {
.addCommand(dummyCommand())
val stx = aliceNode.services.signInitialTransaction(ptx)
aliceNode.registerFlowFactory(WaitingFlows.Waiter::class) {
aliceNode.registerCordappFlowFactory(WaitingFlows.Waiter::class) {
WaitingFlows.Committer(it) { throw Exception("Error") }
}
val waiter = bobNode.services.startFlow(WaitingFlows.Waiter(stx, alice)).resultFuture
@ -307,7 +306,7 @@ class FlowFrameworkTests {
.addCommand(dummyCommand(alice.owningKey))
val stx = aliceNode.services.signInitialTransaction(ptx)
aliceNode.registerFlowFactory(VaultQueryFlow::class) {
aliceNode.registerCordappFlowFactory(VaultQueryFlow::class) {
WaitingFlows.Committer(it)
}
val result = bobNode.services.startFlow(VaultQueryFlow(stx, alice)).resultFuture
@ -318,7 +317,7 @@ class FlowFrameworkTests {
@Test
fun `customised client flow`() {
val receiveFlowFuture = bobNode.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it) }
val receiveFlowFuture = bobNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it) }
aliceNode.services.startFlow(CustomSendFlow("Hello", bob)).resultFuture
mockNet.runNetwork()
assertThat(receiveFlowFuture.getOrThrow().receivedPayloads).containsOnly("Hello")
@ -333,7 +332,7 @@ class FlowFrameworkTests {
@Test
fun `upgraded initiating flow`() {
bobNode.registerFlowFactory(UpgradedFlow::class, initiatedFlowVersion = 1) { InitiatedSendFlow("Old initiated", it) }
bobNode.registerCordappFlowFactory(UpgradedFlow::class, initiatedFlowVersion = 1) { InitiatedSendFlow("Old initiated", it) }
val result = aliceNode.services.startFlow(UpgradedFlow(bob)).resultFuture
mockNet.runNetwork()
assertThat(receivedSessionMessages).startsWith(
@ -347,7 +346,7 @@ class FlowFrameworkTests {
@Test
fun `upgraded initiated flow`() {
bobNode.registerFlowFactory(SendFlow::class, initiatedFlowVersion = 2) { UpgradedFlow(it) }
bobNode.registerCordappFlowFactory(SendFlow::class, initiatedFlowVersion = 2) { UpgradedFlow(it) }
val initiatingFlow = SendFlow("Old initiating", bob)
val flowInfo = aliceNode.services.startFlow(initiatingFlow).resultFuture
mockNet.runNetwork()
@ -387,7 +386,7 @@ class FlowFrameworkTests {
@Test
fun `single inlined sub-flow`() {
bobNode.registerFlowFactory(SendAndReceiveFlow::class) { SingleInlinedSubFlow(it) }
bobNode.registerCordappFlowFactory(SendAndReceiveFlow::class) { SingleInlinedSubFlow(it) }
val result = aliceNode.services.startFlow(SendAndReceiveFlow(bob, "Hello")).resultFuture
mockNet.runNetwork()
assertThat(result.getOrThrow()).isEqualTo("HelloHello")
@ -395,7 +394,7 @@ class FlowFrameworkTests {
@Test
fun `double inlined sub-flow`() {
bobNode.registerFlowFactory(SendAndReceiveFlow::class) { DoubleInlinedSubFlow(it) }
bobNode.registerCordappFlowFactory(SendAndReceiveFlow::class) { DoubleInlinedSubFlow(it) }
val result = aliceNode.services.startFlow(SendAndReceiveFlow(bob, "Hello")).resultFuture
mockNet.runNetwork()
assertThat(result.getOrThrow()).isEqualTo("HelloHello")
@ -403,7 +402,7 @@ class FlowFrameworkTests {
@Test
fun `non-FlowException thrown on other side`() {
val erroringFlowFuture = bobNode.registerFlowFactory(ReceiveFlow::class) {
val erroringFlowFuture = bobNode.registerCordappFlowFactory(ReceiveFlow::class) {
ExceptionFlow { Exception("evil bug!") }
}
val erroringFlowSteps = erroringFlowFuture.flatMap { it.progressSteps }
@ -507,8 +506,8 @@ class FlowFrameworkTripartyTests {
@Test
fun `sending to multiple parties`() {
bobNode.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() }
charlieNode.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() }
bobNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() }
charlieNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() }
val payload = "Hello World"
aliceNode.services.startFlow(SendFlow(payload, bob, charlie))
mockNet.runNetwork()
@ -538,8 +537,8 @@ class FlowFrameworkTripartyTests {
fun `receiving from multiple parties`() {
val bobPayload = "Test 1"
val charliePayload = "Test 2"
bobNode.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(bobPayload, it) }
charlieNode.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(charliePayload, it) }
bobNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(bobPayload, it) }
charlieNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(charliePayload, it) }
val multiReceiveFlow = ReceiveFlow(bob, charlie).nonTerminating()
aliceNode.services.startFlow(multiReceiveFlow)
aliceNode.internals.acceptableLiveFiberCountOnStop = 1
@ -564,8 +563,8 @@ class FlowFrameworkTripartyTests {
@Test
fun `FlowException only propagated to parent`() {
charlieNode.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Chain") } }
bobNode.registerFlowFactory(ReceiveFlow::class) { ReceiveFlow(charlie) }
charlieNode.registerCordappFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Chain") } }
bobNode.registerCordappFlowFactory(ReceiveFlow::class) { ReceiveFlow(charlie) }
val receivingFiber = aliceNode.services.startFlow(ReceiveFlow(bob))
mockNet.runNetwork()
assertThatExceptionOfType(UnexpectedFlowEndException::class.java)
@ -577,9 +576,9 @@ class FlowFrameworkTripartyTests {
// Bob will send its payload and then block waiting for the receive from Alice. Meanwhile Alice will move
// onto Charlie which will throw the exception
val node2Fiber = bobNode
.registerFlowFactory(ReceiveFlow::class) { SendAndReceiveFlow(it, "Hello") }
.registerCordappFlowFactory(ReceiveFlow::class) { SendAndReceiveFlow(it, "Hello") }
.map { it.stateMachine }
charlieNode.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Nothing useful") } }
charlieNode.registerCordappFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Nothing useful") } }
val aliceFiber = aliceNode.services.startFlow(ReceiveFlow(bob, charlie)) as FlowStateMachineImpl
mockNet.runNetwork()
@ -630,6 +629,8 @@ class FlowFrameworkPersistenceTests {
private lateinit var notaryIdentity: Party
private lateinit var alice: Party
private lateinit var bob: Party
private lateinit var aliceFlowManager: MockNodeFlowManager
private lateinit var bobFlowManager: MockNodeFlowManager
@Before
fun start() {
@ -637,8 +638,11 @@ class FlowFrameworkPersistenceTests {
cordappsForAllNodes = cordappsForPackages("net.corda.finance.contracts", "net.corda.testing.contracts"),
servicePeerAllocationStrategy = RoundRobin()
)
aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME))
bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME))
aliceFlowManager = MockNodeFlowManager()
bobFlowManager = MockNodeFlowManager()
aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME, flowManager = aliceFlowManager))
bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME, flowManager = bobFlowManager))
receivedSessionMessagesObservable().forEach { receivedSessionMessages += it }
@ -664,7 +668,7 @@ class FlowFrameworkPersistenceTests {
@Test
fun `flow restarted just after receiving payload`() {
bobNode.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() }
bobNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() }
aliceNode.services.startFlow(SendFlow("Hello", bob))
// We push through just enough messages to get only the payload sent
@ -679,7 +683,7 @@ class FlowFrameworkPersistenceTests {
@Test
fun `flow loaded from checkpoint will respond to messages from before start`() {
aliceNode.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) }
aliceNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) }
bobNode.services.startFlow(ReceiveFlow(alice).nonTerminating()) // Prepare checkpointed receive flow
val restoredFlow = bobNode.restartAndGetRestoredFlow<ReceiveFlow>()
assertThat(restoredFlow.receivedPayloads[0]).isEqualTo("Hello")
@ -694,7 +698,7 @@ class FlowFrameworkPersistenceTests {
var sentCount = 0
mockNet.messagingNetwork.sentMessages.toSessionTransfers().filter { it.isPayloadTransfer }.forEach { sentCount++ }
val charlieNode = mockNet.createNode(InternalMockNodeParameters(legalName = CHARLIE_NAME))
val secondFlow = charlieNode.registerFlowFactory(PingPongFlow::class) { PingPongFlow(it, payload2) }
val secondFlow = charlieNode.registerCordappFlowFactory(PingPongFlow::class) { PingPongFlow(it, payload2) }
mockNet.runNetwork()
val charlie = charlieNode.info.singleIdentity()
@ -802,23 +806,14 @@ private infix fun TestStartedNode.sent(message: SessionMessage): Pair<Int, Sessi
private infix fun Pair<Int, SessionMessage>.to(node: TestStartedNode): SessionTransfer = SessionTransfer(first, second, node.network.myAddress)
private data class SessionTransfer(val from: Int, val message: SessionMessage, val to: MessageRecipients) {
val isPayloadTransfer: Boolean get() =
message is ExistingSessionMessage && message.payload is DataSessionMessage ||
message is InitialSessionMessage && message.firstPayload != null
val isPayloadTransfer: Boolean
get() =
message is ExistingSessionMessage && message.payload is DataSessionMessage ||
message is InitialSessionMessage && message.firstPayload != null
override fun toString(): String = "$from sent $message to $to"
}
private inline fun <reified P : FlowLogic<*>> TestStartedNode.registerFlowFactory(
initiatingFlowClass: KClass<out FlowLogic<*>>,
initiatedFlowVersion: Int = 1,
noinline flowFactory: (FlowSession) -> P): CordaFuture<P> {
val observable = registerFlowFactory(
initiatingFlowClass.java,
InitiatedFlowFactory.CorDapp(initiatedFlowVersion, "", flowFactory),
P::class.java,
track = true)
return observable.toFuture()
}
private fun sessionInit(clientFlowClass: KClass<out FlowLogic<*>>, flowVersion: Int = 1, payload: Any? = null): InitialSessionMessage {
return InitialSessionMessage(SessionId(0), 0, clientFlowClass.java.name, flowVersion, "", payload?.serialize())
@ -1061,7 +1056,8 @@ private class SendAndReceiveFlow(val otherParty: Party, val payload: Any, val ot
constructor(otherPartySession: FlowSession, payload: Any) : this(otherPartySession.counterparty, payload, otherPartySession)
@Suspendable
override fun call(): Any = (otherPartySession ?: initiateFlow(otherParty)).sendAndReceive<Any>(payload).unwrap { it }
override fun call(): Any = (otherPartySession
?: initiateFlow(otherParty)).sendAndReceive<Any>(payload).unwrap { it }
}
private class InlinedSendFlow(val payload: String, val otherPartySession: FlowSession) : FlowLogic<Unit>() {
@ -1098,4 +1094,4 @@ private class ExceptionFlow<E : Exception>(val exception: () -> E) : FlowLogic<N
exceptionThrown = exception()
throw exceptionThrown
}
}
}

View File

@ -3,10 +3,7 @@ package net.corda.node.services.vault
import co.paralleluniverse.fibers.Suspendable
import com.nhaarman.mockito_kotlin.*
import net.corda.core.contracts.*
import net.corda.core.flows.FinalityFlow
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSession
import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.*
import net.corda.core.identity.AbstractParty
import net.corda.core.internal.FlowStateMachine
import net.corda.core.internal.packageName
@ -23,14 +20,12 @@ import net.corda.core.utilities.NonEmptySet
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.unwrap
import net.corda.node.internal.InitiatedFlowFactory
import net.corda.node.services.api.SchemaService
import net.corda.node.services.api.VaultServiceInternal
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.testing.core.singleIdentity
import net.corda.testing.internal.rigorousMock
import net.corda.testing.node.internal.cordappsForPackages
import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.cordappsForPackages
import net.corda.testing.node.internal.startFlow
import org.junit.After
import org.junit.Test
@ -68,7 +63,7 @@ class NodePair(private val mockNet: InternalMockNetwork) {
private set
fun <T> communicate(clientLogic: AbstractClientLogic<T>, rebootClient: Boolean): FlowStateMachine<T> {
server.registerFlowFactory(AbstractClientLogic::class.java, InitiatedFlowFactory.Core { ServerLogic(it, serverRunning) }, ServerLogic::class.java, false)
server.registerCoreFlowFactory(AbstractClientLogic::class.java, ServerLogic::class.java, { ServerLogic(it, serverRunning) }, false)
client.services.startFlow(clientLogic)
while (!serverRunning.get()) mockNet.runNetwork(1)
if (rebootClient) {

View File

@ -56,9 +56,11 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask,
webPort 10007
rpcUsers = [[user: "bankUser", password: "test", permissions: ["ALL"]]]
extraConfig = [
custom : [issuableCurrencies: ["USD"]],
h2Settings: [address: "localhost:10017"]
]
cordapp(project(':finance')) {
config "issuableCurrencies = [ USD ]"
}
}
node {
name "O=BigCorporation,L=New York,C=US"

View File

@ -89,6 +89,20 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask,
}
extraConfig = ['h2Settings.address' : 'localhost:10017']
}
//All other nodes should be using LoggingBuyerFlow as it is a subclass of BuyerFlow
node {
name "O=LoggingBank,L=London,C=GB"
p2pPort 10025
cordapps = ["$project.group:finance:$corda_release_version"]
rpcUsers = ext.rpcUsers
rpcSettings {
address "localhost:10026"
adminAddress "localhost:10027"
}
extraConfig = ['h2Settings.address' : 'localhost:10035']
flowOverride("net.corda.traderdemo.flow.SellerFlow", "net.corda.traderdemo.flow.BuyerFlow")
}
}
task integrationTest(type: Test, dependsOn: []) {

View File

@ -11,20 +11,17 @@ import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.unwrap
import net.corda.finance.contracts.CommercialPaper
import net.corda.finance.contracts.getCashBalances
import net.corda.finance.flows.TwoPartyTradeFlow
import net.corda.traderdemo.TransactionGraphSearch
import java.util.*
@InitiatedBy(SellerFlow::class)
class BuyerFlow(private val otherSideSession: FlowSession) : FlowLogic<Unit>() {
open class BuyerFlow(private val otherSideSession: FlowSession) : FlowLogic<SignedTransaction>() {
object STARTING_BUY : ProgressTracker.Step("Seller connected, purchasing commercial paper asset")
override val progressTracker: ProgressTracker = ProgressTracker(STARTING_BUY)
@Suspendable
override fun call() {
override fun call(): SignedTransaction {
progressTracker.currentStep = STARTING_BUY
// Receive the offered amount and automatically agree to it (in reality this would be a longer negotiation)
@ -43,33 +40,6 @@ class BuyerFlow(private val otherSideSession: FlowSession) : FlowLogic<Unit>() {
println("Purchase complete - we are a happy customer! Final transaction is: " +
"\n\n${Emoji.renderIfSupported(tradeTX.tx)}")
logIssuanceAttachment(tradeTX)
logBalance()
}
private fun logBalance() {
val balances = serviceHub.getCashBalances().entries.map { "${it.key.currencyCode} ${it.value}" }
println("Remaining balance: ${balances.joinToString()}")
}
private fun logIssuanceAttachment(tradeTX: SignedTransaction) {
// Find the original CP issuance.
// TODO: This is potentially very expensive, and requires transaction details we may no longer have once
// SGX is enabled. Should be replaced with including the attachment on all transactions involving
// the state.
val search = TransactionGraphSearch(serviceHub.validatedTransactions, listOf(tradeTX.tx),
TransactionGraphSearch.Query(withCommandOfType = CommercialPaper.Commands.Issue::class.java,
followInputsOfType = CommercialPaper.State::class.java))
val cpIssuance = search.call().single()
// Buyer will fetch the attachment from the seller automatically when it resolves the transaction.
cpIssuance.attachments.first().let {
println("""
The issuance of the commercial paper came with an attachment. You can find it in the attachments directory: $it.jar
${Emoji.renderIfSupported(cpIssuance)}""")
}
return tradeTX
}
}

View File

@ -0,0 +1,48 @@
package net.corda.traderdemo.flow
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.flows.FlowSession
import net.corda.core.flows.InitiatedBy
import net.corda.core.internal.Emoji
import net.corda.core.transactions.SignedTransaction
import net.corda.finance.contracts.CommercialPaper
import net.corda.finance.contracts.getCashBalances
import net.corda.traderdemo.TransactionGraphSearch
@InitiatedBy(SellerFlow::class)
class LoggingBuyerFlow(private val otherSideSession: FlowSession) : BuyerFlow(otherSideSession) {
@Suspendable
override fun call(): SignedTransaction {
val tradeTX = super.call()
logIssuanceAttachment(tradeTX)
logBalance()
return tradeTX
}
private fun logBalance() {
val balances = serviceHub.getCashBalances().entries.map { "${it.key.currencyCode} ${it.value}" }
println("Remaining balance: ${balances.joinToString()}")
}
private fun logIssuanceAttachment(tradeTX: SignedTransaction) {
// Find the original CP issuance.
// TODO: This is potentially very expensive, and requires transaction details we may no longer have once
// SGX is enabled. Should be replaced with including the attachment on all transactions involving
// the state.
val search = TransactionGraphSearch(serviceHub.validatedTransactions, listOf(tradeTX.tx),
TransactionGraphSearch.Query(withCommandOfType = CommercialPaper.Commands.Issue::class.java,
followInputsOfType = CommercialPaper.State::class.java))
val cpIssuance = search.call().single()
// Buyer will fetch the attachment from the seller automatically when it resolves the transaction.
cpIssuance.attachments.first().let {
println("""
The issuance of the commercial paper came with an attachment. You can find it in the attachments directory: $it.jar
${Emoji.renderIfSupported(cpIssuance)}""")
}
}
}

View File

@ -148,7 +148,8 @@ data class NodeParameters(
val maximumHeapSize: String = "512m",
val logLevel: String? = null,
val additionalCordapps: Collection<TestCordapp> = emptySet(),
val regenerateCordappsOnStart: Boolean = false
val regenerateCordappsOnStart: Boolean = false,
val flowOverrides: Map<Class<out FlowLogic<*>>, Class<out FlowLogic<*>>> = emptyMap()
) {
/**
* Helper builder for configuring a [Node] from Java.

Some files were not shown because too many files have changed in this diff Show More