mirror of
https://github.com/corda/corda.git
synced 2024-12-22 06:17:55 +00:00
Merge pull request #7 from corda/tudor_merge_os_master
Tudor merge os master
This commit is contained in:
commit
020a292383
@ -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<String> 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
|
||||
##
|
||||
|
9
.ci/ci-gradle-build-cache-init.sh
Executable file
9
.ci/ci-gradle-build-cache-init.sh
Executable file
@ -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"
|
2
.idea/compiler.xml
generated
2
.idea/compiler.xml
generated
@ -235,4 +235,4 @@
|
||||
<component name="JavacSettings">
|
||||
<option name="ADDITIONAL_OPTIONS_STRING" value="-parameters" />
|
||||
</component>
|
||||
</project>
|
||||
</project>
|
||||
|
@ -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)
|
||||
|
51
build.gradle
51
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'
|
||||
}
|
15
buildCacheSettings.gradle
Normal file
15
buildCacheSettings.gradle
Normal file
@ -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
|
||||
}
|
||||
}
|
@ -1,2 +1,4 @@
|
||||
rootProject.name = 'buildSrc'
|
||||
include 'canonicalizer'
|
||||
|
||||
apply from: '../buildCacheSettings.gradle'
|
@ -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<ObjectNode>(digitalSignature)
|
||||
val (by, bytes) = json.assertHasOnlyFields("by", "bytes")
|
||||
val (by, bytes) = json.assertHasOnlyFields("by", "bytes", "parentCertsChain")
|
||||
assertThat(by.valueAs<X509Certificate>(mapper)).isEqualTo(MINI_CORP.identity.certificate)
|
||||
assertThat(bytes.binaryValue()).isEqualTo(digitalSignature.bytes)
|
||||
assertThat(mapper.convertValue<DigitalSignatureWithCert>(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<X500Principal>(mapper)).isEqualTo(cert.issuerX500Principal)
|
||||
assertThat(json["subject"].valueAs<X500Principal>(mapper)).isEqualTo(cert.subjectX500Principal)
|
||||
assertThat(json["publicKey"].valueAs<PublicKey>(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<PublicKey>(mapper)).isEqualTo(Crypto.toSupportedPublicKey(cert.publicKey))
|
||||
assertThat(json["notAfter"].valueAs<Date>(mapper)).isEqualTo(cert.notAfter)
|
||||
assertThat(json["notBefore"].valueAs<Date>(mapper)).isEqualTo(cert.notBefore)
|
||||
assertThat(json["encoded"].binaryValue()).isEqualTo(cert.encoded)
|
||||
|
@ -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<PublicKey, ObservableValue<NodeInfo?>>("NetworkIdentityModel_identity", { publicKey ->
|
||||
.buildNamed<PublicKey, ObservableValue<NodeInfo?>>("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")
|
||||
|
@ -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()));
|
||||
}
|
||||
|
||||
|
@ -47,7 +47,7 @@ class RPCStabilityTests {
|
||||
}
|
||||
|
||||
object DummyOps : RPCOps {
|
||||
override val protocolVersion = 0
|
||||
override val protocolVersion = 1000
|
||||
}
|
||||
|
||||
private fun waitUntilNumberOfThreadsStable(executorService: ScheduledExecutorService): Map<Thread, List<StackTraceElement>> {
|
||||
@ -107,7 +107,7 @@ class RPCStabilityTests {
|
||||
Try.on {
|
||||
startRpcClient<RPCOps>(
|
||||
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<Nothing> {
|
||||
return PublishSubject.create<Nothing>().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<Nothing> {
|
||||
return PublishSubject.create<Nothing>()
|
||||
}
|
||||
@ -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<Unit>().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<ByteArray> {
|
||||
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())
|
||||
|
@ -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<CordaRPC
|
||||
open class CordaRPCClientConfiguration @JvmOverloads constructor(
|
||||
|
||||
/**
|
||||
* Maximum retry interval.
|
||||
* The maximum retry interval for re-connections. The client will retry connections if the host is lost with
|
||||
* ever increasing spacing until the max is reached. The default is 3 minutes.
|
||||
*/
|
||||
open val connectionMaxRetryInterval: Duration = 3.minutes,
|
||||
|
||||
/**
|
||||
* The minimum protocol version required from the server.
|
||||
* The minimum protocol version required from the server. This is equivalent to the node's platform version
|
||||
* number. If this minimum version is not met, an exception will be thrown at startup. If you use features
|
||||
* introduced in a later version, you can bump this to match the platform version you need and get an early
|
||||
* check that runs before you do anything.
|
||||
*
|
||||
* If you leave it at the default then things will work but attempting to use an RPC added in a version later
|
||||
* than the server supports will throw [UnsupportedOperationException].
|
||||
*
|
||||
* The default value is whatever version of Corda this RPC library was shipped as a part of. Therefore if you
|
||||
* use the RPC library from Corda 4, it will by default only connect to a node of version 4 or above.
|
||||
*/
|
||||
open val minimumServerProtocolVersion: Int = 0,
|
||||
open val minimumServerProtocolVersion: Int = PLATFORM_VERSION,
|
||||
|
||||
/**
|
||||
* If set to true the client will track RPC call sites. If an error occurs subsequently during the RPC or in a
|
||||
* returned Observable stream the stack trace of the originating RPC will be shown as well. Note that
|
||||
* constructing call stacks is a moderately expensive operation.
|
||||
* If set to true the client will track RPC call sites (default is false). If an error occurs subsequently
|
||||
* during the RPC or in a returned Observable stream the stack trace of the originating RPC will be shown as
|
||||
* well. Note that constructing call stacks is a moderately expensive operation.
|
||||
*/
|
||||
open val trackRpcCallSites: Boolean = false,
|
||||
open val trackRpcCallSites: Boolean = java.lang.Boolean.getBoolean("net.corda.client.rpc.trackRpcCallSites"),
|
||||
|
||||
/**
|
||||
* The interval of unused observable reaping. Leaked Observables (unused ones) are detected using weak references
|
||||
* and are cleaned up in batches in this interval. If set too large it will waste server side resources for this
|
||||
* duration. If set too low it wastes client side cycles.
|
||||
* duration. If set too low it wastes client side cycles. The default is to check once per second.
|
||||
*/
|
||||
open val reapInterval: Duration = 1.seconds,
|
||||
|
||||
/**
|
||||
* The number of threads to use for observations (for executing [Observable.onNext]).
|
||||
* The number of threads to use for observations for executing [Observable.onNext]. This only has any effect
|
||||
* if [observableExecutor] is null (which is the default). The default is 4.
|
||||
*/
|
||||
open val observationExecutorPoolSize: Int = 4,
|
||||
|
||||
/**
|
||||
* Determines the concurrency level of the Observable Cache. This is exposed because it implicitly determines
|
||||
* the limit on the number of leaked observables reaped because of garbage collection per reaping.
|
||||
* See the implementation of [com.google.common.cache.LocalCache] for details.
|
||||
* This property is no longer used and has no effect.
|
||||
* @suppress
|
||||
*/
|
||||
@Deprecated("This field is no longer used and has no effect.")
|
||||
open val cacheConcurrencyLevel: Int = 1,
|
||||
|
||||
/**
|
||||
* The retry interval of Artemis connections in milliseconds.
|
||||
* The base retry interval for reconnection attempts. The default is 5 seconds.
|
||||
*/
|
||||
open val connectionRetryInterval: Duration = 5.seconds,
|
||||
|
||||
/**
|
||||
* The retry interval multiplier for exponential backoff.
|
||||
* The retry interval multiplier for exponential backoff. The default is 1.5
|
||||
*/
|
||||
open val connectionRetryIntervalMultiplier: Double = 1.5,
|
||||
|
||||
/**
|
||||
* Maximum reconnect attempts on failover>
|
||||
* 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<NetworkHostAndPort> = emptyList(),
|
||||
private val internalConnection: Boolean = false
|
||||
private val haAddressPool: List<NetworkHostAndPort> = 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<NetworkHostAndPort>, configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT) : this(haAddressPool.first(), configuration, null, null, null, haAddressPool)
|
||||
constructor(haAddressPool: List<NetworkHostAndPort>, 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<CordaRPCOps> {
|
||||
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))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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)
|
@ -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<Unit> {
|
||||
|
||||
setFlowsDrainingModeEnabled(true)
|
||||
return pendingFlowsCount().updates
|
||||
.doOnError { error ->
|
||||
throw error
|
||||
}
|
||||
.doOnCompleted { shutdown() }.map { }
|
||||
}
|
||||
) = CordaRPCClient.createWithSslAndClassLoader(hostAndPort, configuration, sslConfiguration, classLoader)
|
@ -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<I : RPCOps>(
|
||||
|
||||
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<I : RPCOps>(
|
||||
username: String,
|
||||
password: String,
|
||||
externalTrace: Trace? = null,
|
||||
impersonatedActor: Actor? = null
|
||||
impersonatedActor: Actor? = null,
|
||||
targetLegalIdentity: CordaX500Name? = null
|
||||
): RPCConnection<I> {
|
||||
return log.logElapsedTime("Startup") {
|
||||
val clientAddress = SimpleString("${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$username.${random63BitValue()}")
|
||||
@ -85,7 +87,8 @@ class RPCClient<I : RPCOps>(
|
||||
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))
|
||||
|
@ -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<InvocationId, UnicastSubject<Notification<*>>> { 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("<Call site of root RPC '$rpcName'>")
|
||||
|
||||
// This is the general function that transforms a client side RPC to internal Artemis messages.
|
||||
override fun invoke(proxy: Any, method: Method, arguments: Array<out Any?>?): 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("<Call site of root RPC '${method.name}'>"))
|
||||
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<Any?>?) {
|
||||
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<InvocationId, UnicastSubject<Notification<*>>>
|
||||
private typealias RpcReplyMap = ConcurrentHashMap<InvocationId, SettableFuture<Any?>>
|
||||
private typealias CallSiteMap = ConcurrentHashMap<InvocationId, Throwable?>
|
||||
private typealias CallSiteMap = ConcurrentHashMap<InvocationId, RPCClientProxyHandler.CallSite?>
|
||||
|
||||
/**
|
||||
* 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?,
|
||||
|
@ -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<*>>(Observable::class.java) {
|
||||
private val log = loggerFor<RpcClientObservableDeSerializer>()
|
||||
private object RpcObservableContextKey
|
||||
|
||||
fun createContext(
|
||||
@ -96,22 +99,23 @@ object RpcClientObservableDeSerializer : CustomSerializer.Implements<Observable<
|
||||
}
|
||||
|
||||
val rpcCallSite = getRpcCallSite(context, observableContext)
|
||||
|
||||
observableContext.observableMap.put(observableId, observable)
|
||||
observableContext.callSiteMap?.put(observableId, rpcCallSite)
|
||||
log.trace("Deserialising observable $observableId", rpcCallSite)
|
||||
|
||||
// We pin all Observables into a hard reference store (rooted in the RPC proxy) on subscription so that users
|
||||
// don't need to store a reference to the Observables themselves.
|
||||
return pinInSubscriptions(observable, observableContext.hardReferenceStore).doOnUnsubscribe {
|
||||
// This causes Future completions to give warnings because the corresponding OnComplete sent from the server
|
||||
// will arrive after the client unsubscribes from the observable and consequently invalidates the mapping.
|
||||
// The unsubscribe is due to [ObservableToFuture]'s use of first().
|
||||
// The unsubscribe is due to ObservableToFuture's use of first().
|
||||
observableContext.observableMap.invalidate(observableId)
|
||||
}.dematerialize<Any>()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -48,7 +48,7 @@ class ClientRPCInfrastructureTests : AbstractRPCTest() {
|
||||
|
||||
fun makeComplicatedListenableFuture(): CordaFuture<Pair<String, CordaFuture<String>>>
|
||||
|
||||
@RPCSinceVersion(2)
|
||||
@RPCSinceVersion(2000)
|
||||
fun addedLater()
|
||||
|
||||
fun captureUser(): String
|
||||
@ -58,7 +58,7 @@ class ClientRPCInfrastructureTests : AbstractRPCTest() {
|
||||
private lateinit var complicatedListenableFuturee: CordaFuture<Pair<String, CordaFuture<String>>>
|
||||
|
||||
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() {}
|
||||
|
@ -33,7 +33,7 @@ class RPCConcurrencyTests : AbstractRPCTest() {
|
||||
@CordaSerializable
|
||||
data class ObservableRose<out A>(val value: A, val branches: Observable<out ObservableRose<A>>)
|
||||
|
||||
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<Long, CountDownLatch>()
|
||||
override val protocolVersion = 0
|
||||
override val protocolVersion = 1000
|
||||
|
||||
override fun newLatch(numberOfDowns: Int): Long {
|
||||
val id = random63BitValue()
|
||||
|
@ -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<Unserializable> {
|
||||
return openFuture<Unserializable>().apply { capture { getUnserializable() } }
|
||||
|
@ -24,7 +24,7 @@ class RPCHighThroughputObservableTests : AbstractRPCTest() {
|
||||
}
|
||||
|
||||
internal class TestOpsImpl : TestOps {
|
||||
override val protocolVersion = 1
|
||||
override val protocolVersion = 1000
|
||||
|
||||
override fun makeObservable(): Observable<Int> = Observable.interval(0, TimeUnit.MICROSECONDS).map { it.toInt() + 1 }
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -46,7 +46,7 @@
|
||||
|
||||
<Policies>
|
||||
<TimeBasedTriggeringPolicy/>
|
||||
<SizeBasedTriggeringPolicy size="10MB"/>
|
||||
<SizeBasedTriggeringPolicy size="100MB"/>
|
||||
</Policies>
|
||||
|
||||
<DefaultRolloverStrategy min="1" max="100">
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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<StackTraceElement> = 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 <T : Any> deserialize(byteSequence: ByteSequence, clazz: Class<T>, 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 <T : Any> serialize(obj: T, context: CheckpointSerializationContext): SerializedBytes<T> {
|
||||
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 <T> 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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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.*
|
||||
|
@ -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.*
|
||||
|
@ -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<Exception> { 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<TransactionVerificationRequest>()
|
||||
.toLedgerTransaction()
|
||||
}
|
@ -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<Exception> { verifyTransaction(bytesOfResource("txverify/tx-failure.bin")) }
|
||||
assertThat(e).hasMessageContaining("Required ${Move::class.java.canonicalName} command")
|
||||
}
|
||||
}
|
48
core-deterministic/testing/verifier/build.gradle
Normal file
48
core-deterministic/testing/verifier/build.gradle
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
@ -1,5 +1,5 @@
|
||||
@file:JvmName("SampleData")
|
||||
package net.corda.deterministic.common
|
||||
package net.corda.deterministic.verifier
|
||||
|
||||
import net.corda.core.contracts.TypeOnlyCommandData
|
||||
|
@ -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
|
@ -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<TransactionVerificationRequest>()
|
||||
.toLedgerTransaction()
|
||||
}
|
@ -48,20 +48,4 @@ interface Cordapp {
|
||||
val jarPath: URL
|
||||
val cordappClasses: List<String>
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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]).
|
||||
* <i>Note that leaf keys cannot be of type [CompositeKey].</i>
|
||||
*/
|
||||
val PublicKey.keys: Set<PublicKey> 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<PublicKey>): 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].
|
||||
*
|
||||
* <i>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.</i>
|
||||
*/
|
||||
fun PublicKey.containsAny(otherKeys: Iterable<PublicKey>): Boolean {
|
||||
return if (this is CompositeKey) keys.intersect(otherKeys).isNotEmpty()
|
||||
else this in otherKeys
|
||||
|
@ -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.
|
||||
internal fun platformSecureRandomFactory(): SecureRandom = platformSecureRandom() // To minimise diff of CryptoUtils against open-source.
|
||||
|
@ -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
|
||||
|
@ -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<CertRole?>, 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);
|
||||
|
||||
|
@ -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<X509Certificate>, bytes: ByteArray) : DigitalSignature(bytes) {
|
||||
@DeprecatedConstructorForDeserialization(1)
|
||||
constructor(by: X509Certificate, bytes: ByteArray) : this(by, emptyList(), bytes)
|
||||
|
||||
val fullCertChain: List<X509Certificate> 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. */
|
||||
|
@ -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<T : NamedByHash, in W : Any>(
|
||||
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<T : NamedByHash, in W : Any>(
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -29,12 +29,6 @@ fun <K, V> Caffeine<in K, in V>.buildNamed(name: String): Cache<K, V> {
|
||||
return this.build<K, V>()
|
||||
}
|
||||
|
||||
fun <K, V> Caffeine<in K, in V>.buildNamed(name: String, loadFunc: (K) -> V): LoadingCache<K, V> {
|
||||
checkCacheName(name)
|
||||
return this.build<K, V>(loadFunc)
|
||||
}
|
||||
|
||||
|
||||
fun <K, V> Caffeine<in K, in V>.buildNamed(name: String, loader: CacheLoader<K, V>): LoadingCache<K, V> {
|
||||
checkCacheName(name)
|
||||
return this.build<K, V>(loader)
|
||||
|
@ -24,25 +24,29 @@ data class CordappImpl(
|
||||
override val customSchemas: Set<MappedSchema>,
|
||||
override val allFlows: List<Class<out FlowLogic<*>>>,
|
||||
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<String> = (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 }
|
||||
}
|
||||
}
|
||||
|
@ -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<CordappInfoResolver>()
|
||||
private val cordappClasses: ConcurrentHashMap<String, Set<CordappImpl.Info>> = 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<String>, 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()
|
||||
}
|
||||
}
|
@ -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<StateRef>, txId: SecureHash, callerIdentity: Party, requestSignature: NotarisationRequestSignature, timeWindow: TimeWindow?, references: List<StateRef>): CordaFuture<Result>
|
||||
|
||||
/** Commits all input states of the given transaction synchronously. Use [commitAsync] for better performance. */
|
||||
override fun commit(states: List<StateRef>, txId: SecureHash, callerIdentity: Party, requestSignature: NotarisationRequestSignature, timeWindow: TimeWindow?, references: List<StateRef>) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
@ -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<String, String>?,
|
||||
/** 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<StateMachineInfo>
|
||||
|
||||
@ -233,6 +228,9 @@ interface CordaRPCOps : RPCOps {
|
||||
@RPCReturnsObservables
|
||||
fun networkMapFeed(): DataFeed<List<NodeInfo>, 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<Int, Pair<Int, Int>> {
|
||||
/**
|
||||
* 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<Pair<Int, Int>>()
|
||||
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 <reified T : ContractState> CordaRPCOps.vaultQueryBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(),
|
||||
|
@ -28,4 +28,4 @@ interface AppServiceHub : ServiceHub {
|
||||
* TODO it is assumed here that the flow object has an appropriate classloader.
|
||||
*/
|
||||
fun <T> startTrackedFlow(flow: FlowLogic<T>): FlowProgressHandle<T>
|
||||
}
|
||||
}
|
||||
|
@ -140,4 +140,4 @@ interface IdentityService {
|
||||
fun partiesFromName(query: String, exactMatch: Boolean): Set<Party>
|
||||
}
|
||||
|
||||
class UnknownAnonymousPartyException(msg: String) : CordaException(msg)
|
||||
class UnknownAnonymousPartyException(message: String) : CordaException(message)
|
||||
|
@ -160,7 +160,7 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
|
||||
val notary: AbstractParty?,
|
||||
val lockId: String?,
|
||||
val lockUpdateTime: Instant?,
|
||||
val isRelevant: Vault.RelevancyStatus?
|
||||
val relevancyStatus: Vault.RelevancyStatus?
|
||||
) {
|
||||
constructor(ref: StateRef,
|
||||
contractStateClassName: String,
|
||||
|
@ -73,7 +73,7 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
|
||||
|
||||
abstract class CommonQueryCriteria : QueryCriteria() {
|
||||
abstract val status: Vault.StateStatus
|
||||
open val isRelevant: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL
|
||||
open val relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL
|
||||
abstract val contractStateTypes: Set<Class<out ContractState>>?
|
||||
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
|
||||
return parser.parseCriteria(this)
|
||||
@ -90,7 +90,7 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
|
||||
val notary: List<AbstractParty>? = 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<Predicate> {
|
||||
super.visit(parser)
|
||||
@ -125,15 +125,15 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
|
||||
val externalId: List<String>? = null,
|
||||
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
override val contractStateTypes: Set<Class<out ContractState>>? = null,
|
||||
override val isRelevant: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL
|
||||
override val relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL
|
||||
) : CommonQueryCriteria() {
|
||||
constructor(
|
||||
participants: List<AbstractParty>? = null,
|
||||
linearId: List<UniqueIdentifier>? = null,
|
||||
status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
contractStateTypes: Set<Class<out ContractState>>? = 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<AbstractParty>? = null,
|
||||
@ -175,7 +175,7 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
|
||||
val issuerRef: List<OpaqueBytes>? = null,
|
||||
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
override val contractStateTypes: Set<Class<out ContractState>>? = null,
|
||||
override val isRelevant: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL
|
||||
override val relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL
|
||||
) : CommonQueryCriteria() {
|
||||
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
|
||||
super.visit(parser)
|
||||
@ -215,7 +215,7 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
|
||||
val expression: CriteriaExpression<L, Boolean>,
|
||||
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
override val contractStateTypes: Set<Class<out ContractState>>? = null,
|
||||
override val isRelevant: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL
|
||||
override val relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL
|
||||
) : CommonQueryCriteria() {
|
||||
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
|
||||
super.visit(parser)
|
||||
|
@ -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 <R : Comparable<R>> Field.comparePredicate(operator: BinaryComparisonOperator, value: R) = info().comparePredicate(operator, value)
|
||||
fun <R : Comparable<R>> FieldInfo.comparePredicate(operator: BinaryComparisonOperator, value: R) = predicate(compare(operator, value))
|
||||
|
||||
fun <O, R> KProperty1<O, R?>.equal(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value))
|
||||
fun <O, R> KProperty1<O, R?>.notEqual(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value))
|
||||
@JvmOverloads
|
||||
fun <O, R> KProperty1<O, R?>.equal(value: R, exactMatch: Boolean = true) = predicate(Builder.equal(value, exactMatch))
|
||||
|
||||
@JvmOverloads
|
||||
fun <O, R> KProperty1<O, R?>.notEqual(value: R, exactMatch: Boolean = true) = predicate(Builder.notEqual(value, exactMatch))
|
||||
|
||||
fun <O, R : Comparable<R>> KProperty1<O, R?>.lessThan(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN, value)
|
||||
|
||||
fun <O, R : Comparable<R>> KProperty1<O, R?>.lessThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN_OR_EQUAL, value)
|
||||
|
||||
fun <O, R : Comparable<R>> KProperty1<O, R?>.greaterThan(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN, value)
|
||||
|
||||
fun <O, R : Comparable<R>> KProperty1<O, R?>.greaterThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL, value)
|
||||
|
||||
fun <O, R : Comparable<R>> KProperty1<O, R?>.between(from: R, to: R) = predicate(ColumnPredicate.Between(from, to))
|
||||
fun <O, R : Comparable<R>> KProperty1<O, R?>.`in`(collection: Collection<R>) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection))
|
||||
fun <O, R : Comparable<R>> KProperty1<O, R?>.notIn(collection: Collection<R>) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection))
|
||||
|
||||
@JvmOverloads
|
||||
fun <O, R : Comparable<R>> KProperty1<O, R?>.`in`(collection: Collection<R>, exactMatch: Boolean = true) = predicate(Builder.`in`(collection, exactMatch))
|
||||
|
||||
@JvmOverloads
|
||||
fun <O, R : Comparable<R>> KProperty1<O, R?>.notIn(collection: Collection<R>, exactMatch: Boolean = true) = predicate(Builder.notIn(collection, exactMatch))
|
||||
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
@Deprecated("Does not support fields from a MappedSuperclass. Use equivalent on a FieldInfo.")
|
||||
fun <R> Field.equal(value: R) = info().equal(value)
|
||||
@JvmStatic
|
||||
fun <R> FieldInfo.equal(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value))
|
||||
fun <R> 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 <R> Field.notEqual(value: R) = info().notEqual(value)
|
||||
@JvmOverloads
|
||||
fun <R> FieldInfo.equal(value: R, exactMatch: Boolean = true) = predicate(Builder.equal(value, exactMatch))
|
||||
|
||||
@JvmStatic
|
||||
fun <R> 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 <R> Field.notEqual(value: R, exactMatch: Boolean = true) = info().notEqual(value, exactMatch)
|
||||
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun <R> 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 <R : Comparable<R>> 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 <R : Comparable<R>> Field.`in`(collection: Collection<R>) = info().`in`(collection)
|
||||
fun <R : Comparable<R>> FieldInfo.`in`(collection: Collection<R>) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection))
|
||||
fun <R : Comparable<R>> Field.`in`(collection: Collection<R>, exactMatch: Boolean = true) = info().`in`(collection, exactMatch)
|
||||
|
||||
@JvmStatic
|
||||
@Deprecated("Does not support fields from a MappedSuperclass. Use equivalent on a FieldInfo.")
|
||||
fun <R : Comparable<R>> Field.notIn(collection: Collection<R>) = info().notIn(collection)
|
||||
@JvmStatic
|
||||
fun <R : Comparable<R>> FieldInfo.notIn(collection: Collection<R>) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection))
|
||||
@JvmOverloads
|
||||
fun <R : Comparable<R>> FieldInfo.`in`(collection: Collection<R>, exactMatch: Boolean = true) = predicate(Builder.`in`(collection, exactMatch))
|
||||
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
@Deprecated("Does not support fields from a MappedSuperclass. Use equivalent on a FieldInfo.")
|
||||
fun <R : Comparable<R>> Field.notIn(collection: Collection<R>, exactMatch: Boolean = true) = info().notIn(collection, exactMatch)
|
||||
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun <R : Comparable<R>> FieldInfo.notIn(collection: Collection<R>, exactMatch: Boolean = true) = predicate(Builder.notIn(collection, exactMatch))
|
||||
|
||||
@JvmOverloads
|
||||
fun <R> equal(value: R, exactMatch: Boolean = true) = EqualityComparison(if (exactMatch) EQUAL else EQUAL_IGNORE_CASE, value)
|
||||
|
||||
@JvmOverloads
|
||||
fun <R> notEqual(value: R, exactMatch: Boolean = true) = EqualityComparison(if (exactMatch) NOT_EQUAL else NOT_EQUAL_IGNORE_CASE, value)
|
||||
|
||||
fun <R> equal(value: R) = ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value)
|
||||
fun <R> notEqual(value: R) = ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value)
|
||||
fun <R : Comparable<R>> lessThan(value: R) = compare(BinaryComparisonOperator.LESS_THAN, value)
|
||||
|
||||
fun <R : Comparable<R>> lessThanOrEqual(value: R) = compare(BinaryComparisonOperator.LESS_THAN_OR_EQUAL, value)
|
||||
|
||||
fun <R : Comparable<R>> greaterThan(value: R) = compare(BinaryComparisonOperator.GREATER_THAN, value)
|
||||
|
||||
fun <R : Comparable<R>> greaterThanOrEqual(value: R) = compare(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL, value)
|
||||
|
||||
fun <R : Comparable<R>> between(from: R, to: R) = ColumnPredicate.Between(from, to)
|
||||
fun <R : Comparable<R>> `in`(collection: Collection<R>) = ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection)
|
||||
fun <R : Comparable<R>> notIn(collection: Collection<R>) = 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 <R : Comparable<R>> `in`(collection: Collection<R>, exactMatch: Boolean = true) = CollectionExpression(if (exactMatch) IN else IN_IGNORE_CASE, collection)
|
||||
|
||||
@JvmOverloads
|
||||
fun <R : Comparable<R>> notIn(collection: Collection<R>, 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 <R> isNull() = ColumnPredicate.NullExpression<R>(NullOperator.IS_NULL)
|
||||
fun <R> isNotNull() = ColumnPredicate.NullExpression<R>(NullOperator.NOT_NULL)
|
||||
|
||||
@JvmOverloads
|
||||
fun <O> KProperty1<O, String?>.like(string: String, exactMatch: Boolean = true) = predicate(Builder.like(string, exactMatch))
|
||||
|
||||
fun <O> KProperty1<O, String?>.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 <O> KProperty1<O, String?>.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 <O> KProperty1<O, String?>.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 <O, R> KProperty1<O, R?>.isNull() = predicate(ColumnPredicate.NullExpression(NullOperator.IS_NULL))
|
||||
@JvmStatic
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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<StackTraceElement> = 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 <T : Any> deserialize(byteSequence: ByteSequence, clazz: Class<T>, 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 <T : Any> serialize(obj: T, context: CheckpointSerializationContext): SerializedBytes<T> {
|
||||
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<CheckpointSerializationContext?>()
|
||||
|
||||
/**
|
||||
* Change the current context inside the block to that supplied.
|
||||
*/
|
||||
fun <T> 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 <T : Any> deserialize(byteSequence: ByteSequence, clazz: Class<T>, context: CheckpointSerializationContext): T
|
||||
|
||||
@Throws(NotSerializableException::class)
|
||||
fun <T : Any> serialize(obj: T, context: CheckpointSerializationContext): SerializedBytes<T>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<Any, Any>
|
||||
/**
|
||||
* 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<SecureHash>): 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 <reified T : Any> 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 <reified T : Any> SerializedBytes<T>.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 <reified T : Any> 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 : Any> T.checkpointSerialize(serializationFactory: CheckpointSerializationFactory = CheckpointSerializationFactory.defaultFactory,
|
||||
context: CheckpointSerializationContext): SerializedBytes<T> {
|
||||
return serializationFactory.serialize(this, context)
|
||||
}
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<ComponentGroup>) : CoreTransaction() {
|
||||
/** Hashes of the ZIP/JAR files that are needed to interpret the contents of this wire transaction. */
|
||||
val attachments: List<SecureHash> = deserialiseComponentGroup(ComponentGroupEnum.ATTACHMENTS_GROUP, { SerializedBytes<SecureHash>(it).deserialize() })
|
||||
val attachments: List<SecureHash> = deserialiseComponentGroup(SecureHash::class, ATTACHMENTS_GROUP)
|
||||
|
||||
/** Pointers to the input states on the ledger, identified by (tx identity hash, output index). */
|
||||
override val inputs: List<StateRef> = deserialiseComponentGroup(ComponentGroupEnum.INPUTS_GROUP, { SerializedBytes<StateRef>(it).deserialize() })
|
||||
override val inputs: List<StateRef> = deserialiseComponentGroup(StateRef::class, INPUTS_GROUP)
|
||||
|
||||
/** Pointers to reference states, identified by (tx identity hash, output index). */
|
||||
override val references: List<StateRef> = deserialiseComponentGroup(ComponentGroupEnum.REFERENCES_GROUP, { SerializedBytes<StateRef>(it).deserialize() })
|
||||
override val references: List<StateRef> = deserialiseComponentGroup(StateRef::class, REFERENCES_GROUP)
|
||||
|
||||
override val outputs: List<TransactionState<ContractState>> = deserialiseComponentGroup(ComponentGroupEnum.OUTPUTS_GROUP, { SerializedBytes<TransactionState<ContractState>>(it).deserialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentsClassLoader(attachments)) })
|
||||
override val outputs: List<TransactionState<ContractState>> = deserialiseComponentGroup(TransactionState::class, OUTPUTS_GROUP, attachmentsContext = true)
|
||||
|
||||
/** Ordered list of ([CommandData], [PublicKey]) pairs that instruct the contracts what to do. */
|
||||
val commands: List<Command<*>> = deserialiseCommands()
|
||||
|
||||
override val notary: Party? = let {
|
||||
val notaries: List<Party> = deserialiseComponentGroup(ComponentGroupEnum.NOTARY_GROUP, { SerializedBytes<Party>(it).deserialize() })
|
||||
val notaries: List<Party> = 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<TimeWindow> = deserialiseComponentGroup(ComponentGroupEnum.TIMEWINDOW_GROUP, { SerializedBytes<TimeWindow>(it).deserialize() })
|
||||
val timeWindows: List<TimeWindow> = 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<ComponentGr
|
||||
}
|
||||
|
||||
// Helper function to return a meaningful exception if deserialisation of a component fails.
|
||||
private fun <T> deserialiseComponentGroup(groupEnum: ComponentGroupEnum, deserialiseBody: (ByteArray) -> T): List<T> {
|
||||
private fun <T : Any> deserialiseComponentGroup(clazz: KClass<T>,
|
||||
groupEnum: ComponentGroupEnum,
|
||||
attachmentsContext: Boolean = false): List<T> {
|
||||
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<ComponentGr
|
||||
// TODO: we could avoid deserialising unrelated signers.
|
||||
// However, current approach ensures the transaction is not malformed
|
||||
// and it will throw if any of the signers objects is not List of public keys).
|
||||
val signersList = deserialiseComponentGroup(ComponentGroupEnum.SIGNERS_GROUP, { SerializedBytes<List<PublicKey>>(it).deserialize() })
|
||||
val commandDataList = deserialiseComponentGroup(ComponentGroupEnum.COMMANDS_GROUP, { SerializedBytes<CommandData>(it).deserialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentsClassLoader(attachments)) })
|
||||
val group = componentGroups.firstOrNull { it.groupIndex == ComponentGroupEnum.COMMANDS_GROUP.ordinal }
|
||||
val signersList: List<List<PublicKey>> = uncheckedCast(deserialiseComponentGroup(List::class, SIGNERS_GROUP))
|
||||
val commandDataList: List<CommandData> = 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<ComponentGr
|
||||
} else {
|
||||
// It is a WireTransaction
|
||||
// or a FilteredTransaction with no Commands (in which case group is null).
|
||||
check(commandDataList.size == signersList.size) { "Invalid Transaction. Sizes of CommandData (${commandDataList.size}) and Signers (${signersList.size}) do not match" }
|
||||
check(commandDataList.size == signersList.size) {
|
||||
"Invalid Transaction. Sizes of CommandData (${commandDataList.size}) and Signers (${signersList.size}) do not match"
|
||||
}
|
||||
commandDataList.mapIndexed { index, commandData -> Command(commandData, signersList[index]) }
|
||||
}
|
||||
}
|
||||
@ -145,47 +156,47 @@ class FilteredTransaction internal constructor(
|
||||
var signersIncluded = false
|
||||
|
||||
fun <T : Any> 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<FilteredComponentGroup> {
|
||||
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<PublicKey> {
|
||||
try {
|
||||
@ -340,7 +369,10 @@ class FilteredTransaction internal constructor(
|
||||
*/
|
||||
@KeepForDJVM
|
||||
@CordaSerializable
|
||||
data class FilteredComponentGroup(override val groupIndex: Int, override val components: List<OpaqueBytes>, val nonces: List<SecureHash>, val partialMerkleTree: PartialMerkleTree) : ComponentGroup(groupIndex, components) {
|
||||
data class FilteredComponentGroup(override val groupIndex: Int,
|
||||
override val components: List<OpaqueBytes>,
|
||||
val nonces: List<SecureHash>,
|
||||
val partialMerkleTree: PartialMerkleTree) : ComponentGroup(groupIndex, components) {
|
||||
init {
|
||||
check(components.size == nonces.size) { "Size of transaction components and nonces do not match" }
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -106,7 +106,7 @@ class CordappSmokeTest {
|
||||
class SendBackInitiatorFlowContext(private val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||
@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)
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -105,7 +105,7 @@ internal class UseRefState(val linearId: UniqueIdentifier) : FlowLogic<SignedTra
|
||||
val notary = serviceHub.networkMapCache.notaryIdentities.first()
|
||||
val query = QueryCriteria.LinearStateQueryCriteria(
|
||||
linearId = listOf(linearId),
|
||||
isRelevant = Vault.RelevancyStatus.ALL
|
||||
relevancyStatus = Vault.RelevancyStatus.ALL
|
||||
)
|
||||
val referenceState = serviceHub.vaultService.queryBy<ContractState>(query).states.single()
|
||||
return subFlow(FinalityFlow(
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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<Any, Any>()
|
||||
protected var executor = SandboxExecutor<Any, Any>(SandboxConfiguration.DEFAULT)
|
||||
|
||||
private var derivedWhitelist: Whitelist = Whitelist.MINIMAL
|
||||
|
||||
@ -117,7 +114,7 @@ abstract class ClassCommand : CommandBase() {
|
||||
}
|
||||
|
||||
private fun findDiscoverableRunnables(filters: Array<String>): List<Class<*>> {
|
||||
val classes = find<DiscoverableRunnable>()
|
||||
val classes = find<java.util.function.Function<*,*>>()
|
||||
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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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<String> = emptyArray()
|
||||
|
||||
override fun processClasses(classes: List<Class<*>>) {
|
||||
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}")
|
||||
|
@ -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
|
||||
|
||||
|
104
djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/Utilities.kt
Normal file
104
djvm/cli/src/main/kotlin/net/corda/djvm/tools/cli/Utilities.kt
Normal file
@ -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<Path>?.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<Path>?.getFileNames(map: (Path) -> Path = { it }) = this.getFiles(map).map {
|
||||
it.toString()
|
||||
}.toTypedArray()
|
||||
|
||||
/**
|
||||
* Execute inlined action if the collection is empty.
|
||||
*/
|
||||
inline fun <T> List<T>.onEmpty(action: () -> Unit): List<T> {
|
||||
if (!this.any()) {
|
||||
action()
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute inlined action if the array is empty.
|
||||
*/
|
||||
inline fun <reified T> Array<T>?.onEmpty(action: () -> Unit): Array<T> {
|
||||
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 <reified T> find(scanSpec: String = "net/corda/djvm"): List<Class<*>> {
|
||||
val references = mutableListOf<Class<*>>()
|
||||
FastClasspathScanner(scanSpec)
|
||||
.matchClassesImplementing(T::class.java) { clazz ->
|
||||
if (!Modifier.isAbstract(clazz.modifiers) && !Modifier.isStatic(clazz.modifiers)) {
|
||||
references.add(clazz)
|
||||
}
|
||||
}
|
||||
.scan()
|
||||
return references
|
||||
}
|
@ -33,52 +33,53 @@ class WhitelistGenerateCommand : CommandBase() {
|
||||
override fun validateArguments() = paths.isNotEmpty()
|
||||
|
||||
override fun handleCommand(): Boolean {
|
||||
val entries = mutableListOf<String>()
|
||||
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<String>()
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -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<ClassSource>
|
||||
) {
|
||||
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<SandboxRuntimeContext?>() {
|
||||
override fun initialValue(): SandboxRuntimeContext? = null
|
||||
}
|
||||
private val threadLocalContext = ThreadLocal<SandboxRuntimeContext?>()
|
||||
|
||||
/**
|
||||
* When called from within a sandbox, this returns the context for the current sandbox thread.
|
||||
|
@ -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<String> = emptySet(),
|
||||
val minimumSeverityLevel: Severity = Severity.WARNING,
|
||||
val classPath: List<Path> = emptyList(),
|
||||
classPath: List<Path> = emptyList(),
|
||||
bootstrapJar: Path? = null,
|
||||
val analyzeAnnotations: Boolean = false,
|
||||
val prefixFilters: List<String> = 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<String> = setOf(SANDBOXED_OBJECT, RUNTIME_COST_ACCOUNTER) + additionalPinnedClasses
|
||||
val pinnedClasses: Set<String> = 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"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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<ClassSource>
|
||||
val references: ReferenceMap
|
||||
) {
|
||||
|
||||
private val origins = mutableMapOf<String, MutableSet<EntityReference>>()
|
||||
@ -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<ClassSource>): 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("/", ".")
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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) {
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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()
|
||||
)
|
||||
|
||||
/**
|
||||
|
@ -20,7 +20,7 @@ class ClassMutator(
|
||||
private val configuration: AnalysisConfiguration,
|
||||
private val definitionProviders: List<DefinitionProvider> = emptyList(),
|
||||
private val emitters: List<Emitter> = 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<Emitter>(emitters, analysisContext.messages) {
|
||||
// We need to apply the tracing emitters before the non-tracing ones.
|
||||
Processor.processEntriesOfType<Emitter>(emitters.sortedByDescending(Emitter::isTracer), analysisContext.messages) {
|
||||
it.emit(context, instruction)
|
||||
}
|
||||
if (!emitter.emitDefaultInstruction || emitter.hasEmittedCustomCode) {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 <reified T> 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 <reified T> 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 <T : Throwable> throwException(exceptionType: Class<T>, message: String) {
|
||||
hasEmittedCustomCode = true
|
||||
new<java.lang.Exception>()
|
||||
methodVisitor.visitInsn(Opcodes.DUP)
|
||||
val exceptionName = Type.getInternalName(exceptionType)
|
||||
new(exceptionName)
|
||||
methodVisitor.visitInsn(DUP)
|
||||
methodVisitor.visitLdcInsn(message)
|
||||
invokeSpecial<java.lang.Exception>("<init>", "(Ljava/lang/String;)V")
|
||||
methodVisitor.visitInsn(Opcodes.ATHROW)
|
||||
invokeSpecial(exceptionName, "<init>", "(Ljava/lang/String;)V")
|
||||
methodVisitor.visitInsn(ATHROW)
|
||||
}
|
||||
|
||||
inline fun <reified T : Throwable> 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
|
||||
}
|
||||
|
||||
/**
|
||||
|
15
djvm/src/main/kotlin/net/corda/djvm/code/Types.kt
Normal file
15
djvm/src/main/kotlin/net/corda/djvm/code/Types.kt
Normal file
@ -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('.', '/')
|
@ -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<T>(
|
||||
if (thresholdPredicate(newValue)) {
|
||||
val message = errorMessage(currentThread)
|
||||
logger.error("Threshold breached; {}", message)
|
||||
throw ThresholdViolationException(message)
|
||||
throw ThresholdViolationError(message)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<TInput, TOutput>(
|
||||
configuration: SandboxConfiguration = SandboxConfiguration.DEFAULT
|
||||
configuration: SandboxConfiguration
|
||||
) : SandboxExecutor<TInput, TOutput>(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 <reified TRunnable : SandboxedRunnable<TInput, TOutput>> run(input: TInput):
|
||||
ExecutionSummaryWithResult<TOutput?> {
|
||||
inline fun <reified TRunnable : Function<in TInput, out TOutput>> run(input: TInput):
|
||||
ExecutionSummaryWithResult<TOutput> {
|
||||
return run(ClassSource.fromClassName(TRunnable::class.java.name), input)
|
||||
}
|
||||
|
||||
|
@ -1,6 +0,0 @@
|
||||
package net.corda.djvm.execution
|
||||
|
||||
/**
|
||||
* Functionality runnable by a sandbox executor, marked for discoverability.
|
||||
*/
|
||||
interface DiscoverableRunnable
|
@ -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.
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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<in TInput, out TOutput>(
|
||||
protected val configuration: SandboxConfiguration = SandboxConfiguration.DEFAULT
|
||||
protected val configuration: SandboxConfiguration
|
||||
) {
|
||||
|
||||
private val classModule = configuration.analysisConfiguration.classModule
|
||||
@ -32,12 +31,7 @@ open class SandboxExecutor<in TInput, out TOutput>(
|
||||
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<in TInput, out TOutput>(
|
||||
open fun run(
|
||||
runnableClass: ClassSource,
|
||||
input: TInput
|
||||
): ExecutionSummaryWithResult<TOutput?> {
|
||||
): ExecutionSummaryWithResult<TOutput> {
|
||||
// 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<in TInput, out TOutput>(
|
||||
// 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<in TInput, out TOutput>(
|
||||
* @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<in TInput, out TOutput>(
|
||||
@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<in TInput, out TOutput>(
|
||||
}
|
||||
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<in TInput, out TOutput>(
|
||||
private inline fun processClassQueue(
|
||||
vararg elements: ClassSource, action: QueueProcessor<ClassSource>.(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)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user