Add the Corda deterministic libraries to the Node, and split the DJVM sandbox across two SandboxClassLoader instances.

This commit is contained in:
Chris Rankin 2019-08-01 15:34:57 +01:00
parent d4da84cc05
commit 378635475a
11 changed files with 230 additions and 89 deletions

View File

@ -12,6 +12,7 @@ import net.corda.core.node.NetworkParameters
import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
import net.corda.core.utilities.contextLogger
import java.util.*
import java.util.Collections.unmodifiableList
import java.util.function.Predicate
/**
@ -80,6 +81,16 @@ private constructor(
companion object {
private val logger = contextLogger()
private fun <T> protect(list: List<T>?): List<T>? {
return list?.run {
if (isEmpty()) {
emptyList()
} else {
unmodifiableList(this)
}
}
}
@CordaInternal
internal fun create(
inputs: List<StateAndRef<ContractState>>,
@ -108,9 +119,9 @@ private constructor(
privacySalt = privacySalt,
networkParameters = networkParameters,
references = references,
componentGroups = componentGroups,
serializedInputs = serializedInputs,
serializedReferences = serializedReferences,
componentGroups = protect(componentGroups),
serializedInputs = protect(serializedInputs),
serializedReferences = protect(serializedReferences),
isAttachmentTrusted = isAttachmentTrusted,
verifierFactory = ::Verifier
)
@ -164,6 +175,14 @@ private constructor(
}
}
/**
* Pass all of this [LedgerTransaction] object's serialized state to a [transformer] function.
*/
@CordaInternal
fun <T> transform(transformer: (List<ComponentGroup>?, List<SerializedStateAndRef>?, List<SerializedStateAndRef>?) -> T): T {
return transformer(componentGroups, serializedInputs, serializedReferences)
}
/**
* We need a way to customise transaction verification inside the
* Node without changing either the wire format or any public APIs.

View File

@ -42,6 +42,7 @@ configurations {
jdkRt.resolutionStrategy {
cacheChangingModulesFor 0, 'seconds'
}
deterministic
}
sourceSets {
@ -163,6 +164,9 @@ dependencies {
// Sandbox for deterministic contract verification
compile "net.corda:corda-djvm:$djvm_version"
jdkRt "net.corda:deterministic-rt:latest.integration"
deterministic project(path: ':core-deterministic', configuration: 'deterministicArtifacts')
deterministic project(path: ':serialization-deterministic', configuration: 'deterministicArtifacts')
deterministic "org.slf4j:slf4j-nop:$slf4j_version"
testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}"
testImplementation "junit:junit:$junit_version"
@ -299,6 +303,10 @@ quasar {
jar {
baseName 'corda-node'
manifest {
attributes('Corda-Deterministic-Runtime': configurations.jdkRt.singleFile.name)
attributes('Corda-Deterministic-Classpath': configurations.deterministic.collect { it.name }.join(' '))
}
}
publish {

View File

@ -36,7 +36,11 @@ capsule {
version capsule_version
}
task buildCordaJAR(type: FatCapsule, dependsOn: project(':node').tasks.jar) {
task buildCordaJAR(type: FatCapsule, dependsOn: [
project(':node').tasks.jar,
project(':core-deterministic').tasks.assemble,
project(':serialization-deterministic').tasks.assemble
]) {
applicationClass 'net.corda.node.Corda'
archiveBaseName = 'corda'
archiveVersion = corda_release_version
@ -52,9 +56,18 @@ task buildCordaJAR(type: FatCapsule, dependsOn: project(':node').tasks.jar) {
from configurations.capsuleRuntime.files.collect { zipTree(it) }
with jar
def deterministicResolved = project(':node').configurations['deterministic'].resolvedConfiguration
def deterministicLibs = deterministicResolved.firstLevelModuleDependencies.moduleArtifacts.flatten { it.file }.toSet()
def deterministicCordaDependencies = deterministicResolved.files - deterministicLibs
manifest {
// These are the dependencies that the deterministic Corda libraries share with Corda.
attributes('Corda-DJVM-Dependencies': deterministicCordaDependencies.collect { it.name }.join(' '))
}
into('djvm') {
from project(':node').configurations['jdkRt'].singleFile
rename 'deterministic-rt(.*)', 'deterministic-rt.jar'
from deterministicLibs
fileMode = 0444
}

View File

@ -7,20 +7,20 @@ import sun.misc.Signal;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import java.util.stream.Stream;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
public class CordaCaplet extends Capsule {
private static final String DETERMINISTIC_RT = "deterministic-rt.jar";
private static final String DJVM_DIR ="djvm";
private static final String DETERMINISTIC_RT_RESOURCE = "/" + DJVM_DIR + "/" + DETERMINISTIC_RT;
private Config nodeConfig = null;
private String baseDir = null;
@ -93,27 +93,56 @@ public class CordaCaplet extends Capsule {
if (!djvmDir.toFile().mkdir() && !Files.isDirectory(djvmDir)) {
log(LOG_VERBOSE, "DJVM directory could not be created");
} else {
Path deterministicRt = djvmDir.resolve(DETERMINISTIC_RT);
Path sourceRt = appDir().resolve(DJVM_DIR).resolve(DETERMINISTIC_RT);
if (Files.isRegularFile(sourceRt)) {
try {
// Forcibly reinstall the deterministic APIs.
Files.deleteIfExists(deterministicRt);
Files.createSymbolicLink(deterministicRt, sourceRt);
} catch (UnsupportedOperationException | IOException e) {
copyFile(sourceRt, deterministicRt);
}
} else {
URL rtURL = getClass().getResource(DETERMINISTIC_RT_RESOURCE);
if (rtURL == null) {
log(LOG_VERBOSE, DETERMINISTIC_RT_RESOURCE + " missing from Corda capsule");
} else {
copyResource(rtURL, deterministicRt);
try {
Path sourceDir = appDir().resolve(DJVM_DIR);
if (Files.isDirectory(sourceDir)) {
installCordaDependenciesForDJVM(sourceDir, djvmDir);
installTransitiveDependenciesForDJVM(appDir(), djvmDir);
}
} catch (IOException e) {
log(LOG_VERBOSE, "Failed to populate directory " + djvmDir.toAbsolutePath());
log(LOG_VERBOSE, e);
}
}
}
private void installCordaDependenciesForDJVM(Path sourceDir, Path targetDir) throws IOException {
try (DirectoryStream<Path> directory = Files.newDirectoryStream(sourceDir, file -> Files.isRegularFile(file))) {
for (Path sourceFile : directory) {
Path targetFile = targetDir.resolve(sourceFile.getFileName());
installFile(sourceFile, targetFile);
}
}
}
private void installTransitiveDependenciesForDJVM(Path sourceDir, Path targetDir) throws IOException {
Manifest manifest = getManifest();
String[] transitives = manifest.getMainAttributes().getValue("Corda-DJVM-Dependencies").split("\\s++", 0);
for (String transitive : transitives) {
Path source = sourceDir.resolve(transitive);
if (Files.isRegularFile(source)) {
installFile(source, targetDir.resolve(transitive));
}
}
}
private Manifest getManifest() throws IOException {
URL capsule = getClass().getProtectionDomain().getCodeSource().getLocation();
try (JarInputStream jar = new JarInputStream(capsule.openStream())) {
return jar.getManifest();
}
}
private void installFile(Path source, Path target) {
try {
// Forcibly reinstall this dependency.
Files.deleteIfExists(target);
Files.createSymbolicLink(target, source);
} catch (UnsupportedOperationException | IOException e) {
copyFile(source, target);
}
}
private void copyFile(Path source, Path target) {
try {
Files.copy(source, target, REPLACE_EXISTING);
@ -124,16 +153,6 @@ public class CordaCaplet extends Capsule {
}
}
private void copyResource(URL source, Path target) {
try (InputStream input = source.openStream()) {
Files.copy(input, target, REPLACE_EXISTING);
} catch (IOException e) {
//noinspection ResultOfMethodCallIgnored
target.toFile().delete();
log(LOG_VERBOSE, e);
}
}
@Override
protected ProcessBuilder prelaunch(List<String> jvmArgs, List<String> args) {
checkJavaVersion();

View File

@ -34,9 +34,7 @@ import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.days
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.minutes
import net.corda.djvm.source.ApiSource
import net.corda.djvm.source.BootstrapClassLoader
import net.corda.djvm.source.EmptyApi
import net.corda.djvm.source.*
import net.corda.node.CordaClock
import net.corda.node.VersionInfo
import net.corda.node.internal.classloading.requireAnnotation
@ -129,7 +127,8 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
protected val flowManager: FlowManager,
val serverThread: AffinityExecutor.ServiceAffinityExecutor,
val busyNodeLatch: ReusableLatch = ReusableLatch(),
bootstrapSource: ApiSource = EmptyApi) : SingletonSerializeAsToken() {
bootstrapSource: ApiSource = EmptyApi,
djvmCordaSource: UserSource? = null) : SingletonSerializeAsToken() {
protected abstract val log: Logger
@Suppress("LeakingThis")
@ -195,7 +194,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
val pkToIdCache = PublicKeyToOwningIdentityCacheImpl(database, cacheFactory)
@Suppress("LeakingThis")
val keyManagementService = makeKeyManagementService(identityService).tokenize()
val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersStorage, transactionStorage, bootstrapSource).also {
val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersStorage, transactionStorage, bootstrapSource, djvmCordaSource).also {
attachments.servicesForResolution = it
}
@Suppress("LeakingThis")

View File

@ -1,38 +0,0 @@
package net.corda.node.internal
import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.ContractVerifier
import net.corda.core.internal.Verifier
import net.corda.core.transactions.LedgerTransaction
import net.corda.djvm.SandboxConfiguration
import net.corda.djvm.analysis.AnalysisConfiguration
import net.corda.djvm.execution.*
import net.corda.djvm.source.ClassSource
class DeterministicVerifier(
ltx: LedgerTransaction,
transactionClassLoader: ClassLoader,
private val analysisConfiguration: AnalysisConfiguration
) : Verifier(ltx, transactionClassLoader) {
override fun verifyContracts() {
try {
val configuration = SandboxConfiguration.of(
enableTracing = false,
analysisConfiguration = analysisConfiguration
)
val executor = SandboxRawExecutor(configuration)
executor.run(ClassSource.fromClassName(ContractVerifier::class.java.name), ltx)
} catch (e: Exception) {
throw DeterministicVerificationException(ltx.id, e.message ?: "", e)
}
}
override fun close() {
analysisConfiguration.close()
}
}
class DeterministicVerificationException(id: SecureHash, message: String, cause: Throwable)
: TransactionVerificationException(id, message, cause)

View File

@ -4,6 +4,7 @@ import com.codahale.metrics.MetricFilter
import com.codahale.metrics.MetricRegistry
import com.codahale.metrics.jmx.JmxReporter
import com.github.benmanes.caffeine.cache.Caffeine
import com.jcabi.manifests.Manifests
import com.palominolabs.metrics.newrelic.AllEnabledMetricAttributeFilter
import com.palominolabs.metrics.newrelic.NewRelicReporter
import io.netty.util.NettyRuntime
@ -30,9 +31,7 @@ import net.corda.core.serialization.internal.SerializationEnvironment
import net.corda.core.serialization.internal.nodeSerializationEnv
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger
import net.corda.djvm.source.ApiSource
import net.corda.djvm.source.BootstrapClassLoader
import net.corda.djvm.source.EmptyApi
import net.corda.djvm.source.*
import net.corda.node.CordaClock
import net.corda.node.SimpleClock
import net.corda.node.VersionInfo
@ -78,6 +77,7 @@ import java.lang.Long.max
import java.lang.Long.min
import java.net.BindException
import java.net.InetAddress
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.time.Clock
@ -111,13 +111,17 @@ open class Node(configuration: NodeConfiguration,
flowManager,
// Under normal (non-test execution) it will always be "1"
AffinityExecutor.ServiceAffinityExecutor("Node thread-${sameVmNodeCounter.incrementAndGet()}", 1),
bootstrapSource = createBootstrapSource(configuration)
bootstrapSource = createBootstrapSource(configuration),
djvmCordaSource = createDeterministicClasspath(configuration)
) {
override fun createStartedNode(nodeInfo: NodeInfo, rpcOps: CordaRPCOps, notaryService: NotaryService?): NodeInfo =
nodeInfo
companion object {
private const val CORDA_DETERMINISTIC_RUNTIME_ATTR = "Corda-Deterministic-Runtime"
private const val CORDA_DETERMINISTIC_CLASSPATH_ATTR = "Corda-Deterministic-Classpath"
private val staticLog = contextLogger()
var renderBasicInfoToConsole = true
@ -177,10 +181,41 @@ open class Node(configuration: NodeConfiguration,
}
}
private fun manifestValue(attrName: String): String? = if (Manifests.exists(attrName)) Manifests.read(attrName) else null
fun createDeterministicClasspath(config: NodeConfiguration): UserSource? {
val classpathSource = config.baseDirectory.resolve("djvm")
val djvmClasspath = manifestValue(CORDA_DETERMINISTIC_CLASSPATH_ATTR)
return if (djvmClasspath == null) {
staticLog.warn("{} missing from MANIFEST.MF - deterministic contract verification now impossible!",
CORDA_DETERMINISTIC_CLASSPATH_ATTR)
null
} else if (!Files.isDirectory(classpathSource)) {
staticLog.warn("{} directory does not exist - deterministic contract verification now impossible!",
classpathSource.toAbsolutePath())
null
} else {
val files = djvmClasspath.split("\\s++".toRegex(), 0).map { classpathSource.resolve(it) }
.filter { Files.isRegularFile(it) || Files.isSymbolicLink(it) }
staticLog.info("Corda Deterministic Libraries: {}", files.map(Path::getFileName).joinToString())
val jars = files.map { it.toUri().toURL() }.toTypedArray()
UserPathSource(jars)
}
}
fun createBootstrapSource(config: NodeConfiguration): ApiSource {
val bootstrapSource = config.baseDirectory.resolve("djvm").resolve("deterministic-rt.jar")
val deterministicRt = manifestValue(CORDA_DETERMINISTIC_RUNTIME_ATTR)
if (deterministicRt == null) {
staticLog.warn("{} missing from MANIFEST.MF - will use host JVM for deterministic runtime.",
CORDA_DETERMINISTIC_RUNTIME_ATTR)
return EmptyApi
}
val bootstrapSource = config.baseDirectory.resolve("djvm").resolve(deterministicRt)
return if (bootstrapSource.isRegularFile()) {
staticLog.info("Deterministic Runtime: {}", bootstrapSource)
staticLog.info("Deterministic Runtime: {}", bootstrapSource.fileName)
BootstrapClassLoader(bootstrapSource)
} else {
staticLog.warn("NO DETERMINISTIC RUNTIME FOUND - will use host JVM instead.")

View File

@ -19,6 +19,8 @@ import net.corda.djvm.analysis.AnalysisConfiguration
import net.corda.djvm.analysis.Whitelist
import net.corda.djvm.source.ApiSource
import net.corda.djvm.source.UserPathSource
import net.corda.djvm.source.UserSource
import net.corda.node.internal.djvm.DeterministicVerifier
import java.net.URLClassLoader
data class ServicesForResolutionImpl(
@ -27,7 +29,8 @@ data class ServicesForResolutionImpl(
override val cordappProvider: CordappProvider,
override val networkParametersService: NetworkParametersService,
private val validatedTransactions: TransactionStorage,
private val djvmBootstrapSource: ApiSource
private val djvmBootstrapSource: ApiSource,
private val djvmCordaSource: UserSource?
) : ServicesForResolution {
override val networkParameters: NetworkParameters get() = networkParametersService.lookup(networkParametersService.currentHash) ?:
throw IllegalArgumentException("No current parameters in network parameters storage")
@ -79,18 +82,21 @@ data class ServicesForResolutionImpl(
}
override fun specialise(ltx: LedgerTransaction): LedgerTransaction {
// Do nothing unless we have Corda's deterministic libraries.
val cordaSource = djvmCordaSource ?: return ltx
// Specialise the LedgerTransaction here so that
// contracts are verified inside the DJVM!
return ltx.specialise { tx, cl ->
(cl as? URLClassLoader)?.run { DeterministicVerifier(tx, cl, createSandbox(cl)) } ?: Verifier(tx, cl)
(cl as? URLClassLoader)?.run { DeterministicVerifier(tx, cl, createSandbox(cordaSource, cl)) } ?: Verifier(tx, cl)
}
}
private fun createSandbox(classLoader: URLClassLoader): AnalysisConfiguration {
private fun createSandbox(cordaSource: UserSource, classLoader: URLClassLoader): AnalysisConfiguration {
return AnalysisConfiguration.createRoot(
userSource = UserPathSource(classLoader.urLs),
userSource = cordaSource,
whitelist = Whitelist.MINIMAL,
bootstrapSource = djvmBootstrapSource
)
).createChild(UserPathSource(classLoader.urLs), null)
}
}

View File

@ -0,0 +1,56 @@
package net.corda.node.internal.djvm
import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.ContractVerifier
import net.corda.core.internal.Verifier
import net.corda.core.transactions.LedgerTransaction
import net.corda.djvm.SandboxConfiguration
import net.corda.djvm.analysis.AnalysisConfiguration
import net.corda.djvm.execution.*
import net.corda.djvm.messages.Message
import net.corda.djvm.source.ClassSource
class DeterministicVerifier(
ltx: LedgerTransaction,
transactionClassLoader: ClassLoader,
private val analysisConfiguration: AnalysisConfiguration
) : Verifier(ltx, transactionClassLoader) {
override fun verifyContracts() {
val configuration = SandboxConfiguration.of(
enableTracing = false,
analysisConfiguration = analysisConfiguration
)
val verifierClass = ClassSource.fromClassName(ContractVerifier::class.java.name)
val result = IsolatedTask(verifierClass.qualifiedClassName, configuration).run {
val executor = Executor(classLoader)
val sandboxTx = ltx.transform { componentGroups, serializedInputs, serializedReferences ->
}
val verifier = classLoader.loadClassForSandbox(verifierClass).newInstance()
// Now execute the contract verifier task within the sandbox...
executor.execute(verifier, sandboxTx)
}
result.exception?.run {
val sandboxEx = SandboxException(
Message.getMessageFromException(this),
result.identifier,
verifierClass,
ExecutionSummary(result.costs),
this
)
throw DeterministicVerificationException(ltx.id, sandboxEx.message ?: "", sandboxEx)
}
}
override fun close() {
analysisConfiguration.close()
}
}
class DeterministicVerificationException(id: SecureHash, message: String, cause: Throwable)
: TransactionVerificationException(id, message, cause)

View File

@ -0,0 +1,24 @@
package net.corda.node.internal.djvm
import java.lang.reflect.Constructor
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
class Executor(classLoader: ClassLoader) {
private val constructor: Constructor<out Any>
private val executeMethod: Method
init {
val taskClass = classLoader.loadClass("sandbox.RawTask")
constructor = taskClass.getDeclaredConstructor(classLoader.loadClass("sandbox.java.util.function.Function"))
executeMethod = taskClass.getMethod("apply", Any::class.java)
}
fun execute(task: Any, input: Any?): Any? {
return try {
executeMethod.invoke(constructor.newInstance(task), input)
} catch (ex: InvocationTargetException) {
throw ex.targetException
}
}
}

View File

@ -431,7 +431,7 @@ open class MockServices private constructor(
override var networkParametersService: NetworkParametersService = MockNetworkParametersStorage(initialNetworkParameters)
protected val servicesForResolution: ServicesForResolution
get() = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersService, validatedTransactions, EmptyApi)
get() = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersService, validatedTransactions, EmptyApi, null)
internal fun makeVaultService(schemaService: SchemaService, database: CordaPersistence, cordappLoader: CordappLoader): VaultServiceInternal {
return NodeVaultService(clock, keyManagementService, servicesForResolution, database, schemaService, cordappLoader.appClassLoader).apply { start() }