diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index 89c5c2d3c4..2b6a606aeb 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -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 protect(list: List?): List? { + return list?.run { + if (isEmpty()) { + emptyList() + } else { + unmodifiableList(this) + } + } + } + @CordaInternal internal fun create( inputs: List>, @@ -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 transform(transformer: (List?, List?, List?) -> 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. diff --git a/node/build.gradle b/node/build.gradle index a5c2825fda..58777b10bc 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -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 { diff --git a/node/capsule/build.gradle b/node/capsule/build.gradle index eb223f22e4..0c30b71c0b 100644 --- a/node/capsule/build.gradle +++ b/node/capsule/build.gradle @@ -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 } diff --git a/node/capsule/src/main/java/CordaCaplet.java b/node/capsule/src/main/java/CordaCaplet.java index 016fada0b3..083dab8622 100644 --- a/node/capsule/src/main/java/CordaCaplet.java +++ b/node/capsule/src/main/java/CordaCaplet.java @@ -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 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 jvmArgs, List args) { checkJavaVersion(); diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index b0e9c12f1d..3d35c7751c 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -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(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(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") diff --git a/node/src/main/kotlin/net/corda/node/internal/DeterministicVerifier.kt b/node/src/main/kotlin/net/corda/node/internal/DeterministicVerifier.kt deleted file mode 100644 index fefa21e043..0000000000 --- a/node/src/main/kotlin/net/corda/node/internal/DeterministicVerifier.kt +++ /dev/null @@ -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) diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index a1405360bd..37dcd53627 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -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.") diff --git a/node/src/main/kotlin/net/corda/node/internal/ServicesForResolutionImpl.kt b/node/src/main/kotlin/net/corda/node/internal/ServicesForResolutionImpl.kt index f1a92608a9..c4a830d03d 100644 --- a/node/src/main/kotlin/net/corda/node/internal/ServicesForResolutionImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/ServicesForResolutionImpl.kt @@ -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) } } diff --git a/node/src/main/kotlin/net/corda/node/internal/djvm/DeterministicVerifier.kt b/node/src/main/kotlin/net/corda/node/internal/djvm/DeterministicVerifier.kt new file mode 100644 index 0000000000..bb283c95c6 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/djvm/DeterministicVerifier.kt @@ -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) diff --git a/node/src/main/kotlin/net/corda/node/internal/djvm/Executor.kt b/node/src/main/kotlin/net/corda/node/internal/djvm/Executor.kt new file mode 100644 index 0000000000..3f71c3320b --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/djvm/Executor.kt @@ -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 + 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 + } + } +} diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index fcf9759d33..48386df295 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -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() }