diff --git a/.ci/api-current.txt b/.ci/api-current.txt index d3363ad577..a4e640eb8e 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -2532,7 +2532,6 @@ public interface net.corda.core.messaging.CordaRPCOps extends net.corda.core.mes public abstract void clearNetworkMapCache() @NotNull public abstract java.time.Instant currentNodeTime() - public abstract int getProtocolVersion() @NotNull public abstract Iterable getVaultTransactionNotes(net.corda.core.crypto.SecureHash) @RPCReturnsObservables @@ -4449,8 +4448,6 @@ public interface net.corda.core.serialization.SerializationCustomSerializer public abstract PROXY toProxy(OBJ) ## public final class net.corda.core.serialization.SerializationDefaults extends java.lang.Object - @NotNull - public final net.corda.core.serialization.SerializationContext getCHECKPOINT_CONTEXT() @NotNull public final net.corda.core.serialization.SerializationContext getP2P_CONTEXT() @NotNull @@ -6887,8 +6884,6 @@ public final class net.corda.testing.core.SerializationEnvironmentRule extends j @NotNull public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement, org.junit.runner.Description) @NotNull - public final net.corda.core.serialization.SerializationContext getCheckpointContext() - @NotNull public final net.corda.core.serialization.SerializationFactory getSerializationFactory() public static final net.corda.testing.core.SerializationEnvironmentRule$Companion Companion ## diff --git a/.ci/ci-gradle-build-cache-init.sh b/.ci/ci-gradle-build-cache-init.sh new file mode 100755 index 0000000000..1b076beb0d --- /dev/null +++ b/.ci/ci-gradle-build-cache-init.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +export GRADLE_BUILD_CACHE_URL="${GRADLE_BUILD_CACHE_URL:-http://localhost:5071/cache/}" +export USE_GRADLE_DAEMON="${USE_GRADLE_DAEMON:-false}" +export GRADLE_CACHE_DEBUG="${GRADLE_CACHE_DEBUG:-false}" +export PERFORM_GRADLE_SCAN="${PERFORM_GRADLE_SCAN:---scan}" + +# cd %teamcity.build.checkoutDir% +echo "Using Gradle Build Cache: $GRADLE_BUILD_CACHE_URL" \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml index ef6407f0b3..d981d5fd35 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -235,4 +235,4 @@ - \ No newline at end of file + diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index bf3094165c..31cdf7d71c 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -84,6 +84,7 @@ see changes to this list. * Giulio Katis (Westpac) * Giuseppe Cardone (Intesa Sanpaolo) * Guy Hochstetler (R3) +* Hristo Gatsinski (Industria) * Ian Cusden (UBS) * Ian Grigg (R3) * Igor Nitto (R3) @@ -91,6 +92,7 @@ see changes to this list. * Ivan Schasny (R3) * James Brown (R3) * James Carlyle (R3) +* Janis Olekss (Accenture) * Jared Harwayne-Gidansky (BNY Mellon) * Jayavaradhan Sambedu (Société Générale) * Joel Dudley (R3) @@ -116,7 +118,7 @@ see changes to this list. * Lars Stage Thomsen (Danske Bank) * Lee Braine (Barclays) * Lucas Salmen (Itau) -* Lulu Ren (S-Labs) +* Lulu Ren (Monad-Labs) * Maksymilian Pawlak (R3) * Marek Scocovsky (ABSA) * marekdapps @@ -137,6 +139,7 @@ see changes to this list. * Mike Hearn (R3) * Mike Ward (R3) * Mike Reichelt (US Bank) +* Milen Dobrinov (Industria) * Mohamed Amine LEGHERABA * Mustafa Ozturk (Natixis) * Nick Skinner (Northern Trust) @@ -145,6 +148,7 @@ see changes to this list. * Nuam Athaweth (MUFG) * Oscar Zibordi de Paiva (Scopus Soluções em TI) * OP Financial +* Parnika Sharma (BCS Technology) * Patrick Kuo (R3) * Pekka Kaipio (OP Financial) * Phillip Griffin @@ -176,6 +180,7 @@ see changes to this list. * Scott James * Sean Zhang (Wells Fargo) * Shams Asari (R3) +* Shivan Sawant (Persistent Systems Limited) * Siddhartha Sengupta (Tradewind Markets) * Simon Taylor (Barclays) * Sofus Mortensen (Digital Asset Holdings) diff --git a/build.gradle b/build.gradle index ae3ed9e67c..d99ec0aa13 100644 --- a/build.gradle +++ b/build.gradle @@ -17,9 +17,9 @@ buildscript { ext.quasar_group = 'co.paralleluniverse' ext.quasar_version = '0.7.10' - // gradle-capsule-plugin:1.0.2 contains capsule:1.0.1 - // TODO: Upgrade gradle-capsule-plugin to a version with capsule:1.0.3 - ext.capsule_version = '1.0.1' + // gradle-capsule-plugin:1.0.2 contains capsule:1.0.1 by default. + // We must configure it manually to use the latest capsule version. + ext.capsule_version = '1.0.3' ext.asm_version = '5.0.4' ext.artemis_version = '2.6.2' @@ -32,6 +32,7 @@ buildscript { ext.bouncycastle_version = constants.getProperty("bouncycastleVersion") ext.guava_version = constants.getProperty("guavaVersion") ext.caffeine_version = constants.getProperty("caffeineVersion") + ext.disruptor_version = constants.getProperty("disruptorVersion") ext.metrics_version = constants.getProperty("metricsVersion") ext.metrics_new_relic_version = constants.getProperty("metricsNewRelicVersion") ext.okhttp_version = '3.5.0' @@ -46,7 +47,7 @@ buildscript { ext.hibernate_version = '5.3.6.Final' ext.h2_version = '1.4.197' // Update docs if renamed or removed. ext.postgresql_version = '42.1.4' - ext.rxjava_version = '1.2.4' + ext.rxjava_version = '1.3.8' ext.dokka_version = '0.9.17' ext.eddsa_version = '0.2.0' ext.dependency_checker_version = '3.1.0' @@ -55,9 +56,8 @@ buildscript { ext.crash_version = 'cadb53544fbb3c0fb901445da614998a6a419488' ext.jsr305_version = constants.getProperty("jsr305Version") ext.shiro_version = '1.4.0' - ext.shadow_version = '2.0.4' ext.artifactory_plugin_version = constants.getProperty('artifactoryPluginVersion') - ext.liquibase_version = '3.6.2' + ext.liquibase_version = '3.5.5' ext.artifactory_contextUrl = 'https://ci-artifactory.corda.r3cev.com/artifactory' ext.snake_yaml_version = constants.getProperty('snakeYamlVersion') ext.docker_compose_rule_version = '0.33.0' @@ -78,6 +78,7 @@ buildscript { // Update 121 is required for ObjectInputFilter. // Updates [131, 161] also have zip compression bugs on MacOS (High Sierra). + // when the java version in NodeStartup.hasMinimumJavaVersion() changes, so must this check ext.java8_minUpdateVersion = '171' repositories { @@ -115,8 +116,12 @@ buildscript { plugins { // TODO The capsule plugin requires the newer DSL plugin block.It would be nice if we could unify all the plugins into one style, // but the DSL has some restrictions e.g can't be used on the allprojects section. So we should revisit this if there are improvements in Gradle. - // Version 1.0.2 of this plugin uses capsule:1.0.1 - id "us.kirchmeier.capsule" version "1.0.2" + // Version 1.0.2 of this plugin uses capsule:1.0.1 by default. + id 'us.kirchmeier.capsule' version '1.0.2' apply false + + // Add the shadow plugin to the plugins classpath for the entire project. + id 'com.github.johnrengelman.shadow' version '2.0.4' apply false + id "com.gradle.build-scan" version "1.16" } ext { @@ -195,11 +200,18 @@ allprojects { if (System.getProperty("test.maxParallelForks") != null) { maxParallelForks = Integer.valueOf(System.getProperty("test.maxParallelForks")) + logger.debug("System property test.maxParallelForks found - setting max parallel forks to $maxParallelForks for $project") } if (project.path.startsWith(':experimental') && System.getProperty("experimental.test.enable") == null) { enabled = false } + + // Required to use Gradle build cache (until Gradle 5.0 is released with default value of "append" set to false) + // See https://github.com/gradle/gradle/issues/5269 and https://github.com/gradle/gradle/pull/6419 + extensions.configure(TypeOf.typeOf(JacocoTaskExtension)) { ex -> + ex.append = false + } } group 'net.corda' @@ -209,6 +221,7 @@ allprojects { mavenLocal() mavenCentral() jcenter() + maven { url "$artifactory_contextUrl/corda-dependencies" } maven { url 'https://jitpack.io' } } @@ -235,6 +248,8 @@ allprojects { // We want to use SLF4J's version of these bindings: jcl-over-slf4j // Remove any transitive dependency on Apache's version. exclude group: 'commons-logging', module: 'commons-logging' + // Remove any transitive dependency on Logback (e.g. Liquibase 3.6 introduces this dependency) + exclude group: 'ch.qos.logback' // Netty-All is an uber-jar which contains every Netty module. // Exclude it to force us to use the individual Netty modules instead. @@ -255,12 +270,6 @@ allprojects { if (!JavaVersion.current().java8Compatible) throw new GradleException("Corda requires Java 8, please upgrade to at least 1.8.0_$java8_minUpdateVersion") -repositories { - mavenLocal() - mavenCentral() - jcenter() -} - // Required for building out the fat JAR. dependencies { compile project(':node') @@ -333,6 +342,8 @@ bintrayConfig { 'corda-rpc', 'corda-core', 'corda-core-deterministic', + 'corda-deterministic-verifier', + 'corda-djvm', 'corda', 'corda-finance', 'corda-node', @@ -350,7 +361,8 @@ bintrayConfig { 'corda-serialization-deterministic', 'corda-tools-blob-inspector', 'corda-tools-explorer', - 'corda-tools-network-bootstrapper' + 'corda-tools-network-bootstrapper', + 'corda-tools-cliutils' ] license { name = 'Apache-2.0' @@ -382,7 +394,7 @@ artifactory { contextUrl = artifactory_contextUrl repository { repoKey = 'corda-dev' - username = 'teamcity' + username = System.getenv('CORDA_ARTIFACTORY_USERNAME') password = System.getenv('CORDA_ARTIFACTORY_PASSWORD') } @@ -431,6 +443,11 @@ if (file('corda-docs-only-build').exists() || (System.getenv('CORDA_DOCS_ONLY_BU } wrapper { - gradleVersion = "4.8.1" + gradleVersion = "4.10.1" distributionType = Wrapper.DistributionType.ALL } + +buildScan { + termsOfServiceUrl = 'https://gradle.com/terms-of-service' + termsOfServiceAgree = 'yes' +} \ No newline at end of file diff --git a/buildCacheSettings.gradle b/buildCacheSettings.gradle new file mode 100644 index 0000000000..fcfc1513bf --- /dev/null +++ b/buildCacheSettings.gradle @@ -0,0 +1,15 @@ +// Gradle Build Cache configuration recommendation: https://docs.gradle.org/current/userguide/build_cache.html +ext { + isCiServer = System.getenv().containsKey("CORDA_CI") + gradleBuildCacheURL = System.getenv().containsKey("GRADLE_BUILD_CACHE_URL") ? System.getenv().get("GRADLE_BUILD_CACHE_URL") : 'http://localhost:5071/cache/' +} + +buildCache { + local { + enabled = !isCiServer + } + remote(HttpBuildCache) { + url = gradleBuildCacheURL + push = isCiServer + } +} diff --git a/buildSrc/settings.gradle b/buildSrc/settings.gradle index c46d96de90..2449091bd9 100644 --- a/buildSrc/settings.gradle +++ b/buildSrc/settings.gradle @@ -1,2 +1,4 @@ rootProject.name = 'buildSrc' include 'canonicalizer' + +apply from: '../buildCacheSettings.gradle' \ No newline at end of file diff --git a/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt index ae64ab4004..3f5546b2e9 100644 --- a/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt +++ b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt @@ -197,7 +197,7 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory: fun DigitalSignatureWithCert() { val digitalSignature = DigitalSignatureWithCert(MINI_CORP.identity.certificate, secureRandomBytes(128)) val json = mapper.valueToTree(digitalSignature) - val (by, bytes) = json.assertHasOnlyFields("by", "bytes") + val (by, bytes) = json.assertHasOnlyFields("by", "bytes", "parentCertsChain") assertThat(by.valueAs(mapper)).isEqualTo(MINI_CORP.identity.certificate) assertThat(bytes.binaryValue()).isEqualTo(digitalSignature.bytes) assertThat(mapper.convertValue(json)).isEqualTo(digitalSignature) @@ -610,7 +610,8 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory: assertThat(json["serialNumber"].bigIntegerValue()).isEqualTo(cert.serialNumber) assertThat(json["issuer"].valueAs(mapper)).isEqualTo(cert.issuerX500Principal) assertThat(json["subject"].valueAs(mapper)).isEqualTo(cert.subjectX500Principal) - assertThat(json["publicKey"].valueAs(mapper)).isEqualTo(cert.publicKey) + // cert.publicKey should be converted to a supported format (this is required because [Certificate] returns keys as SUN EC keys, not BC). + assertThat(json["publicKey"].valueAs(mapper)).isEqualTo(Crypto.toSupportedPublicKey(cert.publicKey)) assertThat(json["notAfter"].valueAs(mapper)).isEqualTo(cert.notAfter) assertThat(json["notBefore"].valueAs(mapper)).isEqualTo(cert.notBefore) assertThat(json["encoded"].binaryValue()).isEqualTo(cert.encoded) diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NetworkIdentityModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NetworkIdentityModel.kt index 0b09957678..aa7f7e50f2 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NetworkIdentityModel.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NetworkIdentityModel.kt @@ -1,5 +1,6 @@ package net.corda.client.jfx.model +import com.github.benmanes.caffeine.cache.CacheLoader import com.github.benmanes.caffeine.cache.Caffeine import javafx.beans.value.ObservableValue import javafx.collections.FXCollections @@ -31,7 +32,7 @@ class NetworkIdentityModel { private val rpcProxy by observableValue(NodeMonitorModel::proxyObservable) private val identityCache = Caffeine.newBuilder() - .buildNamed>("NetworkIdentityModel_identity", { publicKey -> + .buildNamed>("NetworkIdentityModel_identity", CacheLoader { publicKey: PublicKey -> publicKey.let { rpcProxy.map { it?.cordaRPCOps?.nodeInfoFromParty(AnonymousParty(publicKey)) } } }) val notaries = ChosenList(rpcProxy.map { FXCollections.observableList(it?.cordaRPCOps?.notaryIdentities() ?: emptyList()) }, "notaries") diff --git a/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java b/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java index 44f65f0bc0..366fb6802e 100644 --- a/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java +++ b/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java @@ -55,7 +55,7 @@ public class CordaRPCJavaClientTest extends NodeBasedTest { @Before public void setUp() throws Exception { - node = startNode(ALICE_NAME, 1, singletonList(rpcUser)); + node = startNode(ALICE_NAME, 1000, singletonList(rpcUser)); client = new CordaRPCClient(requireNonNull(node.getNode().getConfiguration().getRpcOptions().getAddress())); } diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt index 6ceb5c2cad..b05e33c729 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt @@ -47,7 +47,7 @@ class RPCStabilityTests { } object DummyOps : RPCOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 } private fun waitUntilNumberOfThreadsStable(executorService: ScheduledExecutorService): Map> { @@ -107,7 +107,7 @@ class RPCStabilityTests { Try.on { startRpcClient( server.get().broker.hostAndPort!!, - configuration = CordaRPCClientConfiguration.DEFAULT.copy(minimumServerProtocolVersion = 1) + configuration = CordaRPCClientConfiguration.DEFAULT.copy(minimumServerProtocolVersion = 1000) ).get() } } @@ -203,7 +203,7 @@ class RPCStabilityTests { rpcDriver { val leakObservableOpsImpl = object : LeakObservableOps { val leakedUnsubscribedCount = AtomicInteger(0) - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun leakObservable(): Observable { return PublishSubject.create().doOnUnsubscribe { leakedUnsubscribedCount.incrementAndGet() @@ -234,7 +234,7 @@ class RPCStabilityTests { fun `client reconnects to rebooted server`() { rpcDriver { val ops = object : ReconnectOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun ping() = "pong" } @@ -259,7 +259,7 @@ class RPCStabilityTests { fun `connection failover fails, rpc calls throw`() { rpcDriver { val ops = object : ReconnectOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun ping() = "pong" } @@ -290,7 +290,7 @@ class RPCStabilityTests { fun `observables error when connection breaks`() { rpcDriver { val ops = object : NoOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun subscribe(): Observable { return PublishSubject.create() } @@ -350,7 +350,7 @@ class RPCStabilityTests { fun `client connects to first available server`() { rpcDriver { val ops = object : ServerOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun serverId() = "server" } val serverFollower = shutdownManager.follower() @@ -371,15 +371,15 @@ class RPCStabilityTests { fun `3 server failover`() { rpcDriver { val ops1 = object : ServerOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun serverId() = "server1" } val ops2 = object : ServerOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun serverId() = "server2" } val ops3 = object : ServerOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun serverId() = "server3" } val serverFollower1 = shutdownManager.follower() @@ -443,7 +443,7 @@ class RPCStabilityTests { fun `server cleans up queues after disconnected clients`() { rpcDriver { val trackSubscriberOpsImpl = object : TrackSubscriberOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 val subscriberCount = AtomicInteger(0) val trackSubscriberCountObservable = UnicastSubject.create().share(). doOnSubscribe { subscriberCount.incrementAndGet() }. @@ -486,7 +486,7 @@ class RPCStabilityTests { } class SlowConsumerRPCOpsImpl : SlowConsumerRPCOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun streamAtInterval(interval: Duration, size: Int): Observable { val chunk = ByteArray(size) @@ -587,7 +587,7 @@ class RPCStabilityTests { val request = RPCApi.ClientToServer.fromClientMessage(it) when (request) { is RPCApi.ClientToServer.RpcRequest -> { - val reply = RPCApi.ServerToClient.RpcReply(request.replyId, Try.Success(0), "server") + val reply = RPCApi.ServerToClient.RpcReply(request.replyId, Try.Success(1000), "server") val message = session.createMessage(false) reply.writeToClientMessage(SerializationDefaults.RPC_SERVER_CONTEXT, message) message.putLongProperty(RPCApi.DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME, dedupeId.getAndIncrement()) diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt index e41a7ed75c..116e6baf84 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt @@ -4,15 +4,16 @@ import net.corda.client.rpc.internal.RPCClient import net.corda.client.rpc.internal.serialization.amqp.AMQPClientSerializationScheme import net.corda.core.context.Actor import net.corda.core.context.Trace +import net.corda.core.identity.CordaX500Name import net.corda.core.messaging.CordaRPCOps import net.corda.core.serialization.internal.effectiveSerializationEnv import net.corda.core.utilities.NetworkHostAndPort -import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcConnectorTcpTransport import net.corda.core.messaging.ClientRpcSslOptions import net.corda.core.utilities.days import net.corda.core.utilities.minutes import net.corda.core.utilities.seconds -import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.rpcConnectorTcpTransport +import net.corda.nodeapi.internal.PLATFORM_VERSION import net.corda.serialization.internal.AMQP_RPC_CLIENT_CONTEXT import java.time.Duration @@ -29,65 +30,76 @@ class CordaRPCConnection internal constructor(connection: RPCConnection + * Maximum reconnect attempts on failover or disconnection. The default is -1 which means unlimited. */ open val maxReconnectAttempts: Int = unlimitedReconnectAttempts, /** - * Maximum file size, in bytes. + * Maximum size of RPC responses, in bytes. Default is 10mb. */ open val maxFileSize: Int = 10485760, // 10 MiB maximum allowed file size for attachments, including message headers. // TODO: acquire this value from Network Map when supported. /** - * The cache expiry of a deduplication watermark per client. + * The cache expiry of a deduplication watermark per client. Default is 1 day. */ open val deduplicationCacheExpiry: Duration = 1.days @@ -97,6 +109,7 @@ open class CordaRPCClientConfiguration @JvmOverloads constructor( private const val unlimitedReconnectAttempts = -1 + /** Provides an instance of this class with the parameters set to our recommended defaults. */ @JvmField val DEFAULT: CordaRPCClientConfiguration = CordaRPCClientConfiguration() @@ -104,7 +117,10 @@ open class CordaRPCClientConfiguration @JvmOverloads constructor( /** * Create a new copy of a configuration object with zero or more parameters modified. + * + * @suppress */ + @Suppress("DEPRECATION") @JvmOverloads fun copy( connectionMaxRetryInterval: Duration = this.connectionMaxRetryInterval, @@ -169,6 +185,7 @@ open class CordaRPCClientConfiguration @JvmOverloads constructor( return result } + @Suppress("DEPRECATION") override fun toString(): String { return "CordaRPCClientConfiguration(" + "connectionMaxRetryInterval=$connectionMaxRetryInterval, " + @@ -180,7 +197,8 @@ open class CordaRPCClientConfiguration @JvmOverloads constructor( "deduplicationCacheExpiry=$deduplicationCacheExpiry)" } - // Left is for backwards compatibility with version 3.1 + // Left in for backwards compatibility with version 3.1 + @Deprecated("Binary compatibility stub") operator fun component1() = connectionMaxRetryInterval } @@ -226,10 +244,8 @@ class CordaRPCClient private constructor( private val hostAndPort: NetworkHostAndPort, private val configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT, private val sslConfiguration: ClientRpcSslOptions? = null, - private val nodeSslConfiguration: SSLConfiguration? = null, private val classLoader: ClassLoader? = null, - private val haAddressPool: List = emptyList(), - private val internalConnection: Boolean = false + private val haAddressPool: List = emptyList() ) { @JvmOverloads constructor(hostAndPort: NetworkHostAndPort, @@ -243,7 +259,7 @@ class CordaRPCClient private constructor( * @param configuration An optional configuration used to tweak client behaviour. */ @JvmOverloads - constructor(haAddressPool: List, configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT) : this(haAddressPool.first(), configuration, null, null, null, haAddressPool) + constructor(haAddressPool: List, configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT) : this(haAddressPool.first(), configuration, null, null, haAddressPool) companion object { fun createWithSsl( @@ -268,16 +284,7 @@ class CordaRPCClient private constructor( sslConfiguration: ClientRpcSslOptions? = null, classLoader: ClassLoader? = null ): CordaRPCClient { - return CordaRPCClient(hostAndPort, configuration, sslConfiguration, null, classLoader) - } - - internal fun createWithInternalSslAndClassLoader( - hostAndPort: NetworkHostAndPort, - configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT, - sslConfiguration: SSLConfiguration?, - classLoader: ClassLoader? = null - ): CordaRPCClient { - return CordaRPCClient(hostAndPort, configuration, null, sslConfiguration, classLoader, internalConnection = true) + return CordaRPCClient(hostAndPort, configuration, sslConfiguration, classLoader) } } @@ -295,9 +302,6 @@ class CordaRPCClient private constructor( private fun getRpcClient(): RPCClient { return when { - // Node->RPC broker, mutually authenticated SSL. This is used when connecting the integrated shell - internalConnection == true -> RPCClient(hostAndPort, nodeSslConfiguration!!) - // Client->RPC broker haAddressPool.isEmpty() -> RPCClient( rpcConnectorTcpTransport(hostAndPort, config = sslConfiguration), @@ -326,6 +330,21 @@ class CordaRPCClient private constructor( return start(username, password, null, null) } + /** + * Logs in to the target server and returns an active connection. The returned connection is a [java.io.Closeable] + * and can be used with a try-with-resources statement. If you don't use that, you should use the + * [RPCConnection.notifyServerAndClose] or [RPCConnection.forceClose] methods to dispose of the connection object + * when done. + * + * @param username The username to authenticate with. + * @param password The password to authenticate with. + * @param targetLegalIdentity in case of multi-identity RPC endpoint specific legal identity to which the calls must be addressed. + * @throws RPCException if the server version is too low or if the server isn't reachable within a reasonable timeout. + */ + fun start(username: String, password: String, targetLegalIdentity: CordaX500Name): CordaRPCConnection { + return start(username, password, null, null, targetLegalIdentity) + } + /** * Logs in to the target server and returns an active connection. The returned connection is a [java.io.Closeable] * and can be used with a try-with-resources statement. If you don't use that, you should use the @@ -335,10 +354,28 @@ class CordaRPCClient private constructor( * @param username The username to authenticate with. * @param password The password to authenticate with. * @param externalTrace external [Trace] for correlation. + * @param impersonatedActor the actor on behalf of which all the invocations will be made. * @throws RPCException if the server version is too low or if the server isn't reachable within a reasonable timeout. */ fun start(username: String, password: String, externalTrace: Trace?, impersonatedActor: Actor?): CordaRPCConnection { - return CordaRPCConnection(getRpcClient().start(CordaRPCOps::class.java, username, password, externalTrace, impersonatedActor)) + return start(username, password, externalTrace, impersonatedActor, null) + } + + /** + * Logs in to the target server and returns an active connection. The returned connection is a [java.io.Closeable] + * and can be used with a try-with-resources statement. If you don't use that, you should use the + * [RPCConnection.notifyServerAndClose] or [RPCConnection.forceClose] methods to dispose of the connection object + * when done. + * + * @param username The username to authenticate with. + * @param password The password to authenticate with. + * @param externalTrace external [Trace] for correlation. + * @param impersonatedActor the actor on behalf of which all the invocations will be made. + * @param targetLegalIdentity in case of multi-identity RPC endpoint specific legal identity to which the calls must be addressed. + * @throws RPCException if the server version is too low or if the server isn't reachable within a reasonable timeout. + */ + fun start(username: String, password: String, externalTrace: Trace?, impersonatedActor: Actor?, targetLegalIdentity: CordaX500Name?): CordaRPCConnection { + return CordaRPCConnection(getRpcClient().start(CordaRPCOps::class.java, username, password, externalTrace, impersonatedActor, targetLegalIdentity)) } /** diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/RPCException.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/RPCException.kt index 32ea9928be..7dc04f1666 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/RPCException.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/RPCException.kt @@ -3,9 +3,13 @@ package net.corda.client.rpc import net.corda.core.CordaRuntimeException /** - * Thrown to indicate a fatal error in the RPC system itself, as opposed to an error generated by the invoked - * method. + * Thrown to indicate a fatal error in the RPC system itself, as opposed to an error generated by the invoked method. */ open class RPCException(message: String?, cause: Throwable?) : CordaRuntimeException(message, cause) { constructor(msg: String) : this(msg, null) } + +/** + * Signals that the underlying [RPCConnection] dropped. + */ +open class ConnectionFailureException(cause: Throwable? = null) : RPCException("Connection failure detected.", cause) \ No newline at end of file diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/CordaRPCClientUtils.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/CordaRPCClientUtils.kt index 7781d0f135..86a74d1728 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/CordaRPCClientUtils.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/CordaRPCClientUtils.kt @@ -2,12 +2,8 @@ package net.corda.client.rpc.internal import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCClientConfiguration -import net.corda.core.messaging.CordaRPCOps -import net.corda.core.messaging.pendingFlowsCount import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.messaging.ClientRpcSslOptions -import net.corda.nodeapi.internal.config.SSLConfiguration -import rx.Observable /** Utility which exposes the internal Corda RPC constructor to other internal Corda components */ fun createCordaRPCClientWithSslAndClassLoader( @@ -15,21 +11,4 @@ fun createCordaRPCClientWithSslAndClassLoader( configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT, sslConfiguration: ClientRpcSslOptions? = null, classLoader: ClassLoader? = null -) = CordaRPCClient.createWithSslAndClassLoader(hostAndPort, configuration, sslConfiguration, classLoader) - -fun createCordaRPCClientWithInternalSslAndClassLoader( - hostAndPort: NetworkHostAndPort, - configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT, - sslConfiguration: SSLConfiguration? = null, - classLoader: ClassLoader? = null -) = CordaRPCClient.createWithInternalSslAndClassLoader(hostAndPort, configuration, sslConfiguration, classLoader) - -fun CordaRPCOps.drainAndShutdown(): Observable { - - setFlowsDrainingModeEnabled(true) - return pendingFlowsCount().updates - .doOnError { error -> - throw error - } - .doOnCompleted { shutdown() }.map { } -} \ No newline at end of file +) = CordaRPCClient.createWithSslAndClassLoader(hostAndPort, configuration, sslConfiguration, classLoader) \ No newline at end of file diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt index 1d7969caaa..a29c481d47 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt @@ -6,6 +6,7 @@ import net.corda.client.rpc.RPCException import net.corda.core.context.Actor import net.corda.core.context.Trace import net.corda.core.crypto.random63BitValue +import net.corda.core.identity.CordaX500Name import net.corda.core.internal.logElapsedTime import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.ClientRpcSslOptions @@ -15,11 +16,11 @@ import net.corda.core.serialization.SerializationDefaults import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger -import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcConnectorTcpTransport -import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcConnectorTcpTransportsFromList -import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcInternalClientTcpTransport import net.corda.nodeapi.RPCApi -import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.rpcConnectorTcpTransport +import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.rpcConnectorTcpTransportsFromList +import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.rpcInternalClientTcpTransport +import net.corda.nodeapi.internal.config.SslConfiguration import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.TransportConfiguration import org.apache.activemq.artemis.api.core.client.ActiveMQClient @@ -43,7 +44,7 @@ class RPCClient( constructor( hostAndPort: NetworkHostAndPort, - sslConfiguration: SSLConfiguration, + sslConfiguration: SslConfiguration, configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT, serializationContext: SerializationContext = SerializationDefaults.RPC_CLIENT_CONTEXT ) : this(rpcInternalClientTcpTransport(hostAndPort, sslConfiguration), configuration, serializationContext) @@ -65,7 +66,8 @@ class RPCClient( username: String, password: String, externalTrace: Trace? = null, - impersonatedActor: Actor? = null + impersonatedActor: Actor? = null, + targetLegalIdentity: CordaX500Name? = null ): RPCConnection { return log.logElapsedTime("Startup") { val clientAddress = SimpleString("${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$username.${random63BitValue()}") @@ -85,7 +87,8 @@ class RPCClient( isUseGlobalPools = nodeSerializationEnv != null } val sessionId = Trace.SessionId.newInstance() - val proxyHandler = RPCClientProxyHandler(rpcConfiguration, username, password, serverLocator, clientAddress, rpcOpsClass, serializationContext, sessionId, externalTrace, impersonatedActor) + val proxyHandler = RPCClientProxyHandler(rpcConfiguration, username, password, serverLocator, clientAddress, + rpcOpsClass, serializationContext, sessionId, externalTrace, impersonatedActor, targetLegalIdentity) try { proxyHandler.start() val ops: I = uncheckedCast(Proxy.newProxyInstance(rpcOpsClass.classLoader, arrayOf(rpcOpsClass), proxyHandler)) diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt index f10607ad97..be74d7b316 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt @@ -7,6 +7,7 @@ import com.github.benmanes.caffeine.cache.RemovalCause import com.github.benmanes.caffeine.cache.RemovalListener import com.google.common.util.concurrent.SettableFuture import com.google.common.util.concurrent.ThreadFactoryBuilder +import net.corda.client.rpc.ConnectionFailureException import net.corda.client.rpc.CordaRPCClientConfiguration import net.corda.client.rpc.RPCException import net.corda.client.rpc.RPCSinceVersion @@ -14,6 +15,7 @@ import net.corda.client.rpc.internal.serialization.amqp.RpcClientObservableDeSer import net.corda.core.context.Actor import net.corda.core.context.Trace import net.corda.core.context.Trace.InvocationId +import net.corda.core.identity.CordaX500Name import net.corda.core.internal.* import net.corda.core.messaging.RPCOps import net.corda.core.serialization.SerializationContext @@ -64,7 +66,7 @@ import kotlin.reflect.jvm.javaMethod * automatically signal the server. This is done using a cache that holds weak references to the [UnicastSubject]s. * The cleanup happens in batches using a dedicated reaper, scheduled on [reaperExecutor]. * - * The client will attempt to failover in case the server become unreachable. Depending on the [ServerLocataor] instance + * The client will attempt to failover in case the server become unreachable. Depending on the [ServerLocator] instance * passed in the constructor, failover is either handle at Artemis level or client level. If only one transport * was used to create the [ServerLocator], failover is handled by Artemis (retrying based on [CordaRPCClientConfiguration]. * If a list of transport configurations was used, failover is handled locally. Artemis is able to do it, however the @@ -80,7 +82,8 @@ class RPCClientProxyHandler( serializationContext: SerializationContext, private val sessionId: Trace.SessionId, private val externalTrace: Trace?, - private val impersonatedActor: Actor? + private val impersonatedActor: Actor?, + private val targetLegalIdentity: CordaX500Name? ) : InvocationHandler { private enum class State { @@ -97,12 +100,18 @@ class RPCClientProxyHandler( // To check whether toString() is being invoked val toStringMethod: Method = Object::toString.javaMethod!! - private fun addRpcCallSiteToThrowable(throwable: Throwable, callSite: Throwable) { + private fun addRpcCallSiteToThrowable(throwable: Throwable, callSite: CallSite) { var currentThrowable = throwable while (true) { val cause = currentThrowable.cause if (cause == null) { - currentThrowable.initCause(callSite) + try { + currentThrowable.initCause(callSite) + } catch (e: IllegalStateException) { + // OK, we did our best, but the first throwable with a null cause was instantiated using + // Throwable(Throwable) or Throwable(String, Throwable) which means initCause can't ever + // be called even if it was passed null. + } break } else { currentThrowable = cause @@ -146,15 +155,17 @@ class RPCClientProxyHandler( private fun createRpcObservableMap(): RpcObservableMap { val onObservableRemove = RemovalListener>> { key, _, cause -> val observableId = key!! - val rpcCallSite = callSiteMap?.remove(observableId) + val rpcCallSite: CallSite? = callSiteMap?.remove(observableId) if (cause == RemovalCause.COLLECTED) { log.warn(listOf( "A hot observable returned from an RPC was never subscribed to.", "This wastes server-side resources because it was queueing observations for retrieval.", "It is being closed now, but please adjust your code to call .notUsed() on the observable", - "to close it explicitly. (Java users: subscribe to it then unsubscribe). This warning", - "will appear less frequently in future versions of the platform and you can ignore it", - "if you want to.").joinToString(" "), rpcCallSite) + "to close it explicitly. (Java users: subscribe to it then unsubscribe). If you aren't sure", + "where the leak is coming from, set -Dnet.corda.client.rpc.trackRpcCallSites=true on the JVM", + "command line and you will get a stack trace with this warning." + ).joinToString(" "), rpcCallSite) + rpcCallSite?.printStackTrace() } observablesToReap.locked { observables.add(observableId) } } @@ -215,6 +226,9 @@ class RPCClientProxyHandler( startSessions() } + /** A throwable that doesn't represent a real error - it's just here to wrap a stack trace. */ + class CallSite(val rpcName: String) : Throwable("") + // This is the general function that transforms a client side RPC to internal Artemis messages. override fun invoke(proxy: Any, method: Method, arguments: Array?): Any? { lifeCycle.requireState { it == State.STARTED || it == State.SERVER_VERSION_NOT_SET } @@ -230,7 +244,7 @@ class RPCClientProxyHandler( throw RPCException("RPC server is not available.") val replyId = InvocationId.newInstance() - callSiteMap?.set(replyId, Throwable("")) + callSiteMap?.set(replyId, CallSite(method.name)) try { val serialisedArguments = (arguments?.toList() ?: emptyList()).serialize(context = serializationContextWithObservableContext) val request = RPCApi.ClientToServer.RpcRequest( @@ -263,6 +277,9 @@ class RPCClientProxyHandler( private fun sendMessage(message: RPCApi.ClientToServer) { val artemisMessage = producerSession!!.createMessage(false) message.writeToClientMessage(artemisMessage) + targetLegalIdentity?.let { + artemisMessage.putStringProperty(RPCApi.RPC_TARGET_LEGAL_IDENTITY, it.toString()) + } sendExecutor!!.submit { artemisMessage.putLongProperty(RPCApi.DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME, deduplicationSequenceNumber.getAndIncrement()) log.debug { "-> RPC -> $message" } @@ -273,7 +290,7 @@ class RPCClientProxyHandler( // The handler for Artemis messages. private fun artemisMessageHandler(message: ClientMessage) { fun completeExceptionally(id: InvocationId, e: Throwable, future: SettableFuture?) { - val rpcCallSite: Throwable? = callSiteMap?.get(id) + val rpcCallSite: CallSite? = callSiteMap?.get(id) if (rpcCallSite != null) addRpcCallSiteToThrowable(e, rpcCallSite) future?.setException(e.cause ?: e) } @@ -536,7 +553,7 @@ class RPCClientProxyHandler( m.keys.forEach { k -> observationExecutorPool.run(k) { try { - m[k]?.onError(RPCException("Connection failure detected.")) + m[k]?.onError(ConnectionFailureException()) } catch (th: Throwable) { log.error("Unexpected exception when RPC connection failure handling", th) } @@ -545,7 +562,7 @@ class RPCClientProxyHandler( observableContext.observableMap.invalidateAll() rpcReplyMap.forEach { _, replyFuture -> - replyFuture.setException(RPCException("Connection failure detected.")) + replyFuture.setException(ConnectionFailureException()) } rpcReplyMap.clear() @@ -555,13 +572,14 @@ class RPCClientProxyHandler( private typealias RpcObservableMap = Cache>> private typealias RpcReplyMap = ConcurrentHashMap> -private typealias CallSiteMap = ConcurrentHashMap +private typealias CallSiteMap = ConcurrentHashMap /** * Holds a context available during de-serialisation of messages that are expected to contain Observables. * - * @param observableMap holds the Observables that are ultimately exposed to the user. - * @param hardReferenceStore holds references to Observables we want to keep alive while they are subscribed to. + * @property observableMap holds the Observables that are ultimately exposed to the user. + * @property hardReferenceStore holds references to Observables we want to keep alive while they are subscribed to. + * @property callSiteMap keeps stack traces captured when an RPC was invoked, useful for debugging when an observable leaks. */ data class ObservableContext( val callSiteMap: CallSiteMap?, diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/RpcClientObservableDeSerializer.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/RpcClientObservableDeSerializer.kt index 52e9dc7cab..17ba71e200 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/RpcClientObservableDeSerializer.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/RpcClientObservableDeSerializer.kt @@ -2,8 +2,10 @@ package net.corda.client.rpc.internal.serialization.amqp import net.corda.client.rpc.internal.ObservableContext +import net.corda.client.rpc.internal.RPCClientProxyHandler import net.corda.core.context.Trace import net.corda.core.serialization.SerializationContext +import net.corda.core.utilities.loggerFor import net.corda.nodeapi.RPCApi import net.corda.serialization.internal.amqp.* import org.apache.qpid.proton.codec.Data @@ -17,11 +19,12 @@ import java.util.concurrent.atomic.AtomicInteger import javax.transaction.NotSupportedException /** - * De-serializer for Rx[Observable] instances for the RPC Client library. Can only be used to deserialize such objects, - * just as the corresponding RPC server side code ([RpcServerObservableSerializer]) can only serialize them. Observables are only notionally serialized, - * what is actually sent is a reference to the observable that can then be subscribed to. + * De-serializer for Rx [Observable] instances for the RPC Client library. Can only be used to deserialize such objects, + * just as the corresponding RPC server side class [RpcServerObservableSerializer] can only serialize them. Observables + * are only notionally serialized, what is actually sent is a reference to the observable that can then be subscribed to. */ object RpcClientObservableDeSerializer : CustomSerializer.Implements>(Observable::class.java) { + private val log = loggerFor() private object RpcObservableContextKey fun createContext( @@ -96,22 +99,23 @@ object RpcClientObservableDeSerializer : CustomSerializer.Implements() } - private fun getRpcCallSite(context: SerializationContext, observableContext: ObservableContext): Throwable? { + private fun getRpcCallSite(context: SerializationContext, observableContext: ObservableContext): RPCClientProxyHandler.CallSite? { val rpcRequestOrObservableId = context.properties[RPCApi.RpcRequestOrObservableIdKey] as Trace.InvocationId + // Will only return non-null if the trackRpcCallSites option in the RPC configuration has been specified. return observableContext.callSiteMap?.get(rpcRequestOrObservableId) } diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/ClientRPCInfrastructureTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/ClientRPCInfrastructureTests.kt index d2a0a2c977..caa363908c 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/ClientRPCInfrastructureTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/ClientRPCInfrastructureTests.kt @@ -48,7 +48,7 @@ class ClientRPCInfrastructureTests : AbstractRPCTest() { fun makeComplicatedListenableFuture(): CordaFuture>> - @RPCSinceVersion(2) + @RPCSinceVersion(2000) fun addedLater() fun captureUser(): String @@ -58,7 +58,7 @@ class ClientRPCInfrastructureTests : AbstractRPCTest() { private lateinit var complicatedListenableFuturee: CordaFuture>> inner class TestOpsImpl : TestOps { - override val protocolVersion = 1 + override val protocolVersion = 1000 // do not remove Unit override fun barf(): Unit = throw IllegalArgumentException("Barf!") override fun void() {} diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCConcurrencyTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCConcurrencyTests.kt index 0b15cc0a5e..b7492db120 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCConcurrencyTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCConcurrencyTests.kt @@ -33,7 +33,7 @@ class RPCConcurrencyTests : AbstractRPCTest() { @CordaSerializable data class ObservableRose(val value: A, val branches: Observable>) - private interface TestOps : RPCOps { + interface TestOps : RPCOps { fun newLatch(numberOfDowns: Int): Long fun waitLatch(id: Long) fun downLatch(id: Long) @@ -43,7 +43,7 @@ class RPCConcurrencyTests : AbstractRPCTest() { class TestOpsImpl(private val pool: Executor) : TestOps { private val latches = ConcurrentHashMap() - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun newLatch(numberOfDowns: Int): Long { val id = random63BitValue() diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCFailureTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCFailureTests.kt index 7806bc9b40..f294d68587 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCFailureTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCFailureTests.kt @@ -26,7 +26,7 @@ class RPCFailureTests { } class OpsImpl : Ops { - override val protocolVersion = 1 + override val protocolVersion = 1000 override fun getUnserializable() = Unserializable() override fun getUnserializableAsync(): CordaFuture { return openFuture().apply { capture { getUnserializable() } } diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCHighThroughputObservableTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCHighThroughputObservableTests.kt index 72013ca955..4f3a09d507 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCHighThroughputObservableTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCHighThroughputObservableTests.kt @@ -24,7 +24,7 @@ class RPCHighThroughputObservableTests : AbstractRPCTest() { } internal class TestOpsImpl : TestOps { - override val protocolVersion = 1 + override val protocolVersion = 1000 override fun makeObservable(): Observable = Observable.interval(0, TimeUnit.MICROSECONDS).map { it.toInt() + 1 } } diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt index f6e9a8aa83..9f38487fb9 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt @@ -5,8 +5,8 @@ import net.corda.core.messaging.RPCOps import net.corda.core.utilities.minutes import net.corda.core.utilities.seconds import net.corda.node.services.messaging.RPCServerConfiguration -import net.corda.testing.node.internal.RPCDriverDSL import net.corda.testing.internal.performance.div +import net.corda.testing.node.internal.RPCDriverDSL import net.corda.testing.node.internal.performance.startPublishingFixedRateInjector import net.corda.testing.node.internal.performance.startReporter import net.corda.testing.node.internal.performance.startTightLoopInjector @@ -34,7 +34,7 @@ class RPCPerformanceTests : AbstractRPCTest() { } class TestOpsImpl : TestOps { - override val protocolVersion = 0 + override val protocolVersion = 1000 override fun simpleReply(input: ByteArray, sizeOfReply: Int): ByteArray { return ByteArray(sizeOfReply) } diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPermissionsTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPermissionsTests.kt index 21ebac1fbd..dee4c07257 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPermissionsTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPermissionsTests.kt @@ -25,7 +25,7 @@ class RPCPermissionsTests : AbstractRPCTest() { } class TestOpsImpl : TestOps { - override val protocolVersion = 1 + override val protocolVersion = 1000 override fun validatePermission(method: String, target: String?) { val authorized = if (target == null) { rpcContext().isPermitted(method) diff --git a/config/dev/log4j2.xml b/config/dev/log4j2.xml index 63681f41a8..b351e59536 100644 --- a/config/dev/log4j2.xml +++ b/config/dev/log4j2.xml @@ -46,7 +46,7 @@ - + diff --git a/constants.properties b/constants.properties index d9f06face2..8d23d66503 100644 --- a/constants.properties +++ b/constants.properties @@ -1,9 +1,12 @@ gradlePluginsVersion=4.0.29 kotlinVersion=1.2.51 +# When adjusting platformVersion upwards please also modify CordaRPCClientConfiguration.minimumServerProtocolVersion \ +# if there have been any RPC changes. Also please modify InternalMockNetwork.kt:MOCK_VERSION_INFO and NodeBasedTest.startNode platformVersion=4 guavaVersion=25.1-jre proguardVersion=6.0.3 -bouncycastleVersion=1.57 +bouncycastleVersion=1.60 +disruptorVersion=3.4.2 typesafeConfigVersion=1.3.1 jsr305Version=3.0.2 artifactoryPluginVersion=4.7.3 diff --git a/core-deterministic/build.gradle b/core-deterministic/build.gradle index 6627a80cd8..f7cdb65e9c 100644 --- a/core-deterministic/build.gradle +++ b/core-deterministic/build.gradle @@ -11,8 +11,8 @@ def javaHome = System.getProperty('java.home') def jarBaseName = "corda-${project.name}".toString() configurations { - runtimeLibraries - runtimeArtifacts.extendsFrom runtimeLibraries + deterministicLibraries + deterministicArtifacts.extendsFrom deterministicLibraries } dependencies { @@ -20,14 +20,14 @@ dependencies { // Configure these by hand. It should be a minimal subset of core's dependencies, // and without any obviously non-deterministic ones such as Hibernate. - runtimeLibraries "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - runtimeLibraries "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - runtimeLibraries "org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final" - runtimeLibraries "org.bouncycastle:bcprov-jdk15on:$bouncycastle_version" - runtimeLibraries "org.bouncycastle:bcpkix-jdk15on:$bouncycastle_version" - runtimeLibraries "com.google.code.findbugs:jsr305:$jsr305_version" - runtimeLibraries "net.i2p.crypto:eddsa:$eddsa_version" - runtimeLibraries "org.slf4j:slf4j-api:$slf4j_version" + deterministicLibraries "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + deterministicLibraries "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + deterministicLibraries "org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final" + deterministicLibraries "org.bouncycastle:bcprov-jdk15on:$bouncycastle_version" + deterministicLibraries "org.bouncycastle:bcpkix-jdk15on:$bouncycastle_version" + deterministicLibraries "com.google.code.findbugs:jsr305:$jsr305_version" + deterministicLibraries "net.i2p.crypto:eddsa:$eddsa_version" + deterministicLibraries "org.slf4j:slf4j-api:$slf4j_version" } jar { @@ -50,6 +50,7 @@ task patchCore(type: Zip, dependsOn: coreJarTask) { from(zipTree(originalJar)) { exclude 'net/corda/core/internal/*ToggleField*.class' exclude 'net/corda/core/serialization/*SerializationFactory*.class' + exclude 'net/corda/core/serialization/internal/CheckpointSerializationFactory*.class' } reproducibleFileOrder = true @@ -112,7 +113,7 @@ task determinise(type: ProGuardTask) { libraryjars file("$javaHome/lib/rt.jar") libraryjars file("$javaHome/lib/jce.jar") - configurations.runtimeLibraries.forEach { + configurations.deterministicLibraries.forEach { libraryjars it, filter: '!META-INF/versions/**' } @@ -152,7 +153,7 @@ task checkDeterminism(type: ProGuardTask, dependsOn: jdkTask) { libraryjars deterministic_rt_jar - configurations.runtimeLibraries.forEach { + configurations.deterministicLibraries.forEach { libraryjars it, filter: '!META-INF/versions/**' } @@ -173,12 +174,12 @@ assemble.dependsOn checkDeterminism def deterministicJar = metafix.outputs.files.singleFile artifacts { - runtimeArtifacts file: deterministicJar, name: jarBaseName, type: 'jar', extension: 'jar', builtBy: metafix + deterministicArtifacts file: deterministicJar, name: jarBaseName, type: 'jar', extension: 'jar', builtBy: metafix publish file: deterministicJar, name: jarBaseName, type: 'jar', extension: 'jar', builtBy: metafix } publish { - dependenciesFrom configurations.runtimeArtifacts + dependenciesFrom configurations.deterministicArtifacts publishSources = false publishJavadoc = false name jarBaseName diff --git a/core-deterministic/src/main/kotlin/net/corda/core/serialization/internal/CheckpointSerializationFactory.kt b/core-deterministic/src/main/kotlin/net/corda/core/serialization/internal/CheckpointSerializationFactory.kt new file mode 100644 index 0000000000..dbb6fb54c0 --- /dev/null +++ b/core-deterministic/src/main/kotlin/net/corda/core/serialization/internal/CheckpointSerializationFactory.kt @@ -0,0 +1,74 @@ +package net.corda.core.serialization.internal + +import net.corda.core.KeepForDJVM +import net.corda.core.serialization.SerializedBytes +import net.corda.core.utilities.ByteSequence +import java.io.NotSerializableException + +/** + * A deterministic version of [CheckpointSerializationFactory] that does not use thread-locals to manage serialization + * context. + */ +@KeepForDJVM +class CheckpointSerializationFactory( + private val scheme: CheckpointSerializationScheme +) { + + val defaultContext: CheckpointSerializationContext get() = _currentContext ?: effectiveSerializationEnv.checkpointContext + + private val creator: List = Exception().stackTrace.asList() + + /** + * Deserialize the bytes in to an object, using the prefixed bytes to determine the format. + * + * @param byteSequence The bytes to deserialize, including a format header prefix. + * @param clazz The class or superclass or the object to be deserialized, or [Any] or [Object] if unknown. + * @param context A context that configures various parameters to deserialization. + */ + @Throws(NotSerializableException::class) + fun deserialize(byteSequence: ByteSequence, clazz: Class, context: CheckpointSerializationContext): T { + return withCurrentContext(context) { scheme.deserialize(byteSequence, clazz, context) } + } + + /** + * Serialize an object to bytes using the preferred serialization format version from the context. + * + * @param obj The object to be serialized. + * @param context A context that configures various parameters to serialization, including the serialization format version. + */ + fun serialize(obj: T, context: CheckpointSerializationContext): SerializedBytes { + return withCurrentContext(context) { scheme.serialize(obj, context) } + } + + override fun toString(): String { + return "${this.javaClass.name} scheme=$scheme ${creator.joinToString("\n")}" + } + + override fun equals(other: Any?): Boolean { + return other is CheckpointSerializationFactory && other.scheme == this.scheme + } + + override fun hashCode(): Int = scheme.hashCode() + + private var _currentContext: CheckpointSerializationContext? = null + + /** + * Change the current context inside the block to that supplied. + */ + fun withCurrentContext(context: CheckpointSerializationContext?, block: () -> T): T { + val priorContext = _currentContext + if (context != null) _currentContext = context + try { + return block() + } finally { + if (context != null) _currentContext = priorContext + } + } + + companion object { + /** + * A default factory for serialization/deserialization. + */ + val defaultFactory: CheckpointSerializationFactory get() = effectiveSerializationEnv.checkpointSerializationFactory + } +} \ No newline at end of file diff --git a/core-deterministic/testing/build.gradle b/core-deterministic/testing/build.gradle index a472f5cde5..bb007715eb 100644 --- a/core-deterministic/testing/build.gradle +++ b/core-deterministic/testing/build.gradle @@ -1,10 +1,10 @@ apply plugin: 'kotlin' dependencies { - testCompile project(path: ':core-deterministic', configuration: 'runtimeArtifacts') - testCompile project(path: ':serialization-deterministic', configuration: 'runtimeArtifacts') + testCompile project(path: ':core-deterministic', configuration: 'deterministicArtifacts') + testCompile project(path: ':serialization-deterministic', configuration: 'deterministicArtifacts') + testCompile project(path: ':core-deterministic:testing:verifier', configuration: 'deterministicArtifacts') testCompile project(path: ':core-deterministic:testing:data', configuration: 'testData') - testCompile project(':core-deterministic:testing:common') testCompile(project(':finance')) { transitive = false } diff --git a/core-deterministic/testing/common/build.gradle b/core-deterministic/testing/common/build.gradle deleted file mode 100644 index f7c48a6c7a..0000000000 --- a/core-deterministic/testing/common/build.gradle +++ /dev/null @@ -1,16 +0,0 @@ -apply from: '../../../deterministic.gradle' -apply plugin: 'idea' - -dependencies { - compileOnly project(path: ':core-deterministic', configuration: 'runtimeArtifacts') - compileOnly project(path: ':serialization-deterministic', configuration: 'runtimeArtifacts') - compileOnly "junit:junit:$junit_version" -} - -idea { - module { - if (project.hasProperty("deterministic_idea_sdk")) { - jdkName project.property("deterministic_idea_sdk") as String - } - } -} diff --git a/core-deterministic/testing/data/build.gradle b/core-deterministic/testing/data/build.gradle index d203ae5572..59992d92eb 100644 --- a/core-deterministic/testing/data/build.gradle +++ b/core-deterministic/testing/data/build.gradle @@ -8,7 +8,7 @@ dependencies { testCompile project(':core') testCompile project(':finance') testCompile project(':node-driver') - testCompile project(':core-deterministic:testing:common') + testCompile project(path: ':core-deterministic:testing:verifier', configuration: 'runtimeArtifacts') testCompile "org.jetbrains.kotlin:kotlin-stdlib-jdk8" testCompile "org.jetbrains.kotlin:kotlin-reflect" diff --git a/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/GenerateData.kt b/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/GenerateData.kt index 0304661183..1ada19e231 100644 --- a/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/GenerateData.kt +++ b/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/GenerateData.kt @@ -1,8 +1,8 @@ package net.corda.deterministic.data import net.corda.core.serialization.deserialize -import net.corda.deterministic.common.LocalSerializationRule -import net.corda.deterministic.common.TransactionVerificationRequest +import net.corda.deterministic.verifier.LocalSerializationRule +import net.corda.deterministic.verifier.TransactionVerificationRequest import org.junit.Before import org.junit.Rule import org.junit.Test diff --git a/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/TransactionGenerator.kt b/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/TransactionGenerator.kt index a6b704077e..d777bf9df1 100644 --- a/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/TransactionGenerator.kt +++ b/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/TransactionGenerator.kt @@ -7,9 +7,9 @@ import net.corda.core.identity.AnonymousParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.serialization.serialize -import net.corda.deterministic.common.MockContractAttachment -import net.corda.deterministic.common.SampleCommandData -import net.corda.deterministic.common.TransactionVerificationRequest +import net.corda.deterministic.verifier.MockContractAttachment +import net.corda.deterministic.verifier.SampleCommandData +import net.corda.deterministic.verifier.TransactionVerificationRequest import net.corda.finance.POUNDS import net.corda.finance.`issued by` import net.corda.finance.contracts.asset.Cash.* diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/TransactionSignatureTest.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/TransactionSignatureTest.kt index 5935c13b3c..4825d4787f 100644 --- a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/TransactionSignatureTest.kt +++ b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/TransactionSignatureTest.kt @@ -3,7 +3,7 @@ package net.corda.deterministic.crypto import net.corda.core.crypto.* import net.corda.deterministic.KeyStoreProvider import net.corda.deterministic.CheatingSecurityProvider -import net.corda.deterministic.common.LocalSerializationRule +import net.corda.deterministic.verifier.LocalSerializationRule import org.junit.* import org.junit.rules.RuleChain import java.security.* diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/txverify/EnclaveletTest.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/txverify/EnclaveletTest.kt deleted file mode 100644 index 88125a4072..0000000000 --- a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/txverify/EnclaveletTest.kt +++ /dev/null @@ -1,52 +0,0 @@ -@file:JvmName("Enclavelet") -package net.corda.deterministic.txverify - -import net.corda.core.serialization.deserialize -import net.corda.core.transactions.LedgerTransaction -import net.corda.deterministic.bytesOfResource -import net.corda.deterministic.common.LocalSerializationRule -import net.corda.deterministic.common.TransactionVerificationRequest -import net.corda.finance.contracts.asset.Cash.Commands.* -import org.assertj.core.api.Assertions.assertThat -import org.junit.ClassRule -import org.junit.Test -import kotlin.test.assertFailsWith - -class EnclaveletTest { - companion object { - @ClassRule - @JvmField - val serialization = LocalSerializationRule(EnclaveletTest::class) - } - - @Test - fun success() { - verifyInEnclave(bytesOfResource("txverify/tx-success.bin")) - } - - @Test - fun failure() { - val e = assertFailsWith { verifyInEnclave(bytesOfResource("txverify/tx-failure.bin")) } - assertThat(e).hasMessageContaining("Required ${Move::class.java.canonicalName} command") - } -} - -/** - * Returns either null to indicate success when the transactions are validated, or a string with the - * contents of the error. Invoked via JNI in response to an enclave RPC. The argument is a serialised - * [TransactionVerificationRequest]. - * - * Note that it is assumed the signatures were already checked outside the sandbox: the purpose of this code - * is simply to check the sensitive, app specific parts of a transaction. - * - * TODO: Transaction data is meant to be encrypted under an enclave-private key. - */ -@Throws(Exception::class) -private fun verifyInEnclave(reqBytes: ByteArray) { - deserialize(reqBytes).verify() -} - -private fun deserialize(reqBytes: ByteArray): LedgerTransaction { - return reqBytes.deserialize() - .toLedgerTransaction() -} diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/txverify/VerifyTransactionTest.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/txverify/VerifyTransactionTest.kt new file mode 100644 index 0000000000..6526ca3c51 --- /dev/null +++ b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/txverify/VerifyTransactionTest.kt @@ -0,0 +1,29 @@ +package net.corda.deterministic.txverify + +import net.corda.deterministic.bytesOfResource +import net.corda.deterministic.verifier.LocalSerializationRule +import net.corda.deterministic.verifier.verifyTransaction +import net.corda.finance.contracts.asset.Cash.Commands.* +import org.assertj.core.api.Assertions.assertThat +import org.junit.ClassRule +import org.junit.Test +import kotlin.test.assertFailsWith + +class VerifyTransactionTest { + companion object { + @ClassRule + @JvmField + val serialization = LocalSerializationRule(VerifyTransactionTest::class) + } + + @Test + fun success() { + verifyTransaction(bytesOfResource("txverify/tx-success.bin")) + } + + @Test + fun failure() { + val e = assertFailsWith { verifyTransaction(bytesOfResource("txverify/tx-failure.bin")) } + assertThat(e).hasMessageContaining("Required ${Move::class.java.canonicalName} command") + } +} diff --git a/core-deterministic/testing/verifier/build.gradle b/core-deterministic/testing/verifier/build.gradle new file mode 100644 index 0000000000..259a9c3110 --- /dev/null +++ b/core-deterministic/testing/verifier/build.gradle @@ -0,0 +1,48 @@ +apply plugin: 'java-library' +apply from: '../../../deterministic.gradle' +apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' +apply plugin: 'idea' + +description 'Test utilities for deterministic contract verification' + +configurations { + deterministicArtifacts + runtimeArtifacts.extendsFrom api +} + +dependencies { + deterministicArtifacts project(path: ':serialization-deterministic', configuration: 'deterministicArtifacts') + deterministicArtifacts project(path: ':core-deterministic', configuration: 'deterministicArtifacts') + + runtimeArtifacts project(':serialization') + runtimeArtifacts project(':core') + + // Compile against the deterministic artifacts to ensure that we use only the deterministic API subset. + compileOnly configurations.deterministicArtifacts + api "junit:junit:$junit_version" +} + +jar { + baseName 'corda-deterministic-verifier' +} + +artifacts { + deterministicArtifacts jar + runtimeArtifacts jar + publish jar +} + +publish { + // Our published POM will contain dependencies on the non-deterministic Corda artifacts. + dependenciesFrom configurations.runtimeArtifacts + name jar.baseName +} + +idea { + module { + if (project.hasProperty("deterministic_idea_sdk")) { + jdkName project.property("deterministic_idea_sdk") as String + } + } +} diff --git a/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/LocalSerializationRule.kt b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/LocalSerializationRule.kt similarity index 98% rename from core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/LocalSerializationRule.kt rename to core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/LocalSerializationRule.kt index 05c8c3bc5c..15848a4be4 100644 --- a/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/LocalSerializationRule.kt +++ b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/LocalSerializationRule.kt @@ -1,4 +1,4 @@ -package net.corda.deterministic.common +package net.corda.deterministic.verifier import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.SerializationContext @@ -83,4 +83,4 @@ class LocalSerializationRule(private val label: String) : TestRule { return canDeserializeVersion(magic) && target == P2P } } -} \ No newline at end of file +} diff --git a/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/MockContractAttachment.kt b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/MockContractAttachment.kt similarity index 93% rename from core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/MockContractAttachment.kt rename to core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/MockContractAttachment.kt index a4b3b8a21e..f7e90ce2cc 100644 --- a/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/MockContractAttachment.kt +++ b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/MockContractAttachment.kt @@ -1,4 +1,4 @@ -package net.corda.deterministic.common +package net.corda.deterministic.verifier import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractClassName diff --git a/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/SampleData.kt b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/SampleData.kt similarity index 76% rename from core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/SampleData.kt rename to core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/SampleData.kt index 025fa148fa..9c4cfdcb59 100644 --- a/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/SampleData.kt +++ b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/SampleData.kt @@ -1,5 +1,5 @@ @file:JvmName("SampleData") -package net.corda.deterministic.common +package net.corda.deterministic.verifier import net.corda.core.contracts.TypeOnlyCommandData diff --git a/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/TransactionVerificationRequest.kt b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/TransactionVerificationRequest.kt similarity index 97% rename from core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/TransactionVerificationRequest.kt rename to core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/TransactionVerificationRequest.kt index f2c825ba61..96a886a618 100644 --- a/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/TransactionVerificationRequest.kt +++ b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/TransactionVerificationRequest.kt @@ -1,4 +1,4 @@ -package net.corda.deterministic.common +package net.corda.deterministic.verifier import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractAttachment diff --git a/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/Verifier.kt b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/Verifier.kt new file mode 100644 index 0000000000..e7a710d707 --- /dev/null +++ b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/Verifier.kt @@ -0,0 +1,21 @@ +@file:JvmName("Verifier") +package net.corda.deterministic.verifier + +import net.corda.core.serialization.deserialize +import net.corda.core.transactions.LedgerTransaction + +/** + * We assume the signatures were already checked outside the sandbox: the purpose of this code + * is simply to check the sensitive, app-specific parts of a transaction. + * + * TODO: Transaction data is meant to be encrypted under an enclave-private key. + */ +@Throws(Exception::class) +fun verifyTransaction(reqBytes: ByteArray) { + deserialize(reqBytes).verify() +} + +private fun deserialize(reqBytes: ByteArray): LedgerTransaction { + return reqBytes.deserialize() + .toLedgerTransaction() +} diff --git a/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt b/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt index 8e23c5c01b..ddf369b668 100644 --- a/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt +++ b/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt @@ -48,20 +48,4 @@ interface Cordapp { val jarPath: URL val cordappClasses: List val jarHash: SecureHash.SHA256 - - /** - * CorDapp's information, including vendor and version. - * - * @property shortName Cordapp's shortName - * @property vendor Cordapp's vendor - * @property version Cordapp's version - */ - @DoNotImplement - interface Info { - val shortName: String - val vendor: String - val version: String - - fun hasUnknownFields(): Boolean - } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt index 0f6b87e6d8..1cc29b5fdd 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt @@ -102,7 +102,11 @@ fun PublicKey.isValid(content: ByteArray, signature: DigitalSignature): Boolean /** Render a public key to its hash (in Base58) of its serialised form using the DL prefix. */ fun PublicKey.toStringShort(): String = "DL" + this.toSHA256Bytes().toBase58() -/** Return a [Set] of the contained keys if this is a [CompositeKey]; otherwise, return a [Set] with a single element (this [PublicKey]). */ +/** + * Return a [Set] of the contained leaf keys if this is a [CompositeKey]. + * Otherwise, return a [Set] with a single element (this [PublicKey]). + * Note that leaf keys cannot be of type [CompositeKey]. + */ val PublicKey.keys: Set get() = (this as? CompositeKey)?.leafKeys ?: setOf(this) /** Return true if [otherKey] fulfils the requirements of this [PublicKey]. */ @@ -110,7 +114,12 @@ fun PublicKey.isFulfilledBy(otherKey: PublicKey): Boolean = isFulfilledBy(setOf( /** Return true if [otherKeys] fulfil the requirements of this [PublicKey]. */ fun PublicKey.isFulfilledBy(otherKeys: Iterable): Boolean = (this as? CompositeKey)?.isFulfilledBy(otherKeys) ?: (this in otherKeys) -/** Checks whether any of the given [keys] matches a leaf on the [CompositeKey] tree or a single [PublicKey]. */ +/** + * Checks whether any of the given [keys] matches a leaf on the [CompositeKey] tree or a single [PublicKey]. + * + * Note that this function checks against leaves, which cannot be of type [CompositeKey]. Due to that, if any of the + * [otherKeys] is a [CompositeKey], this function will not find a match. + */ fun PublicKey.containsAny(otherKeys: Iterable): Boolean { return if (this is CompositeKey) keys.intersect(otherKeys).isNotEmpty() else this in otherKeys diff --git a/core/src/main/kotlin/net/corda/core/crypto/internal/ProviderMap.kt b/core/src/main/kotlin/net/corda/core/crypto/internal/ProviderMap.kt index 010d894453..03c4a01d72 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/internal/ProviderMap.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/internal/ProviderMap.kt @@ -11,9 +11,11 @@ import net.i2p.crypto.eddsa.EdDSASecurityProvider import org.bouncycastle.asn1.ASN1ObjectIdentifier import org.bouncycastle.asn1.pkcs.PrivateKeyInfo import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo +import org.bouncycastle.jcajce.provider.asymmetric.ec.AlgorithmParametersSpi import org.bouncycastle.jcajce.provider.util.AsymmetricKeyInfoConverter import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider +import java.security.SecureRandom import java.security.Security internal val cordaSecurityProvider = CordaSecurityProvider().also { @@ -29,6 +31,8 @@ internal val cordaBouncyCastleProvider = BouncyCastleProvider().apply { override fun generatePublic(keyInfo: SubjectPublicKeyInfo) = decodePublicKey(EDDSA_ED25519_SHA512, keyInfo.encoded) override fun generatePrivate(keyInfo: PrivateKeyInfo) = decodePrivateKey(EDDSA_ED25519_SHA512, keyInfo.encoded) }) + // Required due to [X509CRL].verify() reported issues in network-services after BC 1.60 update. + put("AlgorithmParameters.SHA256WITHECDSA", AlgorithmParametersSpi::class.java.name) }.also { // This registration is needed for reading back EdDSA key from java keystore. // TODO: Find a way to make JKS work with bouncy castle provider or implement our own provide so we don't have to register bouncy castle provider. @@ -46,4 +50,4 @@ internal val bouncyCastlePQCProvider = BouncyCastlePQCProvider().apply { internal val providerMap = listOf(cordaBouncyCastleProvider, cordaSecurityProvider, bouncyCastlePQCProvider).map { it.name to it }.toMap() @DeleteForDJVM -internal fun platformSecureRandomFactory() = platformSecureRandom() // To minimise diff of CryptoUtils against open-source. \ No newline at end of file +internal fun platformSecureRandomFactory(): SecureRandom = platformSecureRandom() // To minimise diff of CryptoUtils against open-source. diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowException.kt b/core/src/main/kotlin/net/corda/core/flows/FlowException.kt index cd00434c37..80a648227a 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowException.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowException.kt @@ -21,13 +21,13 @@ import net.corda.core.CordaRuntimeException * the exception is handled. This ID is propagated to counterparty flows, even when the [FlowException] is * downgraded to an [UnexpectedFlowEndException]. This is so the error conditions may be correlated later on. */ -open class FlowException(message: String?, cause: Throwable?) : +open class FlowException(message: String?, cause: Throwable?, var originalErrorId: Long? = null) : CordaException(message, cause), IdentifiableException { + constructor(message: String?, cause: Throwable?) : this(message, cause, null) constructor(message: String?) : this(message, null) constructor(cause: Throwable?) : this(cause?.toString(), cause) constructor() : this(null, null) - var originalErrorId: Long? = null override fun getErrorId(): Long? = originalErrorId } // DOCEND 1 diff --git a/core/src/main/kotlin/net/corda/core/internal/CertRole.kt b/core/src/main/kotlin/net/corda/core/internal/CertRole.kt index 29d24b80e0..b53f8977e8 100644 --- a/core/src/main/kotlin/net/corda/core/internal/CertRole.kt +++ b/core/src/main/kotlin/net/corda/core/internal/CertRole.kt @@ -24,22 +24,22 @@ import java.security.cert.X509Certificate // also note that IDs are numbered from 1 upwards, matching numbering of other enum types in ASN.1 specifications. // TODO: Link to the specification once it has a permanent URL enum class CertRole(val validParents: NonEmptySet, val isIdentity: Boolean, val isWellKnown: Boolean) : ASN1Encodable { - /** Intermediate CA (Doorman service). */ - INTERMEDIATE_CA(NonEmptySet.of(null), false, false), + /** Signing certificate for the Doorman CA. */ + DOORMAN_CA(NonEmptySet.of(null), false, false), /** Signing certificate for the network map. */ NETWORK_MAP(NonEmptySet.of(null), false, false), /** Well known (publicly visible) identity of a service (such as notary). */ - SERVICE_IDENTITY(NonEmptySet.of(INTERMEDIATE_CA), true, true), + SERVICE_IDENTITY(NonEmptySet.of(DOORMAN_CA), true, true), /** Node level CA from which the TLS and well known identity certificates are issued. */ - NODE_CA(NonEmptySet.of(INTERMEDIATE_CA), false, false), + NODE_CA(NonEmptySet.of(DOORMAN_CA), false, false), /** Transport layer security certificate for a node. */ TLS(NonEmptySet.of(NODE_CA), false, false), /** Well known (publicly visible) identity of a legal entity. */ - // TODO: at the moment, Legal Identity certs are issued by Node CA only. However, [INTERMEDIATE_CA] is also added + // TODO: at the moment, Legal Identity certs are issued by Node CA only. However, [DOORMAN_CA] is also added // as a valid parent of [LEGAL_IDENTITY] for backwards compatibility purposes (eg. if we decide TLS has its - // own Root CA and Intermediate CA directly issues Legal Identities; thus, there won't be a requirement for - // Node CA). Consider removing [INTERMEDIATE_CA] from [validParents] when the model is finalised. - LEGAL_IDENTITY(NonEmptySet.of(INTERMEDIATE_CA, NODE_CA), true, true), + // own Root CA and Doorman CA directly issues Legal Identities; thus, there won't be a requirement for + // Node CA). Consider removing [DOORMAN_CA] from [validParents] when the model is finalised. + LEGAL_IDENTITY(NonEmptySet.of(DOORMAN_CA, NODE_CA), true, true), /** Confidential (limited visibility) identity of a legal entity. */ CONFIDENTIAL_LEGAL_IDENTITY(NonEmptySet.of(LEGAL_IDENTITY), true, false); diff --git a/core/src/main/kotlin/net/corda/core/internal/DigitalSignatureWithCert.kt b/core/src/main/kotlin/net/corda/core/internal/DigitalSignatureWithCert.kt index 04ca25c6cb..c06cfe2e55 100644 --- a/core/src/main/kotlin/net/corda/core/internal/DigitalSignatureWithCert.kt +++ b/core/src/main/kotlin/net/corda/core/internal/DigitalSignatureWithCert.kt @@ -4,17 +4,41 @@ import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SignedData import net.corda.core.crypto.verify import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.DeprecatedConstructorForDeserialization import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize import net.corda.core.utilities.OpaqueBytes -import java.security.cert.X509Certificate +import java.security.cert.* // TODO: Rename this to DigitalSignature.WithCert once we're happy for it to be public API. The methods will need documentation // and the correct exceptions will be need to be annotated -/** A digital signature with attached certificate of the public key. */ -class DigitalSignatureWithCert(val by: X509Certificate, bytes: ByteArray) : DigitalSignature(bytes) { +/** A digital signature with attached certificate of the public key and (optionally) the remaining chain of the certificates from the certificate path. */ +class DigitalSignatureWithCert(val by: X509Certificate, val parentCertsChain: List, bytes: ByteArray) : DigitalSignature(bytes) { + @DeprecatedConstructorForDeserialization(1) + constructor(by: X509Certificate, bytes: ByteArray) : this(by, emptyList(), bytes) + + val fullCertChain: List get() = listOf(by) + parentCertsChain + val fullCertPath: CertPath get() = CertificateFactory.getInstance("X.509").generateCertPath(fullCertChain) + fun verify(content: ByteArray): Boolean = by.publicKey.verify(content, this) fun verify(content: OpaqueBytes): Boolean = verify(content.bytes) + + init { + if (parentCertsChain.isNotEmpty()) { + val parameters = PKIXParameters(setOf(TrustAnchor(parentCertsChain.last(), null))).apply { isRevocationEnabled = false } + try { + CertPathValidator.getInstance("PKIX").validate(fullCertPath, parameters) + } catch (e: CertPathValidatorException) { + throw IllegalArgumentException( + """Cert path failed to validate. +Reason: ${e.reason} +Offending cert index: ${e.index} +Cert path: $fullCertPath +""", e) + } + } + + } } /** Similar to [SignedData] but instead of just attaching the public key, the certificate for the key is attached instead. */ diff --git a/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt b/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt index 4bb32757f5..29ae9b8c2c 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt @@ -17,6 +17,7 @@ import net.corda.core.serialization.SerializeAsTokenContext import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NonEmptySet import net.corda.core.utilities.UntrustworthyData +import net.corda.core.utilities.debug import net.corda.core.utilities.unwrap import java.nio.file.FileAlreadyExistsException import java.util.* @@ -75,7 +76,7 @@ sealed class FetchDataFlow( return if (toFetch.isEmpty()) { Result(fromDisk, emptyList()) } else { - logger.info("Requesting ${toFetch.size} dependency(s) for verification from ${otherSideSession.counterparty.name}") + logger.debug { "Requesting ${toFetch.size} dependency(s) for verification from ${otherSideSession.counterparty.name}" } // TODO: Support "large message" response streaming so response sizes are not limited by RAM. // We can then switch to requesting items in large batches to minimise the latency penalty. @@ -93,7 +94,7 @@ sealed class FetchDataFlow( } // Check for a buggy/malicious peer answering with something that we didn't ask for. val downloaded = validateFetchResponse(UntrustworthyData(maybeItems), toFetch) - logger.info("Fetched ${downloaded.size} elements from ${otherSideSession.counterparty.name}") + logger.debug { "Fetched ${downloaded.size} elements from ${otherSideSession.counterparty.name}" } maybeWriteToDisk(downloaded) Result(fromDisk, downloaded) } diff --git a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt index 9626cf88a0..078cfcaa11 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -4,21 +4,11 @@ package net.corda.core.internal import net.corda.core.DeleteForDJVM import net.corda.core.KeepForDJVM -import net.corda.core.cordapp.Cordapp -import net.corda.core.cordapp.CordappConfig -import net.corda.core.cordapp.CordappContext import net.corda.core.crypto.* -import net.corda.core.flows.FlowLogic -import net.corda.core.node.ServicesForResolution import net.corda.core.serialization.* -import net.corda.core.transactions.LedgerTransaction -import net.corda.core.transactions.SignedTransaction -import net.corda.core.transactions.TransactionBuilder -import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.UntrustworthyData import org.slf4j.Logger -import org.slf4j.MDC import rx.Observable import rx.Observer import rx.subjects.PublishSubject diff --git a/core/src/main/kotlin/net/corda/core/internal/NamedCache.kt b/core/src/main/kotlin/net/corda/core/internal/NamedCache.kt index 7b54b6ceb4..2a96de4835 100644 --- a/core/src/main/kotlin/net/corda/core/internal/NamedCache.kt +++ b/core/src/main/kotlin/net/corda/core/internal/NamedCache.kt @@ -29,12 +29,6 @@ fun Caffeine.buildNamed(name: String): Cache { return this.build() } -fun Caffeine.buildNamed(name: String, loadFunc: (K) -> V): LoadingCache { - checkCacheName(name) - return this.build(loadFunc) -} - - fun Caffeine.buildNamed(name: String, loader: CacheLoader): LoadingCache { checkCacheName(name) return this.build(loader) diff --git a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt index a8021e8663..38891ef254 100644 --- a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt +++ b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt @@ -24,25 +24,29 @@ data class CordappImpl( override val customSchemas: Set, override val allFlows: List>>, override val jarPath: URL, + val info: Info, override val jarHash: SecureHash.SHA256) : Cordapp { - override val name: String = jarPath.toPath().fileName.toString().removeSuffix(".jar") + override val name: String = jarName(jarPath) + + companion object { + fun jarName(url: URL): String = url.toPath().fileName.toString().removeSuffix(".jar") + } /** * An exhaustive list of all classes relevant to the node within this CorDapp * * TODO: Also add [SchedulableFlow] as a Cordapp class */ - override val cordappClasses = ((rpcFlows + initiatedFlows + services + serializationWhitelists.map { javaClass }).map { it.name } + contractClassNames) + override val cordappClasses: List = (rpcFlows + initiatedFlows + services + serializationWhitelists.map { javaClass }).map { it.name } + contractClassNames - data class Info(override val shortName: String, override val vendor: String, override val version: String): Cordapp.Info { + // TODO Why a seperate Info class and not just have the fields directly in CordappImpl? + data class Info(val shortName: String, val vendor: String, val version: String, val minimumPlatformVersion: Int, val targetPlatformVersion: Int) { companion object { private const val UNKNOWN_VALUE = "Unknown" - val UNKNOWN = Info(UNKNOWN_VALUE, UNKNOWN_VALUE, UNKNOWN_VALUE) + val UNKNOWN = Info(UNKNOWN_VALUE, UNKNOWN_VALUE, UNKNOWN_VALUE, 1, 1) } - override fun hasUnknownFields(): Boolean { - return setOf(shortName, vendor, version).any { it == UNKNOWN_VALUE } - } + fun hasUnknownFields(): Boolean = arrayOf(shortName, vendor, version).any { it == UNKNOWN_VALUE } } } diff --git a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappInfoResolver.kt b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappInfoResolver.kt new file mode 100644 index 0000000000..63f7cb2188 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappInfoResolver.kt @@ -0,0 +1,68 @@ +package net.corda.core.internal.cordapp + +import net.corda.core.internal.VisibleForTesting +import net.corda.core.utilities.loggerFor +import java.util.concurrent.ConcurrentHashMap + +/** + * Provides a way to acquire information about the calling CorDapp. + */ +object CordappInfoResolver { + private val logger = loggerFor() + private val cordappClasses: ConcurrentHashMap> = ConcurrentHashMap() + + // TODO use the StackWalker API once we migrate to Java 9+ + private var cordappInfoResolver: () -> CordappImpl.Info? = { + Exception().stackTrace + .mapNotNull { cordappClasses[it.className] } + // If there is more than one cordapp registered for a class name we can't determine the "correct" one and return null. + .firstOrNull { it.size < 2 }?.single() + } + + /* + * Associates class names with CorDapps or logs a warning when a CorDapp is already registered for a given class. + * This could happen when trying to run different versions of the same CorDapp on the same node. + */ + @Synchronized + fun register(classes: List, cordapp: CordappImpl.Info) { + classes.forEach { + if (cordappClasses.containsKey(it)) { + logger.warn("More than one CorDapp registered for $it.") + cordappClasses[it] = cordappClasses[it]!! + cordapp + } else { + cordappClasses[it] = setOf(cordapp) + } + } + } + + /* + * This should only be used when making a change that would break compatibility with existing CorDapps. The change + * can then be version-gated, meaning the old behaviour is used if the calling CorDapp's target version is lower + * than the platform version that introduces the new behaviour. + * In situations where a `[CordappProvider]` is available the CorDapp context should be obtained from there. + * + * @return Information about the CorDapp from which the invoker is called, null if called outside a CorDapp or the + * calling CorDapp cannot be reliably determined.. + */ + fun getCorDappInfo(): CordappImpl.Info? = cordappInfoResolver() + + /** + * Temporarily switch out the internal resolver for another one. For use in testing. + */ + @Synchronized + @VisibleForTesting + fun withCordappInfoResolution(tempResolver: () -> CordappImpl.Info?, block: () -> Unit) { + val resolver = cordappInfoResolver + cordappInfoResolver = tempResolver + try { + block() + } finally { + cordappInfoResolver = resolver + } + } + + @VisibleForTesting + internal fun clear() { + cordappClasses.clear() + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/internal/notary/AsyncUniquenessProvider.kt b/core/src/main/kotlin/net/corda/core/internal/notary/AsyncUniquenessProvider.kt new file mode 100644 index 0000000000..1d9328de88 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/notary/AsyncUniquenessProvider.kt @@ -0,0 +1,35 @@ +package net.corda.core.internal.notary + +import net.corda.core.concurrent.CordaFuture +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TimeWindow +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.NotarisationRequestSignature +import net.corda.core.flows.NotaryError +import net.corda.core.identity.Party + +/** + * A service that records input states of the given transaction and provides conflict information + * if any of the inputs have already been used in another transaction. + */ +interface AsyncUniquenessProvider : UniquenessProvider { + /** Commits all input states of the given transaction. */ + fun commitAsync(states: List, txId: SecureHash, callerIdentity: Party, requestSignature: NotarisationRequestSignature, timeWindow: TimeWindow?, references: List): CordaFuture + + /** Commits all input states of the given transaction synchronously. Use [commitAsync] for better performance. */ + override fun commit(states: List, txId: SecureHash, callerIdentity: Party, requestSignature: NotarisationRequestSignature, timeWindow: TimeWindow?, references: List) { + val result = commitAsync(states, txId, callerIdentity, requestSignature, timeWindow,references).get() + if (result is Result.Failure) { + throw NotaryInternalException(result.error) + } + } + + /** The outcome of committing a transaction. */ + sealed class Result { + /** Indicates that all input states have been committed successfully. */ + object Success : Result() + /** Indicates that the transaction has not been committed. */ + data class Failure(val error: NotaryError) : Result() + } +} + diff --git a/core/src/main/kotlin/net/corda/core/internal/notary/TrustedAuthorityNotaryService.kt b/core/src/main/kotlin/net/corda/core/internal/notary/TrustedAuthorityNotaryService.kt index 1d76244380..148b506e82 100644 --- a/core/src/main/kotlin/net/corda/core/internal/notary/TrustedAuthorityNotaryService.kt +++ b/core/src/main/kotlin/net/corda/core/internal/notary/TrustedAuthorityNotaryService.kt @@ -67,4 +67,4 @@ abstract class TrustedAuthorityNotaryService : NotaryService() { } // TODO: Sign multiple transactions at once by building their Merkle tree and then signing over its root. -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt index f59aa49a40..1b924610d7 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -22,7 +22,6 @@ import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.Try import rx.Observable -import rx.subjects.PublishSubject import java.io.IOException import java.io.InputStream import java.security.PublicKey @@ -42,7 +41,8 @@ data class StateMachineInfo @JvmOverloads constructor( * An object representing information about the initiator of the flow. Note that this field is * superseded by the [invocationContext] property, which has more detail. */ - @Deprecated("There is more info available using 'context'") val initiator: FlowInitiator, + @Deprecated("There is more info available using 'invocationContext'") + val initiator: FlowInitiator, /** A [DataFeed] of the current progress step as a human readable string, and updates to that string. */ val progressTrackerStepAndUpdates: DataFeed?, /** An [InvocationContext] describing why and by whom the flow was started. */ @@ -76,7 +76,8 @@ sealed class StateMachineUpdate { // DOCSTART 1 /** * Data class containing information about the scheduled network parameters update. The info is emitted every time node - * receives network map with [ParametersUpdate] which wasn't seen before. For more information see: [CordaRPCOps.networkParametersFeed] and [CordaRPCOps.acceptNewNetworkParameters]. + * receives network map with [ParametersUpdate] which wasn't seen before. For more information see: [CordaRPCOps.networkParametersFeed] + * and [CordaRPCOps.acceptNewNetworkParameters]. * @property hash new [NetworkParameters] hash * @property parameters new [NetworkParameters] data structure * @property description description of the update @@ -96,12 +97,6 @@ data class StateMachineTransactionMapping(val stateMachineRunId: StateMachineRun /** RPC operations that the node exposes to clients. */ interface CordaRPCOps : RPCOps { - /** - * Returns the RPC protocol version, which is the same the node's Platform Version. Exists since version 1 so guaranteed - * to be present. - */ - override val protocolVersion: Int get() = nodeInfo().platformVersion - /** Returns a list of currently in-progress state machine infos. */ fun stateMachinesSnapshot(): List @@ -233,6 +228,9 @@ interface CordaRPCOps : RPCOps { @RPCReturnsObservables fun networkMapFeed(): DataFeed, NetworkMapCache.MapChange> + /** Returns the network parameters the node is operating under. */ + val networkParameters: NetworkParameters + /** * Returns [DataFeed] object containing information on currently scheduled parameters update (null if none are currently scheduled) * and observable with future update events. Any update that occurs before the deadline automatically cancels the current one. @@ -406,38 +404,20 @@ interface CordaRPCOps : RPCOps { * This does not wait for flows to be completed. */ fun shutdown() -} -/** - * Returns a [DataFeed] that keeps track on the count of pending flows. - */ -fun CordaRPCOps.pendingFlowsCount(): DataFeed> { + /** + * Shuts the node down. Returns immediately. + * @param drainPendingFlows whether the node will wait for pending flows to be completed before exiting. While draining, new flows from RPC will be rejected. + */ + fun terminate(drainPendingFlows: Boolean = false) - val stateMachineState = stateMachinesFeed() - var pendingFlowsCount = stateMachineState.snapshot.size - var completedFlowsCount = 0 - val updates = PublishSubject.create>() - stateMachineState - .updates - .doOnNext { update -> - when (update) { - is StateMachineUpdate.Added -> { - pendingFlowsCount++ - updates.onNext(completedFlowsCount to pendingFlowsCount) - } - is StateMachineUpdate.Removed -> { - completedFlowsCount++ - updates.onNext(completedFlowsCount to pendingFlowsCount) - if (completedFlowsCount == pendingFlowsCount) { - updates.onCompleted() - } - } - } - }.subscribe() - if (completedFlowsCount == 0) { - updates.onCompleted() - } - return DataFeed(pendingFlowsCount, updates) + /** + * Returns whether the node is waiting for pending flows to complete before shutting down. + * Disabling draining mode cancels this state. + * + * @return whether the node will shutdown when the pending flows count reaches zero. + */ + fun isWaitingForShutdown(): Boolean } inline fun CordaRPCOps.vaultQueryBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(), diff --git a/core/src/main/kotlin/net/corda/core/node/AppServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/AppServiceHub.kt index 62e5e2f1f6..60816009cb 100644 --- a/core/src/main/kotlin/net/corda/core/node/AppServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/AppServiceHub.kt @@ -28,4 +28,4 @@ interface AppServiceHub : ServiceHub { * TODO it is assumed here that the flow object has an appropriate classloader. */ fun startTrackedFlow(flow: FlowLogic): FlowProgressHandle -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt b/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt index 91c714b51f..b8f2bc7757 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt @@ -140,4 +140,4 @@ interface IdentityService { fun partiesFromName(query: String, exactMatch: Boolean): Set } -class UnknownAnonymousPartyException(msg: String) : CordaException(msg) +class UnknownAnonymousPartyException(message: String) : CordaException(message) diff --git a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt index a05dbf7f37..20d1984d8d 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt @@ -160,7 +160,7 @@ class Vault(val states: Iterable>) { val notary: AbstractParty?, val lockId: String?, val lockUpdateTime: Instant?, - val isRelevant: Vault.RelevancyStatus? + val relevancyStatus: Vault.RelevancyStatus? ) { constructor(ref: StateRef, contractStateClassName: String, diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt index d8a6f9374b..609434a60b 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt @@ -73,7 +73,7 @@ sealed class QueryCriteria : GenericQueryCriteria>? override fun visit(parser: IQueryCriteriaParser): Collection { return parser.parseCriteria(this) @@ -90,7 +90,7 @@ sealed class QueryCriteria : GenericQueryCriteria? = null, val softLockingCondition: SoftLockingCondition? = null, val timeCondition: TimeCondition? = null, - override val isRelevant: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL + override val relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL ) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { super.visit(parser) @@ -125,15 +125,15 @@ sealed class QueryCriteria : GenericQueryCriteria? = null, override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, override val contractStateTypes: Set>? = null, - override val isRelevant: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL + override val relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL ) : CommonQueryCriteria() { constructor( participants: List? = null, linearId: List? = null, status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, contractStateTypes: Set>? = null, - isRelevant: Vault.RelevancyStatus - ) : this(participants, linearId?.map { it.id }, linearId?.mapNotNull { it.externalId }, status, contractStateTypes, isRelevant) + relevancyStatus: Vault.RelevancyStatus + ) : this(participants, linearId?.map { it.id }, linearId?.mapNotNull { it.externalId }, status, contractStateTypes, relevancyStatus) constructor( participants: List? = null, @@ -175,7 +175,7 @@ sealed class QueryCriteria : GenericQueryCriteria? = null, override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, override val contractStateTypes: Set>? = null, - override val isRelevant: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL + override val relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL ) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { super.visit(parser) @@ -215,7 +215,7 @@ sealed class QueryCriteria : GenericQueryCriteria, override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, override val contractStateTypes: Set>? = null, - override val isRelevant: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL + override val relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL ) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { super.visit(parser) diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt index fa98e1d54b..faac1f7fef 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt @@ -5,6 +5,10 @@ package net.corda.core.node.services.vault import net.corda.core.DoNotImplement import net.corda.core.internal.declaredField import net.corda.core.internal.uncheckedCast +import net.corda.core.node.services.vault.CollectionOperator.* +import net.corda.core.node.services.vault.ColumnPredicate.* +import net.corda.core.node.services.vault.EqualityComparisonOperator.* +import net.corda.core.node.services.vault.LikenessOperator.* import net.corda.core.schemas.PersistentState import net.corda.core.serialization.CordaSerializable import java.lang.reflect.Field @@ -24,7 +28,9 @@ enum class BinaryLogicalOperator : Operator { enum class EqualityComparisonOperator : Operator { EQUAL, - NOT_EQUAL + NOT_EQUAL, + EQUAL_IGNORE_CASE, + NOT_EQUAL_IGNORE_CASE } enum class BinaryComparisonOperator : Operator { @@ -41,12 +47,16 @@ enum class NullOperator : Operator { enum class LikenessOperator : Operator { LIKE, - NOT_LIKE + NOT_LIKE, + LIKE_IGNORE_CASE, + NOT_LIKE_IGNORE_CASE } enum class CollectionOperator : Operator { IN, - NOT_IN + NOT_IN, + IN_IGNORE_CASE, + NOT_IN_IGNORE_CASE } @CordaSerializable @@ -251,27 +261,45 @@ object Builder { fun > Field.comparePredicate(operator: BinaryComparisonOperator, value: R) = info().comparePredicate(operator, value) fun > FieldInfo.comparePredicate(operator: BinaryComparisonOperator, value: R) = predicate(compare(operator, value)) - fun KProperty1.equal(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value)) - fun KProperty1.notEqual(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value)) + @JvmOverloads + fun KProperty1.equal(value: R, exactMatch: Boolean = true) = predicate(Builder.equal(value, exactMatch)) + + @JvmOverloads + fun KProperty1.notEqual(value: R, exactMatch: Boolean = true) = predicate(Builder.notEqual(value, exactMatch)) + fun > KProperty1.lessThan(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN, value) + fun > KProperty1.lessThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN_OR_EQUAL, value) + fun > KProperty1.greaterThan(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN, value) + fun > KProperty1.greaterThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL, value) + fun > KProperty1.between(from: R, to: R) = predicate(ColumnPredicate.Between(from, to)) - fun > KProperty1.`in`(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection)) - fun > KProperty1.notIn(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection)) + + @JvmOverloads + fun > KProperty1.`in`(collection: Collection, exactMatch: Boolean = true) = predicate(Builder.`in`(collection, exactMatch)) + + @JvmOverloads + fun > KProperty1.notIn(collection: Collection, exactMatch: Boolean = true) = predicate(Builder.notIn(collection, exactMatch)) @JvmStatic + @JvmOverloads @Deprecated("Does not support fields from a MappedSuperclass. Use equivalent on a FieldInfo.") - fun Field.equal(value: R) = info().equal(value) - @JvmStatic - fun FieldInfo.equal(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value)) + fun Field.equal(value: R, exactMatch: Boolean = true) = info().equal(value, exactMatch) @JvmStatic - @Deprecated("Does not support fields from a MappedSuperclass. Use equivalent on a FieldInfo.") - fun Field.notEqual(value: R) = info().notEqual(value) + @JvmOverloads + fun FieldInfo.equal(value: R, exactMatch: Boolean = true) = predicate(Builder.equal(value, exactMatch)) + @JvmStatic - fun FieldInfo.notEqual(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value)) + @JvmOverloads + @Deprecated("Does not support fields from a MappedSuperclass. Use equivalent on a FieldInfo.") + fun Field.notEqual(value: R, exactMatch: Boolean = true) = info().notEqual(value, exactMatch) + + @JvmStatic + @JvmOverloads + fun FieldInfo.notEqual(value: R, exactMatch: Boolean = true) = predicate(Builder.equal(value, exactMatch)) @JvmStatic @Deprecated("Does not support fields from a MappedSuperclass. Use equivalent on a FieldInfo.") @@ -304,44 +332,77 @@ object Builder { fun > FieldInfo.between(from: R, to: R) = predicate(ColumnPredicate.Between(from, to)) @JvmStatic + @JvmOverloads @Deprecated("Does not support fields from a MappedSuperclass. Use equivalent on a FieldInfo.") - fun > Field.`in`(collection: Collection) = info().`in`(collection) - fun > FieldInfo.`in`(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection)) + fun > Field.`in`(collection: Collection, exactMatch: Boolean = true) = info().`in`(collection, exactMatch) @JvmStatic - @Deprecated("Does not support fields from a MappedSuperclass. Use equivalent on a FieldInfo.") - fun > Field.notIn(collection: Collection) = info().notIn(collection) - @JvmStatic - fun > FieldInfo.notIn(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection)) + @JvmOverloads + fun > FieldInfo.`in`(collection: Collection, exactMatch: Boolean = true) = predicate(Builder.`in`(collection, exactMatch)) + + @JvmStatic + @JvmOverloads + @Deprecated("Does not support fields from a MappedSuperclass. Use equivalent on a FieldInfo.") + fun > Field.notIn(collection: Collection, exactMatch: Boolean = true) = info().notIn(collection, exactMatch) + + @JvmStatic + @JvmOverloads + fun > FieldInfo.notIn(collection: Collection, exactMatch: Boolean = true) = predicate(Builder.notIn(collection, exactMatch)) + + @JvmOverloads + fun equal(value: R, exactMatch: Boolean = true) = EqualityComparison(if (exactMatch) EQUAL else EQUAL_IGNORE_CASE, value) + + @JvmOverloads + fun notEqual(value: R, exactMatch: Boolean = true) = EqualityComparison(if (exactMatch) NOT_EQUAL else NOT_EQUAL_IGNORE_CASE, value) - fun equal(value: R) = ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value) - fun notEqual(value: R) = ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value) fun > lessThan(value: R) = compare(BinaryComparisonOperator.LESS_THAN, value) + fun > lessThanOrEqual(value: R) = compare(BinaryComparisonOperator.LESS_THAN_OR_EQUAL, value) + fun > greaterThan(value: R) = compare(BinaryComparisonOperator.GREATER_THAN, value) + fun > greaterThanOrEqual(value: R) = compare(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL, value) + fun > between(from: R, to: R) = ColumnPredicate.Between(from, to) - fun > `in`(collection: Collection) = ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection) - fun > notIn(collection: Collection) = ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection) - fun like(string: String) = ColumnPredicate.Likeness(LikenessOperator.LIKE, string) - fun notLike(string: String) = ColumnPredicate.Likeness(LikenessOperator.NOT_LIKE, string) + + @JvmOverloads + fun > `in`(collection: Collection, exactMatch: Boolean = true) = CollectionExpression(if (exactMatch) IN else IN_IGNORE_CASE, collection) + + @JvmOverloads + fun > notIn(collection: Collection, exactMatch: Boolean = true) = CollectionExpression(if (exactMatch) NOT_IN else NOT_IN_IGNORE_CASE, collection) + + @JvmOverloads + fun like(string: String, exactMatch: Boolean = true) = Likeness(if (exactMatch) LIKE else LIKE_IGNORE_CASE, string) + + @JvmOverloads + fun notLike(string: String, exactMatch: Boolean = true) = Likeness(if (exactMatch) NOT_LIKE else NOT_LIKE_IGNORE_CASE, string) + fun isNull() = ColumnPredicate.NullExpression(NullOperator.IS_NULL) fun isNotNull() = ColumnPredicate.NullExpression(NullOperator.NOT_NULL) + @JvmOverloads + fun KProperty1.like(string: String, exactMatch: Boolean = true) = predicate(Builder.like(string, exactMatch)) - fun KProperty1.like(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.LIKE, string)) @JvmStatic + @JvmOverloads @Deprecated("Does not support fields from a MappedSuperclass. Use equivalent on a FieldInfo.") - fun Field.like(string: String) = info().like(string) - @JvmStatic - fun FieldInfo.like(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.LIKE, string)) + fun Field.like(string: String, exactMatch: Boolean = true) = info().like(string, exactMatch) - fun KProperty1.notLike(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.NOT_LIKE, string)) @JvmStatic + @JvmOverloads + fun FieldInfo.like(string: String, exactMatch: Boolean = true) = predicate(Builder.like(string, exactMatch)) + + @JvmOverloads + fun KProperty1.notLike(string: String, exactMatch: Boolean = true) = predicate(Builder.notLike(string, exactMatch)) + + @JvmStatic + @JvmOverloads @Deprecated("Does not support fields from a MappedSuperclass. Use equivalent on a FieldInfo.") - fun Field.notLike(string: String) = info().notLike(string) + fun Field.notLike(string: String, exactMatch: Boolean = true) = info().notLike(string, exactMatch) + @JvmStatic - fun FieldInfo.notLike(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.NOT_LIKE, string)) + @JvmOverloads + fun FieldInfo.notLike(string: String, exactMatch: Boolean = true) = predicate(Builder.notLike(string, exactMatch)) fun KProperty1.isNull() = predicate(ColumnPredicate.NullExpression(NullOperator.IS_NULL)) @JvmStatic diff --git a/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt b/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt index c5df7f7069..3a0ee16ce0 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt @@ -207,7 +207,13 @@ interface SerializationContext { * The use case that we are serializing for, since it influences the implementations chosen. */ @KeepForDJVM - enum class UseCase { P2P, RPCServer, RPCClient, Storage, Checkpoint, Testing } + enum class UseCase { + P2P, + RPCServer, + RPCClient, + Storage, + Testing + } } /** @@ -230,7 +236,6 @@ object SerializationDefaults { @DeleteForDJVM val RPC_SERVER_CONTEXT get() = effectiveSerializationEnv.rpcServerContext @DeleteForDJVM val RPC_CLIENT_CONTEXT get() = effectiveSerializationEnv.rpcClientContext @DeleteForDJVM val STORAGE_CONTEXT get() = effectiveSerializationEnv.storageContext - @DeleteForDJVM val CHECKPOINT_CONTEXT get() = effectiveSerializationEnv.checkpointContext } /** diff --git a/core/src/main/kotlin/net/corda/core/serialization/internal/CheckpointSerializationAPI.kt b/core/src/main/kotlin/net/corda/core/serialization/internal/CheckpointSerializationAPI.kt new file mode 100644 index 0000000000..448d1ab25f --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/serialization/internal/CheckpointSerializationAPI.kt @@ -0,0 +1,198 @@ +package net.corda.core.serialization.internal + +import net.corda.core.DeleteForDJVM +import net.corda.core.DoNotImplement +import net.corda.core.KeepForDJVM +import net.corda.core.crypto.SecureHash +import net.corda.core.serialization.* +import net.corda.core.utilities.ByteSequence +import net.corda.core.utilities.sequence +import java.io.NotSerializableException + + +object CheckpointSerializationDefaults { + @DeleteForDJVM + val CHECKPOINT_CONTEXT get() = effectiveSerializationEnv.checkpointContext + val CHECKPOINT_SERIALIZATION_FACTORY get() = effectiveSerializationEnv.checkpointSerializationFactory +} + +/** + * A class for serializing and deserializing objects at checkpoints, using Kryo serialization. + */ +@KeepForDJVM +class CheckpointSerializationFactory( + private val scheme: CheckpointSerializationScheme +) { + + val defaultContext: CheckpointSerializationContext get() = _currentContext.get() ?: effectiveSerializationEnv.checkpointContext + + private val creator: List = Exception().stackTrace.asList() + + /** + * Deserialize the bytes in to an object, using the prefixed bytes to determine the format. + * + * @param byteSequence The bytes to deserialize, including a format header prefix. + * @param clazz The class or superclass or the object to be deserialized, or [Any] or [Object] if unknown. + * @param context A context that configures various parameters to deserialization. + */ + fun deserialize(byteSequence: ByteSequence, clazz: Class, context: CheckpointSerializationContext): T { + return withCurrentContext(context) { scheme.deserialize(byteSequence, clazz, context) } + } + + /** + * Serialize an object to bytes using the preferred serialization format version from the context. + * + * @param obj The object to be serialized. + * @param context A context that configures various parameters to serialization, including the serialization format version. + */ + fun serialize(obj: T, context: CheckpointSerializationContext): SerializedBytes { + return withCurrentContext(context) { scheme.serialize(obj, context) } + } + + override fun toString(): String { + return "${this.javaClass.name} scheme=$scheme ${creator.joinToString("\n")}" + } + + override fun equals(other: Any?): Boolean { + return other is CheckpointSerializationFactory && other.scheme == this.scheme + } + + override fun hashCode(): Int = scheme.hashCode() + + private val _currentContext = ThreadLocal() + + /** + * Change the current context inside the block to that supplied. + */ + fun withCurrentContext(context: CheckpointSerializationContext?, block: () -> T): T { + val priorContext = _currentContext.get() + if (context != null) _currentContext.set(context) + try { + return block() + } finally { + if (context != null) _currentContext.set(priorContext) + } + } + + companion object { + val defaultFactory: CheckpointSerializationFactory get() = effectiveSerializationEnv.checkpointSerializationFactory + } +} + +@KeepForDJVM +@DoNotImplement +interface CheckpointSerializationScheme { + @Throws(NotSerializableException::class) + fun deserialize(byteSequence: ByteSequence, clazz: Class, context: CheckpointSerializationContext): T + + @Throws(NotSerializableException::class) + fun serialize(obj: T, context: CheckpointSerializationContext): SerializedBytes +} + +/** + * Parameters to checkpoint serialization and deserialization. + */ +@KeepForDJVM +@DoNotImplement +interface CheckpointSerializationContext { + /** + * If non-null, apply this encoding (typically compression) when serializing. + */ + val encoding: SerializationEncoding? + /** + * The class loader to use for deserialization. + */ + val deserializationClassLoader: ClassLoader + /** + * A whitelist that contains (mostly for security purposes) which classes can be serialized and deserialized. + */ + val whitelist: ClassWhitelist + /** + * A whitelist that determines (mostly for security purposes) whether a particular encoding may be used when deserializing. + */ + val encodingWhitelist: EncodingWhitelist + /** + * A map of any addition properties specific to the particular use case. + */ + val properties: Map + /** + * Duplicate references to the same object preserved in the wire format and when deserialized when this is true, + * otherwise they appear as new copies of the object. + */ + val objectReferencesEnabled: Boolean + + /** + * Helper method to return a new context based on this context with the property added. + */ + fun withProperty(property: Any, value: Any): CheckpointSerializationContext + + /** + * Helper method to return a new context based on this context with object references disabled. + */ + fun withoutReferences(): CheckpointSerializationContext + + /** + * Helper method to return a new context based on this context with the deserialization class loader changed. + */ + fun withClassLoader(classLoader: ClassLoader): CheckpointSerializationContext + + /** + * Helper method to return a new context based on this context with the appropriate class loader constructed from the passed attachment identifiers. + * (Requires the attachment storage to have been enabled). + */ + @Throws(MissingAttachmentsException::class) + fun withAttachmentsClassLoader(attachmentHashes: List): CheckpointSerializationContext + + /** + * Helper method to return a new context based on this context with the given class specifically whitelisted. + */ + fun withWhitelisted(clazz: Class<*>): CheckpointSerializationContext + + /** + * A shallow copy of this context but with the given (possibly null) encoding. + */ + fun withEncoding(encoding: SerializationEncoding?): CheckpointSerializationContext + + /** + * A shallow copy of this context but with the given encoding whitelist. + */ + fun withEncodingWhitelist(encodingWhitelist: EncodingWhitelist): CheckpointSerializationContext +} + +/* + * The following extension methods are disambiguated from the AMQP-serialization methods by requiring that an + * explicit [CheckpointSerializationContext] parameter be provided. + */ + +/* + * Convenience extension method for deserializing a ByteSequence, utilising the default factory. + */ +inline fun ByteSequence.checkpointDeserialize(serializationFactory: CheckpointSerializationFactory = CheckpointSerializationFactory.defaultFactory, + context: CheckpointSerializationContext): T { + return serializationFactory.deserialize(this, T::class.java, context) +} + +/** + * Convenience extension method for deserializing SerializedBytes with type matching, utilising the default factory. + */ +inline fun SerializedBytes.checkpointDeserialize(serializationFactory: CheckpointSerializationFactory = CheckpointSerializationFactory.defaultFactory, + context: CheckpointSerializationContext): T { + return serializationFactory.deserialize(this, T::class.java, context) +} + +/** + * Convenience extension method for deserializing a ByteArray, utilising the default factory. + */ +inline fun ByteArray.checkpointDeserialize(serializationFactory: CheckpointSerializationFactory = CheckpointSerializationFactory.defaultFactory, + context: CheckpointSerializationContext): T { + require(isNotEmpty()) { "Empty bytes" } + return this.sequence().checkpointDeserialize(serializationFactory, context) +} + +/** + * Convenience extension method for serializing an object of type T, utilising the default factory. + */ +fun T.checkpointSerialize(serializationFactory: CheckpointSerializationFactory = CheckpointSerializationFactory.defaultFactory, + context: CheckpointSerializationContext): SerializedBytes { + return serializationFactory.serialize(this, context) +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/serialization/internal/SerializationEnvironment.kt b/core/src/main/kotlin/net/corda/core/serialization/internal/SerializationEnvironment.kt index 28c6ad7900..441cd52be4 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/internal/SerializationEnvironment.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/internal/SerializationEnvironment.kt @@ -12,11 +12,12 @@ import net.corda.core.serialization.SerializationFactory @KeepForDJVM interface SerializationEnvironment { val serializationFactory: SerializationFactory + val checkpointSerializationFactory: CheckpointSerializationFactory val p2pContext: SerializationContext val rpcServerContext: SerializationContext val rpcClientContext: SerializationContext val storageContext: SerializationContext - val checkpointContext: SerializationContext + val checkpointContext: CheckpointSerializationContext } @KeepForDJVM @@ -26,18 +27,21 @@ open class SerializationEnvironmentImpl( rpcServerContext: SerializationContext? = null, rpcClientContext: SerializationContext? = null, storageContext: SerializationContext? = null, - checkpointContext: SerializationContext? = null) : SerializationEnvironment { + checkpointContext: CheckpointSerializationContext? = null, + checkpointSerializationFactory: CheckpointSerializationFactory? = null) : SerializationEnvironment { // Those that are passed in as null are never inited: override lateinit var rpcServerContext: SerializationContext override lateinit var rpcClientContext: SerializationContext override lateinit var storageContext: SerializationContext - override lateinit var checkpointContext: SerializationContext + override lateinit var checkpointContext: CheckpointSerializationContext + override lateinit var checkpointSerializationFactory: CheckpointSerializationFactory init { rpcServerContext?.let { this.rpcServerContext = it } rpcClientContext?.let { this.rpcClientContext = it } storageContext?.let { this.storageContext = it } checkpointContext?.let { this.checkpointContext = it } + checkpointSerializationFactory?.let { this.checkpointSerializationFactory = it } } } diff --git a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt index dbc5ef95fb..aef380e153 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt @@ -3,12 +3,15 @@ package net.corda.core.transactions import net.corda.core.CordaException import net.corda.core.KeepForDJVM import net.corda.core.contracts.* +import net.corda.core.contracts.ComponentGroupEnum.* import net.corda.core.crypto.* import net.corda.core.identity.Party +import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.* import net.corda.core.utilities.OpaqueBytes import java.security.PublicKey import java.util.function.Predicate +import kotlin.reflect.KClass /** * Implemented by [WireTransaction] and [FilteredTransaction]. A TraversableTransaction allows you to iterate @@ -18,29 +21,29 @@ import java.util.function.Predicate */ abstract class TraversableTransaction(open val componentGroups: List) : CoreTransaction() { /** Hashes of the ZIP/JAR files that are needed to interpret the contents of this wire transaction. */ - val attachments: List = deserialiseComponentGroup(ComponentGroupEnum.ATTACHMENTS_GROUP, { SerializedBytes(it).deserialize() }) + val attachments: List = deserialiseComponentGroup(SecureHash::class, ATTACHMENTS_GROUP) /** Pointers to the input states on the ledger, identified by (tx identity hash, output index). */ - override val inputs: List = deserialiseComponentGroup(ComponentGroupEnum.INPUTS_GROUP, { SerializedBytes(it).deserialize() }) + override val inputs: List = deserialiseComponentGroup(StateRef::class, INPUTS_GROUP) /** Pointers to reference states, identified by (tx identity hash, output index). */ - override val references: List = deserialiseComponentGroup(ComponentGroupEnum.REFERENCES_GROUP, { SerializedBytes(it).deserialize() }) + override val references: List = deserialiseComponentGroup(StateRef::class, REFERENCES_GROUP) - override val outputs: List> = deserialiseComponentGroup(ComponentGroupEnum.OUTPUTS_GROUP, { SerializedBytes>(it).deserialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentsClassLoader(attachments)) }) + override val outputs: List> = deserialiseComponentGroup(TransactionState::class, OUTPUTS_GROUP, attachmentsContext = true) /** Ordered list of ([CommandData], [PublicKey]) pairs that instruct the contracts what to do. */ val commands: List> = deserialiseCommands() override val notary: Party? = let { - val notaries: List = deserialiseComponentGroup(ComponentGroupEnum.NOTARY_GROUP, { SerializedBytes(it).deserialize() }) + val notaries: List = deserialiseComponentGroup(Party::class, NOTARY_GROUP) check(notaries.size <= 1) { "Invalid Transaction. More than 1 notary party detected." } - if (notaries.isNotEmpty()) notaries[0] else null + notaries.firstOrNull() } val timeWindow: TimeWindow? = let { - val timeWindows: List = deserialiseComponentGroup(ComponentGroupEnum.TIMEWINDOW_GROUP, { SerializedBytes(it).deserialize() }) + val timeWindows: List = deserialiseComponentGroup(TimeWindow::class, TIMEWINDOW_GROUP) check(timeWindows.size <= 1) { "Invalid Transaction. More than 1 time-window detected." } - if (timeWindows.isNotEmpty()) timeWindows[0] else null + timeWindows.firstOrNull() } /** @@ -63,12 +66,16 @@ abstract class TraversableTransaction(open val componentGroups: List deserialiseComponentGroup(groupEnum: ComponentGroupEnum, deserialiseBody: (ByteArray) -> T): List { + private fun deserialiseComponentGroup(clazz: KClass, + groupEnum: ComponentGroupEnum, + attachmentsContext: Boolean = false): List { + val factory = SerializationFactory.defaultFactory + val context = factory.defaultContext.let { if (attachmentsContext) it.withAttachmentsClassLoader(attachments) else it } val group = componentGroups.firstOrNull { it.groupIndex == groupEnum.ordinal } return if (group != null && group.components.isNotEmpty()) { group.components.mapIndexed { internalIndex, component -> try { - deserialiseBody(component.bytes) + factory.deserialize(component, clazz.java, context) } catch (e: MissingAttachmentsException) { throw e } catch (e: Exception) { @@ -87,11 +94,13 @@ abstract class TraversableTransaction(open val componentGroups: List>(it).deserialize() }) - val commandDataList = deserialiseComponentGroup(ComponentGroupEnum.COMMANDS_GROUP, { SerializedBytes(it).deserialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentsClassLoader(attachments)) }) - val group = componentGroups.firstOrNull { it.groupIndex == ComponentGroupEnum.COMMANDS_GROUP.ordinal } + val signersList: List> = uncheckedCast(deserialiseComponentGroup(List::class, SIGNERS_GROUP)) + val commandDataList: List = deserialiseComponentGroup(CommandData::class, COMMANDS_GROUP, attachmentsContext = true) + val group = componentGroups.firstOrNull { it.groupIndex == COMMANDS_GROUP.ordinal } return if (group is FilteredComponentGroup) { - check(commandDataList.size <= signersList.size) { "Invalid Transaction. Less Signers (${signersList.size}) than CommandData (${commandDataList.size}) objects" } + check(commandDataList.size <= signersList.size) { + "Invalid Transaction. Less Signers (${signersList.size}) than CommandData (${commandDataList.size}) objects" + } val componentHashes = group.components.mapIndexed { index, component -> componentHash(group.nonces[index], component) } val leafIndices = componentHashes.map { group.partialMerkleTree.leafIndex(it) } if (leafIndices.isNotEmpty()) @@ -100,7 +109,9 @@ abstract class TraversableTransaction(open val componentGroups: List Command(commandData, signersList[index]) } } } @@ -145,47 +156,47 @@ class FilteredTransaction internal constructor( var signersIncluded = false fun filter(t: T, componentGroupIndex: Int, internalIndex: Int) { - if (filtering.test(t)) { - val group = filteredSerialisedComponents[componentGroupIndex] - // Because the filter passed, we know there is a match. We also use first Vs single as the init function - // of WireTransaction ensures there are no duplicated groups. - val serialisedComponent = wtx.componentGroups.first { it.groupIndex == componentGroupIndex }.components[internalIndex] - if (group == null) { - // As all of the helper Map structures, like availableComponentNonces, availableComponentHashes - // and groupsMerkleRoots, are computed lazily via componentGroups.forEach, there should always be - // a match on Map.get ensuring it will never return null. - filteredSerialisedComponents[componentGroupIndex] = mutableListOf(serialisedComponent) - filteredComponentNonces[componentGroupIndex] = mutableListOf(wtx.availableComponentNonces[componentGroupIndex]!![internalIndex]) - filteredComponentHashes[componentGroupIndex] = mutableListOf(wtx.availableComponentHashes[componentGroupIndex]!![internalIndex]) - } else { - group.add(serialisedComponent) - // If the group[componentGroupIndex] existed, then we guarantee that - // filteredComponentNonces[componentGroupIndex] and filteredComponentHashes[componentGroupIndex] are not null. - filteredComponentNonces[componentGroupIndex]!!.add(wtx.availableComponentNonces[componentGroupIndex]!![internalIndex]) - filteredComponentHashes[componentGroupIndex]!!.add(wtx.availableComponentHashes[componentGroupIndex]!![internalIndex]) - } - // If at least one command is visible, then all command-signers should be visible as well. - // This is required for visibility purposes, see FilteredTransaction.checkAllCommandsVisible() for more details. - if (componentGroupIndex == ComponentGroupEnum.COMMANDS_GROUP.ordinal && !signersIncluded) { - signersIncluded = true - val signersGroupIndex = ComponentGroupEnum.SIGNERS_GROUP.ordinal - // There exist commands, thus the signers group is not empty. - val signersGroupComponents = wtx.componentGroups.first { it.groupIndex == signersGroupIndex } - filteredSerialisedComponents[signersGroupIndex] = signersGroupComponents.components.toMutableList() - filteredComponentNonces[signersGroupIndex] = wtx.availableComponentNonces[signersGroupIndex]!!.toMutableList() - filteredComponentHashes[signersGroupIndex] = wtx.availableComponentHashes[signersGroupIndex]!!.toMutableList() - } + if (!filtering.test(t)) return + + val group = filteredSerialisedComponents[componentGroupIndex] + // Because the filter passed, we know there is a match. We also use first Vs single as the init function + // of WireTransaction ensures there are no duplicated groups. + val serialisedComponent = wtx.componentGroups.first { it.groupIndex == componentGroupIndex }.components[internalIndex] + if (group == null) { + // As all of the helper Map structures, like availableComponentNonces, availableComponentHashes + // and groupsMerkleRoots, are computed lazily via componentGroups.forEach, there should always be + // a match on Map.get ensuring it will never return null. + filteredSerialisedComponents[componentGroupIndex] = mutableListOf(serialisedComponent) + filteredComponentNonces[componentGroupIndex] = mutableListOf(wtx.availableComponentNonces[componentGroupIndex]!![internalIndex]) + filteredComponentHashes[componentGroupIndex] = mutableListOf(wtx.availableComponentHashes[componentGroupIndex]!![internalIndex]) + } else { + group.add(serialisedComponent) + // If the group[componentGroupIndex] existed, then we guarantee that + // filteredComponentNonces[componentGroupIndex] and filteredComponentHashes[componentGroupIndex] are not null. + filteredComponentNonces[componentGroupIndex]!!.add(wtx.availableComponentNonces[componentGroupIndex]!![internalIndex]) + filteredComponentHashes[componentGroupIndex]!!.add(wtx.availableComponentHashes[componentGroupIndex]!![internalIndex]) + } + // If at least one command is visible, then all command-signers should be visible as well. + // This is required for visibility purposes, see FilteredTransaction.checkAllCommandsVisible() for more details. + if (componentGroupIndex == COMMANDS_GROUP.ordinal && !signersIncluded) { + signersIncluded = true + val signersGroupIndex = SIGNERS_GROUP.ordinal + // There exist commands, thus the signers group is not empty. + val signersGroupComponents = wtx.componentGroups.first { it.groupIndex == signersGroupIndex } + filteredSerialisedComponents[signersGroupIndex] = signersGroupComponents.components.toMutableList() + filteredComponentNonces[signersGroupIndex] = wtx.availableComponentNonces[signersGroupIndex]!!.toMutableList() + filteredComponentHashes[signersGroupIndex] = wtx.availableComponentHashes[signersGroupIndex]!!.toMutableList() } } fun updateFilteredComponents() { - wtx.inputs.forEachIndexed { internalIndex, it -> filter(it, ComponentGroupEnum.INPUTS_GROUP.ordinal, internalIndex) } - wtx.outputs.forEachIndexed { internalIndex, it -> filter(it, ComponentGroupEnum.OUTPUTS_GROUP.ordinal, internalIndex) } - wtx.commands.forEachIndexed { internalIndex, it -> filter(it, ComponentGroupEnum.COMMANDS_GROUP.ordinal, internalIndex) } - wtx.attachments.forEachIndexed { internalIndex, it -> filter(it, ComponentGroupEnum.ATTACHMENTS_GROUP.ordinal, internalIndex) } - if (wtx.notary != null) filter(wtx.notary, ComponentGroupEnum.NOTARY_GROUP.ordinal, 0) - if (wtx.timeWindow != null) filter(wtx.timeWindow, ComponentGroupEnum.TIMEWINDOW_GROUP.ordinal, 0) - wtx.references.forEachIndexed { internalIndex, it -> filter(it, ComponentGroupEnum.REFERENCES_GROUP.ordinal, internalIndex) } + wtx.inputs.forEachIndexed { internalIndex, it -> filter(it, INPUTS_GROUP.ordinal, internalIndex) } + wtx.outputs.forEachIndexed { internalIndex, it -> filter(it, OUTPUTS_GROUP.ordinal, internalIndex) } + wtx.commands.forEachIndexed { internalIndex, it -> filter(it, COMMANDS_GROUP.ordinal, internalIndex) } + wtx.attachments.forEachIndexed { internalIndex, it -> filter(it, ATTACHMENTS_GROUP.ordinal, internalIndex) } + if (wtx.notary != null) filter(wtx.notary, NOTARY_GROUP.ordinal, 0) + if (wtx.timeWindow != null) filter(wtx.timeWindow, TIMEWINDOW_GROUP.ordinal, 0) + wtx.references.forEachIndexed { internalIndex, it -> filter(it, REFERENCES_GROUP.ordinal, internalIndex) } // It is highlighted that because there is no a signers property in TraversableTransaction, // one cannot specifically filter them in or out. // The above is very important to ensure someone won't filter out the signers component group if at least one @@ -195,10 +206,17 @@ class FilteredTransaction internal constructor( // we decide to filter and attach this field to a FilteredTransaction. // An example would be to redact certain contract state types, but otherwise leave a transaction alone, // including the unknown new components. - wtx.componentGroups.filter { it.groupIndex >= ComponentGroupEnum.values().size }.forEach { componentGroup -> componentGroup.components.forEachIndexed { internalIndex, component -> filter(component, componentGroup.groupIndex, internalIndex) } } + wtx.componentGroups + .filter { it.groupIndex >= values().size } + .forEach { componentGroup -> componentGroup.components.forEachIndexed { internalIndex, component -> filter(component, componentGroup.groupIndex, internalIndex) } } } - fun createPartialMerkleTree(componentGroupIndex: Int) = PartialMerkleTree.build(MerkleTree.getMerkleTree(wtx.availableComponentHashes[componentGroupIndex]!!), filteredComponentHashes[componentGroupIndex]!!) + fun createPartialMerkleTree(componentGroupIndex: Int): PartialMerkleTree { + return PartialMerkleTree.build( + MerkleTree.getMerkleTree(wtx.availableComponentHashes[componentGroupIndex]!!), + filteredComponentHashes[componentGroupIndex]!! + ) + } fun createFilteredComponentGroups(): List { updateFilteredComponents() @@ -223,8 +241,11 @@ class FilteredTransaction internal constructor( @Throws(FilteredTransactionVerificationException::class) fun verify() { verificationCheck(groupHashes.isNotEmpty()) { "At least one component group hash is required" } - // Verify the top level Merkle tree (group hashes are its leaves, including allOnesHash for empty list or null components in WireTransaction). - verificationCheck(MerkleTree.getMerkleTree(groupHashes).hash == id) { "Top level Merkle tree cannot be verified against transaction's id" } + // Verify the top level Merkle tree (group hashes are its leaves, including allOnesHash for empty list or null + // components in WireTransaction). + verificationCheck(MerkleTree.getMerkleTree(groupHashes).hash == id) { + "Top level Merkle tree cannot be verified against transaction's id" + } // For completely blind verification (no components are included). if (filteredComponentGroups.isEmpty()) return @@ -233,8 +254,12 @@ class FilteredTransaction internal constructor( filteredComponentGroups.forEach { (groupIndex, components, nonces, groupPartialTree) -> verificationCheck(groupIndex < groupHashes.size) { "There is no matching component group hash for group $groupIndex" } val groupMerkleRoot = groupHashes[groupIndex] - verificationCheck(groupMerkleRoot == PartialMerkleTree.rootAndUsedHashes(groupPartialTree.root, mutableListOf())) { "Partial Merkle tree root and advertised full Merkle tree root for component group $groupIndex do not match" } - verificationCheck(groupPartialTree.verify(groupMerkleRoot, components.mapIndexed { index, component -> componentHash(nonces[index], component) })) { "Visible components in group $groupIndex cannot be verified against their partial Merkle tree" } + verificationCheck(groupMerkleRoot == PartialMerkleTree.rootAndUsedHashes(groupPartialTree.root, mutableListOf())) { + "Partial Merkle tree root and advertised full Merkle tree root for component group $groupIndex do not match" + } + verificationCheck(groupPartialTree.verify(groupMerkleRoot, components.mapIndexed { index, component -> componentHash(nonces[index], component) })) { + "Visible components in group $groupIndex cannot be verified against their partial Merkle tree" + } } } @@ -281,7 +306,9 @@ class FilteredTransaction internal constructor( val groupFullRoot = MerkleTree.getMerkleTree(group.components.mapIndexed { index, component -> componentHash(group.nonces[index], component) }).hash visibilityCheck(groupPartialRoot == groupFullRoot) { "Some components for group ${group.groupIndex} are not visible" } // Verify the top level Merkle tree from groupHashes. - visibilityCheck(MerkleTree.getMerkleTree(groupHashes).hash == id) { "Transaction is malformed. Top level Merkle tree cannot be verified against transaction's id" } + visibilityCheck(MerkleTree.getMerkleTree(groupHashes).hash == id) { + "Transaction is malformed. Top level Merkle tree cannot be verified against transaction's id" + } } } @@ -296,15 +323,17 @@ class FilteredTransaction internal constructor( */ @Throws(ComponentVisibilityException::class) fun checkCommandVisibility(publicKey: PublicKey) { - val commandSigners = componentGroups.firstOrNull { it.groupIndex == ComponentGroupEnum.SIGNERS_GROUP.ordinal } + val commandSigners = componentGroups.firstOrNull { it.groupIndex == SIGNERS_GROUP.ordinal } val expectedNumOfCommands = expectedNumOfCommands(publicKey, commandSigners) val receivedForThisKeyNumOfCommands = commands.filter { publicKey in it.signers }.size - visibilityCheck(expectedNumOfCommands == receivedForThisKeyNumOfCommands) { "$expectedNumOfCommands commands were expected, but received $receivedForThisKeyNumOfCommands" } + visibilityCheck(expectedNumOfCommands == receivedForThisKeyNumOfCommands) { + "$expectedNumOfCommands commands were expected, but received $receivedForThisKeyNumOfCommands" + } } // Function to return number of expected commands to sign. private fun expectedNumOfCommands(publicKey: PublicKey, commandSigners: ComponentGroup?): Int { - checkAllComponentsVisible(ComponentGroupEnum.SIGNERS_GROUP) + checkAllComponentsVisible(SIGNERS_GROUP) if (commandSigners == null) return 0 fun signersKeys (internalIndex: Int, opaqueBytes: OpaqueBytes): List { try { @@ -340,7 +369,10 @@ class FilteredTransaction internal constructor( */ @KeepForDJVM @CordaSerializable -data class FilteredComponentGroup(override val groupIndex: Int, override val components: List, val nonces: List, val partialMerkleTree: PartialMerkleTree) : ComponentGroup(groupIndex, components) { +data class FilteredComponentGroup(override val groupIndex: Int, + override val components: List, + val nonces: List, + val partialMerkleTree: PartialMerkleTree) : ComponentGroup(groupIndex, components) { init { check(components.size == nonces.size) { "Size of transaction components and nonces do not match" } } diff --git a/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt b/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt index 1d5ceb3f73..0407bd95bb 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt @@ -33,9 +33,10 @@ sealed class ByteSequence(private val _bytes: ByteArray, val offset: Int, val si fun open() = ByteArrayInputStream(_bytes, offset, size) /** - * Create a sub-sequence, that may be backed by a new byte array. + * Create a sub-sequence of this sequence. A copy of the underlying array may be made, if a subclass overrides + * [bytes] to do so, as [OpaqueBytes] does. * - * @param offset The offset within this sequence to start the new sequence. Note: not the offset within the backing array. + * @param offset The offset within this sequence to start the new sequence. Note: not the offset within the backing array. * @param size The size of the intended sub sequence. */ @Suppress("MemberVisibilityCanBePrivate") @@ -43,7 +44,7 @@ sealed class ByteSequence(private val _bytes: ByteArray, val offset: Int, val si require(offset >= 0) require(offset + size <= this.size) // Intentionally use bytes rather than _bytes, to mirror the copy-or-not behaviour of that property. - return if (offset == 0 && size == this.size) this else OpaqueBytesSubSequence(bytes, this.offset + offset, size) + return if (offset == 0 && size == this.size) this else of(bytes, this.offset + offset, size) } companion object { diff --git a/core/src/smoke-test/kotlin/net/corda/core/cordapp/CordappSmokeTest.kt b/core/src/smoke-test/kotlin/net/corda/core/cordapp/CordappSmokeTest.kt index 44195477d0..69eef61ffd 100644 --- a/core/src/smoke-test/kotlin/net/corda/core/cordapp/CordappSmokeTest.kt +++ b/core/src/smoke-test/kotlin/net/corda/core/cordapp/CordappSmokeTest.kt @@ -106,7 +106,7 @@ class CordappSmokeTest { class SendBackInitiatorFlowContext(private val otherPartySession: FlowSession) : FlowLogic() { @Suspendable override fun call() { - // An initiated flow calling getFlowContext on its initiator will get the context from the session-init + // An initiated flow calling getFlowInfo on its initiator will get the context from the session-init val sessionInitContext = otherPartySession.getCounterpartyFlowInfo() otherPartySession.send(sessionInitContext) } diff --git a/core/src/test/java/net/corda/core/flows/SerializationApiInJavaTest.java b/core/src/test/java/net/corda/core/flows/SerializationApiInJavaTest.java index 297adeff8f..55e66c1766 100644 --- a/core/src/test/java/net/corda/core/flows/SerializationApiInJavaTest.java +++ b/core/src/test/java/net/corda/core/flows/SerializationApiInJavaTest.java @@ -1,11 +1,14 @@ package net.corda.core.flows; +import net.corda.core.serialization.internal.CheckpointSerializationDefaults; +import net.corda.core.serialization.internal.CheckpointSerializationFactory; import net.corda.core.serialization.SerializationDefaults; import net.corda.core.serialization.SerializationFactory; import net.corda.testing.core.SerializationEnvironmentRule; import org.junit.Rule; import org.junit.Test; +import static net.corda.core.serialization.internal.CheckpointSerializationAPIKt.checkpointSerialize; import static net.corda.core.serialization.SerializationAPIKt.serialize; import static org.junit.Assert.assertNull; @@ -28,10 +31,13 @@ public class SerializationApiInJavaTest { public void enforceSerializationDefaultsApi() { SerializationDefaults defaults = SerializationDefaults.INSTANCE; SerializationFactory factory = defaults.getSERIALIZATION_FACTORY(); + + CheckpointSerializationDefaults checkpointDefaults = CheckpointSerializationDefaults.INSTANCE; + CheckpointSerializationFactory checkpointSerializationFactory = checkpointDefaults.getCHECKPOINT_SERIALIZATION_FACTORY(); serialize("hello", factory, defaults.getP2P_CONTEXT()); serialize("hello", factory, defaults.getRPC_SERVER_CONTEXT()); serialize("hello", factory, defaults.getRPC_CLIENT_CONTEXT()); serialize("hello", factory, defaults.getSTORAGE_CONTEXT()); - serialize("hello", factory, defaults.getCHECKPOINT_CONTEXT()); + checkpointSerialize("hello", checkpointSerializationFactory, checkpointDefaults.getCHECKPOINT_CONTEXT()); } } diff --git a/core/src/test/kotlin/net/corda/core/flows/WithReferencedStatesFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/WithReferencedStatesFlowTests.kt index e96eb9cd76..2263f85b39 100644 --- a/core/src/test/kotlin/net/corda/core/flows/WithReferencedStatesFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/WithReferencedStatesFlowTests.kt @@ -105,7 +105,7 @@ internal class UseRefState(val linearId: UniqueIdentifier) : FlowLogic(query).states.single() return subFlow(FinalityFlow( diff --git a/core/src/test/kotlin/net/corda/core/internal/CertRoleTests.kt b/core/src/test/kotlin/net/corda/core/internal/CertRoleTests.kt index 71fba5a51e..60f81927c8 100644 --- a/core/src/test/kotlin/net/corda/core/internal/CertRoleTests.kt +++ b/core/src/test/kotlin/net/corda/core/internal/CertRoleTests.kt @@ -8,7 +8,7 @@ import kotlin.test.assertFailsWith class CertRoleTests { @Test fun `should deserialize valid value`() { - val expected = CertRole.INTERMEDIATE_CA + val expected = CertRole.DOORMAN_CA val actual = CertRole.getInstance(ASN1Integer(1L)) assertEquals(expected, actual) } diff --git a/core/src/test/kotlin/net/corda/core/internal/cordapp/CordappInfoResolverTest.kt b/core/src/test/kotlin/net/corda/core/internal/cordapp/CordappInfoResolverTest.kt new file mode 100644 index 0000000000..2ae7403e1a --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/internal/cordapp/CordappInfoResolverTest.kt @@ -0,0 +1,42 @@ +package net.corda.core.internal.cordapp + +import org.junit.After +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals + +class CordappInfoResolverTest { + + @Before + @After + fun clearCordappInfoResolver() { + CordappInfoResolver.clear() + } + + @Test() + fun `The correct cordapp resolver is used after calling withCordappResolution`() { + val defaultTargetVersion = 222 + + CordappInfoResolver.register(listOf(javaClass.name), CordappImpl.Info("test", "test", "2", 3, defaultTargetVersion)) + assertEquals(defaultTargetVersion, returnCallingTargetVersion()) + + val expectedTargetVersion = 555 + CordappInfoResolver.withCordappInfoResolution( { CordappImpl.Info("foo", "bar", "1", 2, expectedTargetVersion) }) + { + val actualTargetVersion = returnCallingTargetVersion() + assertEquals(expectedTargetVersion, actualTargetVersion) + } + assertEquals(defaultTargetVersion, returnCallingTargetVersion()) + } + + @Test() + fun `When more than one cordapp is registered for the same class, the resolver returns null`() { + CordappInfoResolver.register(listOf(javaClass.name), CordappImpl.Info("test", "test", "2", 3, 222)) + CordappInfoResolver.register(listOf(javaClass.name), CordappImpl.Info("test1", "test1", "1", 2, 456)) + assertEquals(0, returnCallingTargetVersion()) + } + + private fun returnCallingTargetVersion(): Int { + return CordappInfoResolver.getCorDappInfo()?.targetPlatformVersion ?: 0 + } +} \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/utilities/KotlinUtilsTest.kt b/core/src/test/kotlin/net/corda/core/utilities/KotlinUtilsTest.kt index fefb890213..debb6307f0 100644 --- a/core/src/test/kotlin/net/corda/core/utilities/KotlinUtilsTest.kt +++ b/core/src/test/kotlin/net/corda/core/utilities/KotlinUtilsTest.kt @@ -3,9 +3,10 @@ package net.corda.core.utilities import com.esotericsoftware.kryo.KryoException import net.corda.core.crypto.random63BitValue import net.corda.core.serialization.* +import net.corda.core.serialization.internal.checkpointDeserialize +import net.corda.core.serialization.internal.checkpointSerialize import net.corda.node.serialization.kryo.KRYO_CHECKPOINT_CONTEXT -import net.corda.node.serialization.kryo.kryoMagic -import net.corda.serialization.internal.SerializationContextImpl +import net.corda.serialization.internal.CheckpointSerializationContextImpl import net.corda.testing.core.SerializationEnvironmentRule import org.assertj.core.api.Assertions.assertThat import org.junit.Rule @@ -24,12 +25,11 @@ class KotlinUtilsTest { @Rule val expectedEx: ExpectedException = ExpectedException.none() - private val KRYO_CHECKPOINT_NOWHITELIST_CONTEXT = SerializationContextImpl(kryoMagic, + private val KRYO_CHECKPOINT_NOWHITELIST_CONTEXT = CheckpointSerializationContextImpl( javaClass.classLoader, EmptyWhitelist, emptyMap(), true, - SerializationContext.UseCase.Checkpoint, null) @Test @@ -44,7 +44,7 @@ class KotlinUtilsTest { fun `checkpointing a transient property with non-capturing lambda`() { val original = NonCapturingTransientProperty() val originalVal = original.transientVal - val copy = original.serialize(context = KRYO_CHECKPOINT_CONTEXT).deserialize(context = KRYO_CHECKPOINT_CONTEXT) + val copy = original.checkpointSerialize(context = KRYO_CHECKPOINT_CONTEXT).checkpointDeserialize(context = KRYO_CHECKPOINT_CONTEXT) val copyVal = copy.transientVal assertThat(copyVal).isNotEqualTo(originalVal) assertThat(copy.transientVal).isEqualTo(copyVal) @@ -55,14 +55,14 @@ class KotlinUtilsTest { expectedEx.expect(KryoException::class.java) expectedEx.expectMessage("is not annotated or on the whitelist, so cannot be used in serialization") val original = NonCapturingTransientProperty() - original.serialize(context = KRYO_CHECKPOINT_CONTEXT).deserialize(context = KRYO_CHECKPOINT_NOWHITELIST_CONTEXT) + original.checkpointSerialize(context = KRYO_CHECKPOINT_CONTEXT).checkpointDeserialize(context = KRYO_CHECKPOINT_NOWHITELIST_CONTEXT) } @Test fun `checkpointing a transient property with capturing lambda`() { val original = CapturingTransientProperty("Hello") val originalVal = original.transientVal - val copy = original.serialize(context = KRYO_CHECKPOINT_CONTEXT).deserialize(context = KRYO_CHECKPOINT_CONTEXT) + val copy = original.checkpointSerialize(context = KRYO_CHECKPOINT_CONTEXT).checkpointDeserialize(context = KRYO_CHECKPOINT_CONTEXT) val copyVal = copy.transientVal assertThat(copyVal).isNotEqualTo(originalVal) assertThat(copy.transientVal).isEqualTo(copyVal) @@ -76,7 +76,7 @@ class KotlinUtilsTest { val original = CapturingTransientProperty("Hello") - original.serialize(context = KRYO_CHECKPOINT_CONTEXT).deserialize(context = KRYO_CHECKPOINT_NOWHITELIST_CONTEXT) + original.checkpointSerialize(context = KRYO_CHECKPOINT_CONTEXT).checkpointDeserialize(context = KRYO_CHECKPOINT_NOWHITELIST_CONTEXT) } private class NullTransientProperty { diff --git a/djvm/build.gradle b/djvm/build.gradle index 270bfc2375..db88e8c4c5 100644 --- a/djvm/build.gradle +++ b/djvm/build.gradle @@ -1,18 +1,35 @@ plugins { - id 'com.github.johnrengelman.shadow' version '2.0.4' + id 'com.github.johnrengelman.shadow' } +apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' +apply plugin: 'idea' + +description 'Corda deterministic JVM sandbox' ext { // Shaded version of ASM to avoid conflict with root project. - asm_version = '6.1.1' + asm_version = '6.2.1' +} + +repositories { + maven { + url "$artifactory_contextUrl/corda-dev" + } +} + +configurations { + testCompile.extendsFrom shadow + jdkRt.resolutionStrategy { + // Always check the repository for a newer SNAPSHOT. + cacheChangingModulesFor 0, 'seconds' + } } dependencies { - compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - compile "org.slf4j:jul-to-slf4j:$slf4j_version" - compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" - compile "com.jcabi:jcabi-manifests:$jcabi_manifests_version" + shadow "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + shadow "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + shadow "org.slf4j:slf4j-api:$slf4j_version" // ASM: byte code manipulation library compile "org.ow2.asm:asm:$asm_version" @@ -20,30 +37,40 @@ dependencies { compile "org.ow2.asm:asm-commons:$asm_version" // Classpath scanner - compile "io.github.lukehutch:fast-classpath-scanner:$fast_classpath_scanner_version" + shadow "io.github.lukehutch:fast-classpath-scanner:$fast_classpath_scanner_version" // Test utilities testCompile "junit:junit:$junit_version" testCompile "org.assertj:assertj-core:$assertj_version" + testCompile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" + jdkRt "net.corda:deterministic-rt:latest.integration" } jar.enabled = false shadowJar { - baseName = "djvm" - classifier = "" - dependencies { - exclude(dependency('com.jcabi:.*:.*')) - exclude(dependency('org.apache.*:.*:.*')) - exclude(dependency('org.jetbrains.*:.*:.*')) - exclude(dependency('org.slf4j:.*:.*')) - exclude(dependency('io.github.lukehutch:.*:.*')) - } + baseName 'corda-djvm' + classifier '' relocate 'org.objectweb.asm', 'djvm.org.objectweb.asm' - artifacts { - shadow(tasks.shadowJar.archivePath) { - builtBy shadowJar - } - } } assemble.dependsOn shadowJar + +tasks.withType(Test) { + systemProperty 'deterministic-rt.path', configurations.jdkRt.asPath +} + +artifacts { + publish shadowJar +} + +publish { + dependenciesFrom configurations.shadow + name shadowJar.baseName +} + +idea { + module { + downloadJavadoc = true + downloadSources = true + } +} diff --git a/djvm/cli/build.gradle b/djvm/cli/build.gradle index a3543bcc54..d72a4a74c0 100644 --- a/djvm/cli/build.gradle +++ b/djvm/cli/build.gradle @@ -15,12 +15,10 @@ configurations { dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - compile "org.slf4j:jul-to-slf4j:$slf4j_version" compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" compile "com.jcabi:jcabi-manifests:$jcabi_manifests_version" compile "info.picocli:picocli:$picocli_version" - compile "io.github.lukehutch:fast-classpath-scanner:$fast_classpath_scanner_version" compile project(path: ":djvm", configuration: "shadow") // Deterministic runtime - used in whitelist generation diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/BuildCommand.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/BuildCommand.kt index 298ebcb1ce..7fafd5d743 100644 --- a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/BuildCommand.kt +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/BuildCommand.kt @@ -1,8 +1,5 @@ package net.corda.djvm.tools.cli -import net.corda.djvm.tools.Utilities.createCodePath -import net.corda.djvm.tools.Utilities.getFileNames -import net.corda.djvm.tools.Utilities.jarPath import picocli.CommandLine.Command import picocli.CommandLine.Parameters import java.nio.file.Path diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/ClassCommand.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/ClassCommand.kt index e3538535d0..e22d9c084f 100644 --- a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/ClassCommand.kt +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/ClassCommand.kt @@ -7,9 +7,6 @@ import net.corda.djvm.execution.* import net.corda.djvm.references.ClassModule import net.corda.djvm.source.ClassSource import net.corda.djvm.source.SourceClassLoader -import net.corda.djvm.tools.Utilities.find -import net.corda.djvm.tools.Utilities.onEmpty -import net.corda.djvm.tools.Utilities.userClassPath import net.corda.djvm.utilities.Discovery import djvm.org.objectweb.asm.ClassReader import picocli.CommandLine.Option @@ -66,7 +63,7 @@ abstract class ClassCommand : CommandBase() { private lateinit var classLoader: ClassLoader - protected var executor = SandboxExecutor() + protected var executor = SandboxExecutor(SandboxConfiguration.DEFAULT) private var derivedWhitelist: Whitelist = Whitelist.MINIMAL @@ -117,7 +114,7 @@ abstract class ClassCommand : CommandBase() { } private fun findDiscoverableRunnables(filters: Array): List> { - val classes = find() + val classes = find>() val applicableFilters = filters .filter { !isJarFile(it) && !isFullClassName(it) } val filteredClasses = applicableFilters @@ -128,7 +125,7 @@ abstract class ClassCommand : CommandBase() { } if (applicableFilters.isNotEmpty() && filteredClasses.isEmpty()) { - throw Exception("Could not find any classes implementing ${SandboxedRunnable::class.java.simpleName} " + + throw Exception("Could not find any classes implementing ${java.util.function.Function::class.java.simpleName} " + "whose name matches '${applicableFilters.joinToString(" ")}'") } @@ -192,7 +189,7 @@ abstract class ClassCommand : CommandBase() { profile = profile, rules = if (ignoreRules) { emptyList() } else { Discovery.find() }, emitters = ignoreEmitters.emptyListIfTrueOtherwiseNull(), - definitionProviders = if(ignoreDefinitionProviders) { emptyList() } else { Discovery.find() }, + definitionProviders = if (ignoreDefinitionProviders) { emptyList() } else { Discovery.find() }, enableTracing = !disableTracing, analysisConfiguration = AnalysisConfiguration( whitelist = whitelist, diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/InspectionCommand.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/InspectionCommand.kt index f6d779ceb2..32ce08ec6e 100644 --- a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/InspectionCommand.kt +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/InspectionCommand.kt @@ -1,7 +1,6 @@ package net.corda.djvm.tools.cli import net.corda.djvm.source.ClassSource -import net.corda.djvm.tools.Utilities.createCodePath import picocli.CommandLine.Command import picocli.CommandLine.Parameters import java.nio.file.Files diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/NewCommand.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/NewCommand.kt index e2df2f4d0f..8e6302de4e 100644 --- a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/NewCommand.kt +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/NewCommand.kt @@ -1,9 +1,5 @@ package net.corda.djvm.tools.cli -import net.corda.djvm.tools.Utilities.baseName -import net.corda.djvm.tools.Utilities.createCodePath -import net.corda.djvm.tools.Utilities.getFiles -import net.corda.djvm.tools.Utilities.openOptions import picocli.CommandLine.* import java.nio.file.Files import java.nio.file.Path diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/RunCommand.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/RunCommand.kt index 3f4fd93108..62fab057ba 100644 --- a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/RunCommand.kt +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/RunCommand.kt @@ -1,6 +1,5 @@ package net.corda.djvm.tools.cli -import net.corda.djvm.execution.SandboxedRunnable import net.corda.djvm.source.ClassSource import picocli.CommandLine.Command import picocli.CommandLine.Parameters @@ -20,7 +19,7 @@ class RunCommand : ClassCommand() { var classes: Array = emptyArray() override fun processClasses(classes: List>) { - val interfaceName = SandboxedRunnable::class.java.simpleName + val interfaceName = java.util.function.Function::class.java.simpleName for (clazz in classes) { if (!clazz.interfaces.any { it.simpleName == interfaceName }) { printError("Class is not an instance of $interfaceName; ${clazz.name}") diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/TreeCommand.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/TreeCommand.kt index 47b24ffa44..26b5dbc7d8 100644 --- a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/TreeCommand.kt +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/TreeCommand.kt @@ -1,6 +1,5 @@ package net.corda.djvm.tools.cli -import net.corda.djvm.tools.Utilities.workingDirectory import picocli.CommandLine.Command import java.nio.file.Files diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/Utilities.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/Utilities.kt new file mode 100644 index 0000000000..66d5d4d918 --- /dev/null +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/Utilities.kt @@ -0,0 +1,104 @@ +@file:JvmName("Utilities") +package net.corda.djvm.tools.cli + +import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner +import java.lang.reflect.Modifier +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.StandardOpenOption + +/** + * Get the expanded file name of each path in the provided array. + */ +fun Array?.getFiles(map: (Path) -> Path = { it }) = (this ?: emptyArray()).map { + val pathString = it.toString() + val path = map(it) + when { + '/' in pathString || '\\' in pathString -> + throw Exception("Please provide a pathless file name") + pathString.endsWith(".java", true) -> path + else -> Paths.get("$path.java") + } +} + +/** + * Get the string representation of each expanded file name in the provided array. + */ +fun Array?.getFileNames(map: (Path) -> Path = { it }) = this.getFiles(map).map { + it.toString() +}.toTypedArray() + +/** + * Execute inlined action if the collection is empty. + */ +inline fun List.onEmpty(action: () -> Unit): List { + if (!this.any()) { + action() + } + return this +} + +/** + * Execute inlined action if the array is empty. + */ +inline fun Array?.onEmpty(action: () -> Unit): Array { + return (this ?: emptyArray()).toList().onEmpty(action).toTypedArray() +} + +/** + * Derive the set of [StandardOpenOption]'s to use for a file operation. + */ +fun openOptions(force: Boolean) = if (force) { + arrayOf(StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING) +} else { + arrayOf(StandardOpenOption.CREATE_NEW) +} + +/** + * Get the path of where any generated code will be placed. Create the directory if it does not exist. + */ +fun createCodePath(): Path { + return Paths.get("tmp", "net", "corda", "djvm").let { + Files.createDirectories(it) + } +} + +/** + * Return the base name of a file (i.e., its name without extension) + */ +val Path.baseName: String + get() = this.fileName.toString() + .replaceAfterLast('.', "") + .removeSuffix(".") + +/** + * The path of the executing JAR. + */ +val jarPath: String = object {}.javaClass.protectionDomain.codeSource.location.toURI().path + + +/** + * The path of the current working directory. + */ +val workingDirectory: Path = Paths.get(System.getProperty("user.dir")) + +/** + * The class path for the current execution context. + */ +val userClassPath: String = System.getProperty("java.class.path") + +/** + * Get a reference of each concrete class that implements interface or class [T]. + */ +inline fun find(scanSpec: String = "net/corda/djvm"): List> { + val references = mutableListOf>() + FastClasspathScanner(scanSpec) + .matchClassesImplementing(T::class.java) { clazz -> + if (!Modifier.isAbstract(clazz.modifiers) && !Modifier.isStatic(clazz.modifiers)) { + references.add(clazz) + } + } + .scan() + return references +} diff --git a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/WhitelistGenerateCommand.kt b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/WhitelistGenerateCommand.kt index 4882c9355f..b578dbffb3 100644 --- a/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/WhitelistGenerateCommand.kt +++ b/djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/WhitelistGenerateCommand.kt @@ -33,52 +33,53 @@ class WhitelistGenerateCommand : CommandBase() { override fun validateArguments() = paths.isNotEmpty() override fun handleCommand(): Boolean { - val entries = mutableListOf() - val visitor = object : ClassAndMemberVisitor() { - override fun visitClass(clazz: ClassRepresentation): ClassRepresentation { - entries.add(clazz.name) - return super.visitClass(clazz) - } + val entries = AnalysisConfiguration().use { configuration -> + val entries = mutableListOf() + val visitor = object : ClassAndMemberVisitor(configuration, null) { + override fun visitClass(clazz: ClassRepresentation): ClassRepresentation { + entries.add(clazz.name) + return super.visitClass(clazz) + } - override fun visitMethod(clazz: ClassRepresentation, method: Member): Member { - visitMember(clazz, method) - return super.visitMethod(clazz, method) - } + override fun visitMethod(clazz: ClassRepresentation, method: Member): Member { + visitMember(clazz, method) + return super.visitMethod(clazz, method) + } - override fun visitField(clazz: ClassRepresentation, field: Member): Member { - visitMember(clazz, field) - return super.visitField(clazz, field) - } + override fun visitField(clazz: ClassRepresentation, field: Member): Member { + visitMember(clazz, field) + return super.visitField(clazz, field) + } - private fun visitMember(clazz: ClassRepresentation, member: Member) { - entries.add("${clazz.name}.${member.memberName}:${member.signature}") + private fun visitMember(clazz: ClassRepresentation, member: Member) { + entries.add("${clazz.name}.${member.memberName}:${member.signature}") + } } + val context = AnalysisContext.fromConfiguration(configuration) + for (path in paths) { + ClassSource.fromPath(path).getStreamIterator().forEach { + visitor.analyze(it, context) + } + } + entries } - val context = AnalysisContext.fromConfiguration(AnalysisConfiguration(), emptyList()) - for (path in paths) { - ClassSource.fromPath(path).getStreamIterator().forEach { - visitor.analyze(it, context) - } - } - val output = output - if (output != null) { - Files.newOutputStream(output, StandardOpenOption.CREATE).use { - GZIPOutputStream(it).use { - PrintStream(it).use { - it.println(""" + output?.also { + Files.newOutputStream(it, StandardOpenOption.CREATE).use { out -> + GZIPOutputStream(out).use { gzip -> + PrintStream(gzip).use { pout -> + pout.println(""" |java/.* |javax/.* |jdk/.* + |com/sun/.* |sun/.* |--- """.trimMargin().trim()) - printEntries(it, entries) + printEntries(pout, entries) } } } - } else { - printEntries(System.out, entries) - } + } ?: printEntries(System.out, entries) return true } diff --git a/djvm/src/main/resources/log4j2.xml b/djvm/cli/src/main/resources/log4j2.xml similarity index 100% rename from djvm/src/main/resources/log4j2.xml rename to djvm/cli/src/main/resources/log4j2.xml diff --git a/djvm/src/main/kotlin/net/corda/djvm/SandboxRuntimeContext.kt b/djvm/src/main/kotlin/net/corda/djvm/SandboxRuntimeContext.kt index 49013b9ba3..d717c9074e 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/SandboxRuntimeContext.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/SandboxRuntimeContext.kt @@ -3,25 +3,20 @@ package net.corda.djvm import net.corda.djvm.analysis.AnalysisContext import net.corda.djvm.costing.RuntimeCostSummary import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.djvm.source.ClassSource /** * The context in which a sandboxed operation is run. * * @property configuration The configuration of the sandbox. - * @property inputClasses The classes passed in for analysis. */ -class SandboxRuntimeContext( - val configuration: SandboxConfiguration, - private val inputClasses: List -) { +class SandboxRuntimeContext(val configuration: SandboxConfiguration) { /** * The class loader to use inside the sandbox. */ val classLoader: SandboxClassLoader = SandboxClassLoader( configuration, - AnalysisContext.fromConfiguration(configuration.analysisConfiguration, inputClasses) + AnalysisContext.fromConfiguration(configuration.analysisConfiguration) ) /** @@ -35,7 +30,7 @@ class SandboxRuntimeContext( fun use(action: SandboxRuntimeContext.() -> Unit) { SandboxRuntimeContext.instance = this try { - this.action() + action(this) } finally { threadLocalContext.remove() } @@ -43,9 +38,7 @@ class SandboxRuntimeContext( companion object { - private val threadLocalContext = object : ThreadLocal() { - override fun initialValue(): SandboxRuntimeContext? = null - } + private val threadLocalContext = ThreadLocal() /** * When called from within a sandbox, this returns the context for the current sandbox thread. diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisConfiguration.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisConfiguration.kt index 4114aa32af..2a1e7d63cf 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisConfiguration.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisConfiguration.kt @@ -1,9 +1,15 @@ package net.corda.djvm.analysis +import net.corda.djvm.code.ruleViolationError +import net.corda.djvm.code.thresholdViolationError import net.corda.djvm.messages.Severity import net.corda.djvm.references.ClassModule import net.corda.djvm.references.MemberModule +import net.corda.djvm.source.BootstrapClassLoader +import net.corda.djvm.source.SourceClassLoader import sandbox.net.corda.djvm.costing.RuntimeCostAccounter +import java.io.Closeable +import java.io.IOException import java.nio.file.Path /** @@ -13,7 +19,8 @@ import java.nio.file.Path * @param additionalPinnedClasses Classes that have already been declared in the sandbox namespace and that should be * made available inside the sandboxed environment. * @property minimumSeverityLevel The minimum severity level to log and report. - * @property classPath The extended class path to use for the analysis. + * @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. @@ -24,32 +31,47 @@ class AnalysisConfiguration( val whitelist: Whitelist = Whitelist.MINIMAL, additionalPinnedClasses: Set = emptySet(), val minimumSeverityLevel: Severity = Severity.WARNING, - val classPath: List = emptyList(), + classPath: List = emptyList(), + bootstrapJar: Path? = null, val analyzeAnnotations: Boolean = false, val prefixFilters: List = emptyList(), val classModule: ClassModule = ClassModule(), val memberModule: MemberModule = MemberModule() -) { +) : Closeable { /** * Classes that have already been declared in the sandbox namespace and that should be made * available inside the sandboxed environment. */ - val pinnedClasses: Set = setOf(SANDBOXED_OBJECT, RUNTIME_COST_ACCOUNTER) + additionalPinnedClasses + val pinnedClasses: Set = setOf( + SANDBOXED_OBJECT, + RuntimeCostAccounter.TYPE_NAME, + ruleViolationError, + thresholdViolationError + ) + additionalPinnedClasses /** * Functionality used to resolve the qualified name and relevant information about a class. */ val classResolver: ClassResolver = ClassResolver(pinnedClasses, whitelist, 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() + } + } + companion object { /** * The package name prefix to use for classes loaded into a sandbox. */ private const val SANDBOX_PREFIX: String = "sandbox/" - private const val SANDBOXED_OBJECT = "sandbox/java/lang/Object" - private const val RUNTIME_COST_ACCOUNTER = RuntimeCostAccounter.TYPE_NAME + private const val SANDBOXED_OBJECT = SANDBOX_PREFIX + "java/lang/Object" } } diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisContext.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisContext.kt index c37beab616..3ea9b55b1b 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisContext.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisContext.kt @@ -1,10 +1,10 @@ package net.corda.djvm.analysis +import net.corda.djvm.code.asPackagePath import net.corda.djvm.messages.MessageCollection import net.corda.djvm.references.ClassHierarchy import net.corda.djvm.references.EntityReference import net.corda.djvm.references.ReferenceMap -import net.corda.djvm.source.ClassSource /** * The context in which one or more classes are analysed. @@ -13,13 +13,11 @@ import net.corda.djvm.source.ClassSource * @property classes List of class definitions that have been analyzed. * @property references A collection of all referenced members found during analysis together with the locations from * where each member has been accessed or invoked. - * @property inputClasses The classes passed in for analysis. */ class AnalysisContext private constructor( val messages: MessageCollection, val classes: ClassHierarchy, - val references: ReferenceMap, - val inputClasses: List + val references: ReferenceMap ) { private val origins = mutableMapOf>() @@ -28,7 +26,7 @@ class AnalysisContext private constructor( * Record a class origin in the current analysis context. */ fun recordClassOrigin(name: String, origin: EntityReference) { - origins.getOrPut(name.normalize()) { mutableSetOf() }.add(origin) + origins.getOrPut(name.asPackagePath) { mutableSetOf() }.add(origin) } /** @@ -42,20 +40,14 @@ class AnalysisContext private constructor( /** * Create a new analysis context from provided configuration. */ - fun fromConfiguration(configuration: AnalysisConfiguration, classes: List): AnalysisContext { + fun fromConfiguration(configuration: AnalysisConfiguration): AnalysisContext { return AnalysisContext( MessageCollection(configuration.minimumSeverityLevel, configuration.prefixFilters), ClassHierarchy(configuration.classModule, configuration.memberModule), - ReferenceMap(configuration.classModule), - classes + ReferenceMap(configuration.classModule) ) } - /** - * Local extension method for normalizing a class name. - */ - private fun String.normalize() = this.replace("/", ".") - } } \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitor.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitor.kt index 53a934cd80..d0d9cb4e8c 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitor.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitor.kt @@ -4,30 +4,25 @@ import net.corda.djvm.code.EmitterModule import net.corda.djvm.code.Instruction import net.corda.djvm.code.instructions.* import net.corda.djvm.messages.Message -import net.corda.djvm.references.ClassReference -import net.corda.djvm.references.ClassRepresentation -import net.corda.djvm.references.Member -import net.corda.djvm.references.MemberReference -import net.corda.djvm.source.SourceClassLoader +import net.corda.djvm.references.* import org.objectweb.asm.* import java.io.InputStream /** * Functionality for traversing a class and its members. * - * @property classVisitor Class visitor to use when traversing the structure of classes. * @property configuration The configuration to use for the analysis + * @property classVisitor Class visitor to use when traversing the structure of classes. */ open class ClassAndMemberVisitor( - private val classVisitor: ClassVisitor? = null, - private val configuration: AnalysisConfiguration = AnalysisConfiguration() + private val configuration: AnalysisConfiguration, + private val classVisitor: ClassVisitor? ) { /** * Holds a reference to the currently used analysis context. */ - protected var analysisContext: AnalysisContext = - AnalysisContext.fromConfiguration(configuration, emptyList()) + protected var analysisContext: AnalysisContext = AnalysisContext.fromConfiguration(configuration) /** * Holds a link to the class currently being traversed. @@ -44,12 +39,6 @@ open class ClassAndMemberVisitor( */ private var sourceLocation = SourceLocation() - /** - * The class loader used to find classes on the extended class path. - */ - private val supportingClassLoader = - SourceClassLoader(configuration.classPath, configuration.classResolver) - /** * Analyze class by using the provided qualified name of the class. */ @@ -63,7 +52,7 @@ open class ClassAndMemberVisitor( * @param origin The originating class for the analysis. */ fun analyze(className: String, context: AnalysisContext, origin: String? = null) { - supportingClassLoader.classReader(className, context, origin).apply { + configuration.supportingClassLoader.classReader(className, context, origin).apply { analyze(this, context) } } @@ -167,7 +156,8 @@ open class ClassAndMemberVisitor( } /** - * Run action with a guard that populates [messages] based on the output. + * Run action with a guard that populates [AnalysisRuntimeContext.messages] + * based on the output. */ private inline fun captureExceptions(action: () -> Unit): Boolean { return try { @@ -229,9 +219,7 @@ open class ClassAndMemberVisitor( ClassRepresentation(version, access, name, superClassName, interfaceNames, genericsDetails = signature ?: "").also { currentClass = it currentMember = null - sourceLocation = SourceLocation( - className = name - ) + sourceLocation = SourceLocation(className = name) } captureExceptions { currentClass = visitClass(currentClass!!) @@ -251,7 +239,7 @@ open class ClassAndMemberVisitor( override fun visitEnd() { configuration.classModule .getClassReferencesFromClass(currentClass!!, configuration.analyzeAnnotations) - .forEach { recordTypeReference(it) } + .forEach(::recordTypeReference) captureExceptions { visitClassEnd(currentClass!!) } @@ -306,14 +294,15 @@ open class ClassAndMemberVisitor( configuration.memberModule.addToClass(clazz, visitedMember ?: member) return if (processMember) { val derivedMember = visitedMember ?: member - val targetVisitor = super.visitMethod( - derivedMember.access, - derivedMember.memberName, - derivedMember.signature, - signature, - derivedMember.exceptions.toTypedArray() - ) - MethodVisitorImpl(targetVisitor) + super.visitMethod( + derivedMember.access, + derivedMember.memberName, + derivedMember.signature, + signature, + derivedMember.exceptions.toTypedArray() + )?.let { targetVisitor -> + MethodVisitorImpl(targetVisitor, derivedMember) + } } else { null } @@ -340,14 +329,15 @@ open class ClassAndMemberVisitor( configuration.memberModule.addToClass(clazz, visitedMember ?: member) return if (processMember) { val derivedMember = visitedMember ?: member - val targetVisitor = super.visitField( - derivedMember.access, - derivedMember.memberName, - derivedMember.signature, - signature, - derivedMember.value - ) - FieldVisitorImpl(targetVisitor) + super.visitField( + derivedMember.access, + derivedMember.memberName, + derivedMember.signature, + signature, + derivedMember.value + )?.let { targetVisitor -> + FieldVisitorImpl(targetVisitor) + } } else { null } @@ -359,7 +349,8 @@ open class ClassAndMemberVisitor( * Visitor used to traverse and analyze a method. */ private inner class MethodVisitorImpl( - targetVisitor: MethodVisitor? + targetVisitor: MethodVisitor, + private val method: Member ) : MethodVisitor(API_VERSION, targetVisitor) { /** @@ -387,6 +378,16 @@ open class ClassAndMemberVisitor( return super.visitAnnotation(desc, visible) } + /** + * Write any new method body code, assuming the definition providers + * have provided any. This handler will not be visited if this method + * has no existing code. + */ + override fun visitCode() { + tryReplaceMethodBody() + super.visitCode() + } + /** * Extract information about provided field access instruction. */ @@ -493,6 +494,29 @@ open class ClassAndMemberVisitor( } } + /** + * Finish visiting this method, writing any new method body byte-code + * if we haven't written it already. This would (presumably) only happen + * for methods that previously had no body, e.g. native methods. + */ + override fun visitEnd() { + tryReplaceMethodBody() + super.visitEnd() + } + + private fun tryReplaceMethodBody() { + if (method.body.isNotEmpty() && (mv != null)) { + EmitterModule(mv).apply { + for (body in method.body) { + body(this) + } + } + mv.visitMaxs(-1, -1) + mv.visitEnd() + mv = null + } + } + /** * Helper function used to streamline the access to an instruction and to catch any related processing errors. */ @@ -517,7 +541,7 @@ open class ClassAndMemberVisitor( * Visitor used to traverse and analyze a field. */ private inner class FieldVisitorImpl( - targetVisitor: FieldVisitor? + targetVisitor: FieldVisitor ) : FieldVisitor(API_VERSION, targetVisitor) { /** diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassResolver.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassResolver.kt index e2d23b3f70..b1aa3ae541 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassResolver.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassResolver.kt @@ -1,5 +1,8 @@ package net.corda.djvm.analysis +import net.corda.djvm.code.asPackagePath +import net.corda.djvm.code.asResourcePath + /** * Functionality for resolving the class name of a sandboxable class. * @@ -32,12 +35,12 @@ class ClassResolver( */ fun resolve(name: String): String { return when { - name.startsWith("[") && name.endsWith(";") -> { + name.startsWith('[') && name.endsWith(';') -> { complexArrayTypeRegex.replace(name) { "${it.groupValues[1]}L${resolveName(it.groupValues[2])};" } } - name.startsWith("[") && !name.endsWith(";") -> name + name.startsWith('[') && !name.endsWith(';') -> name else -> resolveName(name) } } @@ -46,7 +49,7 @@ class ClassResolver( * Resolve the class name from a fully qualified normalized name. */ fun resolveNormalized(name: String): String { - return resolve(name.replace('.', '/')).replace('/', '.') + return resolve(name.asResourcePath).asPackagePath } /** @@ -96,7 +99,7 @@ class ClassResolver( * Reverse the resolution of a class name from a fully qualified normalized name. */ fun reverseNormalized(name: String): String { - return reverse(name.replace('.', '/')).replace('/', '.') + return reverse(name.asResourcePath).asPackagePath } /** diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/Whitelist.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/Whitelist.kt index 95d8c2ff39..3cbbfe8223 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/analysis/Whitelist.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/Whitelist.kt @@ -117,7 +117,8 @@ open class Whitelist private constructor( "^java/lang/Throwable(\\..*)?$".toRegex(), "^java/lang/Void(\\..*)?$".toRegex(), "^java/lang/.*Error(\\..*)?$".toRegex(), - "^java/lang/.*Exception(\\..*)?$".toRegex() + "^java/lang/.*Exception(\\..*)?$".toRegex(), + "^java/lang/reflect/Array(\\..*)?$".toRegex() ) /** diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/ClassMutator.kt b/djvm/src/main/kotlin/net/corda/djvm/code/ClassMutator.kt index b8d2fa8a93..777e69f9fe 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/code/ClassMutator.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/code/ClassMutator.kt @@ -20,7 +20,7 @@ class ClassMutator( private val configuration: AnalysisConfiguration, private val definitionProviders: List = emptyList(), private val emitters: List = emptyList() -) : ClassAndMemberVisitor(classVisitor, configuration = configuration) { +) : ClassAndMemberVisitor(configuration, classVisitor) { /** * Tracks whether any modifications have been applied to any of the processed class(es) and pertinent members. @@ -82,7 +82,8 @@ class ClassMutator( */ override fun visitInstruction(method: Member, emitter: EmitterModule, instruction: Instruction) { val context = EmitterContext(currentAnalysisContext(), configuration, emitter) - Processor.processEntriesOfType(emitters, analysisContext.messages) { + // We need to apply the tracing emitters before the non-tracing ones. + Processor.processEntriesOfType(emitters.sortedByDescending(Emitter::isTracer), analysisContext.messages) { it.emit(context, instruction) } if (!emitter.emitDefaultInstruction || emitter.hasEmittedCustomCode) { diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/Emitter.kt b/djvm/src/main/kotlin/net/corda/djvm/code/Emitter.kt index 7d953a28e1..f904d276b7 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/code/Emitter.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/code/Emitter.kt @@ -20,6 +20,7 @@ interface Emitter { /** * Indication of whether or not the emitter performs instrumentation for tracing inside the sandbox. */ + @JvmDefault val isTracer: Boolean get() = false diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/EmitterModule.kt b/djvm/src/main/kotlin/net/corda/djvm/code/EmitterModule.kt index 8d0f25bd02..afe9b5165d 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/code/EmitterModule.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/code/EmitterModule.kt @@ -1,7 +1,9 @@ package net.corda.djvm.code +import org.objectweb.asm.Label import org.objectweb.asm.MethodVisitor -import org.objectweb.asm.Opcodes +import org.objectweb.asm.Opcodes.* +import org.objectweb.asm.Type import sandbox.net.corda.djvm.costing.RuntimeCostAccounter /** @@ -29,7 +31,7 @@ class EmitterModule( /** * Emit instruction for creating a new object of type [typeName]. */ - fun new(typeName: String, opcode: Int = Opcodes.NEW) { + fun new(typeName: String, opcode: Int = NEW) { hasEmittedCustomCode = true methodVisitor.visitTypeInsn(opcode, typeName) } @@ -38,7 +40,7 @@ class EmitterModule( * Emit instruction for creating a new object of type [T]. */ inline fun new() { - new(T::class.java.name) + new(Type.getInternalName(T::class.java)) } /** @@ -62,7 +64,7 @@ class EmitterModule( */ fun invokeStatic(owner: String, name: String, descriptor: String, isInterface: Boolean = false) { hasEmittedCustomCode = true - methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, owner, name, descriptor, isInterface) + methodVisitor.visitMethodInsn(INVOKESTATIC, owner, name, descriptor, isInterface) } /** @@ -70,14 +72,14 @@ class EmitterModule( */ fun invokeSpecial(owner: String, name: String, descriptor: String, isInterface: Boolean = false) { hasEmittedCustomCode = true - methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, owner, name, descriptor, isInterface) + methodVisitor.visitMethodInsn(INVOKESPECIAL, owner, name, descriptor, isInterface) } /** * Emit instruction for invoking a special method on class [T], e.g. a constructor or a method on a super-type. */ inline fun invokeSpecial(name: String, descriptor: String, isInterface: Boolean = false) { - invokeSpecial(T::class.java.name, name, descriptor, isInterface) + invokeSpecial(Type.getInternalName(T::class.java), name, descriptor, isInterface) } /** @@ -85,7 +87,7 @@ class EmitterModule( */ fun pop() { hasEmittedCustomCode = true - methodVisitor.visitInsn(Opcodes.POP) + methodVisitor.visitInsn(POP) } /** @@ -93,19 +95,40 @@ class EmitterModule( */ fun duplicate() { hasEmittedCustomCode = true - methodVisitor.visitInsn(Opcodes.DUP) + methodVisitor.visitInsn(DUP) } /** * Emit a sequence of instructions for instantiating and throwing an exception based on the provided message. */ - fun throwError(message: String) { + fun throwException(exceptionType: Class, message: String) { hasEmittedCustomCode = true - new() - methodVisitor.visitInsn(Opcodes.DUP) + val exceptionName = Type.getInternalName(exceptionType) + new(exceptionName) + methodVisitor.visitInsn(DUP) methodVisitor.visitLdcInsn(message) - invokeSpecial("", "(Ljava/lang/String;)V") - methodVisitor.visitInsn(Opcodes.ATHROW) + invokeSpecial(exceptionName, "", "(Ljava/lang/String;)V") + methodVisitor.visitInsn(ATHROW) + } + + inline fun throwException(message: String) = throwException(T::class.java, message) + + /** + * Emit instruction for returning from "void" method. + */ + fun returnVoid() { + methodVisitor.visitInsn(RETURN) + hasEmittedCustomCode = true + } + + /** + * Emit instructions for a new line number. + */ + fun lineNumber(line: Int) { + val label = Label() + methodVisitor.visitLabel(label) + methodVisitor.visitLineNumber(line, label) + hasEmittedCustomCode = true } /** diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/Types.kt b/djvm/src/main/kotlin/net/corda/djvm/code/Types.kt new file mode 100644 index 0000000000..e137f196d5 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/code/Types.kt @@ -0,0 +1,15 @@ +@file:JvmName("Types") +package net.corda.djvm.code + +import org.objectweb.asm.Type +import sandbox.net.corda.djvm.costing.ThresholdViolationError +import sandbox.net.corda.djvm.rules.RuleViolationError + +val ruleViolationError: String = Type.getInternalName(RuleViolationError::class.java) +val thresholdViolationError: String = Type.getInternalName(ThresholdViolationError::class.java) + +/** + * Local extension method for normalizing a class name. + */ +val String.asPackagePath: String get() = this.replace('/', '.') +val String.asResourcePath: String get() = this.replace('.', '/') \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/costing/TypedRuntimeCost.kt b/djvm/src/main/kotlin/net/corda/djvm/costing/TypedRuntimeCost.kt index e184ebe674..b3ca604701 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/costing/TypedRuntimeCost.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/costing/TypedRuntimeCost.kt @@ -1,6 +1,7 @@ package net.corda.djvm.costing import net.corda.djvm.utilities.loggerFor +import sandbox.net.corda.djvm.costing.ThresholdViolationError /** * Cost metric to be used in a sandbox environment. The metric has a threshold and a mechanism for reporting violations. @@ -41,7 +42,7 @@ open class TypedRuntimeCost( if (thresholdPredicate(newValue)) { val message = errorMessage(currentThread) logger.error("Threshold breached; {}", message) - throw ThresholdViolationException(message) + throw ThresholdViolationError(message) } } diff --git a/djvm/src/main/kotlin/net/corda/djvm/execution/DeterministicSandboxExecutor.kt b/djvm/src/main/kotlin/net/corda/djvm/execution/DeterministicSandboxExecutor.kt index 305dab489c..28e7b7226d 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/execution/DeterministicSandboxExecutor.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/execution/DeterministicSandboxExecutor.kt @@ -2,6 +2,7 @@ package net.corda.djvm.execution import net.corda.djvm.SandboxConfiguration import net.corda.djvm.source.ClassSource +import java.util.function.Function /** * The executor is responsible for spinning up a deterministic, sandboxed environment and launching the referenced code @@ -12,14 +13,14 @@ import net.corda.djvm.source.ClassSource * @param configuration The configuration of the sandbox. */ class DeterministicSandboxExecutor( - configuration: SandboxConfiguration = SandboxConfiguration.DEFAULT + configuration: SandboxConfiguration ) : SandboxExecutor(configuration) { /** - * Short-hand for running a [SandboxedRunnable] in a sandbox by its type reference. + * Short-hand for running a [Function] in a sandbox by its type reference. */ - inline fun > run(input: TInput): - ExecutionSummaryWithResult { + inline fun > run(input: TInput): + ExecutionSummaryWithResult { return run(ClassSource.fromClassName(TRunnable::class.java.name), input) } diff --git a/djvm/src/main/kotlin/net/corda/djvm/execution/DiscoverableRunnable.kt b/djvm/src/main/kotlin/net/corda/djvm/execution/DiscoverableRunnable.kt deleted file mode 100644 index 7447d10bb4..0000000000 --- a/djvm/src/main/kotlin/net/corda/djvm/execution/DiscoverableRunnable.kt +++ /dev/null @@ -1,6 +0,0 @@ -package net.corda.djvm.execution - -/** - * Functionality runnable by a sandbox executor, marked for discoverability. - */ -interface DiscoverableRunnable \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/execution/ExecutionProfile.kt b/djvm/src/main/kotlin/net/corda/djvm/execution/ExecutionProfile.kt index a9273d44b8..409455bc46 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/execution/ExecutionProfile.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/execution/ExecutionProfile.kt @@ -1,7 +1,7 @@ package net.corda.djvm.execution /** - * The execution profile of a [SandboxedRunnable] when run in a sandbox. + * The execution profile of a [java.util.function.Function] when run in a sandbox. * * @property allocationCostThreshold The threshold placed on allocations. * @property invocationCostThreshold The threshold placed on invocations. diff --git a/djvm/src/main/kotlin/net/corda/djvm/execution/ExecutionSummary.kt b/djvm/src/main/kotlin/net/corda/djvm/execution/ExecutionSummary.kt index 95ed43700f..651440731e 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/execution/ExecutionSummary.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/execution/ExecutionSummary.kt @@ -1,9 +1,9 @@ package net.corda.djvm.execution /** - * The summary of the execution of a [SandboxedRunnable] in a sandbox. This class has no representation of the outcome, - * and is typically used when there has been a pre-mature exit from the sandbox, for instance, if an exception was - * thrown. + * The summary of the execution of a [java.util.function.Function] in a sandbox. This class has no representation of the + * outcome, and is typically used when there has been a pre-mature exit from the sandbox, for instance, if an exception + * was thrown. * * @property costs The costs accumulated when running the sandboxed code. */ diff --git a/djvm/src/main/kotlin/net/corda/djvm/execution/ExecutionSummaryWithResult.kt b/djvm/src/main/kotlin/net/corda/djvm/execution/ExecutionSummaryWithResult.kt index 509c12c702..c32f7c7df0 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/execution/ExecutionSummaryWithResult.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/execution/ExecutionSummaryWithResult.kt @@ -1,7 +1,7 @@ package net.corda.djvm.execution /** - * The summary of the execution of a [SandboxedRunnable] in a sandbox. + * The summary of the execution of a [java.util.function.Function] in a sandbox. * * @property result The outcome of the sandboxed operation. * @see ExecutionSummary diff --git a/djvm/src/main/kotlin/net/corda/djvm/execution/IsolatedTask.kt b/djvm/src/main/kotlin/net/corda/djvm/execution/IsolatedTask.kt index 6d1a9d7521..7d2ae05153 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/execution/IsolatedTask.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/execution/IsolatedTask.kt @@ -2,7 +2,6 @@ package net.corda.djvm.execution import net.corda.djvm.SandboxConfiguration import net.corda.djvm.SandboxRuntimeContext -import net.corda.djvm.analysis.AnalysisContext import net.corda.djvm.messages.MessageCollection import net.corda.djvm.rewiring.SandboxClassLoader import net.corda.djvm.rewiring.SandboxClassLoadingException @@ -16,8 +15,7 @@ import kotlin.concurrent.thread */ class IsolatedTask( private val identifier: String, - private val configuration: SandboxConfiguration, - private val context: AnalysisContext + private val configuration: SandboxConfiguration ) { /** @@ -32,12 +30,12 @@ class IsolatedTask( var exception: Throwable? = null thread(name = threadName, isDaemon = true) { logger.trace("Entering isolated runtime environment...") - SandboxRuntimeContext(configuration, context.inputClasses).use { + SandboxRuntimeContext(configuration).use { output = try { action(runnable) } catch (ex: Throwable) { logger.error("Exception caught in isolated runtime environment", ex) - exception = ex + exception = (ex as? LinkageError)?.cause ?: ex null } costs = CostSummary( @@ -84,7 +82,7 @@ class IsolatedTask( ) /** - * The class loader to use for loading the [SandboxedRunnable] and any referenced code in [SandboxExecutor.run]. + * The class loader to use for loading the [java.util.function.Function] and any referenced code in [SandboxExecutor.run]. */ val classLoader: SandboxClassLoader get() = SandboxRuntimeContext.instance.classLoader diff --git a/djvm/src/main/kotlin/net/corda/djvm/execution/SandboxExecutor.kt b/djvm/src/main/kotlin/net/corda/djvm/execution/SandboxExecutor.kt index b671348370..b69585538f 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/execution/SandboxExecutor.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/execution/SandboxExecutor.kt @@ -11,7 +11,6 @@ import net.corda.djvm.rewiring.SandboxClassLoadingException import net.corda.djvm.source.ClassSource import net.corda.djvm.utilities.loggerFor import net.corda.djvm.validation.ReferenceValidationSummary -import net.corda.djvm.validation.ReferenceValidator import java.lang.reflect.InvocationTargetException /** @@ -22,7 +21,7 @@ import java.lang.reflect.InvocationTargetException * @property configuration The configuration of sandbox. */ open class SandboxExecutor( - protected val configuration: SandboxConfiguration = SandboxConfiguration.DEFAULT + protected val configuration: SandboxConfiguration ) { private val classModule = configuration.analysisConfiguration.classModule @@ -32,12 +31,7 @@ open class SandboxExecutor( private val whitelist = configuration.analysisConfiguration.whitelist /** - * Module used to validate all traversable references before instantiating and executing a [SandboxedRunnable]. - */ - private val referenceValidator = ReferenceValidator(configuration.analysisConfiguration) - - /** - * Executes a [SandboxedRunnable] implementation. + * Executes a [java.util.function.Function] implementation. * * @param runnableClass The entry point of the sandboxed code to run. * @param input The input to provide to the sandboxed environment. @@ -50,7 +44,7 @@ open class SandboxExecutor( open fun run( runnableClass: ClassSource, input: TInput - ): ExecutionSummaryWithResult { + ): ExecutionSummaryWithResult { // 1. We first do a breath first traversal of the class hierarchy, starting from the requested class. // The branching is defined by class references from referencesFromLocation. // 2. For each class we run validation against defined rules. @@ -63,22 +57,22 @@ open class SandboxExecutor( // 6. For execution, we then load the top-level class, implementing the SandboxedRunnable interface, again and // and consequently hit the cache. Once loaded, we can execute the code on the spawned thread, i.e., in an // isolated environment. - logger.trace("Executing {} with input {}...", runnableClass, input) + logger.debug("Executing {} with input {}...", runnableClass, input) // TODO Class sources can be analyzed in parallel, although this require making the analysis context thread-safe // To do so, one could start by batching the first X classes from the class sources and analyse each one in // parallel, caching any intermediate state and subsequently process enqueued sources in parallel batches as well. // Note that this would require some rework of the [IsolatedTask] and the class loader to bypass the limitation // of caching and state preserved in thread-local contexts. val classSources = listOf(runnableClass) - val context = AnalysisContext.fromConfiguration(configuration.analysisConfiguration, classSources) - val result = IsolatedTask(runnableClass.qualifiedClassName, configuration, context).run { + val context = AnalysisContext.fromConfiguration(configuration.analysisConfiguration) + val result = IsolatedTask(runnableClass.qualifiedClassName, configuration).run { validate(context, classLoader, classSources) val loadedClass = classLoader.loadClassAndBytes(runnableClass, context) val instance = loadedClass.type.newInstance() - val method = loadedClass.type.getMethod("run", Any::class.java) + val method = loadedClass.type.getMethod("apply", Any::class.java) try { @Suppress("UNCHECKED_CAST") - method.invoke(instance, input) as? TOutput? + method.invoke(instance, input) as? TOutput } catch (ex: InvocationTargetException) { throw ex.targetException } @@ -105,8 +99,8 @@ open class SandboxExecutor( * @return A [LoadedClass] with the class' byte code, type and name. */ fun load(classSource: ClassSource): LoadedClass { - val context = AnalysisContext.fromConfiguration(configuration.analysisConfiguration, listOf(classSource)) - val result = IsolatedTask("LoadClass", configuration, context).run { + val context = AnalysisContext.fromConfiguration(configuration.analysisConfiguration) + val result = IsolatedTask("LoadClass", configuration).run { classLoader.loadClassAndBytes(classSource, context) } return result.output ?: throw ClassNotFoundException(classSource.qualifiedClassName) @@ -125,8 +119,8 @@ open class SandboxExecutor( @Throws(SandboxClassLoadingException::class) fun validate(vararg classSources: ClassSource): ReferenceValidationSummary { logger.trace("Validating {}...", classSources) - val context = AnalysisContext.fromConfiguration(configuration.analysisConfiguration, classSources.toList()) - val result = IsolatedTask("Validation", configuration, context).run { + val context = AnalysisContext.fromConfiguration(configuration.analysisConfiguration) + val result = IsolatedTask("Validation", configuration).run { validate(context, classLoader, classSources.toList()) } logger.trace("Validation of {} resulted in {}", classSources, result) @@ -172,10 +166,6 @@ open class SandboxExecutor( } failOnReportedErrorsInContext(context) - // Validate all references in class hierarchy before proceeding. - referenceValidator.validate(context, classLoader.analyzer) - failOnReportedErrorsInContext(context) - return ReferenceValidationSummary(context.classes, context.messages, context.classOrigins) } @@ -185,7 +175,7 @@ open class SandboxExecutor( private inline fun processClassQueue( vararg elements: ClassSource, action: QueueProcessor.(ClassSource, String) -> Unit ) { - QueueProcessor({ it.qualifiedClassName }, *elements).process { classSource -> + QueueProcessor(ClassSource::qualifiedClassName, *elements).process { classSource -> val className = classResolver.reverse(classModule.getBinaryClassName(classSource.qualifiedClassName)) if (!whitelist.matches(className)) { action(classSource, className) diff --git a/djvm/src/main/kotlin/net/corda/djvm/execution/SandboxedRunnable.kt b/djvm/src/main/kotlin/net/corda/djvm/execution/SandboxedRunnable.kt deleted file mode 100644 index d3cbcde31b..0000000000 --- a/djvm/src/main/kotlin/net/corda/djvm/execution/SandboxedRunnable.kt +++ /dev/null @@ -1,19 +0,0 @@ -package net.corda.djvm.execution - -/** - * Functionality runnable by a sandbox executor. - */ -interface SandboxedRunnable : DiscoverableRunnable { - - /** - * The entry point of the sandboxed functionality to be run. - * - * @param input The input to pass in to the entry point. - * - * @returns The output to pass back to the caller after the sandboxed code has finished running. - * @throws Exception The function can throw an exception, in which case the exception gets passed to the caller. - */ - @Throws(Exception::class) - fun run(input: TInput): TOutput? - -} diff --git a/djvm/src/main/kotlin/net/corda/djvm/formatting/MemberFormatter.kt b/djvm/src/main/kotlin/net/corda/djvm/formatting/MemberFormatter.kt index c03f81006f..090b6acbdf 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/formatting/MemberFormatter.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/formatting/MemberFormatter.kt @@ -53,7 +53,7 @@ class MemberFormatter( * Check whether or not a signature is for a method. */ fun isMethod(abbreviatedSignature: String): Boolean { - return abbreviatedSignature.startsWith("(") + return abbreviatedSignature.startsWith('(') } /** diff --git a/djvm/src/main/kotlin/net/corda/djvm/references/ClassHierarchy.kt b/djvm/src/main/kotlin/net/corda/djvm/references/ClassHierarchy.kt index 7929a97eb7..a68c40b4b3 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/references/ClassHierarchy.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/references/ClassHierarchy.kt @@ -82,8 +82,8 @@ class ClassHierarchy( return findAncestors(get(className)).plus(get(OBJECT_NAME)) .asSequence() .filterNotNull() - .map { memberModule.getFromClass(it, memberName, signature) } - .firstOrNull { it != null } + .mapNotNull { memberModule.getFromClass(it, memberName, signature) } + .firstOrNull() .apply { logger.trace("Getting rooted member for {}.{}:{} yields {}", className, memberName, signature, this) } diff --git a/djvm/src/main/kotlin/net/corda/djvm/references/ClassModule.kt b/djvm/src/main/kotlin/net/corda/djvm/references/ClassModule.kt index f3fa1bf62b..62673d3780 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/references/ClassModule.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/references/ClassModule.kt @@ -1,5 +1,8 @@ package net.corda.djvm.references +import net.corda.djvm.code.asPackagePath +import net.corda.djvm.code.asResourcePath + /** * Class-specific functionality. */ @@ -42,14 +45,12 @@ class ClassModule : AnnotationModule() { /** * Get the binary version of a class name. */ - fun getBinaryClassName(name: String) = - normalizeClassName(name).replace('.', '/') + fun getBinaryClassName(name: String) = normalizeClassName(name).asResourcePath /** * Get the formatted version of a class name. */ - fun getFormattedClassName(name: String) = - normalizeClassName(name).replace('/', '.') + fun getFormattedClassName(name: String) = normalizeClassName(name).asPackagePath /** * Get the short name of a class. diff --git a/djvm/src/main/kotlin/net/corda/djvm/references/Member.kt b/djvm/src/main/kotlin/net/corda/djvm/references/Member.kt index 14bc723a4c..56a4b78982 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/references/Member.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/references/Member.kt @@ -1,5 +1,13 @@ package net.corda.djvm.references +import net.corda.djvm.code.EmitterModule + +/** + * Alias for a handler which will replace an entire + * method body with a block of byte-code. + */ +typealias MethodBody = (EmitterModule) -> Unit + /** * Representation of a class member. * @@ -11,6 +19,7 @@ package net.corda.djvm.references * @property annotations The names of the annotations the member is attributed. * @property exceptions The names of the exceptions that the member can throw. * @property value The default value of a field. + * @property body One or more handlers to replace the method body with new byte-code. */ data class Member( override val access: Int, @@ -20,5 +29,6 @@ data class Member( val genericsDetails: String, val annotations: MutableSet = mutableSetOf(), val exceptions: MutableSet = mutableSetOf(), - val value: Any? = null + val value: Any? = null, + val body: List = emptyList() ) : MemberInformation, EntityWithAccessFlag diff --git a/djvm/src/main/kotlin/net/corda/djvm/references/MemberInformation.kt b/djvm/src/main/kotlin/net/corda/djvm/references/MemberInformation.kt index f890fe7c89..09ccb21836 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/references/MemberInformation.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/references/MemberInformation.kt @@ -12,5 +12,6 @@ interface MemberInformation { val className: String val memberName: String val signature: String + @JvmDefault val reference: String get() = "$className.$memberName:$signature" } diff --git a/djvm/src/main/kotlin/net/corda/djvm/references/MemberModule.kt b/djvm/src/main/kotlin/net/corda/djvm/references/MemberModule.kt index 88ad3880a7..952d787656 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/references/MemberModule.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/references/MemberModule.kt @@ -33,14 +33,14 @@ class MemberModule : AnnotationModule() { * Check if member is a field. */ fun isField(member: MemberInformation): Boolean { - return !member.signature.startsWith("(") + return !member.signature.startsWith('(') } /** * Check if member is a method. */ fun isMethod(member: MemberInformation): Boolean { - return member.signature.startsWith("(") + return member.signature.startsWith('(') } /** diff --git a/djvm/src/main/kotlin/net/corda/djvm/references/ReferenceMap.kt b/djvm/src/main/kotlin/net/corda/djvm/references/ReferenceMap.kt index a84b816775..995bd6f84f 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/references/ReferenceMap.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/references/ReferenceMap.kt @@ -16,7 +16,11 @@ class ReferenceMap( private val referencesPerLocation: MutableMap> = hashMapOf() - private var numberOfReferences = 0 + /** + * The number of references in the map. + */ + var numberOfReferences = 0 + private set /** * Add source location association to a target member. @@ -50,12 +54,6 @@ class ReferenceMap( return referencesPerLocation.getOrElse(key(className, memberName, signature)) { emptySet() } } - /** - * The number of member references in the map. - */ - val size: Int - get() = numberOfReferences - /** * Get iterator for all the references in the map. */ diff --git a/djvm/src/main/kotlin/net/corda/djvm/rewiring/ClassRewriter.kt b/djvm/src/main/kotlin/net/corda/djvm/rewiring/ClassRewriter.kt index 9fe6d83e59..473718512a 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rewiring/ClassRewriter.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rewiring/ClassRewriter.kt @@ -27,7 +27,7 @@ open class ClassRewriter( * @param context The context in which the class is being analyzed and processed. */ fun rewrite(reader: ClassReader, context: AnalysisContext): ByteCode { - logger.trace("Rewriting class {}...", reader.className) + logger.debug("Rewriting class {}...", reader.className) val writer = SandboxClassWriter(reader, classLoader) val classRemapper = ClassRemapper(writer, remapper) val visitor = ClassMutator( diff --git a/djvm/src/main/kotlin/net/corda/djvm/rewiring/LoadedClass.kt b/djvm/src/main/kotlin/net/corda/djvm/rewiring/LoadedClass.kt index 8db11e85d1..fdbeed7161 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rewiring/LoadedClass.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rewiring/LoadedClass.kt @@ -1,5 +1,7 @@ package net.corda.djvm.rewiring +import org.objectweb.asm.Type + /** * A class or interface running in a Java application, together with its raw byte code representation and all references * made from within the type. @@ -16,7 +18,7 @@ class LoadedClass( * The name of the loaded type. */ val name: String - get() = type.name.replace('.', '/') + get() = Type.getInternalName(type) override fun toString(): String { return "Class(type=$name, size=${byteCode.bytes.size}, isModified=${byteCode.isModified})" diff --git a/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassLoader.kt b/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassLoader.kt index cb17fc4aba..5740534526 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassLoader.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassLoader.kt @@ -3,29 +3,31 @@ package net.corda.djvm.rewiring import net.corda.djvm.SandboxConfiguration import net.corda.djvm.analysis.AnalysisContext import net.corda.djvm.analysis.ClassAndMemberVisitor +import net.corda.djvm.code.asResourcePath import net.corda.djvm.references.ClassReference import net.corda.djvm.source.ClassSource -import net.corda.djvm.source.SourceClassLoader import net.corda.djvm.utilities.loggerFor import net.corda.djvm.validation.RuleValidator /** * Class loader that enables registration of rewired classes. * - * @property configuration The configuration to use for the sandbox. + * @param configuration The configuration to use for the sandbox. * @property context The context in which analysis and processing is performed. */ class SandboxClassLoader( - val configuration: SandboxConfiguration, - val context: AnalysisContext -) : ClassLoader() { + configuration: SandboxConfiguration, + private val context: AnalysisContext +) : ClassLoader(null) { + + 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 = configuration.analysisConfiguration + configuration = analysisConfiguration ) /** @@ -37,12 +39,12 @@ class SandboxClassLoader( /** * Set of classes that should be left untouched due to pinning. */ - private val pinnedClasses = configuration.analysisConfiguration.pinnedClasses + private val pinnedClasses = analysisConfiguration.pinnedClasses /** * Set of classes that should be left untouched due to whitelisting. */ - private val whitelistedClasses = configuration.analysisConfiguration.whitelist + private val whitelistedClasses = analysisConfiguration.whitelist /** * Cache of loaded classes. @@ -52,10 +54,7 @@ class SandboxClassLoader( /** * The class loader used to find classes on the extended class path. */ - private val supportingClassLoader = SourceClassLoader( - configuration.analysisConfiguration.classPath, - configuration.analysisConfiguration.classResolver - ) + private val supportingClassLoader = analysisConfiguration.supportingClassLoader /** * The re-writer to use for registered classes. @@ -83,9 +82,9 @@ class SandboxClassLoader( * @return The resulting Class object and its byte code representation. */ fun loadClassAndBytes(source: ClassSource, context: AnalysisContext): LoadedClass { - logger.trace("Loading class {}, origin={}...", source.qualifiedClassName, source.origin) - val name = configuration.analysisConfiguration.classResolver.reverseNormalized(source.qualifiedClassName) - val resolvedName = configuration.analysisConfiguration.classResolver.resolveNormalized(name) + logger.debug("Loading class {}, origin={}...", source.qualifiedClassName, source.origin) + val name = analysisConfiguration.classResolver.reverseNormalized(source.qualifiedClassName) + val resolvedName = analysisConfiguration.classResolver.resolveNormalized(name) // Check if the class has already been loaded. val loadedClass = loadedClasses[name] @@ -99,14 +98,14 @@ class SandboxClassLoader( // Analyse the class if not matching the whitelist. val readClassName = reader.className - if (!configuration.analysisConfiguration.whitelist.matches(readClassName)) { + if (!analysisConfiguration.whitelist.matches(readClassName)) { logger.trace("Class {} does not match with the whitelist", source.qualifiedClassName) logger.trace("Analyzing class {}...", source.qualifiedClassName) analyzer.analyze(reader, context) } // Check if the class should be left untouched. - val qualifiedName = name.replace('.', '/') + val qualifiedName = name.asResourcePath if (qualifiedName in pinnedClasses) { logger.trace("Class {} is marked as pinned", source.qualifiedClassName) val pinnedClasses = LoadedClass( @@ -146,7 +145,7 @@ class SandboxClassLoader( context.recordClassOrigin(name, ClassReference(source.origin)) } - logger.trace("Loaded class {}, bytes={}, isModified={}", + logger.debug("Loaded class {}, bytes={}, isModified={}", source.qualifiedClassName, byteCode.bytes.size, byteCode.isModified) return classWithByteCode diff --git a/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassWriter.kt b/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassWriter.kt index 9704421cd5..e1a051d45c 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassWriter.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassWriter.kt @@ -1,9 +1,11 @@ package net.corda.djvm.rewiring +import net.corda.djvm.code.asPackagePath import org.objectweb.asm.ClassReader import org.objectweb.asm.ClassWriter import org.objectweb.asm.ClassWriter.COMPUTE_FRAMES import org.objectweb.asm.ClassWriter.COMPUTE_MAXS +import org.objectweb.asm.Type /** * Class writer for sandbox execution, with configurable a [classLoader] to ensure correct deduction of the used class @@ -20,26 +22,28 @@ import org.objectweb.asm.ClassWriter.COMPUTE_MAXS */ open class SandboxClassWriter( classReader: ClassReader, - private val classLoader: ClassLoader, + private val cloader: ClassLoader, flags: Int = COMPUTE_FRAMES or COMPUTE_MAXS ) : ClassWriter(classReader, flags) { + override fun getClassLoader(): ClassLoader = cloader + /** * Get the common super type of [type1] and [type2]. */ override fun getCommonSuperClass(type1: String, type2: String): String { - // Need to override [getCommonSuperClass] to ensure that the correct class loader is used. + // Need to override [getCommonSuperClass] to ensure that we use ClassLoader.loadClass(). when { type1 == OBJECT_NAME -> return type1 type2 == OBJECT_NAME -> return type2 } val class1 = try { - classLoader.loadClass(type1.replace('/', '.')) + classLoader.loadClass(type1.asPackagePath) } catch (exception: Exception) { throw TypeNotPresentException(type1, exception) } val class2 = try { - classLoader.loadClass(type2.replace('/', '.')) + classLoader.loadClass(type2.asPackagePath) } catch (exception: Exception) { throw TypeNotPresentException(type2, exception) } @@ -52,7 +56,7 @@ open class SandboxClassWriter( do { clazz = clazz.superclass } while (!clazz.isAssignableFrom(class2)) - clazz.name.replace('.', '/') + Type.getInternalName(clazz) } } } diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/ClassRule.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/ClassRule.kt index 396acc30e7..d4004ee72d 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/ClassRule.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/ClassRule.kt @@ -18,7 +18,7 @@ abstract class ClassRule : Rule { */ abstract fun validate(context: RuleContext, clazz: ClassRepresentation) - override fun validate(context: RuleContext, clazz: ClassRepresentation?, member: Member?, instruction: Instruction?) { + final override fun validate(context: RuleContext, clazz: ClassRepresentation?, member: Member?, instruction: Instruction?) { // Only run validation step if applied to the class itself. if (clazz != null && member == null && instruction == null) { validate(context, clazz) diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/InstructionRule.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/InstructionRule.kt index b8f6aa5305..202f6ab435 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/InstructionRule.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/InstructionRule.kt @@ -18,7 +18,7 @@ abstract class InstructionRule : Rule { */ abstract fun validate(context: RuleContext, instruction: Instruction) - override fun validate(context: RuleContext, clazz: ClassRepresentation?, member: Member?, instruction: Instruction?) { + final override fun validate(context: RuleContext, clazz: ClassRepresentation?, member: Member?, instruction: Instruction?) { // Only run validation step if applied to the class member itself. if (clazz != null && member != null && instruction != null) { validate(context, instruction) diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/MemberRule.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/MemberRule.kt index f94c9f6a8c..a2c626851e 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/MemberRule.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/MemberRule.kt @@ -18,7 +18,7 @@ abstract class MemberRule : Rule { */ abstract fun validate(context: RuleContext, member: Member) - override fun validate(context: RuleContext, clazz: ClassRepresentation?, member: Member?, instruction: Instruction?) { + final override fun validate(context: RuleContext, clazz: ClassRepresentation?, member: Member?, instruction: Instruction?) { // Only run validation step if applied to the class member itself. if (clazz != null && member != null && instruction == null) { validate(context, member) diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowBreakpoints.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowBreakpoints.kt deleted file mode 100644 index 603e9bab21..0000000000 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowBreakpoints.kt +++ /dev/null @@ -1,17 +0,0 @@ -package net.corda.djvm.rules.implementation - -import net.corda.djvm.code.Instruction -import net.corda.djvm.code.Instruction.Companion.OP_BREAKPOINT -import net.corda.djvm.rules.InstructionRule -import net.corda.djvm.validation.RuleContext - -/** - * Rule that checks for invalid breakpoint instructions. - */ -class DisallowBreakpoints : InstructionRule() { - - override fun validate(context: RuleContext, instruction: Instruction) = context.validate { - fail("Disallowed breakpoint in method") given (instruction.operation == OP_BREAKPOINT) - } - -} diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowCatchingBlacklistedExceptions.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowCatchingBlacklistedExceptions.kt index 8d27c84c71..a5524ec12b 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowCatchingBlacklistedExceptions.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowCatchingBlacklistedExceptions.kt @@ -1,36 +1,16 @@ package net.corda.djvm.rules.implementation -import net.corda.djvm.code.Emitter -import net.corda.djvm.code.EmitterContext -import net.corda.djvm.code.Instruction +import net.corda.djvm.code.* import net.corda.djvm.code.instructions.CodeLabel import net.corda.djvm.code.instructions.TryCatchBlock -import net.corda.djvm.costing.ThresholdViolationException -import net.corda.djvm.rules.InstructionRule -import net.corda.djvm.validation.RuleContext import org.objectweb.asm.Label +import sandbox.net.corda.djvm.costing.ThresholdViolationError /** - * Rule that checks for attempted catches of [ThreadDeath], [ThresholdViolationException], [StackOverflowError], - * [OutOfMemoryError], [Error] or [Throwable]. + * Rule that checks for attempted catches of [ThreadDeath], [ThresholdViolationError], + * [StackOverflowError], [OutOfMemoryError], [Error] or [Throwable]. */ -class DisallowCatchingBlacklistedExceptions : InstructionRule(), Emitter { - - override fun validate(context: RuleContext, instruction: Instruction) = context.validate { - if (instruction is TryCatchBlock) { - val typeName = context.classModule.getFormattedClassName(instruction.typeName) - warn("Injected runtime check for catch-block for type $typeName") given - (instruction.typeName in disallowedExceptionTypes) - fail("Disallowed catch of ThreadDeath exception") given - (instruction.typeName == threadDeathException) - fail("Disallowed catch of stack overflow exception") given - (instruction.typeName == stackOverflowException) - fail("Disallowed catch of out of memory exception") given - (instruction.typeName == outOfMemoryException) - fail("Disallowed catch of threshold violation exception") given - (instruction.typeName.endsWith(ThresholdViolationException::class.java.simpleName)) - } - } +class DisallowCatchingBlacklistedExceptions : Emitter { override fun emit(context: EmitterContext, instruction: Instruction) = context.emit { if (instruction is TryCatchBlock && instruction.typeName in disallowedExceptionTypes) { @@ -46,13 +26,27 @@ class DisallowCatchingBlacklistedExceptions : InstructionRule(), Emitter { private fun isExceptionHandler(label: Label) = label in handlers companion object { - - private const val threadDeathException = "java/lang/ThreadDeath" - private const val stackOverflowException = "java/lang/StackOverflowError" - private const val outOfMemoryException = "java/lang/OutOfMemoryError" - - // Any of [ThreadDeath]'s throwable super-classes need explicit checking. private val disallowedExceptionTypes = setOf( + ruleViolationError, + thresholdViolationError, + + /** + * These errors indicate that the JVM is failing, + * so don't allow these to be caught either. + */ + "java/lang/StackOverflowError", + "java/lang/OutOfMemoryError", + + /** + * These are immediate super-classes for our explicit errors. + */ + "java/lang/VirtualMachineError", + "java/lang/ThreadDeath", + + /** + * Any of [ThreadDeath] and [VirtualMachineError]'s throwable + * super-classes also need explicit checking. + */ "java/lang/Throwable", "java/lang/Error" ) diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowFinalizerMethods.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowFinalizerMethods.kt deleted file mode 100644 index 43725aedf7..0000000000 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowFinalizerMethods.kt +++ /dev/null @@ -1,17 +0,0 @@ -package net.corda.djvm.rules.implementation - -import net.corda.djvm.references.Member -import net.corda.djvm.rules.MemberRule -import net.corda.djvm.validation.RuleContext - -/** - * Rule that checks for invalid use of finalizers. - */ -class DisallowFinalizerMethods : MemberRule() { - - override fun validate(context: RuleContext, member: Member) = context.validate { - fail("Disallowed finalizer method") given ("${member.memberName}:${member.signature}" == "finalize:()V") - // TODO Make this rule simply erase the finalize() method and continue execution. - } - -} diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowNativeMethods.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowNativeMethods.kt deleted file mode 100644 index 5b1fbfb392..0000000000 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowNativeMethods.kt +++ /dev/null @@ -1,17 +0,0 @@ -package net.corda.djvm.rules.implementation - -import net.corda.djvm.references.Member -import net.corda.djvm.rules.MemberRule -import net.corda.djvm.validation.RuleContext -import java.lang.reflect.Modifier - -/** - * Rule that checks for invalid use of native methods. - */ -class DisallowNativeMethods : MemberRule() { - - override fun validate(context: RuleContext, member: Member) = context.validate { - fail("Disallowed native method") given Modifier.isNative(member.access) - } - -} diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowNonDeterministicMethods.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowNonDeterministicMethods.kt new file mode 100644 index 0000000000..04ef9e3d5c --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowNonDeterministicMethods.kt @@ -0,0 +1,42 @@ +package net.corda.djvm.rules.implementation + +import net.corda.djvm.code.Emitter +import net.corda.djvm.code.EmitterContext +import net.corda.djvm.code.Instruction +import net.corda.djvm.code.instructions.MemberAccessInstruction +import net.corda.djvm.formatting.MemberFormatter +import org.objectweb.asm.Opcodes.* +import sandbox.net.corda.djvm.rules.RuleViolationError + +/** + * Some non-deterministic APIs belong to pinned classes and so cannot be stubbed out. + * Replace their invocations with exceptions instead. + */ +class DisallowNonDeterministicMethods : Emitter { + + override fun emit(context: EmitterContext, instruction: Instruction) = context.emit { + if (instruction is MemberAccessInstruction && isForbidden(instruction)) { + when (instruction.operation) { + INVOKEVIRTUAL -> { + throwException("Disallowed reference to API; ${memberFormatter.format(instruction.member)}") + preventDefault() + } + } + } + } + + private fun isClassReflection(instruction: MemberAccessInstruction): Boolean = + (instruction.owner == "java/lang/Class") && ( + ((instruction.memberName == "newInstance" && instruction.signature == "()Ljava/lang/Object;") + || instruction.signature.contains("Ljava/lang/reflect/")) + ) + + private fun isObjectMonitor(instruction: MemberAccessInstruction): Boolean = + (instruction.signature == "()V" && (instruction.memberName == "notify" || instruction.memberName == "notifyAll" || instruction.memberName == "wait")) + || (instruction.memberName == "wait" && (instruction.signature == "(J)V" || instruction.signature == "(JI)V")) + + private fun isForbidden(instruction: MemberAccessInstruction): Boolean + = instruction.isMethod && (isClassReflection(instruction) || isObjectMonitor(instruction)) + + private val memberFormatter = MemberFormatter() +} \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowReflection.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowReflection.kt deleted file mode 100644 index cdc0b73f42..0000000000 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowReflection.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.corda.djvm.rules.implementation - -import net.corda.djvm.code.Instruction -import net.corda.djvm.code.instructions.MemberAccessInstruction -import net.corda.djvm.formatting.MemberFormatter -import net.corda.djvm.rules.InstructionRule -import net.corda.djvm.validation.RuleContext - -/** - * Rule that checks for illegal references to reflection APIs. - */ -class DisallowReflection : InstructionRule() { - - override fun validate(context: RuleContext, instruction: Instruction) = context.validate { - // TODO Enable controlled use of reflection APIs - if (instruction is MemberAccessInstruction) { - invalidReflectionUsage(instruction) given - ("java/lang/Class" in instruction.owner && instruction.memberName == "newInstance") - invalidReflectionUsage(instruction) given (instruction.owner.startsWith("java/lang/reflect/")) - invalidReflectionUsage(instruction) given (instruction.owner.startsWith("java/lang/invoke/")) - invalidReflectionUsage(instruction) given (instruction.owner.startsWith("sun/")) - } - } - - private fun RuleContext.invalidReflectionUsage(instruction: MemberAccessInstruction) = - this.fail("Disallowed reference to reflection API; ${memberFormatter.format(instruction.member)}") - - private val memberFormatter = MemberFormatter() - -} diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/IgnoreBreakpoints.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/IgnoreBreakpoints.kt new file mode 100644 index 0000000000..88c1a08e49 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/IgnoreBreakpoints.kt @@ -0,0 +1,19 @@ +package net.corda.djvm.rules.implementation + +import net.corda.djvm.code.Emitter +import net.corda.djvm.code.EmitterContext +import net.corda.djvm.code.Instruction +import net.corda.djvm.code.Instruction.Companion.OP_BREAKPOINT + +/** + * Rule that deletes invalid breakpoint instructions. + */ +class IgnoreBreakpoints : Emitter { + + override fun emit(context: EmitterContext, instruction: Instruction) = context.emit { + when (instruction.operation) { + OP_BREAKPOINT -> preventDefault() + } + } + +} diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/IgnoreSynchronizedBlocks.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/IgnoreSynchronizedBlocks.kt index 9430d94312..3b1cb80f91 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/IgnoreSynchronizedBlocks.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/IgnoreSynchronizedBlocks.kt @@ -3,20 +3,13 @@ package net.corda.djvm.rules.implementation import net.corda.djvm.code.Emitter import net.corda.djvm.code.EmitterContext import net.corda.djvm.code.Instruction -import net.corda.djvm.rules.InstructionRule -import net.corda.djvm.validation.RuleContext import org.objectweb.asm.Opcodes.* /** - * Rule that warns about the use of synchronized code blocks. This class also exposes an emitter that rewrites pertinent - * monitoring instructions to [POP]'s, as these replacements will remove the object references that [MONITORENTER] and - * [MONITOREXIT] anticipate to be on the stack. + * An emitter that rewrites monitoring instructions to [POP]s, as these replacements will remove + * the object references that [MONITORENTER] and [MONITOREXIT] anticipate to be on the stack. */ -class IgnoreSynchronizedBlocks : InstructionRule(), Emitter { - - override fun validate(context: RuleContext, instruction: Instruction) = context.validate { - inform("Stripped monitoring instruction") given (instruction.operation in setOf(MONITORENTER, MONITOREXIT)) - } +class IgnoreSynchronizedBlocks : Emitter { override fun emit(context: EmitterContext, instruction: Instruction) = context.emit { when (instruction.operation) { diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StubOutFinalizerMethods.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StubOutFinalizerMethods.kt new file mode 100644 index 0000000000..48c714b843 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StubOutFinalizerMethods.kt @@ -0,0 +1,35 @@ +package net.corda.djvm.rules.implementation + +import net.corda.djvm.analysis.AnalysisRuntimeContext +import net.corda.djvm.code.EmitterModule +import net.corda.djvm.code.MemberDefinitionProvider +import net.corda.djvm.references.Member +import java.lang.reflect.Modifier + +/** + * Rule that replaces a finalize() method with a simple stub. + */ +class StubOutFinalizerMethods : MemberDefinitionProvider { + + override fun define(context: AnalysisRuntimeContext, member: Member) = when { + /** + * Discard any other method body and replace with stub that just returns. + * Other [MemberDefinitionProvider]s are expected to append to this list + * and not replace its contents! + */ + isFinalizer(member) -> member.copy(body = listOf(::writeMethodBody)) + else -> member + } + + private fun writeMethodBody(emitter: EmitterModule): Unit = with(emitter) { + returnVoid() + } + + /** + * No need to rewrite [Object.finalize] or [Enum.finalize]; ignore these. + */ + private fun isFinalizer(member: Member): Boolean + = member.memberName == "finalize" && member.signature == "()V" + && !member.className.startsWith("java/lang/") + && !Modifier.isAbstract(member.access) +} diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StubOutNativeMethods.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StubOutNativeMethods.kt new file mode 100644 index 0000000000..74a58f6c7f --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StubOutNativeMethods.kt @@ -0,0 +1,36 @@ +package net.corda.djvm.rules.implementation + +import net.corda.djvm.analysis.AnalysisRuntimeContext +import net.corda.djvm.code.EmitterModule +import net.corda.djvm.code.MemberDefinitionProvider +import net.corda.djvm.references.Member +import org.objectweb.asm.Opcodes.* +import sandbox.net.corda.djvm.rules.RuleViolationError +import java.lang.reflect.Modifier + +/** + * Rule that replaces a native method with a stub that throws an exception. + */ +class StubOutNativeMethods : MemberDefinitionProvider { + + override fun define(context: AnalysisRuntimeContext, member: Member) = when { + isNative(member) -> member.copy( + access = member.access and ACC_NATIVE.inv(), + body = member.body + if (isForStubbing(member)) ::writeStubMethodBody else ::writeExceptionMethodBody + ) + else -> member + } + + private fun writeExceptionMethodBody(emitter: EmitterModule): Unit = with(emitter) { + lineNumber(0) + throwException(RuleViolationError::class.java, "Native method has been deleted") + } + + private fun writeStubMethodBody(emitter: EmitterModule): Unit = with(emitter) { + returnVoid() + } + + private fun isForStubbing(member: Member): Boolean = member.signature == "()V" && member.memberName == "registerNatives" + + private fun isNative(member: Member): Boolean = Modifier.isNative(member.access) +} diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StubOutReflectionMethods.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StubOutReflectionMethods.kt new file mode 100644 index 0000000000..4e486bf289 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/StubOutReflectionMethods.kt @@ -0,0 +1,35 @@ +package net.corda.djvm.rules.implementation + +import net.corda.djvm.analysis.AnalysisRuntimeContext +import net.corda.djvm.code.EmitterModule +import net.corda.djvm.code.MemberDefinitionProvider +import net.corda.djvm.references.Member +import org.objectweb.asm.Opcodes.* +import sandbox.net.corda.djvm.rules.RuleViolationError + +/** + * Replace reflection APIs with stubs that throw exceptions. Only for unpinned classes. + */ +class StubOutReflectionMethods : MemberDefinitionProvider { + + override fun define(context: AnalysisRuntimeContext, member: Member): Member = when { + isConcreteApi(member) && isReflection(member) -> member.copy(body = member.body + ::writeMethodBody) + else -> member + } + + private fun writeMethodBody(emitter: EmitterModule): Unit = with(emitter) { + lineNumber(0) + throwException(RuleViolationError::class.java, "Disallowed reference to reflection API") + } + + // The method must be public and with a Java implementation. + private fun isConcreteApi(member: Member): Boolean = member.access and (ACC_PUBLIC or ACC_ABSTRACT or ACC_NATIVE) == ACC_PUBLIC + + private fun isReflection(member: Member): Boolean { + return member.className.startsWith("java/lang/reflect/") + || member.className.startsWith("java/lang/invoke/") + || member.className.startsWith("sun/reflect/") + || member.className == "sun/misc/Unsafe" + || member.className == "sun/misc/VM" + } +} diff --git a/djvm/src/main/kotlin/net/corda/djvm/source/SourceClassLoader.kt b/djvm/src/main/kotlin/net/corda/djvm/source/SourceClassLoader.kt index 47d0544bcd..8b4789f8df 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/source/SourceClassLoader.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/source/SourceClassLoader.kt @@ -3,6 +3,7 @@ package net.corda.djvm.source import net.corda.djvm.analysis.AnalysisContext import net.corda.djvm.analysis.ClassResolver import net.corda.djvm.analysis.SourceLocation +import net.corda.djvm.code.asResourcePath import net.corda.djvm.messages.Message import net.corda.djvm.messages.Severity import net.corda.djvm.rewiring.SandboxClassLoadingException @@ -17,18 +18,11 @@ import java.nio.file.Path import java.nio.file.Paths import kotlin.streams.toList -/** - * Customizable class loader that allows the user to explicitly specify additional JARs and directories to scan. - * - * @param paths The directories and explicit JAR files to scan. - * @property classResolver The resolver to use to derive the original name of a requested class. - * @property resolvedUrls The resolved URLs that get passed to the underlying class loader. - */ -open class SourceClassLoader( - paths: List, - private val classResolver: ClassResolver, - val resolvedUrls: Array = resolvePaths(paths) -) : URLClassLoader(resolvedUrls, SourceClassLoader::class.java.classLoader) { +abstract class AbstractSourceClassLoader( + paths: List, + private val classResolver: ClassResolver, + parent: ClassLoader? +) : URLClassLoader(resolvePaths(paths), parent) { /** * Open a [ClassReader] for the provided class name. @@ -36,7 +30,7 @@ open class SourceClassLoader( fun classReader( className: String, context: AnalysisContext, origin: String? = null ): ClassReader { - val originalName = classResolver.reverse(className.replace('.', '/')) + val originalName = classResolver.reverse(className.asResourcePath) return try { logger.trace("Opening ClassReader for class {}...", originalName) getResourceAsStream("$originalName.class").use { @@ -71,16 +65,16 @@ open class SourceClassLoader( return super.loadClass(originalName, resolve) } - private companion object { - - private val logger = loggerFor() + protected companion object { + @JvmStatic + protected val logger = loggerFor() private fun resolvePaths(paths: List): Array { 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(it) }.map { it.toURL() }.toList() + 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") @@ -100,11 +94,76 @@ open class SourceClassLoader( private fun isJarFile(path: Path) = path.toString().endsWith(".jar", true) - private fun Path.toURL() = this.toUri().toURL() + 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) { + + /** + * Only search our own jars for the given resource. + */ + override fun getResource(name: String): URL? = findResource(name) +} + +/** + * Customizable class loader that allows the user to explicitly specify additional JARs and directories to scan. + * + * @param paths The directories and explicit JAR files to scan. + * @property classResolver The resolver to use to derive the original name of a requested class. + * @property bootstrap The [BootstrapClassLoader] containing the Java APIs for the sandbox. + */ +class SourceClassLoader( + paths: List, + classResolver: ClassResolver, + private val bootstrap: BootstrapClassLoader? = null +) : AbstractSourceClassLoader(paths, classResolver, SourceClassLoader::class.java.classLoader) { + + /** + * First check the bootstrap classloader, if we have one. + * Otherwise check our parent classloader, followed by + * the user-supplied jars. + */ + override fun getResource(name: String): URL? { + if (bootstrap != null) { + val resource = bootstrap.findResource(name) + if (resource != null) { + return resource + } else if (isJvmInternal(name)) { + logger.error("Denying request for actual {}", name) + return null + } + } + + return parent?.getResource(name) ?: findResource(name) + } + + /** + * Deny all requests for DJVM classes from any user-supplied jars. + */ + override fun findResource(name: String): URL? { + 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/") } \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/tools/Utilities.kt b/djvm/src/main/kotlin/net/corda/djvm/tools/Utilities.kt deleted file mode 100644 index f66b14d1cd..0000000000 --- a/djvm/src/main/kotlin/net/corda/djvm/tools/Utilities.kt +++ /dev/null @@ -1,114 +0,0 @@ -package net.corda.djvm.tools - -import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner -import java.lang.reflect.Modifier -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.Paths -import java.nio.file.StandardOpenOption - -/** - * Various utility functions. - */ -@Suppress("unused") -object Utilities { - - /** - * Get the expanded file name of each path in the provided array. - */ - fun Array?.getFiles(map: (Path) -> Path = { it }) = (this ?: emptyArray()).map { - val pathString = it.toString() - val path = map(it) - when { - '/' in pathString || '\\' in pathString -> - throw Exception("Please provide a pathless file name") - pathString.endsWith(".java", true) -> path - else -> Paths.get("$path.java") - } - } - - /** - * Get the string representation of each expanded file name in the provided array. - */ - fun Array?.getFileNames(map: (Path) -> Path = { it }) = this.getFiles(map).map { - it.toString() - }.toTypedArray() - - /** - * Execute inlined action if the collection is empty. - */ - inline fun List.onEmpty(action: () -> Unit): List { - if (!this.any()) { - action() - } - return this - } - - /** - * Execute inlined action if the array is empty. - */ - inline fun Array?.onEmpty(action: () -> Unit): Array { - return (this ?: emptyArray()).toList().onEmpty(action).toTypedArray() - } - - /** - * Derive the set of [StandardOpenOption]'s to use for a file operation. - */ - fun openOptions(force: Boolean) = if (force) { - arrayOf(StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING) - } else { - arrayOf(StandardOpenOption.CREATE_NEW) - } - - /** - * Get the path of where any generated code will be placed. Create the directory if it does not exist. - */ - fun createCodePath(): Path { - val root = Paths.get("tmp") - .resolve("net") - .resolve("corda") - .resolve("djvm") - Files.createDirectories(root) - return root - } - - /** - * Return the base name of a file (i.e., its name without extension) - */ - val Path.baseName: String - get() = this.fileName.toString() - .replaceAfterLast('.', "") - .removeSuffix(".") - - /** - * The path of the executing JAR. - */ - val jarPath: String = Utilities::class.java.protectionDomain.codeSource.location.toURI().path - - - /** - * The path of the current working directory. - */ - val workingDirectory: Path = Paths.get(System.getProperty("user.dir")) - - /** - * The class path for the current execution context. - */ - val userClassPath: String = System.getProperty("java.class.path") - - /** - * Get a reference of each concrete class that implements interface or class [T]. - */ - inline fun find(scanSpec: String = "net/corda/djvm"): List> { - val references = mutableListOf>() - FastClasspathScanner(scanSpec) - .matchClassesImplementing(T::class.java) { clazz -> - if (!Modifier.isAbstract(clazz.modifiers) && !Modifier.isStatic(clazz.modifiers)) { - references.add(clazz) - } - } - .scan() - return references - } - -} \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/utilities/Discovery.kt b/djvm/src/main/kotlin/net/corda/djvm/utilities/Discovery.kt index 8deadf7d0b..9092e5c044 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/utilities/Discovery.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/utilities/Discovery.kt @@ -7,6 +7,7 @@ import java.lang.reflect.Modifier * Find and instantiate types that implement a certain interface. */ object Discovery { + const val FORBIDDEN_CLASS_MASK = (Modifier.STATIC or Modifier.ABSTRACT) /** * Get an instance of each concrete class that implements interface or class [T]. @@ -15,7 +16,7 @@ object Discovery { val instances = mutableListOf() FastClasspathScanner("net/corda/djvm") .matchClassesImplementing(T::class.java) { clazz -> - if (!Modifier.isAbstract(clazz.modifiers) && !Modifier.isStatic(clazz.modifiers)) { + if (clazz.modifiers and FORBIDDEN_CLASS_MASK == 0) { try { instances.add(clazz.newInstance()) } catch (exception: Throwable) { diff --git a/djvm/src/main/kotlin/net/corda/djvm/validation/ReferenceValidator.kt b/djvm/src/main/kotlin/net/corda/djvm/validation/ReferenceValidator.kt deleted file mode 100644 index 89fa22c992..0000000000 --- a/djvm/src/main/kotlin/net/corda/djvm/validation/ReferenceValidator.kt +++ /dev/null @@ -1,219 +0,0 @@ -package net.corda.djvm.validation - -import net.corda.djvm.analysis.AnalysisConfiguration -import net.corda.djvm.analysis.AnalysisContext -import net.corda.djvm.analysis.ClassAndMemberVisitor -import net.corda.djvm.execution.SandboxedRunnable -import net.corda.djvm.formatting.MemberFormatter -import net.corda.djvm.messages.Message -import net.corda.djvm.messages.Severity -import net.corda.djvm.references.* -import net.corda.djvm.rewiring.SandboxClassLoadingException -import net.corda.djvm.utilities.loggerFor - -/** - * Module used to validate all traversable references before instantiating and executing a [SandboxedRunnable]. - * - * @param configuration The analysis configuration to use for the validation. - * @property memberFormatter Module with functionality for formatting class members. - */ -class ReferenceValidator( - private val configuration: AnalysisConfiguration, - private val memberFormatter: MemberFormatter = MemberFormatter() -) { - - /** - * Container holding the current state of the validation. - * - * @property context The context in which references are to be validated. - * @property analyzer Underlying analyzer used for processing classes. - */ - private class State( - val context: AnalysisContext, - val analyzer: ClassAndMemberVisitor - ) - - /** - * Validate whether or not the classes in a class hierarchy can be safely instantiated and run in a sandbox by - * checking that all references are rooted in deterministic code. - * - * @param context The context in which the check should be made. - * @param analyzer Underlying analyzer used for processing classes. - */ - fun validate(context: AnalysisContext, analyzer: ClassAndMemberVisitor): ReferenceValidationSummary = - State(context, analyzer).let { state -> - logger.trace("Validating {} references across {} class(es)...", - context.references.size, context.classes.size) - context.references.process { validateReference(state, it) } - logger.trace("Reference validation completed; {} class(es) and {} message(s)", - context.references.size, context.classes.size) - ReferenceValidationSummary(state.context.classes, state.context.messages, state.context.classOrigins) - } - - /** - * Construct a message from an invalid reference and its source location. - */ - private fun referenceToMessage(referenceWithLocation: ReferenceWithLocation): Message { - val (location, reference, description) = referenceWithLocation - val referenceMessage = when { - reference is ClassReference -> - "Invalid reference to class ${configuration.classModule.getFormattedClassName(reference.className)}" - reference is MemberReference && configuration.memberModule.isConstructor(reference) -> - "Invalid reference to constructor ${memberFormatter.format(reference)}" - reference is MemberReference && configuration.memberModule.isField(reference) -> - "Invalid reference to field ${memberFormatter.format(reference)}" - reference is MemberReference && configuration.memberModule.isMethod(reference) -> - "Invalid reference to method ${memberFormatter.format(reference)}" - else -> - "Invalid reference to $reference" - } - val message = if (description.isNotBlank()) { - "$referenceMessage, $description" - } else { - referenceMessage - } - return Message(message, Severity.ERROR, location) - } - - /** - * Validate a reference made from a class or class member. - */ - private fun validateReference(state: State, reference: EntityReference) { - if (configuration.whitelist.matches(reference.className)) { - // The referenced class has been whitelisted - no need to go any further. - return - } - when (reference) { - is ClassReference -> { - logger.trace("Validating class reference {}", reference) - val clazz = getClass(state, reference.className) - val reason = when (clazz) { - null -> Reason(Reason.Code.NON_EXISTENT_CLASS) - else -> getReasonFromEntity(clazz) - } - if (reason != null) { - logger.trace("Recorded invalid class reference to {}; reason = {}", reference, reason) - state.context.messages.addAll(state.context.references.locationsFromReference(reference).map { - referenceToMessage(ReferenceWithLocation(it, reference, reason.description)) - }) - } - } - is MemberReference -> { - logger.trace("Validating member reference {}", reference) - // Ensure that the dependent class is loaded and analyzed - val clazz = getClass(state, reference.className) - val member = state.context.classes.getMember( - reference.className, reference.memberName, reference.signature - ) - val reason = when { - clazz == null -> Reason(Reason.Code.NON_EXISTENT_CLASS) - member == null -> Reason(Reason.Code.NON_EXISTENT_MEMBER) - else -> getReasonFromEntity(state, member) - } - if (reason != null) { - logger.trace("Recorded invalid member reference to {}; reason = {}", reference, reason) - state.context.messages.addAll(state.context.references.locationsFromReference(reference).map { - referenceToMessage(ReferenceWithLocation(it, reference, reason.description)) - }) - } - } - } - } - - /** - * Get a class from the class hierarchy by its binary name. - */ - private fun getClass(state: State, className: String, originClass: String? = null): ClassRepresentation? { - val name = if (configuration.classModule.isArray(className)) { - val arrayType = arrayTypeExtractor.find(className)?.groupValues?.get(1) - when (arrayType) { - null -> "java/lang/Object" - else -> arrayType - } - } else { - className - } - var clazz = state.context.classes[name] - if (clazz == null) { - logger.trace("Loading and analyzing referenced class {}...", name) - val origin = state.context.references - .locationsFromReference(ClassReference(name)) - .map { it.className } - .firstOrNull() ?: originClass - state.analyzer.analyze(name, state.context, origin) - clazz = state.context.classes[name] - } - if (clazz == null) { - logger.warn("Failed to load class {}", name) - state.context.messages.add(Message("Referenced class not found; $name", Severity.ERROR)) - } - clazz?.apply { - val ancestors = listOf(superClass) + interfaces - for (ancestor in ancestors.filter(String::isNotBlank)) { - getClass(state, ancestor, clazz.name) - } - } - return clazz - } - - /** - * Check if a top-level class definition is considered safe or not. - */ - private fun isNonDeterministic(state: State, className: String): Boolean = when { - configuration.whitelist.matches(className) -> false - else -> { - try { - getClass(state, className)?.let { - isNonDeterministic(it) - } ?: true - } catch (exception: SandboxClassLoadingException) { - true // Failed to load the class, which means the class is non-deterministic. - } - } - } - - /** - * Check if a top-level class definition is considered safe or not. - */ - private fun isNonDeterministic(clazz: ClassRepresentation) = - getReasonFromEntity(clazz) != null - - /** - * Derive what reason to give to the end-user for an invalid class. - */ - private fun getReasonFromEntity(clazz: ClassRepresentation): Reason? = when { - configuration.whitelist.matches(clazz.name) -> null - configuration.whitelist.inNamespace(clazz.name) -> Reason(Reason.Code.NOT_WHITELISTED) - configuration.classModule.isNonDeterministic(clazz) -> Reason(Reason.Code.ANNOTATED) - else -> null - } - - /** - * Derive what reason to give to the end-user for an invalid member. - */ - private fun getReasonFromEntity(state: State, member: Member): Reason? = when { - configuration.whitelist.matches(member.reference) -> null - configuration.whitelist.inNamespace(member.reference) -> Reason(Reason.Code.NOT_WHITELISTED) - configuration.memberModule.isNonDeterministic(member) -> Reason(Reason.Code.ANNOTATED) - else -> { - val invalidClasses = configuration.memberModule.findReferencedClasses(member) - .filter { isNonDeterministic(state, it) } - if (invalidClasses.isNotEmpty()) { - Reason(Reason.Code.INVALID_CLASS, invalidClasses.map { - configuration.classModule.getFormattedClassName(it) - }) - } else { - null - } - } - } - - private companion object { - - private val logger = loggerFor() - - private val arrayTypeExtractor = "^\\[*L([^;]+);$".toRegex() - - } - -} diff --git a/djvm/src/main/kotlin/net/corda/djvm/validation/RuleValidator.kt b/djvm/src/main/kotlin/net/corda/djvm/validation/RuleValidator.kt index 0580566bb6..1f4ead8cd1 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/validation/RuleValidator.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/validation/RuleValidator.kt @@ -21,9 +21,9 @@ import org.objectweb.asm.ClassVisitor */ class RuleValidator( private val rules: List = emptyList(), - configuration: AnalysisConfiguration = AnalysisConfiguration(), + configuration: AnalysisConfiguration, classVisitor: ClassVisitor? = null -) : ClassAndMemberVisitor(classVisitor, configuration = configuration) { +) : ClassAndMemberVisitor(configuration, classVisitor) { /** * Apply the set of rules to the traversed class and record any violations. diff --git a/djvm/src/main/kotlin/sandbox/net/corda/djvm/costing/RuntimeCostAccounter.kt b/djvm/src/main/kotlin/sandbox/net/corda/djvm/costing/RuntimeCostAccounter.kt index 94487b1c91..3445e31ea0 100644 --- a/djvm/src/main/kotlin/sandbox/net/corda/djvm/costing/RuntimeCostAccounter.kt +++ b/djvm/src/main/kotlin/sandbox/net/corda/djvm/costing/RuntimeCostAccounter.kt @@ -2,7 +2,7 @@ package sandbox.net.corda.djvm.costing import net.corda.djvm.SandboxRuntimeContext import net.corda.djvm.costing.RuntimeCostSummary -import net.corda.djvm.costing.ThresholdViolationException +import org.objectweb.asm.Type /** * Class for keeping a tally on various runtime metrics, like number of jumps, allocations, invocations, etc. The @@ -24,7 +24,8 @@ object RuntimeCostAccounter { /** * The type name of the [RuntimeCostAccounter] class; referenced from instrumentors. */ - const val TYPE_NAME: String = "sandbox/net/corda/djvm/costing/RuntimeCostAccounter" + @JvmField + val TYPE_NAME: String = Type.getInternalName(this::class.java) /** * Known / estimated allocation costs. @@ -35,14 +36,12 @@ object RuntimeCostAccounter { ) /** - * Re-throw exception if it is of type [ThreadDeath] or [ThresholdViolationException]. + * Re-throw exception if it is of type [ThreadDeath] or [VirtualMachineError]. */ @JvmStatic fun checkCatch(exception: Throwable) { - if (exception is ThreadDeath) { - throw exception - } else if (exception is ThresholdViolationException) { - throw exception + when (exception) { + is ThreadDeath, is VirtualMachineError -> throw exception } } diff --git a/djvm/src/main/kotlin/net/corda/djvm/costing/ThresholdViolationException.kt b/djvm/src/main/kotlin/sandbox/net/corda/djvm/costing/ThresholdViolationError.kt similarity index 64% rename from djvm/src/main/kotlin/net/corda/djvm/costing/ThresholdViolationException.kt rename to djvm/src/main/kotlin/sandbox/net/corda/djvm/costing/ThresholdViolationError.kt index 4e8635ba8d..0fe4283caf 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/costing/ThresholdViolationException.kt +++ b/djvm/src/main/kotlin/sandbox/net/corda/djvm/costing/ThresholdViolationError.kt @@ -1,4 +1,4 @@ -package net.corda.djvm.costing +package sandbox.net.corda.djvm.costing /** * Exception thrown when a sandbox threshold is violated. This will kill the current thread and consequently exit the @@ -6,6 +6,4 @@ package net.corda.djvm.costing * * @property message The description of the condition causing the problem. */ -class ThresholdViolationException( - override val message: String -) : ThreadDeath() +class ThresholdViolationError(override val message: String) : ThreadDeath() diff --git a/djvm/src/main/kotlin/sandbox/net/corda/djvm/rules/RuleViolationError.kt b/djvm/src/main/kotlin/sandbox/net/corda/djvm/rules/RuleViolationError.kt new file mode 100644 index 0000000000..24b0e73775 --- /dev/null +++ b/djvm/src/main/kotlin/sandbox/net/corda/djvm/rules/RuleViolationError.kt @@ -0,0 +1,10 @@ +package sandbox.net.corda.djvm.rules + +/** + * Exception thrown when a sandbox rule is violated at runtime. + * This will kill the current thread and consequently exit the + * sandbox. + * + * @property message The description of the condition causing the problem. + */ +class RuleViolationError(override val message: String) : ThreadDeath() \ No newline at end of file diff --git a/djvm/src/test/kotlin/foo/bar/sandbox/StrictFloat.kt b/djvm/src/test/kotlin/foo/bar/sandbox/StrictFloat.kt index 0d30565a98..82f721ab9a 100644 --- a/djvm/src/test/kotlin/foo/bar/sandbox/StrictFloat.kt +++ b/djvm/src/test/kotlin/foo/bar/sandbox/StrictFloat.kt @@ -4,7 +4,7 @@ class StrictFloat : Callable { override fun call() { val d = java.lang.Double.MIN_VALUE val x = d / 2 * 2 - assert(x.toString() == "0.0") + require(x.toString() == "0.0") } } diff --git a/djvm/src/test/kotlin/net/corda/djvm/TestBase.kt b/djvm/src/test/kotlin/net/corda/djvm/TestBase.kt index 333516c06a..b54d92b16e 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/TestBase.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/TestBase.kt @@ -16,11 +16,18 @@ import net.corda.djvm.rules.Rule import net.corda.djvm.source.ClassSource import net.corda.djvm.utilities.Discovery import net.corda.djvm.validation.RuleValidator +import org.junit.After +import org.junit.Assert.assertEquals import org.objectweb.asm.ClassReader -import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.Type import java.lang.reflect.InvocationTargetException +import java.nio.file.Path +import java.nio.file.Paths +import kotlin.concurrent.thread +import kotlin.reflect.jvm.jvmName -open class TestBase { +abstract class TestBase { companion object { @@ -32,27 +39,33 @@ open class TestBase { val BLANK = emptySet() - val DEFAULT = (ALL_RULES + ALL_EMITTERS + ALL_DEFINITION_PROVIDERS) - .toSet().distinctBy { it.javaClass } + val DEFAULT = (ALL_RULES + ALL_EMITTERS + ALL_DEFINITION_PROVIDERS).distinctBy(Any::javaClass) + + val DETERMINISTIC_RT: Path = Paths.get( + System.getProperty("deterministic-rt.path") ?: throw AssertionError("deterministic-rt.path property not set")) /** * Get the full name of type [T]. */ - inline fun nameOf(prefix: String = "") = - "$prefix${T::class.java.name.replace('.', '/')}" + inline fun nameOf(prefix: String = "") = "$prefix${Type.getInternalName(T::class.java)}" } /** * Default analysis configuration. */ - val configuration = AnalysisConfiguration(Whitelist.MINIMAL) + val configuration = AnalysisConfiguration(Whitelist.MINIMAL, bootstrapJar = DETERMINISTIC_RT) /** * Default analysis context */ val context: AnalysisContext - get() = AnalysisContext.fromConfiguration(configuration, emptyList()) + get() = AnalysisContext.fromConfiguration(configuration) + + @After + fun destroy() { + configuration.close() + } /** * Short-hand for analysing and validating a class. @@ -62,14 +75,15 @@ open class TestBase { noinline block: (RuleValidator.(AnalysisContext) -> Unit) ) { val reader = ClassReader(T::class.java.name) - val configuration = AnalysisConfiguration(minimumSeverityLevel = minimumSeverityLevel) - val validator = RuleValidator(ALL_RULES, configuration) - val context = AnalysisContext.fromConfiguration( - configuration, - listOf(ClassSource.fromClassName(reader.className)) - ) - validator.analyze(reader, context) - block(validator, context) + AnalysisConfiguration( + minimumSeverityLevel = minimumSeverityLevel, + classPath = listOf(DETERMINISTIC_RT) + ).use { analysisConfiguration -> + val validator = RuleValidator(ALL_RULES, analysisConfiguration) + val context = AnalysisContext.fromConfiguration(analysisConfiguration) + validator.analyze(reader, context) + block(validator, context) + } } /** @@ -113,27 +127,26 @@ open class TestBase { } } var thrownException: Throwable? = null - Thread { + thread { try { - val pinnedTestClasses = pinnedClasses.map { it.name.replace('.', '/') }.toSet() - val analysisConfiguration = AnalysisConfiguration( - whitelist = whitelist, - additionalPinnedClasses = pinnedTestClasses, - minimumSeverityLevel = minimumSeverityLevel - ) - SandboxRuntimeContext(SandboxConfiguration.of( - executionProfile, rules, emitters, definitionProviders, enableTracing, analysisConfiguration - ), classSources).use { - assertThat(runtimeCosts).areZero() - action(this) + val pinnedTestClasses = pinnedClasses.map(Type::getInternalName).toSet() + AnalysisConfiguration( + whitelist = whitelist, + bootstrapJar = DETERMINISTIC_RT, + additionalPinnedClasses = pinnedTestClasses, + minimumSeverityLevel = minimumSeverityLevel + ).use { analysisConfiguration -> + SandboxRuntimeContext(SandboxConfiguration.of( + executionProfile, rules, emitters, definitionProviders, enableTracing, analysisConfiguration + )).use { + assertThat(runtimeCosts).areZero() + action(this) + } } } catch (exception: Throwable) { thrownException = exception } - }.apply { - start() - join() - } + }.join() throw thrownException ?: return } @@ -145,8 +158,12 @@ open class TestBase { /** * Create a new instance of a class using the sandbox class loader. */ - inline fun SandboxRuntimeContext.newCallable() = - classLoader.loadClassAndBytes(ClassSource.fromClassName(T::class.java.name), context) + inline fun SandboxRuntimeContext.newCallable(): LoadedClass = loadClass() + + inline fun SandboxRuntimeContext.loadClass(): LoadedClass = loadClass(T::class.jvmName) + + fun SandboxRuntimeContext.loadClass(className: String): LoadedClass = + classLoader.loadClassAndBytes(ClassSource.fromClassName(className), context) /** * Run the entry-point of the loaded [Callable] class. @@ -164,6 +181,10 @@ open class TestBase { /** * Stub visitor. */ - protected class Visitor : ClassVisitor(ClassAndMemberVisitor.API_VERSION) + protected class Writer : ClassWriter(COMPUTE_FRAMES or COMPUTE_MAXS) { + init { + assertEquals(ClassAndMemberVisitor.API_VERSION, api) + } + } } diff --git a/djvm/src/test/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitorTest.kt b/djvm/src/test/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitorTest.kt index fa5d9ffbf0..4bd8295927 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitorTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitorTest.kt @@ -21,7 +21,7 @@ class ClassAndMemberVisitorTest : TestBase() { @Test fun `can traverse classes`() { val classesVisited = mutableSetOf() - val visitor = object : ClassAndMemberVisitor() { + val visitor = object : ClassAndMemberVisitor(configuration, null) { override fun visitClass(clazz: ClassRepresentation): ClassRepresentation { classesVisited.add(clazz) return clazz @@ -47,7 +47,7 @@ class ClassAndMemberVisitorTest : TestBase() { @Test fun `can traverse fields`() { val membersVisited = mutableSetOf() - val visitor = object : ClassAndMemberVisitor() { + val visitor = object : ClassAndMemberVisitor(configuration, null) { override fun visitField(clazz: ClassRepresentation, field: Member): Member { membersVisited.add(field) return field @@ -77,7 +77,7 @@ class ClassAndMemberVisitorTest : TestBase() { @Test fun `can traverse methods`() { val membersVisited = mutableSetOf() - val visitor = object : ClassAndMemberVisitor() { + val visitor = object : ClassAndMemberVisitor(configuration, null) { override fun visitMethod(clazz: ClassRepresentation, method: Member): Member { membersVisited.add(method) return method @@ -102,7 +102,7 @@ class ClassAndMemberVisitorTest : TestBase() { @Test fun `can traverse class annotations`() { val annotations = mutableSetOf() - val visitor = object : ClassAndMemberVisitor() { + val visitor = object : ClassAndMemberVisitor(configuration, null) { override fun visitClassAnnotation(clazz: ClassRepresentation, descriptor: String) { annotations.add(descriptor) } @@ -118,9 +118,21 @@ class ClassAndMemberVisitorTest : TestBase() { private class TestClassWithAnnotations @Test - fun `can traverse member annotations`() { + fun `cannot traverse member annotations when reading`() { val annotations = mutableSetOf() - val visitor = object : ClassAndMemberVisitor() { + val visitor = object : ClassAndMemberVisitor(configuration, null) { + override fun visitMemberAnnotation(clazz: ClassRepresentation, member: Member, descriptor: String) { + annotations.add("${member.memberName}:$descriptor") + } + } + visitor.analyze(context) + assertThat(annotations).isEmpty() + } + + @Test + fun `can traverse member annotations when writing`() { + val annotations = mutableSetOf() + val visitor = object : ClassAndMemberVisitor(configuration, Writer()) { override fun visitMemberAnnotation(clazz: ClassRepresentation, member: Member, descriptor: String) { annotations.add("${member.memberName}:$descriptor") } @@ -146,7 +158,7 @@ class ClassAndMemberVisitorTest : TestBase() { @Test fun `can traverse class sources`() { val sources = mutableSetOf() - val visitor = object : ClassAndMemberVisitor() { + val visitor = object : ClassAndMemberVisitor(configuration, null) { override fun visitSource(clazz: ClassRepresentation, source: String) { sources.add(source) } @@ -160,9 +172,21 @@ class ClassAndMemberVisitorTest : TestBase() { } @Test - fun `can traverse instructions`() { + fun `does not traverse instructions when reading`() { val instructions = mutableSetOf>() - val visitor = object : ClassAndMemberVisitor() { + val visitor = object : ClassAndMemberVisitor(configuration, null) { + override fun visitInstruction(method: Member, emitter: EmitterModule, instruction: Instruction) { + instructions.add(Pair(method, instruction)) + } + } + visitor.analyze(context) + assertThat(instructions).isEmpty() + } + + @Test + fun `can traverse instructions when writing`() { + val instructions = mutableSetOf>() + val visitor = object : ClassAndMemberVisitor(configuration, Writer()) { override fun visitInstruction(method: Member, emitter: EmitterModule, instruction: Instruction) { instructions.add(Pair(method, instruction)) } diff --git a/djvm/src/test/kotlin/net/corda/djvm/analysis/ReferenceValidatorTest.kt b/djvm/src/test/kotlin/net/corda/djvm/analysis/ReferenceValidatorTest.kt deleted file mode 100644 index 816b232864..0000000000 --- a/djvm/src/test/kotlin/net/corda/djvm/analysis/ReferenceValidatorTest.kt +++ /dev/null @@ -1,66 +0,0 @@ -package net.corda.djvm.analysis - -import net.corda.djvm.TestBase -import net.corda.djvm.execution.SandboxedRunnable -import net.corda.djvm.validation.ReferenceValidator -import org.assertj.core.api.Assertions.assertThat -import org.junit.Test - -class ReferenceValidatorTest : TestBase() { - - private fun validator(whitelist: Whitelist = Whitelist.MINIMAL) = - ReferenceValidator(AnalysisConfiguration(whitelist)) - - @Test - fun `can validate when there are no references`() = analyze { context -> - analyze(context) - val (_, messages) = validator().validate(context, this) - assertThat(messages.count).isEqualTo(0) - } - - private class EmptyRunnable : SandboxedRunnable { - override fun run(input: Int): Int? { - return null - } - } - - @Test - fun `can validate when there are references`() = analyze { context -> - analyze(context) - analyze(context) - val (_, messages) = validator().validate(context, this) - assertThat(messages.count).isEqualTo(0) - } - - private class RunnableWithReferences : SandboxedRunnable { - override fun run(input: Int): Int? { - return TestRandom().nextInt() - } - } - - private class TestRandom { - external fun nextInt(): Int - } - - @Test - fun `can validate when there are transient references`() = analyze { context -> - analyze(context) - analyze(context) - analyze(context) - val (_, messages) = validator().validate(context, this) - assertThat(messages.count).isEqualTo(0) - } - - private class RunnableWithTransientReferences : SandboxedRunnable { - override fun run(input: Int): Int? { - return ReferencedClass().test() - } - } - - private class ReferencedClass { - fun test(): Int { - return TestRandom().nextInt() - } - } - -} diff --git a/djvm/src/test/kotlin/net/corda/djvm/assertions/AssertiveClassWithByteCode.kt b/djvm/src/test/kotlin/net/corda/djvm/assertions/AssertiveClassWithByteCode.kt index 457b71c96f..cc122b7a8f 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/assertions/AssertiveClassWithByteCode.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/assertions/AssertiveClassWithByteCode.kt @@ -1,27 +1,31 @@ package net.corda.djvm.assertions import net.corda.djvm.rewiring.LoadedClass -import org.assertj.core.api.Assertions +import org.assertj.core.api.Assertions.* class AssertiveClassWithByteCode(private val loadedClass: LoadedClass) { fun isSandboxed(): AssertiveClassWithByteCode { - Assertions.assertThat(loadedClass.type.name).startsWith("sandbox.") + assertThat(loadedClass.type.name).startsWith("sandbox.") return this } fun hasNotBeenModified(): AssertiveClassWithByteCode { - Assertions.assertThat(loadedClass.byteCode.isModified) + assertThat(loadedClass.byteCode.isModified) .`as`("Byte code has been modified") .isEqualTo(false) return this } fun hasBeenModified(): AssertiveClassWithByteCode { - Assertions.assertThat(loadedClass.byteCode.isModified) - .`as`("Byte code has been modified") + assertThat(loadedClass.byteCode.isModified) + .`as`("Byte code has not been modified") .isEqualTo(true) return this } + fun hasClassName(className: String): AssertiveClassWithByteCode { + assertThat(loadedClass.type.name).isEqualTo(className) + return this + } } diff --git a/djvm/src/test/kotlin/net/corda/djvm/assertions/AssertiveReferenceMap.kt b/djvm/src/test/kotlin/net/corda/djvm/assertions/AssertiveReferenceMap.kt index 4168e318db..5b349edbe3 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/assertions/AssertiveReferenceMap.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/assertions/AssertiveReferenceMap.kt @@ -10,7 +10,7 @@ open class AssertiveReferenceMap(private val references: ReferenceMap) { fun hasCount(count: Int): AssertiveReferenceMap { val allReferences = references.joinToString("\n") { " - $it" } - Assertions.assertThat(references.size) + Assertions.assertThat(references.numberOfReferences) .overridingErrorMessage("Expected $count reference(s), found:\n$allReferences") .isEqualTo(count) return this diff --git a/djvm/src/test/kotlin/net/corda/djvm/code/ClassMutatorTest.kt b/djvm/src/test/kotlin/net/corda/djvm/code/ClassMutatorTest.kt index c5dc170b9c..d729d9de2f 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/code/ClassMutatorTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/code/ClassMutatorTest.kt @@ -21,7 +21,7 @@ class ClassMutatorTest : TestBase() { } } val context = context - val mutator = ClassMutator(Visitor(), configuration, listOf(definitionProvider)) + val mutator = ClassMutator(Writer(), configuration, listOf(definitionProvider)) mutator.analyze(context) assertThat(hasProvidedDefinition).isTrue() assertThat(context.classes.get().access or ACC_STRICT).isNotEqualTo(0) @@ -39,7 +39,7 @@ class ClassMutatorTest : TestBase() { } } val context = context - val mutator = ClassMutator(Visitor(), configuration, listOf(definitionProvider)) + val mutator = ClassMutator(Writer(), configuration, listOf(definitionProvider)) mutator.analyze(context) assertThat(hasProvidedDefinition).isTrue() for (member in context.classes.get().members.values) { @@ -67,7 +67,7 @@ class ClassMutatorTest : TestBase() { } } val context = context - val mutator = ClassMutator(Visitor(), configuration, emitters = listOf(emitter)) + val mutator = ClassMutator(Writer(), configuration, emitters = listOf(emitter)) mutator.analyze(context) assertThat(hasEmittedCode).isTrue() assertThat(shouldPreventDefault).isTrue() diff --git a/djvm/src/test/kotlin/net/corda/djvm/code/EmitterModuleTest.kt b/djvm/src/test/kotlin/net/corda/djvm/code/EmitterModuleTest.kt index 0289fbd4d9..fb37676669 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/code/EmitterModuleTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/code/EmitterModuleTest.kt @@ -7,6 +7,7 @@ import org.junit.Test import org.objectweb.asm.ClassVisitor import org.objectweb.asm.MethodVisitor import org.objectweb.asm.Opcodes.NEW +import org.objectweb.asm.Type class EmitterModuleTest : TestBase() { @@ -14,15 +15,15 @@ class EmitterModuleTest : TestBase() { fun `can emit code to method body`() { var hasEmittedTypeInstruction = false val methodVisitor = object : MethodVisitor(ClassAndMemberVisitor.API_VERSION) { - override fun visitTypeInsn(opcode: Int, type: String?) { - if (opcode == NEW && type == java.lang.String::class.java.name) { + override fun visitTypeInsn(opcode: Int, type: String) { + if (opcode == NEW && type == Type.getInternalName(java.lang.String::class.java)) { hasEmittedTypeInstruction = true } } } val visitor = object : ClassVisitor(ClassAndMemberVisitor.API_VERSION) { override fun visitMethod( - access: Int, name: String?, descriptor: String?, signature: String?, exceptions: Array? + access: Int, name: String, descriptor: String, signature: String?, exceptions: Array? ): MethodVisitor { return methodVisitor } diff --git a/djvm/src/test/kotlin/net/corda/djvm/costing/RuntimeCostTest.kt b/djvm/src/test/kotlin/net/corda/djvm/costing/RuntimeCostTest.kt index e44fb883db..0e68806d33 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/costing/RuntimeCostTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/costing/RuntimeCostTest.kt @@ -3,12 +3,13 @@ package net.corda.djvm.costing import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.junit.Test +import sandbox.net.corda.djvm.costing.ThresholdViolationError class RuntimeCostTest { @Test fun `can increment cost`() { - val cost = RuntimeCost(10, { "failed" }) + val cost = RuntimeCost(10) { "failed" } cost.increment(1) assertThat(cost.value).isEqualTo(1) } @@ -16,8 +17,8 @@ class RuntimeCostTest { @Test fun `cannot increment cost beyond threshold`() { Thread { - val cost = RuntimeCost(10, { "failed in ${it.name}" }) - assertThatExceptionOfType(ThresholdViolationException::class.java) + val cost = RuntimeCost(10) { "failed in ${it.name}" } + assertThatExceptionOfType(ThresholdViolationError::class.java) .isThrownBy { cost.increment(11) } .withMessage("failed in Foo") assertThat(cost.value).isEqualTo(11) diff --git a/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxExecutorTest.kt b/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxExecutorTest.kt index a677b4fd35..92fe59e159 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxExecutorTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxExecutorTest.kt @@ -6,13 +6,15 @@ import foo.bar.sandbox.toNumber import net.corda.djvm.TestBase import net.corda.djvm.analysis.Whitelist import net.corda.djvm.assertions.AssertionExtensions.withProblem -import net.corda.djvm.costing.ThresholdViolationException 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.net.corda.djvm.costing.ThresholdViolationError +import sandbox.net.corda.djvm.rules.RuleViolationError import java.nio.file.Files import java.util.* +import java.util.function.Function class SandboxExecutorTest : TestBase() { @@ -24,8 +26,8 @@ class SandboxExecutorTest : TestBase() { assertThat(result).isEqualTo("sandbox") } - class TestSandboxedRunnable : SandboxedRunnable { - override fun run(input: Int): String? { + class TestSandboxedRunnable : Function { + override fun apply(input: Int): String { return "sandbox" } } @@ -42,8 +44,8 @@ class SandboxExecutorTest : TestBase() { .withMessageContaining("Contract constraint violated") } - class Contract : SandboxedRunnable { - override fun run(input: Transaction?) { + class Contract : Function { + override fun apply(input: Transaction?) { throw IllegalArgumentException("Contract constraint violated") } } @@ -58,12 +60,12 @@ class SandboxExecutorTest : TestBase() { assertThat(result).isEqualTo(0xfed_c0de + 2) } - class TestObjectHashCode : SandboxedRunnable { - override fun run(input: Int): Int? { + class TestObjectHashCode : Function { + override fun apply(input: Int): Int { val obj = Object() val hash1 = obj.hashCode() val hash2 = obj.hashCode() - assert(hash1 == hash2) + require(hash1 == hash2) return Object().hashCode() } } @@ -76,8 +78,8 @@ class SandboxExecutorTest : TestBase() { assertThat(result).isEqualTo(0xfed_c0de + 1) } - class TestObjectHashCodeWithHierarchy : SandboxedRunnable { - override fun run(input: Int): Int? { + class TestObjectHashCodeWithHierarchy : Function { + override fun apply(input: Int): Int { val obj = MyObject() return obj.hashCode() } @@ -91,9 +93,9 @@ class SandboxExecutorTest : TestBase() { .withMessageContaining("terminated due to excessive use of looping") } - class TestThresholdBreach : SandboxedRunnable { + class TestThresholdBreach : Function { private var x = 0 - override fun run(input: Int): Int? { + override fun apply(input: Int): Int { for (i in 0..1_000_000) { x += 1 } @@ -109,8 +111,8 @@ class SandboxExecutorTest : TestBase() { .withCauseInstanceOf(StackOverflowError::class.java) } - class TestStackOverflow : SandboxedRunnable { - override fun run(input: Int): Int? { + class TestStackOverflow : Function { + override fun apply(input: Int): Int { return a() } @@ -124,11 +126,12 @@ class SandboxExecutorTest : TestBase() { val contractExecutor = DeterministicSandboxExecutor(configuration) assertThatExceptionOfType(SandboxException::class.java) .isThrownBy { contractExecutor.run(0) } - .withMessageContaining("java/util/Random.(): Disallowed reference to reflection API; sun.misc.Unsafe.getUnsafe()") + .withCauseInstanceOf(RuleViolationError::class.java) + .withMessageContaining("Disallowed reference to reflection API") } - class TestKotlinMetaClasses : SandboxedRunnable { - override fun run(input: Int): Int? { + class TestKotlinMetaClasses : Function { + override fun apply(input: Int): Int { val someNumber = testRandom() return "12345".toNumber() * someNumber } @@ -139,30 +142,32 @@ class SandboxExecutorTest : TestBase() { val contractExecutor = DeterministicSandboxExecutor(configuration) assertThatExceptionOfType(SandboxException::class.java) .isThrownBy { contractExecutor.run(0) } - .withCauseInstanceOf(SandboxClassLoadingException::class.java) - .withProblem("java/util/Random.(): Disallowed reference to reflection API; sun.misc.Unsafe.getUnsafe()") + .withCauseInstanceOf(RuleViolationError::class.java) + .withProblem("Disallowed reference to reflection API") } - class TestNonDeterministicCode : SandboxedRunnable { - override fun run(input: Int): Int? { + class TestNonDeterministicCode : Function { + override fun apply(input: Int): Int { return Random().nextInt() } } @Test fun `cannot execute runnable that catches ThreadDeath`() = sandbox(DEFAULT) { + TestCatchThreadDeath().apply { + assertThat(apply(0)).isEqualTo(1) + } + val contractExecutor = DeterministicSandboxExecutor(configuration) assertThatExceptionOfType(SandboxException::class.java) .isThrownBy { contractExecutor.run(0) } - .withCauseInstanceOf(SandboxClassLoadingException::class.java) - .withMessageContaining("Disallowed catch of ThreadDeath exception") - .withMessageContaining(TestCatchThreadDeath::class.java.simpleName) + .withCauseExactlyInstanceOf(ThreadDeath::class.java) } - class TestCatchThreadDeath : SandboxedRunnable { - override fun run(input: Int): Int? { + class TestCatchThreadDeath : Function { + override fun apply(input: Int): Int { return try { - 0 + throw ThreadDeath() } catch (exception: ThreadDeath) { 1 } @@ -170,20 +175,46 @@ class SandboxExecutorTest : TestBase() { } @Test - fun `cannot execute runnable that catches ThresholdViolationException`() = sandbox(DEFAULT) { + fun `cannot execute runnable that catches ThresholdViolationError`() = sandbox(DEFAULT) { + TestCatchThresholdViolationError().apply { + assertThat(apply(0)).isEqualTo(1) + } + val contractExecutor = DeterministicSandboxExecutor(configuration) assertThatExceptionOfType(SandboxException::class.java) - .isThrownBy { contractExecutor.run(0) } - .withCauseInstanceOf(SandboxClassLoadingException::class.java) - .withMessageContaining("Disallowed catch of threshold violation exception") - .withMessageContaining(TestCatchThresholdViolationException::class.java.simpleName) + .isThrownBy { contractExecutor.run(0) } + .withCauseExactlyInstanceOf(ThresholdViolationError::class.java) + .withMessageContaining("Can't catch this!") } - class TestCatchThresholdViolationException : SandboxedRunnable { - override fun run(input: Int): Int? { + class TestCatchThresholdViolationError : Function { + override fun apply(input: Int): Int { return try { - 0 - } catch (exception: ThresholdViolationException) { + throw ThresholdViolationError("Can't catch this!") + } catch (exception: ThresholdViolationError) { + 1 + } + } + } + + @Test + fun `cannot execute runnable that catches RuleViolationError`() = sandbox(DEFAULT) { + TestCatchRuleViolationError().apply { + assertThat(apply(0)).isEqualTo(1) + } + + val contractExecutor = DeterministicSandboxExecutor(configuration) + assertThatExceptionOfType(SandboxException::class.java) + .isThrownBy { contractExecutor.run(0) } + .withCauseExactlyInstanceOf(RuleViolationError::class.java) + .withMessageContaining("Can't catch this!") + } + + class TestCatchRuleViolationError : Function { + override fun apply(input: Int): Int { + return try { + throw RuleViolationError("Can't catch this!") + } catch (exception: RuleViolationError) { 1 } } @@ -209,12 +240,12 @@ class SandboxExecutorTest : TestBase() { fun `cannot catch ThreadDeath`() = sandbox(DEFAULT) { val contractExecutor = DeterministicSandboxExecutor(configuration) assertThatExceptionOfType(SandboxException::class.java) - .isThrownBy { contractExecutor.run(3) } + .isThrownBy { contractExecutor.run(3) } .withCauseInstanceOf(ThreadDeath::class.java) } - class TestCatchThrowableAndError : SandboxedRunnable { - override fun run(input: Int): Int? { + class TestCatchThrowableAndError : Function { + override fun apply(input: Int): Int { return try { when (input) { 1 -> throw Throwable() @@ -229,13 +260,27 @@ class SandboxExecutorTest : TestBase() { } } - class TestCatchThrowableErrorAndThreadDeath : SandboxedRunnable { - override fun run(input: Int): Int? { + class TestCatchThrowableErrorsAndThreadDeath : Function { + override fun apply(input: Int): Int { return try { when (input) { 1 -> throw Throwable() 2 -> throw Error() - 3 -> throw ThreadDeath() + 3 -> try { + throw ThreadDeath() + } catch (ex: ThreadDeath) { + 3 + } + 4 -> try { + throw StackOverflowError("FAKE OVERFLOW!") + } catch (ex: StackOverflowError) { + 4 + } + 5 -> try { + throw OutOfMemoryError("FAKE OOM!") + } catch (ex: OutOfMemoryError) { + 5 + } else -> 0 } } catch (exception: Error) { @@ -246,6 +291,24 @@ class SandboxExecutorTest : TestBase() { } } + @Test + fun `cannot catch stack-overflow error`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor(configuration) + assertThatExceptionOfType(SandboxException::class.java) + .isThrownBy { contractExecutor.run(4) } + .withCauseInstanceOf(StackOverflowError::class.java) + .withMessageContaining("FAKE OVERFLOW!") + } + + @Test + fun `cannot catch out-of-memory error`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor(configuration) + assertThatExceptionOfType(SandboxException::class.java) + .isThrownBy { contractExecutor.run(5) } + .withCauseInstanceOf(OutOfMemoryError::class.java) + .withMessageContaining("FAKE OOM!") + } + @Test fun `cannot persist state across sessions`() = sandbox(DEFAULT) { val contractExecutor = DeterministicSandboxExecutor(configuration) @@ -256,8 +319,8 @@ class SandboxExecutorTest : TestBase() { .isEqualTo(1) } - class TestStatePersistence : SandboxedRunnable { - override fun run(input: Int): Int? { + class TestStatePersistence : Function { + override fun apply(input: Int): Int { ReferencedClass.value += 1 return ReferencedClass.value } @@ -274,11 +337,11 @@ class SandboxExecutorTest : TestBase() { assertThatExceptionOfType(SandboxException::class.java) .isThrownBy { contractExecutor.run(0) } .withCauseInstanceOf(SandboxClassLoadingException::class.java) - .withMessageContaining("Files.walk(Path, Integer, FileVisitOption[]): Disallowed dynamic invocation in method") + .withMessageContaining("Class file not found; java/nio/file/Files.class") } - class TestIO : SandboxedRunnable { - override fun run(input: Int): Int? { + class TestIO : Function { + override fun apply(input: Int): Int { val file = Files.createTempFile("test", ".dat") Files.newBufferedWriter(file).use { it.write("Hello world!") @@ -292,14 +355,13 @@ class SandboxExecutorTest : TestBase() { val contractExecutor = DeterministicSandboxExecutor(configuration) assertThatExceptionOfType(SandboxException::class.java) .isThrownBy { contractExecutor.run(0) } - .withCauseInstanceOf(SandboxClassLoadingException::class.java) - .withMessageContaining("Disallowed reference to reflection API") + .withCauseInstanceOf(RuleViolationError::class.java) + .withMessageContaining("Disallowed reference to API;") .withMessageContaining("java.lang.Class.newInstance()") - .withMessageContaining("java.lang.reflect.Method.invoke(Object, Object[])") } - class TestReflection : SandboxedRunnable { - override fun run(input: Int): Int? { + class TestReflection : Function { + override fun apply(input: Int): Int { val clazz = Object::class.java val obj = clazz.newInstance() val result = clazz.methods.first().invoke(obj) @@ -307,4 +369,150 @@ class SandboxExecutorTest : TestBase() { } } + @Test + fun `can load and execute code that uses notify()`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor(configuration) + assertThatExceptionOfType(SandboxException::class.java) + .isThrownBy { contractExecutor.run(1) } + .withCauseInstanceOf(RuleViolationError::class.java) + .withMessageContaining("Disallowed reference to API;") + .withMessageContaining("java.lang.Object.notify()") + } + + @Test + fun `can load and execute code that uses notifyAll()`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor(configuration) + assertThatExceptionOfType(SandboxException::class.java) + .isThrownBy { contractExecutor.run(2) } + .withCauseInstanceOf(RuleViolationError::class.java) + .withMessageContaining("Disallowed reference to API;") + .withMessageContaining("java.lang.Object.notifyAll()") + } + + @Test + fun `can load and execute code that uses wait()`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor(configuration) + assertThatExceptionOfType(SandboxException::class.java) + .isThrownBy { contractExecutor.run(3) } + .withCauseInstanceOf(RuleViolationError::class.java) + .withMessageContaining("Disallowed reference to API;") + .withMessageContaining("java.lang.Object.wait()") + } + + @Test + fun `can load and execute code that uses wait(long)`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor(configuration) + assertThatExceptionOfType(SandboxException::class.java) + .isThrownBy { contractExecutor.run(4) } + .withCauseInstanceOf(RuleViolationError::class.java) + .withMessageContaining("Disallowed reference to API;") + .withMessageContaining("java.lang.Object.wait(Long)") + } + + @Test + fun `can load and execute code that uses wait(long,int)`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor(configuration) + assertThatExceptionOfType(SandboxException::class.java) + .isThrownBy { contractExecutor.run(5) } + .withCauseInstanceOf(RuleViolationError::class.java) + .withMessageContaining("Disallowed reference to API;") + .withMessageContaining("java.lang.Object.wait(Long, Integer)") + } + + @Test + fun `code after forbidden APIs is intact`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor(configuration) + assertThat(contractExecutor.run(0).result) + .isEqualTo("unknown") + } + + class TestMonitors : Function { + override fun apply(input: Int): String { + return synchronized(this) { + @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") + val javaObject = this as java.lang.Object + when(input) { + 1 -> { + javaObject.notify() + "notify" + } + 2 -> { + javaObject.notifyAll() + "notifyAll" + } + 3 -> { + javaObject.wait() + "wait" + } + 4 -> { + javaObject.wait(100) + "wait(100)" + } + 5 -> { + javaObject.wait(100, 10) + "wait(100, 10)" + } + else -> "unknown" + } + } + } + } + + @Test + fun `can load and execute code that has a native method`() = sandbox(DEFAULT) { + assertThatExceptionOfType(UnsatisfiedLinkError::class.java) + .isThrownBy { TestNativeMethod().apply(0) } + .withMessageContaining("TestNativeMethod.evilDeeds()I") + + val contractExecutor = DeterministicSandboxExecutor(configuration) + assertThatExceptionOfType(SandboxException::class.java) + .isThrownBy { contractExecutor.run(0) } + .withCauseInstanceOf(RuleViolationError::class.java) + .withMessageContaining("Native method has been deleted") + } + + class TestNativeMethod : Function { + override fun apply(input: Int): Int { + return evilDeeds() + } + + private external fun evilDeeds(): Int + } + + @Test + fun `check arrays still work`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor>(configuration) + contractExecutor.run(100).apply { + assertThat(result).isEqualTo(arrayOf(100)) + } + } + + class TestArray : Function> { + override fun apply(input: Int): Array { + return listOf(input).toTypedArray() + } + } + + @Test + fun `can load and execute class that has finalize`() = sandbox(DEFAULT) { + assertThatExceptionOfType(UnsupportedOperationException::class.java) + .isThrownBy { TestFinalizeMethod().apply(100) } + .withMessageContaining("Very Bad Thing") + + val contractExecutor = DeterministicSandboxExecutor(configuration) + contractExecutor.run(100).apply { + assertThat(result).isEqualTo(100) + } + } + + class TestFinalizeMethod : Function { + override fun apply(input: Int): Int { + finalize() + return input + } + + private fun finalize() { + throw UnsupportedOperationException("Very Bad Thing") + } + } } diff --git a/djvm/src/test/kotlin/net/corda/djvm/references/MemberModuleTest.kt b/djvm/src/test/kotlin/net/corda/djvm/references/MemberModuleTest.kt index 616a7ca7b6..71cd99cbf7 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/references/MemberModuleTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/references/MemberModuleTest.kt @@ -4,6 +4,7 @@ import net.corda.djvm.annotations.NonDeterministic import org.assertj.core.api.Assertions.assertThat import org.jetbrains.annotations.NotNull import org.junit.Test +import org.objectweb.asm.Type class MemberModuleTest { @@ -132,7 +133,7 @@ class MemberModuleTest { } private val java.lang.Class<*>.descriptor: String - get() = "L${name.replace('.', '/')};" + get() = Type.getDescriptor(this) private fun member(member: String) = MemberReference("", member, "") diff --git a/djvm/src/test/kotlin/net/corda/djvm/rewiring/ClassRewriterTest.kt b/djvm/src/test/kotlin/net/corda/djvm/rewiring/ClassRewriterTest.kt index 6abcca8e21..bd7e86dac0 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/rewiring/ClassRewriterTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/rewiring/ClassRewriterTest.kt @@ -6,10 +6,11 @@ import foo.bar.sandbox.Empty import foo.bar.sandbox.StrictFloat import net.corda.djvm.TestBase import net.corda.djvm.assertions.AssertionExtensions.assertThat -import net.corda.djvm.costing.ThresholdViolationException import net.corda.djvm.execution.ExecutionProfile import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.junit.Test +import sandbox.net.corda.djvm.costing.ThresholdViolationError +import java.nio.file.Paths class ClassRewriterTest : TestBase() { @@ -45,7 +46,7 @@ class ClassRewriterTest : TestBase() { val callable = newCallable() assertThat(callable).hasBeenModified() assertThat(callable).isSandboxed() - assertThatExceptionOfType(ThresholdViolationException::class.java).isThrownBy { + assertThatExceptionOfType(ThresholdViolationError::class.java).isThrownBy { callable.createAndInvoke() }.withMessageContaining("terminated due to excessive use of looping") assertThat(runtimeCosts) @@ -61,4 +62,44 @@ class ClassRewriterTest : TestBase() { callable.createAndInvoke() } + @Test + fun `can load a Java API that still exists in Java runtime`() = sandbox(DEFAULT) { + assertThat(loadClass>()) + .hasClassName("sandbox.java.util.List") + .hasBeenModified() + } + + @Test + fun `cannot load a Java API that was deleted from Java runtime`() = sandbox(DEFAULT) { + assertThatExceptionOfType(SandboxClassLoadingException::class.java) + .isThrownBy { loadClass() } + .withMessageContaining("Class file not found; java/nio/file/Paths.class") + } + + @Test + fun `load internal Sun class that still exists in Java runtime`() = sandbox(DEFAULT) { + assertThat(loadClass()) + .hasClassName("sandbox.sun.misc.Unsafe") + .hasBeenModified() + } + + @Test + fun `cannot load internal Sun class that was deleted from Java runtime`() = sandbox(DEFAULT) { + assertThatExceptionOfType(SandboxClassLoadingException::class.java) + .isThrownBy { loadClass() } + .withMessageContaining("Class file not found; sun/misc/Timer.class") + } + + @Test + fun `can load local class`() = sandbox(DEFAULT) { + assertThat(loadClass()) + .hasClassName("sandbox.net.corda.djvm.rewiring.ClassRewriterTest\$Example") + .hasBeenModified() + } + + class Example : java.util.function.Function { + override fun apply(input: Int): Int { + return input + } + } } diff --git a/djvm/src/test/kotlin/net/corda/djvm/rules/ReferenceExtractorTest.kt b/djvm/src/test/kotlin/net/corda/djvm/rules/ReferenceExtractorTest.kt index 3ea655c24f..bde75b7feb 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/rules/ReferenceExtractorTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/rules/ReferenceExtractorTest.kt @@ -4,23 +4,11 @@ import foo.bar.sandbox.Callable import net.corda.djvm.TestBase import net.corda.djvm.assertions.AssertionExtensions.assertThat import org.junit.Test +import org.objectweb.asm.Type import java.util.* class ReferenceExtractorTest : TestBase() { - @Test - fun `can find method references`() = validate { context -> - assertThat(context.references) - .hasClass("java/util/Random") - .withLocationCount(1) - .hasMember("java/lang/Object", "", "()V") - .withLocationCount(1) - .hasMember("java/util/Random", "", "()V") - .withLocationCount(1) - .hasMember("java/util/Random", "nextInt", "()I") - .withLocationCount(1) - } - class A : Callable { override fun call() { synchronized(this) { @@ -29,25 +17,10 @@ class ReferenceExtractorTest : TestBase() { } } - @Test - fun `can find field references`() = validate { context -> - assertThat(context.references) - .hasMember(B::class.java.name.replace('.', '/'), "foo", "Ljava/lang/String;") - } - - class B { - @JvmField - val foo: String = "" - - fun test(): String { - return foo - } - } - @Test fun `can find class references`() = validate { context -> assertThat(context.references) - .hasClass(A::class.java.name.replace('.', '/')) + .hasClass(Type.getInternalName(A::class.java)) } class C { diff --git a/djvm/src/test/kotlin/net/corda/djvm/rules/RuleValidatorTest.kt b/djvm/src/test/kotlin/net/corda/djvm/rules/RuleValidatorTest.kt index 260cfc6488..cd37df166a 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/rules/RuleValidatorTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/rules/RuleValidatorTest.kt @@ -42,8 +42,7 @@ class RuleValidatorTest : TestBase() { assertThat(context.messages) .hasErrorCount(0) .hasWarningCount(0) - .hasInfoCount(1) - .withMessage("Stripped monitoring instruction") + .hasInfoCount(0) .hasTraceCount(4) .withMessage("Synchronization specifier will be ignored") .withMessage("Strict floating-point arithmetic will be applied") diff --git a/djvm/src/test/kotlin/net/corda/djvm/source/SourceClassLoaderTest.kt b/djvm/src/test/kotlin/net/corda/djvm/source/SourceClassLoaderTest.kt index 3837948678..85f4596f1f 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/source/SourceClassLoaderTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/source/SourceClassLoaderTest.kt @@ -67,7 +67,7 @@ class SourceClassLoaderTest { val (first, second) = this val directory = first.parent val classLoader = SourceClassLoader(listOf(directory), classResolver) - assertThat(classLoader.resolvedUrls).anySatisfy { + assertThat(classLoader.urLs).anySatisfy { assertThat(it).isEqualTo(first.toUri().toURL()) }.anySatisfy { assertThat(it).isEqualTo(second.toUri().toURL()) diff --git a/djvm/src/test/resources/log4j2-test.xml b/djvm/src/test/resources/log4j2-test.xml new file mode 100644 index 0000000000..b12cea5b2d --- /dev/null +++ b/djvm/src/test/resources/log4j2-test.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/djvm/src/test/resources/log4j2.xml b/djvm/src/test/resources/log4j2.xml deleted file mode 100644 index 93e84b6252..0000000000 --- a/djvm/src/test/resources/log4j2.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/source/api-flows.rst b/docs/source/api-flows.rst index 5e15223144..d001529ecd 100644 --- a/docs/source/api-flows.rst +++ b/docs/source/api-flows.rst @@ -219,13 +219,13 @@ There are several ways to retrieve a notary from the network map: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 01 :end-before: DOCEND 01 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 01 :end-before: DOCEND 01 @@ -237,13 +237,13 @@ We can also use the network map to retrieve a specific counterparty: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 02 :end-before: DOCEND 02 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 02 :end-before: DOCEND 02 @@ -281,13 +281,13 @@ InitiateFlow .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART initiateFlow :end-before: DOCEND initiateFlow :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART initiateFlow :end-before: DOCEND initiateFlow @@ -306,13 +306,13 @@ Once we have a ``FlowSession`` object we can send arbitrary data to a counterpar .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 04 :end-before: DOCEND 04 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 04 :end-before: DOCEND 04 @@ -338,13 +338,13 @@ be what it appears to be! We must unwrap the ``UntrustworthyData`` using a lambd .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 05 :end-before: DOCEND 05 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 05 :end-before: DOCEND 05 @@ -355,13 +355,13 @@ as it likes, and each party can invoke a different response flow: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 06 :end-before: DOCEND 06 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 06 :end-before: DOCEND 06 @@ -380,13 +380,13 @@ type of data sent doesn't need to match the type of the data received back: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 07 :end-before: DOCEND 07 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 07 :end-before: DOCEND 07 @@ -405,13 +405,13 @@ Our side of the flow must mirror these calls. We could do this as follows: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 08 :end-before: DOCEND 08 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 08 :end-before: DOCEND 08 @@ -431,11 +431,16 @@ Consider the following contrived example using the old ``Party`` based API: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/LaunchSpaceshipFlow.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/LaunchSpaceshipFlow.kt :language: kotlin :start-after: DOCSTART LaunchSpaceshipFlow :end-before: DOCEND LaunchSpaceshipFlow + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/LaunchSpaceshipFlow.java + :language: java + :start-after: DOCSTART LaunchSpaceshipFlow + :end-before: DOCEND LaunchSpaceshipFlow + The intention of the flows is very clear: LaunchSpaceshipFlow asks the president whether a spaceship should be launched. It is expecting a boolean reply. The president in return first tells the secretary that they need coffee, which is also communicated with a boolean. Afterwards the president replies to the launcher that they don't want to launch. @@ -452,11 +457,16 @@ of flows would look like this: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/LaunchSpaceshipFlow.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/LaunchSpaceshipFlow.kt :language: kotlin :start-after: DOCSTART LaunchSpaceshipFlowCorrect :end-before: DOCEND LaunchSpaceshipFlowCorrect + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/LaunchSpaceshipFlow.java + :language: java + :start-after: DOCSTART LaunchSpaceshipFlowCorrect + :end-before: DOCEND LaunchSpaceshipFlowCorrect + Note how the president is now explicit about which session it wants to send to. Porting from the old Party-based API @@ -467,13 +477,13 @@ explicit in the ``initiateFlow`` function call. To port existing code: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART FlowSession porting :end-before: DOCEND FlowSession porting :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART FlowSession porting :end-before: DOCEND FlowSession porting @@ -550,13 +560,13 @@ the transaction's states: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 09 :end-before: DOCEND 09 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 09 :end-before: DOCEND 09 @@ -566,13 +576,13 @@ We can also choose to send the transaction to additional parties who aren't one .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 10 :end-before: DOCEND 10 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 10 :end-before: DOCEND 10 @@ -601,13 +611,13 @@ transaction ourselves, we can automatically gather the signatures of the other r .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 15 :end-before: DOCEND 15 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 15 :end-before: DOCEND 15 @@ -618,13 +628,13 @@ transaction (by implementing the ``checkTransaction`` method) and provide their .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 16 :end-before: DOCEND 16 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 16 :end-before: DOCEND 16 @@ -647,13 +657,13 @@ transaction data vending requests as the receiver walks the dependency chain usi .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 12 :end-before: DOCEND 12 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 12 :end-before: DOCEND 12 @@ -664,13 +674,13 @@ dependencies and verify the transaction: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 13 :end-before: DOCEND 13 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 13 :end-before: DOCEND 13 @@ -680,13 +690,13 @@ We can also send and receive a ``StateAndRef`` dependency chain and automaticall .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 14 :end-before: DOCEND 14 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 14 :end-before: DOCEND 14 @@ -738,13 +748,13 @@ To provide a progress tracker, we have to override ``FlowLogic.progressTracker`` .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 17 :end-before: DOCEND 17 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 17 :end-before: DOCEND 17 @@ -754,13 +764,13 @@ We then update the progress tracker's current step as we progress through the fl .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 18 :end-before: DOCEND 18 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 18 :end-before: DOCEND 18 diff --git a/docs/source/api-persistence.rst b/docs/source/api-persistence.rst index 67d1bd2767..8fabd37e17 100644 --- a/docs/source/api-persistence.rst +++ b/docs/source/api-persistence.rst @@ -136,14 +136,14 @@ JDBC session's can be used in Flows and Service Plugins (see ":doc:`flow-state-m The following example illustrates the creation of a custom corda service using a jdbcSession: -.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/CustomVaultQuery.kt +.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/CustomVaultQuery.kt :language: kotlin :start-after: DOCSTART CustomVaultQuery :end-before: DOCEND CustomVaultQuery which is then referenced within a custom flow: -.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/CustomVaultQuery.kt +.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/CustomVaultQuery.kt :language: kotlin :start-after: DOCSTART TopupIssuer :end-before: DOCEND TopupIssuer diff --git a/docs/source/api-testing.rst b/docs/source/api-testing.rst index 78bf5e828f..80b8134827 100644 --- a/docs/source/api-testing.rst +++ b/docs/source/api-testing.rst @@ -315,13 +315,13 @@ You can create dummy identities to use in test transactions using the ``TestIden .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 14 :end-before: DOCEND 14 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java :language: java :start-after: DOCSTART 14 :end-before: DOCEND 14 @@ -368,13 +368,13 @@ construct and check transactions. .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 11 :end-before: DOCEND 11 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java :language: java :start-after: DOCSTART 11 :end-before: DOCEND 11 @@ -388,13 +388,13 @@ for you, using all the given identities. .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 12 :end-before: DOCEND 12 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java :language: java :start-after: DOCSTART 12 :end-before: DOCEND 12 @@ -411,13 +411,13 @@ transaction has been executed, and any ``attachments``, as shown in this example .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 13 :end-before: DOCEND 13 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java :language: java :start-after: DOCSTART 13 :end-before: DOCEND 13 @@ -434,13 +434,13 @@ verifying the message, there is also a ``fails`` method. .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 4 :end-before: DOCEND 4 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java :language: java :start-after: DOCSTART 4 :end-before: DOCEND 4 @@ -459,13 +459,13 @@ add the relevant output state and check that the contract verifies successfully, .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 5 :end-before: DOCEND 5 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java :language: java :start-after: DOCSTART 5 :end-before: DOCEND 5 @@ -476,13 +476,13 @@ and then return to the original, unmodified transaction. As in the following exa .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 7 :end-before: DOCEND 7 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java :language: java :start-after: DOCSTART 7 :end-before: DOCEND 7 @@ -500,13 +500,13 @@ be verified separately by placing a ``verifies`` or ``fails`` statement within .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt + .. literalinclude:: ../../docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt :language: kotlin :start-after: DOCSTART 9 :end-before: DOCEND 9 :dedent: 4 - .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java + .. literalinclude:: ../../docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java :language: java :start-after: DOCSTART 9 :end-before: DOCEND 9 diff --git a/docs/source/api-transactions.rst b/docs/source/api-transactions.rst index 0dfd5863a9..3c286cc831 100644 --- a/docs/source/api-transactions.rst +++ b/docs/source/api-transactions.rst @@ -56,13 +56,13 @@ An input state is added to a transaction as a ``StateAndRef``, which combines: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 21 :end-before: DOCEND 21 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 21 :end-before: DOCEND 21 @@ -75,13 +75,13 @@ A ``StateRef`` uniquely identifies an input state, allowing the notary to mark i .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 20 :end-before: DOCEND 20 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 20 :end-before: DOCEND 20 @@ -111,13 +111,13 @@ obtained from a ``StateAndRef`` by calling the ``StateAndRef.referenced()`` meth .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 55 :end-before: DOCEND 55 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 55 :end-before: DOCEND 55 @@ -146,13 +146,13 @@ add them to the transaction directly: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 22 :end-before: DOCEND 22 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 22 :end-before: DOCEND 22 @@ -163,13 +163,13 @@ it on the input state: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 23 :end-before: DOCEND 23 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 23 :end-before: DOCEND 23 @@ -183,13 +183,13 @@ wrapping the output state in a ``StateAndContract``, which combines: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 47 :end-before: DOCEND 47 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 47 :end-before: DOCEND 47 @@ -204,13 +204,13 @@ A command is added to the transaction as a ``Command``, which combines: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 24 :end-before: DOCEND 24 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 24 :end-before: DOCEND 24 @@ -222,13 +222,13 @@ Attachments are identified by their hash: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 25 :end-before: DOCEND 25 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 25 :end-before: DOCEND 25 @@ -243,13 +243,13 @@ time, or be open at either end: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 26 :end-before: DOCEND 26 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 26 :end-before: DOCEND 26 @@ -259,13 +259,13 @@ We can also define a time window as an ``Instant`` plus/minus a time tolerance ( .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 42 :end-before: DOCEND 42 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 42 :end-before: DOCEND 42 @@ -275,13 +275,13 @@ Or as a start-time plus a duration: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 43 :end-before: DOCEND 43 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 43 :end-before: DOCEND 43 @@ -299,13 +299,13 @@ that will notarise the inputs and verify the time-window: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 19 :end-before: DOCEND 19 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 19 :end-before: DOCEND 19 @@ -318,13 +318,13 @@ instantiated without one: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 46 :end-before: DOCEND 46 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 46 :end-before: DOCEND 46 @@ -362,13 +362,13 @@ Here's an example usage of ``TransactionBuilder.withItems``: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 27 :end-before: DOCEND 27 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 27 :end-before: DOCEND 27 @@ -380,13 +380,13 @@ Here are the methods for adding inputs and attachments: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 28 :end-before: DOCEND 28 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 28 :end-before: DOCEND 28 @@ -396,13 +396,13 @@ An output state can be added as a ``ContractState``, contract class name and not .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 49 :end-before: DOCEND 49 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 49 :end-before: DOCEND 49 @@ -412,13 +412,13 @@ We can also leave the notary field blank, in which case the transaction's defaul .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 50 :end-before: DOCEND 50 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 50 :end-before: DOCEND 50 @@ -428,13 +428,13 @@ Or we can add the output state as a ``TransactionState``, which already specifie .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 51 :end-before: DOCEND 51 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 51 :end-before: DOCEND 51 @@ -444,13 +444,13 @@ Commands can be added as a ``Command``: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 52 :end-before: DOCEND 52 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 52 :end-before: DOCEND 52 @@ -460,13 +460,13 @@ Or as ``CommandData`` and a ``vararg PublicKey``: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 53 :end-before: DOCEND 53 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 53 :end-before: DOCEND 53 @@ -476,13 +476,13 @@ For the time-window, we can set a time-window directly: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 44 :end-before: DOCEND 44 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 44 :end-before: DOCEND 44 @@ -492,13 +492,13 @@ Or define the time-window as a time plus a duration (e.g. 45 seconds): .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 45 :end-before: DOCEND 45 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 45 :end-before: DOCEND 45 @@ -512,13 +512,13 @@ We can either sign with our legal identity key: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 29 :end-before: DOCEND 29 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 29 :end-before: DOCEND 29 @@ -528,13 +528,13 @@ Or we can also choose to use another one of our public keys: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 30 :end-before: DOCEND 30 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 30 :end-before: DOCEND 30 @@ -572,13 +572,13 @@ and output states: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 33 :end-before: DOCEND 33 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 33 :end-before: DOCEND 33 @@ -597,13 +597,13 @@ We achieve this by using the ``ServiceHub`` to convert the ``SignedTransaction`` .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 32 :end-before: DOCEND 32 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 32 :end-before: DOCEND 32 @@ -613,13 +613,13 @@ We can now perform our additional verification. Here's a simple example: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 34 :end-before: DOCEND 34 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 34 :end-before: DOCEND 34 @@ -634,13 +634,13 @@ We can verify that all the transaction's required signatures are present and val .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 35 :end-before: DOCEND 35 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 35 :end-before: DOCEND 35 @@ -652,13 +652,13 @@ which the signatures are allowed to be missing: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 36 :end-before: DOCEND 36 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 36 :end-before: DOCEND 36 @@ -669,13 +669,13 @@ public keys for which the signatures are allowed to be missing: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 54 :end-before: DOCEND 54 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 54 :end-before: DOCEND 54 @@ -689,13 +689,13 @@ We can also choose to simply verify the signatures that are present: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 37 :end-before: DOCEND 37 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 37 :end-before: DOCEND 37 @@ -713,13 +713,13 @@ We can sign using our legal identity key, as follows: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 38 :end-before: DOCEND 38 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 38 :end-before: DOCEND 38 @@ -729,13 +729,13 @@ Or we can choose to sign using another one of our public keys: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 39 :end-before: DOCEND 39 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 39 :end-before: DOCEND 39 @@ -747,13 +747,13 @@ We can do this with our legal identity key: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 40 :end-before: DOCEND 40 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 40 :end-before: DOCEND 40 @@ -763,13 +763,13 @@ Or using another one of our public keys: .. container:: codeset - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt :language: kotlin :start-after: DOCSTART 41 :end-before: DOCEND 41 :dedent: 8 - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java :language: java :start-after: DOCSTART 41 :end-before: DOCEND 41 diff --git a/docs/source/api-vault-query.rst b/docs/source/api-vault-query.rst index 1b80d50dcf..a012b84ea7 100644 --- a/docs/source/api-vault-query.rst +++ b/docs/source/api-vault-query.rst @@ -166,6 +166,44 @@ An example of a custom query in Java is illustrated here: where an anonymous party does not resolve to an X500 name via the ``IdentityService``, no query results will ever be produced. For performance reasons, queries do not use ``PublicKey`` as search criteria. +Custom queries can be either case sensitive or case insensitive. They are defined via a ``Boolean`` as one of the function parameters of each operator function. By default each operator is case sensitive. + +An example of a case sensitive custom query operator is illustrated here: + +.. container:: codeset + + .. sourcecode:: kotlin + + val currencyIndex = PersistentCashState::currency.equal(USD.currencyCode, true) + +.. note:: The ``Boolean`` input of ``true`` in this example could be removed since the function will default to ``true`` when not provided. + +An example of a case insensitive custom query operator is illustrated here: + +.. container:: codeset + + .. sourcecode:: kotlin + + val currencyIndex = PersistentCashState::currency.equal(USD.currencyCode, false) + +An example of a case sensitive custom query operator in Java is illustrated here: + +.. container:: codeset + + .. sourcecode:: java + + FieldInfo attributeCurrency = getField("currency", CashSchemaV1.PersistentCashState.class); + CriteriaExpression currencyIndex = Builder.equal(attributeCurrency, "USD", true); + +An example of a case insensitive custom query operator in Java is illustrated here: + +.. container:: codeset + + .. sourcecode:: java + + FieldInfo attributeCurrency = getField("currency", CashSchemaV1.PersistentCashState.class); + CriteriaExpression currencyIndex = Builder.equal(attributeCurrency, "USD", false); + Pagination ---------- The API provides support for paging where large numbers of results are expected (by default, a page size is set to 200 diff --git a/docs/source/blob-inspector.rst b/docs/source/blob-inspector.rst index 90c3e96976..fe58b63a3e 100644 --- a/docs/source/blob-inspector.rst +++ b/docs/source/blob-inspector.rst @@ -90,3 +90,25 @@ Here's what a node-info file from the node's data directory may look like: Notice the file is actually a serialised ``SignedNodeInfo`` object, which has a ``raw`` property of type ``SerializedBytes``. This property is materialised into a ``NodeInfo`` and is output under the ``deserialized`` field. + +Command-line options +~~~~~~~~~~~~~~~~~~~~ + +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=] [SOURCE] + +* ``--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 + never normally need to specify this. Possible values [BINARY, HEX, BASE64]. Default: BINARY. +* ``--full-parties``: Display the owningKey and certPath properties of Party and PartyAndReference objects respectively. +* ``--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=``: 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. \ No newline at end of file diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 403b1155ba..78e75bd607 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -6,7 +6,20 @@ release, see :doc:`upgrade-notes`. Unreleased ---------- -* Removed experimental feature `CordformDefinition` + +* Introduce minimum and target platform version for CorDapps. + +* New overload for ``CordaRPCClient.start()`` method allowing to specify target legal identity to use for RPC call. + +* Case insensitive vault queries can be specified via a boolean on applicable SQL criteria builder operators. By default queries will be case sensitive. + +* Getter added to ``CordaRPCOps`` for the node's network parameters. + +* The RPC client library now checks at startup whether the server is of the client libraries major version or higher. + Therefore to connect to a Corda 4 node you must use version 4 or lower of the library. This behaviour can be overridden + by specifying a lower number in the ``CordaRPCClientConfiguration`` class. + +* Removed experimental feature ``CordformDefinition`` * Vault query fix: support query by parent classes of Contract State classes (see https://github.com/corda/corda/issues/3714) @@ -17,7 +30,7 @@ Unreleased * Introduced ``TestCorDapp`` and utilities to support asymmetric setups for nodes through ``DriverDSL``, ``MockNetwork`` and ``MockServices``. -* Change type of the `checkpoint_value` column. Please check the upgrade-notes on how to update your database. +* Change type of the ``checkpoint_value`` column. Please check the upgrade-notes on how to update your database. * Removed buggy :serverNameTablePrefix: configuration. @@ -94,6 +107,9 @@ Unreleased * ``WireTransaction.Companion.createComponentGroups`` has been marked as ``@CordaInternal``. It was never intended to be public and was already internal for Kotlin code. +* RPC server will now mask internal errors to RPC clients if not in devMode. ``Throwable``s implementing ``ClientRelevantError`` + will continue to be propagated to clients. + * RPC Framework moved from Kryo to the Corda AMQP implementation [Corda-847]. This completes the removal of ``Kryo`` from general use within Corda, remaining only for use in flow checkpointing. @@ -144,7 +160,7 @@ Unreleased Values are: [FAIL, WARN, IGNORE], default to FAIL if unspecified. * Introduced a placeholder for custom properties within ``node.conf``; the property key is "custom". * The deprecated web server now has its own ``web-server.conf`` file, separate from ``node.conf``. - * Property keys with double quotes (e.g. `"key"`) in ``node.conf`` are no longer allowed, for rationale refer to :doc:`corda-configuration-file`. + * Property keys with double quotes (e.g. "key") in ``node.conf`` are no longer allowed, for rationale refer to :doc:`corda-configuration-file`. * Added public support for creating ``CordaRPCClient`` using SSL. For this to work the node needs to provide client applications a certificate to be added to a truststore. See :doc:`tutorial-clientrpc-api` @@ -161,7 +177,7 @@ Unreleased * The whitelist.txt file is no longer needed. The existing network parameters file is used to update the current contracts whitelist. - * The CorDapp jars are also copied to each nodes' `cordapps` directory. + * The CorDapp jars are also copied to each nodes' ``cordapps`` directory. * Errors thrown by a Corda node will now reported to a calling RPC client with attention to serialization and obfuscation of internal data. @@ -171,7 +187,7 @@ Unreleased reference to the outer class) as per the Java documentation `here `_ we are disallowing this as the paradigm in general makes little sense for contract states. -* Node can be shut down abruptly by ``shutdown`` function in `CordaRPCOps` or gracefully (draining flows first) through ``gracefulShutdown`` command from shell. +* Node can be shut down abruptly by ``shutdown`` function in ``CordaRPCOps`` or gracefully (draining flows first) through ``gracefulShutdown`` command from shell. * API change: ``net.corda.core.schemas.PersistentStateRef`` fields (index and txId) are now non-nullable. The fields were always effectively non-nullable - values were set from non-nullable fields of other objects. @@ -185,8 +201,8 @@ Unreleased * Table name with a typo changed from ``NODE_ATTCHMENTS_CONTRACTS`` to ``NODE_ATTACHMENTS_CONTRACTS``. -* Node logs a warning for any ``MappedSchema`` containing a JPA entity referencing another JPA entity from a different ``MappedSchema`. - The log entry starts with `Cross-reference between MappedSchemas.`. +* Node logs a warning for any ``MappedSchema`` containing a JPA entity referencing another JPA entity from a different ``MappedSchema``. + The log entry starts with "Cross-reference between MappedSchemas". API: Persistence documentation no longer suggests mapping between different schemas. * Upgraded Artemis to v2.6.2. @@ -206,7 +222,7 @@ Version 3.1 * Update the fast-classpath-scanner dependent library version from 2.0.21 to 2.12.3 .. note:: Whilst this is not the latest version of this library, that being 2.18.1 at time of writing, versions - later than 2.12.3 (including 2.12.4) exhibit a different issue. +later than 2.12.3 (including 2.12.4) exhibit a different issue. * Updated the api scanner gradle plugin to work the same way as the version in master. These changes make the api scanner more accurate and fix a couple of bugs, and change the format of the api-current.txt file slightly. Backporting these changes @@ -1024,15 +1040,15 @@ Special thank you to `Qian Hong `_, `Marek Skocovsk to Corda in M10. .. warning:: Due to incompatibility between older version of IntelliJ and gradle 3.4, you will need to upgrade Intellij - to 2017.1 (with kotlin-plugin v1.1.1) in order to run Corda demos in IntelliJ. You can download the latest IntelliJ +to 2017.1 (with kotlin-plugin v1.1.1) in order to run Corda demos in IntelliJ. You can download the latest IntelliJ from `JetBrains `_. .. warning:: The Kapt-generated models are no longer included in our codebase. If you experience ``unresolved references`` - errors when building in IntelliJ, please rebuild the schema model by running ``gradlew kaptKotlin`` in Windows or +errors when building in IntelliJ, please rebuild the schema model by running ``gradlew kaptKotlin`` in Windows or ``./gradlew kaptKotlin`` in other systems. Alternatively, perform a full gradle build or install. .. note:: Kapt is used to generate schema model and entity code (from annotations in the codebase) using the Kotlin Annotation - processor. +processor. * Corda DemoBench: * DemoBench is a new tool to make it easy to configure and launch local Corda nodes. A very useful tool to demonstrate diff --git a/docs/source/cipher-suites.rst b/docs/source/cipher-suites.rst index d923fdbdee..13a4bf9ae7 100644 --- a/docs/source/cipher-suites.rst +++ b/docs/source/cipher-suites.rst @@ -73,7 +73,7 @@ are compatible with TLS 1.2, while the default scheme per key type is also shown | | and SHA-256 | | vendors. | | - tls | | | | | - network map (CN) | +-------------------------+---------------------------------------------------------------+-----+-------------------------+ -| | ECDSA using the | | secp256k1 is the curve adopted by Bitcoin and as such there | YES | | +| | ECDSA using the | | secp256k1 is the curve adopted by Bitcoin and as such there | NO | | | | Koblitz k1 curve | | is a wealth of infrastructure, code and advanced algorithms | | | | | (secp256k1) | | designed for use with it. This curve is standardised by | | | | | and SHA-256 | | NIST as part of the "Suite B" cryptographic algorithms and | | | diff --git a/docs/source/cli-application-shell-extensions.rst b/docs/source/cli-application-shell-extensions.rst new file mode 100644 index 0000000000..4c81ea6c07 --- /dev/null +++ b/docs/source/cli-application-shell-extensions.rst @@ -0,0 +1,76 @@ +Shell extensions for CLI Applications +===================================== + +.. _installing-shell-extensions: + +Installing shell extensions +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Users of ``bash`` or ``zsh`` can install an alias and auto-completion for Corda applications that contain a command line interface. Run: + +.. code-block:: shell + + java -jar .jar --install-shell-extensions + +Then, either restart your shell, or for ``bash`` users run: + +.. code-block:: shell + + . ~/.bashrc + +Or, for ``zsh`` run: + +.. code-block:: shell + + . ~/.zshrc + +You will now be able to run the command line application from anywhere by running the following: + +.. code-block:: shell + + --