mirror of
https://github.com/corda/corda.git
synced 2025-01-29 15:43:55 +00:00
Merge branch 'master' into wn-redo-node-conf-docs
This commit is contained in:
commit
2f18ce9440
@ -6505,10 +6505,6 @@ public final class net.corda.testing.node.NotarySpec extends java.lang.Object
|
||||
public int hashCode()
|
||||
public String toString()
|
||||
##
|
||||
public interface net.corda.testing.node.ResponderFlowFactory
|
||||
@NotNull
|
||||
public abstract F invoke(net.corda.core.flows.FlowSession)
|
||||
##
|
||||
public final class net.corda.testing.node.StartedMockNode extends java.lang.Object
|
||||
@NotNull
|
||||
public final java.util.List<kotlin.Pair<F, net.corda.core.concurrent.CordaFuture<?>>> findStateMachines(Class<F>)
|
||||
|
@ -44,10 +44,12 @@ $newAbstracts
|
||||
EOF
|
||||
`
|
||||
|
||||
#Get a list of any methods that expose classes in .internal. namespaces, and any classes which extend/implement
|
||||
#an internal class
|
||||
# Get a list of any methods that expose internal classes, which includes:
|
||||
# - classes in .internal. namespaces (excluding the kotlin.jvm.internal namespace)
|
||||
# - classes which extend/implement an internal class (see above)
|
||||
# - classes in the net.corda.node. namespace
|
||||
#TODO: check that only classes in a whitelist are part of the API rather than look for specific invalid cases going forward
|
||||
newInternalExposures=$(echo "$userDiffContents" | grep "^+" | grep "\.internal\." )
|
||||
newInternalExposures=$(echo "$userDiffContents" | grep "^+" | grep "(?<!kotlin\.jvm)\.internal\." )
|
||||
newNodeExposures=$(echo "$userDiffContents" | grep "^+" | grep "net\.corda\.node\.")
|
||||
|
||||
internalCount=`grep -v "^$" <<EOF | wc -l
|
||||
|
4
.idea/compiler.xml
generated
4
.idea/compiler.xml
generated
@ -44,6 +44,8 @@
|
||||
<module name="corda-core_integrationTest" target="1.8" />
|
||||
<module name="corda-core_smokeTest" target="1.8" />
|
||||
<module name="corda-finance_integrationTest" target="1.8" />
|
||||
<module name="corda-isolated_main" target="1.8" />
|
||||
<module name="corda-isolated_test" target="1.8" />
|
||||
<module name="corda-project_buildSrc_main" target="1.8" />
|
||||
<module name="corda-project_buildSrc_test" target="1.8" />
|
||||
<module name="corda-project_canonicalizer_main" target="1.8" />
|
||||
@ -158,6 +160,8 @@
|
||||
<module name="net.corda-blobinspector_test" target="1.8" />
|
||||
<module name="net.corda-finance-contracts-states_main" target="1.8" />
|
||||
<module name="net.corda-finance-contracts-states_test" target="1.8" />
|
||||
<module name="net.corda-isolated_main" target="1.8" />
|
||||
<module name="net.corda-isolated_test" target="1.8" />
|
||||
<module name="net.corda-verifier_main" target="1.8" />
|
||||
<module name="net.corda-verifier_test" target="1.8" />
|
||||
<module name="net.corda_buildSrc_main" target="1.8" />
|
||||
|
@ -14,6 +14,7 @@ see changes to this list.
|
||||
* Ajitha Thayaharan (BCS Technology International)
|
||||
* Alberto Arri (R3)
|
||||
* amiracam
|
||||
* Amol Pednekar
|
||||
* Andras Slemmer (R3)
|
||||
* Andrius Dagys (R3)
|
||||
* Andrzej Cichocki (R3)
|
||||
@ -79,6 +80,7 @@ see changes to this list.
|
||||
* Emanuel Russo (NTT DATA Italy)
|
||||
* Farzad Pezeshkpour (RBS)
|
||||
* fracting
|
||||
* Francisco Spadafora (NTT DATA Italy)
|
||||
* Frederic Dalibard (Natixis)
|
||||
* Garrett Macey (Wells Fargo)
|
||||
* gary-rowe
|
||||
@ -212,6 +214,7 @@ see changes to this list.
|
||||
* verymahler
|
||||
* Viktor Kolomeyko (R3)
|
||||
* Vipin Bharathan
|
||||
* Vitalij Reicherdt (Commerzbank AG)
|
||||
* Wawrzek Niewodniczanski (R3)
|
||||
* Wei Wu Zhang (Commonwealth Bank of Australia)
|
||||
* Zabrina Smith (Northern Trust)
|
||||
|
2
LICENSE
2
LICENSE
@ -1,4 +1,4 @@
|
||||
Copyright 2016 - 2018, R3 Limited.
|
||||
Copyright 2016 - 2019, R3 Limited.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -37,7 +37,9 @@ Corda is an open source blockchain project, designed for business from the start
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions to Corda! Please see our [CONTRIBUTING.md](./CONTRIBUTING.md).
|
||||
Corda is an open-source project and contributions are welcome!
|
||||
|
||||
To find out how to contribute, please see our [contributing docs](https://docs.corda.net/head/contributing-index.html).
|
||||
|
||||
## License
|
||||
|
||||
|
@ -4,7 +4,7 @@ buildscript {
|
||||
file("$projectDir/constants.properties").withInputStream { constants.load(it) }
|
||||
|
||||
// Our version: bump this on release.
|
||||
ext.corda_release_version = "4.0-SNAPSHOT"
|
||||
ext.corda_release_version = "5.0-SNAPSHOT"
|
||||
ext.corda_platform_version = constants.getProperty("platformVersion")
|
||||
ext.gradle_plugins_version = constants.getProperty("gradlePluginsVersion")
|
||||
|
||||
@ -383,8 +383,6 @@ bintrayConfig {
|
||||
'corda-tools-explorer',
|
||||
'corda-tools-network-bootstrapper',
|
||||
'corda-tools-cliutils',
|
||||
'corda-notary-raft',
|
||||
'corda-notary-bft-smart',
|
||||
'corda-common-configuration-parsing',
|
||||
'corda-common-validation'
|
||||
]
|
||||
@ -474,4 +472,4 @@ wrapper {
|
||||
buildScan {
|
||||
termsOfServiceUrl = 'https://gradle.com/terms-of-service'
|
||||
termsOfServiceAgree = 'yes'
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
|
||||
import com.fasterxml.jackson.module.kotlin.convertValue
|
||||
import com.nhaarman.mockito_kotlin.any
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.client.jackson.internal.childrenAs
|
||||
import net.corda.client.jackson.internal.valueAs
|
||||
@ -24,7 +25,8 @@ import net.corda.core.internal.DigitalSignatureWithCert
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.AttachmentStorage
|
||||
import net.corda.core.node.services.NetworkParametersStorage
|
||||
import net.corda.core.node.services.NetworkParametersService
|
||||
import net.corda.core.node.services.TransactionStorage
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.serialize
|
||||
@ -94,10 +96,10 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
|
||||
services = rigorousMock()
|
||||
cordappProvider = rigorousMock()
|
||||
val networkParameters = testNetworkParameters(minimumPlatformVersion = 4)
|
||||
val networkParametersStorage = rigorousMock<NetworkParametersStorage>().also {
|
||||
val networkParametersService = rigorousMock<NetworkParametersService>().also {
|
||||
doReturn(networkParameters.serialize().hash).whenever(it).currentHash
|
||||
}
|
||||
doReturn(networkParametersStorage).whenever(services).networkParametersStorage
|
||||
doReturn(networkParametersService).whenever(services).networkParametersService
|
||||
doReturn(cordappProvider).whenever(services).cordappProvider
|
||||
doReturn(networkParameters).whenever(services).networkParameters
|
||||
doReturn(attachments).whenever(services).attachments
|
||||
@ -237,6 +239,7 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
|
||||
doReturn(attachmentId).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID)
|
||||
val attachmentStorage = rigorousMock<AttachmentStorage>()
|
||||
doReturn(attachmentStorage).whenever(services).attachments
|
||||
doReturn(mock<TransactionStorage>()).whenever(services).validatedTransactions
|
||||
val attachment = rigorousMock<ContractAttachment>()
|
||||
doReturn(attachment).whenever(attachmentStorage).openAttachment(attachmentId)
|
||||
doReturn(attachmentId).whenever(attachment).id
|
||||
|
@ -29,6 +29,7 @@ import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.internal.chooseIdentity
|
||||
import net.corda.testing.node.User
|
||||
import net.corda.testing.node.internal.FINANCE_CORDAPPS
|
||||
import org.junit.Test
|
||||
import rx.Observable
|
||||
|
||||
@ -49,7 +50,7 @@ class NodeMonitorModelTest {
|
||||
private lateinit var newNode: (CordaX500Name) -> NodeInfo
|
||||
|
||||
private fun setup(runTest: () -> Unit) {
|
||||
driver(DriverParameters(extraCordappPackagesToScan = listOf("net.corda.finance"))) {
|
||||
driver(DriverParameters(cordappsForAllNodes = FINANCE_CORDAPPS)) {
|
||||
val cashUser = User("user1", "test", permissions = setOf(all()))
|
||||
val aliceNodeHandle = startNode(providedName = ALICE_NAME, rpcUsers = listOf(cashUser)).getOrThrow()
|
||||
aliceNode = aliceNodeHandle.nodeInfo
|
||||
|
@ -57,10 +57,13 @@ processSmokeTestResources {
|
||||
rename 'corda-(.*)', 'corda.jar'
|
||||
}
|
||||
from(project(':finance:workflows').tasks['jar']) {
|
||||
rename 'finance-workflows-(.*)', 'finance-workflows.jar'
|
||||
rename '.*finance-workflows-.*', 'cordapp-finance-workflows.jar'
|
||||
}
|
||||
from(project(':finance:contracts').tasks['jar']) {
|
||||
rename 'finance-contracts-(.*)', 'finance-contracts.jar'
|
||||
rename '.*finance-contracts-.*', 'cordapp-finance-contracts.jar'
|
||||
}
|
||||
from(project(':confidential-identities').tasks['jar']) {
|
||||
rename '.*confidential-identities-.*', 'cordapp-confidential-identities.jar'
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,6 +90,7 @@ dependencies {
|
||||
smokeTestCompile project(':smoke-test-utils')
|
||||
smokeTestCompile project(':finance:contracts')
|
||||
smokeTestCompile project(':finance:workflows')
|
||||
smokeTestCompile project(':confidential-identities')
|
||||
smokeTestCompile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
|
||||
smokeTestCompile "org.apache.logging.log4j:log4j-core:$log4j_version"
|
||||
smokeTestCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||
|
@ -3,12 +3,15 @@ package net.corda.client.rpc
|
||||
import com.github.benmanes.caffeine.cache.Caffeine
|
||||
import net.corda.client.rpc.internal.RPCClient
|
||||
import net.corda.client.rpc.internal.serialization.amqp.AMQPClientSerializationScheme
|
||||
import net.corda.core.internal.loadClassesImplementing
|
||||
import net.corda.core.context.Actor
|
||||
import net.corda.core.context.Trace
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.PLATFORM_VERSION
|
||||
import net.corda.core.messaging.ClientRpcSslOptions
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.serialization.SerializationCustomSerializer
|
||||
import net.corda.core.serialization.SerializationWhitelist
|
||||
import net.corda.core.serialization.internal.effectiveSerializationEnv
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.days
|
||||
@ -19,6 +22,7 @@ import net.corda.serialization.internal.AMQP_RPC_CLIENT_CONTEXT
|
||||
import net.corda.serialization.internal.amqp.SerializationFactoryCacheKey
|
||||
import net.corda.serialization.internal.amqp.SerializerFactory
|
||||
import java.time.Duration
|
||||
import java.util.ServiceLoader
|
||||
|
||||
/**
|
||||
* This class is essentially just a wrapper for an RPCConnection<CordaRPCOps> and can be treated identically.
|
||||
@ -240,6 +244,7 @@ open class CordaRPCClientConfiguration @JvmOverloads constructor(
|
||||
* @param configuration An optional configuration used to tweak client behaviour.
|
||||
* @param sslConfiguration An optional [ClientRpcSslOptions] used to enable secure communication with the server.
|
||||
* @param haAddressPool A list of [NetworkHostAndPort] representing the addresses of servers in HA mode.
|
||||
* @param classLoader a classloader, which will be used (if provided) to discover available [SerializationCustomSerializer]s and [SerializationWhitelist]s
|
||||
* The client will attempt to connect to a live server by trying each address in the list. If the servers are not in
|
||||
* HA mode, the client will round-robin from the beginning of the list and try all servers.
|
||||
*/
|
||||
@ -252,8 +257,9 @@ class CordaRPCClient private constructor(
|
||||
) {
|
||||
@JvmOverloads
|
||||
constructor(hostAndPort: NetworkHostAndPort,
|
||||
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT)
|
||||
: this(hostAndPort, configuration, null)
|
||||
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT,
|
||||
classLoader: ClassLoader? = null)
|
||||
: this(hostAndPort, configuration, null, classLoader = classLoader)
|
||||
|
||||
/**
|
||||
* @param haAddressPool A list of [NetworkHostAndPort] representing the addresses of servers in HA mode.
|
||||
@ -287,7 +293,7 @@ class CordaRPCClient private constructor(
|
||||
sslConfiguration: ClientRpcSslOptions? = null,
|
||||
classLoader: ClassLoader? = null
|
||||
): CordaRPCClient {
|
||||
return CordaRPCClient(hostAndPort, configuration, sslConfiguration, classLoader)
|
||||
return CordaRPCClient(hostAndPort, configuration, sslConfiguration, classLoader = classLoader)
|
||||
}
|
||||
}
|
||||
|
||||
@ -296,7 +302,14 @@ class CordaRPCClient private constructor(
|
||||
effectiveSerializationEnv
|
||||
} catch (e: IllegalStateException) {
|
||||
try {
|
||||
AMQPClientSerializationScheme.initialiseSerialization(classLoader, Caffeine.newBuilder().maximumSize(128).build<SerializationFactoryCacheKey, SerializerFactory>().asMap())
|
||||
// If the client has provided a classloader, the associated classpath is checked for available custom serializers and serialization whitelists.
|
||||
if (classLoader != null) {
|
||||
val customSerializers = loadClassesImplementing(classLoader, SerializationCustomSerializer::class.java)
|
||||
val serializationWhitelists = ServiceLoader.load(SerializationWhitelist::class.java, classLoader).toSet()
|
||||
AMQPClientSerializationScheme.initialiseSerialization(classLoader, customSerializers, serializationWhitelists, Caffeine.newBuilder().maximumSize(128).build<SerializationFactoryCacheKey, SerializerFactory>().asMap())
|
||||
} else {
|
||||
AMQPClientSerializationScheme.initialiseSerialization(classLoader, serializerFactoriesForContexts = Caffeine.newBuilder().maximumSize(128).build<SerializationFactoryCacheKey, SerializerFactory>().asMap())
|
||||
}
|
||||
} catch (e: IllegalStateException) {
|
||||
// Race e.g. two of these constructed in parallel, ignore.
|
||||
}
|
||||
|
@ -10,4 +10,4 @@ import net.corda.nodeapi.exceptions.RpcSerializableError
|
||||
* Thrown to indicate that the calling user does not have permission for something they have requested (for example
|
||||
* calling a method).
|
||||
*/
|
||||
class PermissionException(message: String) : CordaRuntimeException(message), RpcSerializableError, ClientRelevantError
|
||||
class PermissionException(val msg: String) : CordaRuntimeException(msg), RpcSerializableError, ClientRelevantError
|
@ -5,6 +5,7 @@ import net.corda.core.internal.toSynchronised
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationContext.UseCase
|
||||
import net.corda.core.serialization.SerializationCustomSerializer
|
||||
import net.corda.core.serialization.SerializationWhitelist
|
||||
import net.corda.core.serialization.internal.SerializationEnvironment
|
||||
import net.corda.core.serialization.internal.nodeSerializationEnv
|
||||
import net.corda.serialization.internal.*
|
||||
@ -17,24 +18,25 @@ import net.corda.serialization.internal.amqp.custom.RxNotificationSerializer
|
||||
*/
|
||||
class AMQPClientSerializationScheme(
|
||||
cordappCustomSerializers: Set<SerializationCustomSerializer<*,*>>,
|
||||
cordappSerializationWhitelists: Set<SerializationWhitelist>,
|
||||
serializerFactoriesForContexts: MutableMap<SerializationFactoryCacheKey, SerializerFactory>
|
||||
) : AbstractAMQPSerializationScheme(cordappCustomSerializers, serializerFactoriesForContexts) {
|
||||
constructor(cordapps: List<Cordapp>) : this(cordapps.customSerializers, AccessOrderLinkedHashMap<SerializationFactoryCacheKey, SerializerFactory>(128).toSynchronised())
|
||||
constructor(cordapps: List<Cordapp>, serializerFactoriesForContexts: MutableMap<SerializationFactoryCacheKey, SerializerFactory>) : this(cordapps.customSerializers, serializerFactoriesForContexts)
|
||||
) : AbstractAMQPSerializationScheme(cordappCustomSerializers, cordappSerializationWhitelists, serializerFactoriesForContexts) {
|
||||
constructor(cordapps: List<Cordapp>) : this(cordapps.customSerializers, cordapps.serializationWhitelists, AccessOrderLinkedHashMap<SerializationFactoryCacheKey, SerializerFactory>(128).toSynchronised())
|
||||
constructor(cordapps: List<Cordapp>, serializerFactoriesForContexts: MutableMap<SerializationFactoryCacheKey, SerializerFactory>) : this(cordapps.customSerializers, cordapps.serializationWhitelists, serializerFactoriesForContexts)
|
||||
|
||||
@Suppress("UNUSED")
|
||||
constructor() : this(emptySet(), AccessOrderLinkedHashMap<SerializationFactoryCacheKey, SerializerFactory>(128).toSynchronised())
|
||||
constructor() : this(emptySet(), emptySet(), AccessOrderLinkedHashMap<SerializationFactoryCacheKey, SerializerFactory>(128).toSynchronised())
|
||||
|
||||
companion object {
|
||||
/** Call from main only. */
|
||||
fun initialiseSerialization(classLoader: ClassLoader? = null, serializerFactoriesForContexts: MutableMap<SerializationFactoryCacheKey, SerializerFactory> = AccessOrderLinkedHashMap<SerializationFactoryCacheKey, SerializerFactory>(128).toSynchronised()) {
|
||||
nodeSerializationEnv = createSerializationEnv(classLoader, serializerFactoriesForContexts)
|
||||
fun initialiseSerialization(classLoader: ClassLoader? = null, customSerializers: Set<SerializationCustomSerializer<*, *>> = emptySet(), serializationWhitelists: Set<SerializationWhitelist> = emptySet(), serializerFactoriesForContexts: MutableMap<SerializationFactoryCacheKey, SerializerFactory> = AccessOrderLinkedHashMap<SerializationFactoryCacheKey, SerializerFactory>(128).toSynchronised()) {
|
||||
nodeSerializationEnv = createSerializationEnv(classLoader, customSerializers, serializationWhitelists, serializerFactoriesForContexts)
|
||||
}
|
||||
|
||||
fun createSerializationEnv(classLoader: ClassLoader? = null, serializerFactoriesForContexts: MutableMap<SerializationFactoryCacheKey, SerializerFactory> = AccessOrderLinkedHashMap<SerializationFactoryCacheKey, SerializerFactory>(128).toSynchronised()): SerializationEnvironment {
|
||||
fun createSerializationEnv(classLoader: ClassLoader? = null, customSerializers: Set<SerializationCustomSerializer<*, *>> = emptySet(), serializationWhitelists: Set<SerializationWhitelist> = emptySet(), serializerFactoriesForContexts: MutableMap<SerializationFactoryCacheKey, SerializerFactory> = AccessOrderLinkedHashMap<SerializationFactoryCacheKey, SerializerFactory>(128).toSynchronised()): SerializationEnvironment {
|
||||
return SerializationEnvironment.with(
|
||||
SerializationFactoryImpl().apply {
|
||||
registerScheme(AMQPClientSerializationScheme(emptyList(), serializerFactoriesForContexts))
|
||||
registerScheme(AMQPClientSerializationScheme(customSerializers, serializationWhitelists, serializerFactoriesForContexts))
|
||||
},
|
||||
storageContext = AMQP_STORAGE_CONTEXT,
|
||||
p2pContext = if (classLoader != null) AMQP_P2P_CONTEXT.withClassLoader(classLoader) else AMQP_P2P_CONTEXT,
|
||||
|
@ -30,6 +30,27 @@ import static kotlin.test.AssertionsKt.fail;
|
||||
import static net.corda.finance.contracts.GetBalances.getCashBalance;
|
||||
|
||||
public class StandaloneCordaRPCJavaClientTest {
|
||||
|
||||
public static void copyCordapps(NodeProcess.Factory factory, NodeConfig notaryConfig) {
|
||||
Path cordappsDir = (factory.baseDirectory(notaryConfig).resolve(NodeProcess.CORDAPPS_DIR_NAME));
|
||||
try {
|
||||
Files.createDirectories(cordappsDir);
|
||||
} catch (IOException ex) {
|
||||
fail("Failed to create directories");
|
||||
}
|
||||
try (Stream<Path> paths = Files.walk(Paths.get("build", "resources", "smokeTest"))) {
|
||||
paths.filter(path -> path.toFile().getName().startsWith("cordapp")).forEach(file -> {
|
||||
try {
|
||||
Files.copy(file, cordappsDir.resolve(file.getFileName()));
|
||||
} catch (IOException ex) {
|
||||
fail("Failed to copy cordapp jar");
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
fail("Failed to walk files");
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> perms = Collections.singletonList("ALL");
|
||||
private Set<String> permSet = new HashSet<>(perms);
|
||||
private User rpcUser = new User("user1", "test", permSet);
|
||||
@ -55,7 +76,7 @@ public class StandaloneCordaRPCJavaClientTest {
|
||||
@Before
|
||||
public void setUp() {
|
||||
factory = new NodeProcess.Factory();
|
||||
copyFinanceCordapp();
|
||||
copyCordapps(factory, notaryConfig);
|
||||
notary = factory.create(notaryConfig);
|
||||
connection = notary.connect();
|
||||
rpcProxy = connection.getProxy();
|
||||
@ -73,28 +94,6 @@ public class StandaloneCordaRPCJavaClientTest {
|
||||
}
|
||||
}
|
||||
|
||||
private void copyFinanceCordapp() {
|
||||
Path cordappsDir = (factory.baseDirectory(notaryConfig).resolve(NodeProcess.CORDAPPS_DIR_NAME));
|
||||
try {
|
||||
Files.createDirectories(cordappsDir);
|
||||
} catch (IOException ex) {
|
||||
fail("Failed to create directories");
|
||||
}
|
||||
try (Stream<Path> paths = Files.walk(Paths.get("build", "resources", "smokeTest"))) {
|
||||
paths.forEach(file -> {
|
||||
if (file.toString().contains("corda-finance")) {
|
||||
try {
|
||||
Files.copy(file, cordappsDir.resolve(file.getFileName()));
|
||||
} catch (IOException ex) {
|
||||
fail("Failed to copy finance jar");
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
fail("Failed to walk files");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCashBalances() throws NoSuchFieldException, ExecutionException, InterruptedException {
|
||||
Amount<Currency> dollars123 = new Amount<>(123, Currency.getInstance("USD"));
|
||||
|
@ -6,12 +6,15 @@ import net.corda.client.rpc.CordaRPCConnection
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.internal.InputStreamAndHash
|
||||
import net.corda.core.messaging.*
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.node.services.vault.*
|
||||
import net.corda.core.utilities.*
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.POUNDS
|
||||
import net.corda.finance.SWISS_FRANCS
|
||||
@ -21,6 +24,7 @@ import net.corda.finance.contracts.getCashBalance
|
||||
import net.corda.finance.contracts.getCashBalances
|
||||
import net.corda.finance.flows.CashIssueFlow
|
||||
import net.corda.finance.flows.CashPaymentFlow
|
||||
import net.corda.java.rpc.StandaloneCordaRPCJavaClientTest
|
||||
import net.corda.nodeapi.internal.config.User
|
||||
import net.corda.smoketesting.NodeConfig
|
||||
import net.corda.smoketesting.NodeProcess
|
||||
@ -31,11 +35,9 @@ import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import java.io.FilterInputStream
|
||||
import java.io.InputStream
|
||||
import java.nio.file.Paths
|
||||
import java.util.*
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.streams.toList
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotEquals
|
||||
@ -69,7 +71,7 @@ class StandaloneCordaRPClientTest {
|
||||
@Before
|
||||
fun setUp() {
|
||||
factory = NodeProcess.Factory()
|
||||
copyFinanceCordapp()
|
||||
StandaloneCordaRPCJavaClientTest.copyCordapps(factory, notaryConfig)
|
||||
notary = factory.create(notaryConfig)
|
||||
connection = notary.connect()
|
||||
rpcProxy = connection.proxy
|
||||
@ -86,15 +88,6 @@ class StandaloneCordaRPClientTest {
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyFinanceCordapp() {
|
||||
val cordappsDir = (factory.baseDirectory(notaryConfig) / NodeProcess.CORDAPPS_DIR_NAME).createDirectories()
|
||||
// Find the finance jar file for the smoke tests of this module
|
||||
val financeJars = Paths.get("build", "resources", "smokeTest").list {
|
||||
it.filter { "corda-finance" in it.toString() }.toList()
|
||||
}
|
||||
financeJars.forEach { it.copyToDirectory(cordappsDir) }
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `test attachments`() {
|
||||
|
@ -6,12 +6,13 @@ apply plugin: 'kotlin'
|
||||
apply plugin: CanonicalizerPlugin
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||
apply plugin: 'net.corda.plugins.cordapp'
|
||||
apply plugin: 'com.jfrog.artifactory'
|
||||
|
||||
description 'Corda Experimental Confidential Identities'
|
||||
|
||||
dependencies {
|
||||
compile project(':core')
|
||||
cordaCompile project(':core')
|
||||
|
||||
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||
testCompile "junit:junit:$junit_version"
|
||||
@ -26,6 +27,17 @@ dependencies {
|
||||
testCompile "org.assertj:assertj-core:$assertj_version"
|
||||
}
|
||||
|
||||
cordapp {
|
||||
targetPlatformVersion corda_platform_version.toInteger()
|
||||
minimumPlatformVersion 1
|
||||
workflow {
|
||||
name "Corda Experimental Confidential Identities"
|
||||
versionId 1
|
||||
vendor "R3"
|
||||
licence "Open Source (Apache 2)"
|
||||
}
|
||||
}
|
||||
|
||||
jar {
|
||||
baseName 'corda-confidential-identities'
|
||||
}
|
||||
|
@ -4,29 +4,49 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.CordaInternal
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.verify
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
import net.corda.core.internal.cordapp.CordappResolver
|
||||
import net.corda.core.internal.warnOnce
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.unwrap
|
||||
import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Very basic flow which generates new confidential identities for parties in a transaction and exchanges the transaction
|
||||
* key and certificate paths between the parties. This is intended for use as a sub-flow of another flow which builds a
|
||||
* transaction. The flow running on the other side must also call this flow at the correct location.
|
||||
*
|
||||
* NOTE: This is an inlined flow but for backwards compatibility is annotated with [InitiatingFlow].
|
||||
*/
|
||||
class SwapIdentitiesFlow @JvmOverloads constructor(private val otherSideSession: FlowSession,
|
||||
override val progressTracker: ProgressTracker = tracker()) : FlowLogic<SwapIdentitiesFlow.AnonymousResult>() {
|
||||
@InitiatingFlow
|
||||
class SwapIdentitiesFlow
|
||||
private constructor(private val otherSideSession: FlowSession?,
|
||||
private val otherParty: Party?,
|
||||
override val progressTracker: ProgressTracker) : FlowLogic<LinkedHashMap<Party, AnonymousParty>>() {
|
||||
|
||||
@JvmOverloads
|
||||
constructor(otherSideSession: FlowSession, progressTracker: ProgressTracker = tracker()) : this(otherSideSession, null, progressTracker)
|
||||
|
||||
@Deprecated("It is unsafe to use this constructor as it requires nodes to automatically vend anonymous identities without first " +
|
||||
"checking if they should. Instead, use the constructor that takes in an existing FlowSession.")
|
||||
constructor(otherParty: Party, revocationEnabled: Boolean, progressTracker: ProgressTracker) : this(null, otherParty, progressTracker)
|
||||
|
||||
@Deprecated("It is unsafe to use this constructor as it requires nodes to automatically vend anonymous identities without first " +
|
||||
"checking if they should. Instead, use the constructor that takes in an existing FlowSession.")
|
||||
constructor(otherParty: Party) : this(null, otherParty, tracker())
|
||||
|
||||
companion object {
|
||||
object GENERATING_IDENTITY : ProgressTracker.Step("Generating our anonymous identity")
|
||||
object SIGNING_IDENTITY : ProgressTracker.Step("Signing our anonymous identity")
|
||||
@ -69,38 +89,49 @@ class SwapIdentitiesFlow @JvmOverloads constructor(private val otherSideSession:
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): AnonymousResult {
|
||||
override fun call(): LinkedHashMap<Party, AnonymousParty> {
|
||||
val session = otherSideSession ?: run {
|
||||
logger.warnOnce("The current usage of SwapIdentitiesFlow is unsafe. Please consider upgrading your CorDapp to use " +
|
||||
"SwapIdentitiesFlow with FlowSessions. (${CordappResolver.currentCordapp?.info})")
|
||||
initiateFlow(otherParty!!)
|
||||
}
|
||||
progressTracker.currentStep = GENERATING_IDENTITY
|
||||
val ourAnonymousIdentity = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, false)
|
||||
val data = buildDataToSign(ourAnonymousIdentity)
|
||||
progressTracker.currentStep = SIGNING_IDENTITY
|
||||
val signature = serviceHub.keyManagementService.sign(data, ourAnonymousIdentity.owningKey).withoutKey()
|
||||
val ourIdentWithSig = IdentityWithSignature(ourAnonymousIdentity, signature)
|
||||
val ourIdentWithSig = IdentityWithSignature(ourAnonymousIdentity.serialize(), signature)
|
||||
progressTracker.currentStep = AWAITING_IDENTITY
|
||||
val theirAnonymousIdentity = otherSideSession.sendAndReceive<IdentityWithSignature>(ourIdentWithSig).unwrap { theirIdentWithSig ->
|
||||
val theirAnonymousIdentity = session.sendAndReceive<IdentityWithSignature>(ourIdentWithSig).unwrap { theirIdentWithSig ->
|
||||
progressTracker.currentStep = VERIFYING_IDENTITY
|
||||
validateAndRegisterIdentity(serviceHub, otherSideSession.counterparty, theirIdentWithSig.identity, theirIdentWithSig.signature)
|
||||
validateAndRegisterIdentity(serviceHub, session.counterparty, theirIdentWithSig.identity.deserialize(), theirIdentWithSig.signature)
|
||||
}
|
||||
return AnonymousResult(ourAnonymousIdentity.party.anonymise(), theirAnonymousIdentity.party.anonymise())
|
||||
|
||||
val identities = LinkedHashMap<Party, AnonymousParty>()
|
||||
identities[ourIdentity] = ourAnonymousIdentity.party.anonymise()
|
||||
identities[session.counterparty] = theirAnonymousIdentity.party.anonymise()
|
||||
return identities
|
||||
}
|
||||
|
||||
/**
|
||||
* Result class containing the caller's anonymous identity ([ourIdentity]) and the counterparty's ([theirIdentity]).
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class AnonymousResult(val ourIdentity: AnonymousParty, val theirIdentity: AnonymousParty)
|
||||
|
||||
@CordaSerializable
|
||||
private data class IdentityWithSignature(val identity: PartyAndCertificate, val signature: DigitalSignature)
|
||||
|
||||
/**
|
||||
* Data class used only in the context of asserting that the owner of the private key for the listed key wants to use it
|
||||
* to represent the named entity. This is paired with an X.509 certificate (which asserts the signing identity says
|
||||
* the key represents the named entity) and protects against a malicious party incorrectly claiming others'
|
||||
* keys.
|
||||
*/
|
||||
@CordaSerializable
|
||||
private data class CertificateOwnershipAssertion(val name: CordaX500Name, val owningKey: PublicKey)
|
||||
data class IdentityWithSignature(val identity: SerializedBytes<PartyAndCertificate>, val signature: DigitalSignature)
|
||||
}
|
||||
|
||||
// Data class used only in the context of asserting that the owner of the private key for the listed key wants to use it
|
||||
// to represent the named entity. This is paired with an X.509 certificate (which asserts the signing identity says
|
||||
// the key represents the named entity) and protects against a malicious party incorrectly claiming others'
|
||||
// keys.
|
||||
@CordaSerializable
|
||||
data class CertificateOwnershipAssertion(val x500Name: CordaX500Name, val publicKey: PublicKey)
|
||||
|
||||
open class SwapIdentitiesException @JvmOverloads constructor(message: String, cause: Throwable? = null) : FlowException(message, cause)
|
||||
|
||||
// This only exists for backwards compatibility
|
||||
@InitiatedBy(SwapIdentitiesFlow::class)
|
||||
private class SwapIdentitiesHandler(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
subFlow(SwapIdentitiesFlow(otherSide))
|
||||
logger.warnOnce("Insecure API to swap anonymous identities was used by ${otherSide.counterparty} (${otherSide.getCounterpartyFlowInfo()})")
|
||||
}
|
||||
}
|
||||
|
@ -171,10 +171,7 @@ class SwapIdentitiesFlowTests {
|
||||
@InitiatingFlow
|
||||
private class SwapIdentitiesInitiator(private val otherSide: Party) : FlowLogic<Map<Party, AnonymousParty>>() {
|
||||
@Suspendable
|
||||
override fun call(): Map<Party, AnonymousParty> {
|
||||
val (anonymousUs, anonymousThem) = subFlow(SwapIdentitiesFlow(initiateFlow(otherSide)))
|
||||
return mapOf(ourIdentity to anonymousUs, otherSide to anonymousThem)
|
||||
}
|
||||
override fun call(): Map<Party, AnonymousParty> = subFlow(SwapIdentitiesFlow(initiateFlow(otherSide)))
|
||||
}
|
||||
|
||||
@InitiatedBy(SwapIdentitiesInitiator::class)
|
||||
|
@ -10,27 +10,59 @@
|
||||
</Properties>
|
||||
|
||||
<Appenders>
|
||||
<Console name="Console-Appender" target="SYSTEM_OUT">
|
||||
<PatternLayout>
|
||||
<ScriptPatternSelector defaultPattern="%highlight{[%level{length=5}] %date{HH:mm:ssZ} [%t] %c{2}.%method - %msg%n%throwable{short.message}%n}{INFO=white,WARN=red,FATAL=bright red}">
|
||||
<Script name="MDCSelector" language="javascript"><![CDATA[
|
||||
result = null;
|
||||
if (!logEvent.getContextData().size() == 0) {
|
||||
result = "WithMDC";
|
||||
} else {
|
||||
result = null;
|
||||
}
|
||||
result;
|
||||
]]>
|
||||
</Script>
|
||||
<PatternMatch key="WithMDC" pattern="%highlight{[%level{length=5}] %date{HH:mm:ssZ} [%t] %c{2}.%method - %msg %X%n%throwable{short.message}%n}{INFO=white,WARN=red,FATAL=bright red}"/>
|
||||
</ScriptPatternSelector>
|
||||
</PatternLayout>
|
||||
</Console>
|
||||
<ScriptAppenderSelector name="Console-Selector">
|
||||
<Script language="nashorn"><![CDATA[
|
||||
var System = Java.type('java.lang.System');
|
||||
var level = System.getProperty("consoleLogLevel");
|
||||
var enabled = System.getProperty("consoleLoggingEnabled");
|
||||
enabled == "true" && (level == "debug" || level == "trace") ? "Console-Debug-Appender" : "Console-Appender";
|
||||
]]></Script>
|
||||
<AppenderSet>
|
||||
|
||||
<!-- The default console appender - prints no exception information -->
|
||||
<Console name="Console-Appender" target="SYSTEM_OUT">
|
||||
<PatternLayout>
|
||||
<ScriptPatternSelector
|
||||
defaultPattern="%highlight{[%level{length=5}] %date{HH:mm:ssZ} [%t] %c{2}.%method - %msg%n%throwable{0}}{INFO=white,WARN=red,FATAL=bright red}">
|
||||
<Script name="MDCSelector" language="javascript"><![CDATA[
|
||||
result = null;
|
||||
if (!logEvent.getContextData().size() == 0) {
|
||||
result = "WithMDC";
|
||||
} else {
|
||||
result = null;
|
||||
}
|
||||
result;
|
||||
]]>
|
||||
</Script>
|
||||
<PatternMatch key="WithMDC" pattern="%highlight{[%level{length=5}] %date{HH:mm:ssZ} [%t] %c{2}.%method - %msg %X%n%throwable{0}}{INFO=white,WARN=red,FATAL=bright red}"/>
|
||||
</ScriptPatternSelector>
|
||||
</PatternLayout>
|
||||
</Console>
|
||||
|
||||
<!-- The console appender when debug or trace level logging is specified. Prints full stack trace -->
|
||||
<Console name="Console-Debug-Appender" target="SYSTEM_OUT">
|
||||
<PatternLayout>
|
||||
<ScriptPatternSelector defaultPattern="%highlight{[%level{length=5}] %date{HH:mm:ssZ} [%t] %c{2}.%method - %msg%n%throwable{}}{INFO=white,WARN=red,FATAL=bright red}">
|
||||
<Script name="MDCSelector" language="javascript"><![CDATA[
|
||||
result = null;
|
||||
if (!logEvent.getContextData().size() == 0) {
|
||||
result = "WithMDC";
|
||||
} else {
|
||||
result = null;
|
||||
}
|
||||
result;
|
||||
]]>
|
||||
</Script>
|
||||
<PatternMatch key="WithMDC" pattern="%highlight{[%level{length=5}] %date{HH:mm:ssZ} [%t] %c{2}.%method - %msg %X%n%throwable{}}{INFO=white,WARN=red,FATAL=bright red}"/>
|
||||
</ScriptPatternSelector>
|
||||
</PatternLayout>
|
||||
</Console>
|
||||
</AppenderSet>
|
||||
</ScriptAppenderSelector>
|
||||
|
||||
<!-- Required for printBasicInfo -->
|
||||
<Console name="Console-Appender-Println" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%msg%n%throwable{short.message}" />
|
||||
<PatternLayout pattern="%msg%n%throwable{0}" />
|
||||
</Console>
|
||||
|
||||
<!-- Will generate up to 100 log files for a given day. During every rollover it will delete
|
||||
@ -73,8 +105,8 @@
|
||||
|
||||
</RollingRandomAccessFile>
|
||||
|
||||
<Rewrite name="Console-ErrorCode-Appender">
|
||||
<AppenderRef ref="Console-Appender"/>
|
||||
<Rewrite name="Console-ErrorCode-Selector">
|
||||
<AppenderRef ref="Console-Selector"/>
|
||||
<ErrorCodeRewritePolicy/>
|
||||
</Rewrite>
|
||||
|
||||
@ -91,7 +123,7 @@
|
||||
|
||||
<Loggers>
|
||||
<Root level="${defaultLogLevel}">
|
||||
<AppenderRef ref="Console-ErrorCode-Appender" level="${consoleLogLevel}"/>
|
||||
<AppenderRef ref="Console-ErrorCode-Selector" level="${consoleLogLevel}"/>
|
||||
<AppenderRef ref="RollingFile-ErrorCode-Appender"/>
|
||||
</Root>
|
||||
<Logger name="BasicInfo" additivity="false">
|
||||
@ -99,11 +131,11 @@
|
||||
<AppenderRef ref="RollingFile-ErrorCode-Appender"/>
|
||||
</Logger>
|
||||
<Logger name="org.hibernate.SQL" level="info" additivity="false">
|
||||
<AppenderRef ref="Console-ErrorCode-Appender"/>
|
||||
<AppenderRef ref="Console-ErrorCode-Selector"/>
|
||||
<AppenderRef ref="RollingFile-ErrorCode-Appender"/>
|
||||
</Logger>
|
||||
<Logger name="org.apache.activemq.artemis.core.server" level="error" additivity="false">
|
||||
<AppenderRef ref="Console-ErrorCode-Appender"/>
|
||||
<AppenderRef ref="Console-ErrorCode-Selector"/>
|
||||
<AppenderRef ref="RollingFile-ErrorCode-Appender"/>
|
||||
</Logger>
|
||||
<Logger name="org.jolokia" additivity="true" level="warn">
|
||||
|
@ -1,10 +1,10 @@
|
||||
gradlePluginsVersion=4.0.37
|
||||
gradlePluginsVersion=4.0.38
|
||||
kotlinVersion=1.2.71
|
||||
# ***************************************************************#
|
||||
# When incrementing platformVersion make sure to update #
|
||||
# net.corda.core.internal.CordaUtilsKt.PLATFORM_VERSION as well. #
|
||||
# ***************************************************************#
|
||||
platformVersion=4
|
||||
platformVersion=5
|
||||
guavaVersion=25.1-jre
|
||||
proguardVersion=6.0.3
|
||||
bouncycastleVersion=1.60
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.deterministic.data
|
||||
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.crypto.entropyToKeyPair
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
@ -39,7 +40,7 @@ object TransactionGenerator {
|
||||
private val MEGA_CORP_PUBKEY: PublicKey = megaCorp.keyPair.public
|
||||
private val MINI_CORP_PUBKEY: PublicKey = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")).keyPair.public
|
||||
|
||||
private val ledgerServices = MockServices(emptyList(), MEGA_CORP.name, rigorousMock<IdentityServiceInternal>().also {
|
||||
private val ledgerServices = MockServices(emptyList(), MEGA_CORP.name, mock<IdentityServiceInternal>().also {
|
||||
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
|
||||
doReturn(DUMMY_CASH_ISSUER.party).whenever(it).partyFromKey(DUMMY_CASH_ISSUER_KEY.public)
|
||||
})
|
||||
|
@ -0,0 +1,8 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
/**
|
||||
* Stubbing out non-deterministic method.
|
||||
*/
|
||||
fun <T: Any> loadClassesImplementing(classloader: ClassLoader, clazz: Class<T>): Set<T> {
|
||||
return emptySet()
|
||||
}
|
@ -4,6 +4,7 @@ import net.corda.core.serialization.ClassWhitelist
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationContext.UseCase.P2P
|
||||
import net.corda.core.serialization.SerializationCustomSerializer
|
||||
import net.corda.core.serialization.SerializationWhitelist
|
||||
import net.corda.core.serialization.internal.SerializationEnvironment
|
||||
import net.corda.core.serialization.internal._contextSerializationEnv
|
||||
import net.corda.serialization.internal.*
|
||||
@ -57,15 +58,16 @@ class LocalSerializationRule(private val label: String) : TestRule {
|
||||
|
||||
private fun createTestSerializationEnv(): SerializationEnvironment {
|
||||
val factory = SerializationFactoryImpl(mutableMapOf()).apply {
|
||||
registerScheme(AMQPSerializationScheme(emptySet(), AccessOrderLinkedHashMap(128)))
|
||||
registerScheme(AMQPSerializationScheme(emptySet(), emptySet(), AccessOrderLinkedHashMap(128)))
|
||||
}
|
||||
return SerializationEnvironment.with(factory, AMQP_P2P_CONTEXT)
|
||||
}
|
||||
|
||||
private class AMQPSerializationScheme(
|
||||
cordappCustomSerializers: Set<SerializationCustomSerializer<*, *>>,
|
||||
cordappSerializationWhitelists: Set<SerializationWhitelist>,
|
||||
serializerFactoriesForContexts: AccessOrderLinkedHashMap<SerializationFactoryCacheKey, SerializerFactory>
|
||||
) : AbstractAMQPSerializationScheme(cordappCustomSerializers, serializerFactoriesForContexts) {
|
||||
) : AbstractAMQPSerializationScheme(cordappCustomSerializers, cordappSerializationWhitelists, serializerFactoriesForContexts) {
|
||||
override fun rpcServerSerializerFactory(context: SerializationContext): SerializerFactory {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
@ -2,8 +2,8 @@ package net.corda.deterministic.verifier
|
||||
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.contracts.ContractAttachment
|
||||
import net.corda.core.contracts.ContractClassName
|
||||
import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER
|
||||
import net.corda.core.internal.toLtxDjvmInternal
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
@ -25,13 +25,10 @@ class TransactionVerificationRequest(val wtxToVerify: SerializedBytes<WireTransa
|
||||
val attachmentMap = attachments
|
||||
.mapNotNull { it as? MockContractAttachment }
|
||||
.associateBy(Attachment::id) { ContractAttachment(it, it.contract, uploader = DEPLOYED_CORDAPP_UPLOADER) }
|
||||
val contractAttachmentMap = emptyMap<ContractClassName, ContractAttachment>()
|
||||
@Suppress("DEPRECATION")
|
||||
return wtxToVerify.deserialize().toLedgerTransaction(
|
||||
resolveIdentity = { null },
|
||||
return wtxToVerify.deserialize().toLtxDjvmInternal(
|
||||
resolveAttachment = { attachmentMap[it] },
|
||||
resolveStateRef = { deps[it.txhash]?.outputs?.get(it.index) },
|
||||
resolveContractAttachment = { contractAttachmentMap[it.contract]?.id },
|
||||
resolveParameters = { networkParameters.deserialize() }
|
||||
)
|
||||
}
|
||||
|
@ -111,6 +111,8 @@ dependencies {
|
||||
|
||||
// required to use @Type annotation
|
||||
compile "org.hibernate:hibernate-core:$hibernate_version"
|
||||
|
||||
compile group: "io.github.classgraph", name: "classgraph", version: class_graph_version
|
||||
}
|
||||
|
||||
// TODO Consider moving it to quasar-utils in the future (introduced with PR-1388)
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import kotlin.reflect.KClass
|
||||
import net.corda.core.contracts.TransactionVerificationException.TransactionContractConflictException
|
||||
|
||||
/**
|
||||
* This annotation is required by any [ContractState] which needs to ensure that it is only ever processed as part of a
|
||||
@ -12,7 +13,8 @@ import kotlin.reflect.KClass
|
||||
* checked to ensure that their [ContractState]s match with their [Contract]s as specified either by this annotation, or
|
||||
* by their inner/outer class relationship.
|
||||
*
|
||||
* The transaction will write a warning to the log if any mismatch is detected.
|
||||
* The transaction will write a warning to the log (for corDapps with a target version less than 4) or
|
||||
* fail with a [TransactionContractConflictException] if any mismatch is detected.
|
||||
*
|
||||
* @param value The class of the [Contract] to which states of the annotated [ContractState] belong.
|
||||
*/
|
||||
|
@ -65,6 +65,19 @@ class StaticPointer<T : ContractState>(override val pointer: StateRef, override
|
||||
override fun resolve(ltx: LedgerTransaction): StateAndRef<T> {
|
||||
return ltx.referenceInputRefsOfType(type).single { pointer == it.ref }
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is StaticPointer<*>) return false
|
||||
|
||||
if (pointer != other.pointer) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return pointer.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -118,4 +131,17 @@ class LinearPointer<T : LinearState>(override val pointer: UniqueIdentifier, ove
|
||||
override fun resolve(ltx: LedgerTransaction): StateAndRef<T> {
|
||||
return ltx.referenceInputRefsOfType(type).single { pointer == it.state.data.linearId }
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is LinearPointer<*>) return false
|
||||
|
||||
if (pointer != other.pointer) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return pointer.hashCode()
|
||||
}
|
||||
}
|
@ -237,6 +237,7 @@ interface MoveCommand : CommandData {
|
||||
data class CommandWithParties<out T : CommandData>(
|
||||
val signers: List<PublicKey>,
|
||||
/** If any public keys were recognised, the looked up institutions are available here */
|
||||
@Deprecated("Should not be used in contract verification code as it is non-deterministic, will be disabled for some future target platform version onwards and will take effect only for CorDapps targeting those versions.")
|
||||
val signingParties: List<Party>,
|
||||
val value: T
|
||||
)
|
||||
|
@ -7,11 +7,13 @@ import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.groupAbstractPartyByWellKnownParty
|
||||
import net.corda.core.internal.cordapp.CordappResolver
|
||||
import net.corda.core.internal.pushToLoggingContext
|
||||
import net.corda.core.internal.warnOnce
|
||||
import net.corda.core.node.StatesToRecord
|
||||
import net.corda.core.node.StatesToRecord.ONLY_RELEVANT
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.debug
|
||||
|
||||
/**
|
||||
* Verifies the given transaction, then sends it to the named notary. If the notary agrees that the transaction
|
||||
@ -29,44 +31,74 @@ import net.corda.core.utilities.ProgressTracker
|
||||
* The flow returns the same transaction but with the additional signatures from the notary.
|
||||
*
|
||||
* NOTE: This is an inlined flow but for backwards compatibility is annotated with [InitiatingFlow].
|
||||
*
|
||||
* @param transaction What to commit.
|
||||
* @param sessions A collection of [FlowSession]s who will be given the notarised transaction. This list **must** include
|
||||
* all participants in the transaction (excluding the local identity).
|
||||
*/
|
||||
// To maintain backwards compatibility with the old API, FinalityFlow can act both as an initiating flow and as an inlined flow.
|
||||
// This is only possible because a flow is only truly initiating when the first call to initiateFlow is made (where the
|
||||
// presence of @InitiatingFlow is checked). So the new API is inlined simply because that code path doesn't call initiateFlow.
|
||||
@InitiatingFlow
|
||||
class FinalityFlow private constructor(val transaction: SignedTransaction,
|
||||
private val extraRecipients: Set<Party>,
|
||||
private val oldParticipants: Collection<Party>,
|
||||
override val progressTracker: ProgressTracker,
|
||||
private val sessions: Collection<FlowSession>?) : FlowLogic<SignedTransaction>() {
|
||||
private val sessions: Collection<FlowSession>,
|
||||
private val newApi: Boolean) : FlowLogic<SignedTransaction>() {
|
||||
@Deprecated(DEPRECATION_MSG)
|
||||
constructor(transaction: SignedTransaction, extraRecipients: Set<Party>, progressTracker: ProgressTracker) : this(
|
||||
transaction, extraRecipients, progressTracker, null
|
||||
transaction, extraRecipients, progressTracker, emptyList(), false
|
||||
)
|
||||
@Deprecated(DEPRECATION_MSG)
|
||||
constructor(transaction: SignedTransaction, extraRecipients: Set<Party>) : this(transaction, extraRecipients, tracker(), null)
|
||||
constructor(transaction: SignedTransaction, extraRecipients: Set<Party>) : this(transaction, extraRecipients, tracker(), emptyList(), false)
|
||||
@Deprecated(DEPRECATION_MSG)
|
||||
constructor(transaction: SignedTransaction) : this(transaction, emptySet(), tracker(), null)
|
||||
constructor(transaction: SignedTransaction) : this(transaction, emptySet(), tracker(), emptyList(), false)
|
||||
@Deprecated(DEPRECATION_MSG)
|
||||
constructor(transaction: SignedTransaction, progressTracker: ProgressTracker) : this(transaction, emptySet(), progressTracker, null)
|
||||
constructor(transaction: SignedTransaction, progressTracker: ProgressTracker) : this(transaction, emptySet(), progressTracker, emptyList(), false)
|
||||
|
||||
constructor(transaction: SignedTransaction, sessions: Collection<FlowSession>, progressTracker: ProgressTracker) : this(
|
||||
transaction, emptySet(), progressTracker, sessions
|
||||
)
|
||||
constructor(transaction: SignedTransaction, sessions: Collection<FlowSession>) : this(
|
||||
transaction, emptySet(), tracker(), sessions
|
||||
)
|
||||
/**
|
||||
* Notarise the given transaction and broadcast it to the given [FlowSession]s. This list **must** at least include
|
||||
* all the non-local participants of the transaction. Sessions to non-participants can also be provided.
|
||||
*
|
||||
* @param transaction What to commit.
|
||||
*/
|
||||
constructor(transaction: SignedTransaction, firstSession: FlowSession, vararg restSessions: FlowSession) : this(
|
||||
transaction, emptySet(), tracker(), listOf(firstSession) + restSessions.asList()
|
||||
transaction, listOf(firstSession) + restSessions.asList()
|
||||
)
|
||||
|
||||
/**
|
||||
* Notarise the given transaction and broadcast it to all the participants.
|
||||
*
|
||||
* @param transaction What to commit.
|
||||
* @param sessions A collection of [FlowSession]s for each non-local participant of the transaction. Sessions to non-participants can
|
||||
* also be provided.
|
||||
*/
|
||||
@JvmOverloads
|
||||
constructor(
|
||||
transaction: SignedTransaction,
|
||||
sessions: Collection<FlowSession>,
|
||||
progressTracker: ProgressTracker = tracker()
|
||||
) : this(transaction, emptyList(), progressTracker, sessions, true)
|
||||
|
||||
/**
|
||||
* Notarise the given transaction and broadcast it to all the participants.
|
||||
*
|
||||
* @param transaction What to commit.
|
||||
* @param sessions A collection of [FlowSession]s for each non-local participant.
|
||||
* @param oldParticipants An **optional** collection of parties for participants who are still using the old API.
|
||||
*
|
||||
* You will only need to use this parameter if you have upgraded your CorDapp from the V3 FinalityFlow API but are required to provide
|
||||
* backwards compatibility with participants running V3 nodes. If you're writing a new CorDapp then this does not apply and this
|
||||
* parameter should be ignored.
|
||||
*/
|
||||
@Deprecated(DEPRECATION_MSG)
|
||||
constructor(
|
||||
transaction: SignedTransaction,
|
||||
sessions: Collection<FlowSession>,
|
||||
oldParticipants: Collection<Party>,
|
||||
progressTracker: ProgressTracker
|
||||
) : this(transaction, oldParticipants, progressTracker, sessions, true)
|
||||
|
||||
companion object {
|
||||
private const val DEPRECATION_MSG = "It is unsafe to use this constructor as it requires nodes to automatically " +
|
||||
"accept notarised transactions without first checking their relevancy. Instead, use one of the constructors " +
|
||||
"that takes in existing FlowSessions."
|
||||
"that requires only FlowSessions."
|
||||
|
||||
object NOTARISING : ProgressTracker.Step("Requesting signature by notary service") {
|
||||
override fun childProgressTracker() = NotaryFlow.Client.tracker()
|
||||
@ -81,13 +113,13 @@ class FinalityFlow private constructor(val transaction: SignedTransaction,
|
||||
@Suspendable
|
||||
@Throws(NotaryException::class)
|
||||
override fun call(): SignedTransaction {
|
||||
if (sessions == null) {
|
||||
if (!newApi) {
|
||||
require(CordappResolver.currentTargetVersion < 4) {
|
||||
"A flow session for each external participant to the transaction must be provided. If you wish to continue " +
|
||||
"using this insecure API then specify a target platform version of less than 4 for your CorDapp."
|
||||
}
|
||||
logger.warn("The current usage of FinalityFlow is unsafe. Please consider upgrading your CorDapp to use " +
|
||||
"FinalityFlow with FlowSessions.")
|
||||
logger.warnOnce("The current usage of FinalityFlow is unsafe. Please consider upgrading your CorDapp to use " +
|
||||
"FinalityFlow with FlowSessions. (${CordappResolver.currentCordapp?.info})")
|
||||
} else {
|
||||
require(sessions.none { serviceHub.myInfo.isLegalIdentity(it.counterparty) }) {
|
||||
"Do not provide flow sessions for the local node. FinalityFlow will record the notarised transaction locally."
|
||||
@ -103,30 +135,25 @@ class FinalityFlow private constructor(val transaction: SignedTransaction,
|
||||
transaction.pushToLoggingContext()
|
||||
logCommandData()
|
||||
val ledgerTransaction = verifyTx()
|
||||
val externalParticipants = extractExternalParticipants(ledgerTransaction)
|
||||
val externalTxParticipants = extractExternalParticipants(ledgerTransaction)
|
||||
|
||||
if (sessions != null) {
|
||||
val missingRecipients = externalParticipants - sessions.map { it.counterparty }
|
||||
if (newApi) {
|
||||
val sessionParties = sessions.map { it.counterparty }
|
||||
val missingRecipients = externalTxParticipants - sessionParties - oldParticipants
|
||||
require(missingRecipients.isEmpty()) {
|
||||
"Flow sessions were not provided for the following transaction participants: $missingRecipients"
|
||||
}
|
||||
sessionParties.intersect(oldParticipants).let {
|
||||
require(it.isEmpty()) { "The following parties are specified both in flow sessions and in the oldParticipants list: $it" }
|
||||
}
|
||||
}
|
||||
|
||||
val notarised = notariseAndRecord()
|
||||
|
||||
// Each transaction has its own set of recipients, but extra recipients get them all.
|
||||
progressTracker.currentStep = BROADCASTING
|
||||
|
||||
if (sessions == null) {
|
||||
val recipients = externalParticipants + (extraRecipients - serviceHub.myInfo.legalIdentities)
|
||||
logger.info("Broadcasting transaction to parties ${recipients.joinToString(", ", "[", "]")}.")
|
||||
for (recipient in recipients) {
|
||||
logger.info("Sending transaction to party ${recipient.name}.")
|
||||
val session = initiateFlow(recipient)
|
||||
subFlow(SendTransactionFlow(session, notarised))
|
||||
logger.info("Party $recipient received the transaction.")
|
||||
}
|
||||
} else {
|
||||
if (newApi) {
|
||||
oldV3Broadcast(notarised, oldParticipants.toSet())
|
||||
for (session in sessions) {
|
||||
try {
|
||||
subFlow(SendTransactionFlow(session, notarised))
|
||||
@ -140,6 +167,8 @@ class FinalityFlow private constructor(val transaction: SignedTransaction,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
oldV3Broadcast(notarised, (externalTxParticipants + oldParticipants).toSet())
|
||||
}
|
||||
|
||||
logger.info("All parties received the transaction successfully.")
|
||||
@ -147,6 +176,18 @@ class FinalityFlow private constructor(val transaction: SignedTransaction,
|
||||
return notarised
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun oldV3Broadcast(notarised: SignedTransaction, recipients: Set<Party>) {
|
||||
for (recipient in recipients) {
|
||||
if (!serviceHub.myInfo.isLegalIdentity(recipient)) {
|
||||
logger.debug { "Sending transaction to party $recipient." }
|
||||
val session = initiateFlow(recipient)
|
||||
subFlow(SendTransactionFlow(session, notarised))
|
||||
logger.info("Party $recipient received the transaction.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun logCommandData() {
|
||||
if (logger.isDebugEnabled) {
|
||||
val commandDataTypes = transaction.tx.commands.asSequence().mapNotNull { it.value::class.qualifiedName }.distinct()
|
||||
|
@ -95,6 +95,8 @@ abstract class FlowLogic<out T> {
|
||||
fiber.suspend(request, maySkipCheckpoint = maySkipCheckpoint)
|
||||
}
|
||||
}
|
||||
|
||||
private val DEFAULT_TRACKER = { ProgressTracker() }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -345,7 +347,7 @@ abstract class FlowLogic<out T> {
|
||||
* Note that this has to return a tracker before the flow is invoked. You can't change your mind half way
|
||||
* through.
|
||||
*/
|
||||
open val progressTracker: ProgressTracker? = ProgressTracker.DEFAULT_TRACKER()
|
||||
open val progressTracker: ProgressTracker? = DEFAULT_TRACKER()
|
||||
|
||||
/**
|
||||
* This is where you fill out your business logic.
|
||||
|
@ -34,7 +34,7 @@ class NotaryChangeFlow<out T : ContractState>(
|
||||
inputs.map { it.ref },
|
||||
originalState.state.notary,
|
||||
modification,
|
||||
serviceHub.networkParametersStorage.currentHash
|
||||
serviceHub.networkParametersService.currentHash
|
||||
).build()
|
||||
|
||||
val participantKeys = inputs.flatMap { it.state.data.participants }.map { it.owningKey }.toSet()
|
||||
|
@ -28,7 +28,7 @@ sealed class NotaryError {
|
||||
/** Specifies which states have already been consumed in another transaction. */
|
||||
val consumedStates: Map<StateRef, StateConsumptionDetails>
|
||||
) : NotaryError() {
|
||||
override fun toString() = "One or more input states have already been used in other transactions. Conflicting state count: ${consumedStates.size}, consumption details:\n" +
|
||||
override fun toString() = "One or more input states or referenced states have already been used as input states in other transactions. Conflicting state count: ${consumedStates.size}, consumption details:\n" +
|
||||
"${consumedStates.asSequence().joinToString(",\n", limit = 5) { it.key.toString() + " -> " + it.value }}.\n" +
|
||||
"To find out if any of the conflicting transactions have been generated by this node you can use the hashLookup Corda shell command."
|
||||
}
|
||||
@ -76,7 +76,7 @@ sealed class NotaryError {
|
||||
*/
|
||||
// TODO: include notary timestamp?
|
||||
@CordaSerializable
|
||||
data class StateConsumptionDetails(
|
||||
data class StateConsumptionDetails(
|
||||
val hashOfTransactionId: SecureHash,
|
||||
val type: ConsumedStateType
|
||||
) {
|
||||
|
@ -10,7 +10,7 @@ import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.BackpressureAwareTimedFlow
|
||||
import net.corda.core.internal.FetchDataFlow
|
||||
import net.corda.core.internal.notary.HistoricNetworkParameterStorage
|
||||
import net.corda.core.internal.NetworkParametersServiceInternal
|
||||
import net.corda.core.internal.notary.generateSignature
|
||||
import net.corda.core.internal.notary.validateSignatures
|
||||
import net.corda.core.internal.pushToLoggingContext
|
||||
@ -107,7 +107,7 @@ class NotaryFlow {
|
||||
check(stx.coreTransaction is NotaryChangeWireTransaction) {
|
||||
"Notary $notaryParty is not on the network parameter whitelist. A non-whitelisted notary can only be used for notary change transactions"
|
||||
}
|
||||
val historicNotary = (serviceHub.networkParametersStorage as HistoricNetworkParameterStorage).getHistoricNotary(notaryParty)
|
||||
val historicNotary = (serviceHub.networkParametersService as NetworkParametersServiceInternal).getHistoricNotary(notaryParty)
|
||||
?: throw IllegalStateException("The notary party $notaryParty specified by transaction ${stx.id}, is not recognised as a current or historic notary.")
|
||||
historicNotary.validating
|
||||
|
||||
|
@ -43,7 +43,7 @@ open class ReceiveTransactionFlow @JvmOverloads constructor(private val otherSid
|
||||
it.pushToLoggingContext()
|
||||
logger.info("Received transaction acknowledgement request from party ${otherSideSession.counterparty}.")
|
||||
checkParameterHash(it.networkParametersHash)
|
||||
subFlow(ResolveTransactionsFlow(it, otherSideSession))
|
||||
subFlow(ResolveTransactionsFlow(it, otherSideSession, statesToRecord))
|
||||
logger.info("Transaction dependencies resolution completed.")
|
||||
try {
|
||||
it.verify(serviceHub, checkSufficientSignatures)
|
||||
|
@ -4,11 +4,10 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.contextLogger
|
||||
|
||||
/**
|
||||
* Given a flow which uses reference states, the [WithReferencedStatesFlow] will execute the the flow as a subFlow.
|
||||
* If the flow fails due to a [NotaryError.Conflict] for a reference state, then it will be suspended until the
|
||||
* Given a flow which uses reference states, the [WithReferencedStatesFlow] will execute the flow as a subFlow.
|
||||
* If the flow fails due to a [NotaryError.Conflict] for a reference state, then WithReferencedStatesFlow will be suspended until the
|
||||
* state refs for the reference states are consumed. In this case, a consumption means that:
|
||||
*
|
||||
* 1. the owner of the reference state has updated the state with a valid, notarised transaction
|
||||
@ -21,17 +20,16 @@ import net.corda.core.utilities.contextLogger
|
||||
* reference states. The flow using reference states should include checks to ensure that the reference data is
|
||||
* reasonable, especially if some economics transaction depends upon it.
|
||||
*
|
||||
* @param flowLogic a flow which uses reference states.
|
||||
* @param progressTracker a progress tracker instance.
|
||||
* @param flowLogicProducer a lambda which creates the [FlowLogic] instance using reference states. This will be executed at least once.
|
||||
* It is recommended a new [FlowLogic] instance be returned each time.
|
||||
*/
|
||||
class WithReferencedStatesFlow<T : Any>(
|
||||
val flowLogic: FlowLogic<T>,
|
||||
override val progressTracker: ProgressTracker = WithReferencedStatesFlow.tracker()
|
||||
class WithReferencedStatesFlow<T : Any> @JvmOverloads constructor(
|
||||
override val progressTracker: ProgressTracker = tracker(),
|
||||
private val flowLogicProducer: () -> FlowLogic<T>
|
||||
) : FlowLogic<T>() {
|
||||
|
||||
companion object {
|
||||
val logger = contextLogger()
|
||||
|
||||
object ATTEMPT : ProgressTracker.Step("Attempting to run flow which uses reference states.")
|
||||
object RETRYING : ProgressTracker.Step("Reference states are out of date! Waiting for updated states...")
|
||||
object SUCCESS : ProgressTracker.Step("Flow ran successfully.")
|
||||
@ -40,10 +38,10 @@ class WithReferencedStatesFlow<T : Any>(
|
||||
fun tracker() = ProgressTracker(ATTEMPT, RETRYING, SUCCESS)
|
||||
}
|
||||
|
||||
private sealed class FlowResult {
|
||||
data class Success<T : Any>(val value: T) : FlowResult()
|
||||
data class Conflict(val stateRefs: Set<StateRef>) : FlowResult()
|
||||
}
|
||||
// This is not a sealed data class as that requires exposing Success and Conflict
|
||||
private interface FlowResult
|
||||
private data class Success<T : Any>(val value: T) : FlowResult
|
||||
private data class Conflict(val stateRefs: Set<StateRef>) : FlowResult
|
||||
|
||||
/**
|
||||
* Process the flow result. We don't care about anything other than NotaryExceptions. If it is a
|
||||
@ -58,13 +56,13 @@ class WithReferencedStatesFlow<T : Any>(
|
||||
val conflictingReferenceStateRefs = error.consumedStates.filter {
|
||||
it.value.type == StateConsumptionDetails.ConsumedStateType.REFERENCE_INPUT_STATE
|
||||
}.map { it.key }.toSet()
|
||||
FlowResult.Conflict(conflictingReferenceStateRefs)
|
||||
Conflict(conflictingReferenceStateRefs)
|
||||
} else {
|
||||
throw result
|
||||
}
|
||||
}
|
||||
is FlowException -> throw result
|
||||
else -> FlowResult.Success(result)
|
||||
else -> Success(result)
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,6 +73,7 @@ class WithReferencedStatesFlow<T : Any>(
|
||||
// Loop until the flow successfully completes. We need to
|
||||
// do this because there might be consecutive update races.
|
||||
while (true) {
|
||||
val flowLogic = flowLogicProducer()
|
||||
// Return a successful flow result or a FlowException.
|
||||
logger.info("Attempting to run the supplied flow ${flowLogic.javaClass.canonicalName}.")
|
||||
val result = try {
|
||||
@ -91,12 +90,12 @@ class WithReferencedStatesFlow<T : Any>(
|
||||
// states have been updated.
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
when (processedResult) {
|
||||
is FlowResult.Success<*> -> {
|
||||
is Success<*> -> {
|
||||
logger.info("Flow ${flowLogic.javaClass.canonicalName} completed successfully.")
|
||||
progressTracker.currentStep = SUCCESS
|
||||
return uncheckedCast(processedResult.value)
|
||||
}
|
||||
is FlowResult.Conflict -> {
|
||||
is Conflict -> {
|
||||
val conflicts = processedResult.stateRefs
|
||||
logger.info("Flow ${flowLogic.javaClass.name} failed due to reference state conflicts: $conflicts.")
|
||||
|
||||
@ -112,4 +111,4 @@ class WithReferencedStatesFlow<T : Any>(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,31 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
import io.github.classgraph.ClassGraph
|
||||
import net.corda.core.CordaInternal
|
||||
import net.corda.core.DeleteForDJVM
|
||||
import net.corda.core.StubOutForDJVM
|
||||
import kotlin.reflect.full.createInstance
|
||||
|
||||
/**
|
||||
* Creates instances of all the classes in the classpath of the provided classloader, which implement the interface of the provided class.
|
||||
* @param classloader the classloader, which will be searched for the classes.
|
||||
* @param clazz the class of the interface, which the classes - to be returned - must implement.
|
||||
*
|
||||
* @return instances of the identified classes.
|
||||
* @throws IllegalArgumentException if the classes found do not have proper constructors.
|
||||
*
|
||||
* Note: In order to be instantiated, the associated classes must:
|
||||
* - be non-abstract
|
||||
* - either be a Kotlin object or have a constructor with no parameters (or only optional ones)
|
||||
*/
|
||||
@StubOutForDJVM
|
||||
fun <T: Any> loadClassesImplementing(classloader: ClassLoader, clazz: Class<T>): Set<T> {
|
||||
return ClassGraph().addClassLoader(classloader)
|
||||
.enableAllInfo()
|
||||
.scan()
|
||||
.getClassesImplementing(clazz.name)
|
||||
.filterNot { it.isAbstract }
|
||||
.mapNotNull { classloader.loadClass(it.name).asSubclass(clazz) }
|
||||
.map { it.kotlin.objectInstance ?: it.kotlin.createInstance() }
|
||||
.toSet()
|
||||
}
|
@ -21,7 +21,7 @@ object ContractUpgradeUtils {
|
||||
else -> getContractAttachmentId(stateAndRef.state.contract, services)
|
||||
}
|
||||
val upgradedContractAttachmentId = getContractAttachmentId(upgradedContractClass.name, services)
|
||||
val networkParametersHash = services.networkParametersStorage.currentHash
|
||||
val networkParametersHash = services.networkParametersService.currentHash
|
||||
|
||||
val inputs = listOf(stateAndRef.ref)
|
||||
return ContractUpgradeTransactionBuilder(
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
import net.corda.core.DeleteForDJVM
|
||||
import net.corda.core.contracts.ContractAttachment
|
||||
import net.corda.core.contracts.ContractClassName
|
||||
import net.corda.core.cordapp.Cordapp
|
||||
import net.corda.core.cordapp.CordappConfig
|
||||
@ -11,18 +12,28 @@ import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.node.ZoneVersionTooLowException
|
||||
import net.corda.core.node.services.AttachmentStorage
|
||||
import net.corda.core.node.services.vault.AttachmentQueryCriteria
|
||||
import net.corda.core.node.services.vault.AttachmentSort
|
||||
import net.corda.core.node.services.vault.Builder
|
||||
import net.corda.core.node.services.vault.Sort
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
|
||||
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 org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.slf4j.MDC
|
||||
import java.security.PublicKey
|
||||
import java.util.jar.JarEntry
|
||||
import java.util.jar.JarInputStream
|
||||
|
||||
// *Internal* Corda-specific utilities.
|
||||
|
||||
const val PLATFORM_VERSION = 4
|
||||
const val PLATFORM_VERSION = 5
|
||||
|
||||
fun ServicesForResolution.ensureMinimumPlatformVersion(requiredMinPlatformVersion: Int, feature: String) {
|
||||
checkMinimumPlatformVersion(networkParameters.minimumPlatformVersion, requiredMinPlatformVersion, feature)
|
||||
@ -116,3 +127,34 @@ internal fun NetworkParameters.getPackageOwnerOf(contractClassNames: Set<Contrac
|
||||
fun noPackageOverlap(packages: Collection<String>): Boolean {
|
||||
return packages.all { outer -> packages.none { inner -> inner != outer && inner.startsWith("$outer.") } }
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans trusted (installed locally) contract attachments to find all that contain the [className].
|
||||
* This is required as a workaround until explicit cordapp dependencies are implemented.
|
||||
* DO NOT USE IN CLIENT code.
|
||||
*
|
||||
* @return the contract attachments with the highest version.
|
||||
*
|
||||
* TODO: Should throw when the class is found in multiple contract attachments (not different versions).
|
||||
*/
|
||||
fun AttachmentStorage.internalFindTrustedAttachmentForClass(className: String): ContractAttachment?{
|
||||
val allTrusted = queryAttachments(
|
||||
AttachmentQueryCriteria.AttachmentsQueryCriteria().withUploader(Builder.`in`(TRUSTED_UPLOADERS)),
|
||||
AttachmentSort(listOf(AttachmentSort.AttachmentSortColumn(AttachmentSort.AttachmentSortAttribute.VERSION, Sort.Direction.DESC))))
|
||||
|
||||
// TODO - add caching if performance is affected.
|
||||
for (attId in allTrusted) {
|
||||
val attch = openAttachment(attId)!!
|
||||
if (attch is ContractAttachment && attch.openAsJAR().use { hasFile(it, "$className.class") }) return attch
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun hasFile(jarStream: JarInputStream, className: String): Boolean {
|
||||
while (true) {
|
||||
val e = jarStream.nextJarEntry ?: return false
|
||||
if (e.name == className) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
20
core/src/main/kotlin/net/corda/core/internal/DjvmUtils.kt
Normal file
20
core/src/main/kotlin/net/corda/core/internal/DjvmUtils.kt
Normal file
@ -0,0 +1,20 @@
|
||||
@file:KeepForDJVM
|
||||
|
||||
package net.corda.core.internal
|
||||
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TransactionState
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
|
||||
fun WireTransaction.toLtxDjvmInternal(
|
||||
resolveAttachment: (SecureHash) -> Attachment?,
|
||||
resolveStateRef: (StateRef) -> TransactionState<*>?,
|
||||
resolveParameters: (SecureHash?) -> NetworkParameters?
|
||||
): LedgerTransaction {
|
||||
return toLtxDjvmInternalBridge(resolveAttachment, resolveStateRef, resolveParameters)
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
package net.corda.core.internal.notary
|
||||
package net.corda.core.internal
|
||||
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.NotaryInfo
|
||||
import net.corda.core.node.services.NetworkParametersService
|
||||
|
||||
interface HistoricNetworkParameterStorage {
|
||||
interface NetworkParametersServiceInternal : NetworkParametersService {
|
||||
/**
|
||||
* Returns the [NotaryInfo] for a notary [party] in the current or any historic network parameter whitelist, or null if not found.
|
||||
*/
|
@ -75,6 +75,9 @@ inline val Path.isReadable: Boolean get() = Files.isReadable(this)
|
||||
/** @see Files.size */
|
||||
inline val Path.size: Long get() = Files.size(this)
|
||||
|
||||
/** @see Files.readAttributes */
|
||||
fun Path.attributes(vararg options: LinkOption): BasicFileAttributes = Files.readAttributes(this, BasicFileAttributes::class.java, *options)
|
||||
|
||||
/** @see Files.getLastModifiedTime */
|
||||
fun Path.lastModifiedTime(vararg options: LinkOption): FileTime = Files.getLastModifiedTime(this, *options)
|
||||
|
||||
|
@ -20,12 +20,11 @@ import kotlin.math.min
|
||||
/**
|
||||
* Resolves transactions for the specified [txHashes] along with their full history (dependency graph) from [otherSide].
|
||||
* Each retrieved transaction is validated and inserted into the local transaction storage.
|
||||
*
|
||||
* @return a list of verified [SignedTransaction] objects, in a depth-first order.
|
||||
*/
|
||||
@DeleteForDJVM
|
||||
class ResolveTransactionsFlow(txHashesArg: Set<SecureHash>,
|
||||
private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||
private val otherSide: FlowSession,
|
||||
private val statesToRecord: StatesToRecord = StatesToRecord.NONE) : FlowLogic<Unit>() {
|
||||
|
||||
// Need it ordered in terms of iteration. Needs to be a variable for the check-pointing logic to work.
|
||||
private val txHashes = txHashesArg.toList()
|
||||
@ -40,6 +39,10 @@ class ResolveTransactionsFlow(txHashesArg: Set<SecureHash>,
|
||||
this.signedTransaction = signedTransaction
|
||||
}
|
||||
|
||||
constructor(signedTransaction: SignedTransaction, otherSide: FlowSession, statesToRecord: StatesToRecord) : this(dependencyIDs(signedTransaction), otherSide, statesToRecord) {
|
||||
this.signedTransaction = signedTransaction
|
||||
}
|
||||
|
||||
@DeleteForDJVM
|
||||
companion object {
|
||||
private fun dependencyIDs(stx: SignedTransaction) = stx.inputs.map { it.txhash }.toSet() + stx.references.map { it.txhash }.toSet()
|
||||
@ -87,13 +90,16 @@ class ResolveTransactionsFlow(txHashesArg: Set<SecureHash>,
|
||||
// Finish fetching data.
|
||||
|
||||
val result = topologicalSort(newTxns)
|
||||
// If transaction resolution is performed for a transaction where some states are relevant, then those should be
|
||||
// recorded if this has not already occurred.
|
||||
val usedStatesToRecord = if (statesToRecord == StatesToRecord.NONE) StatesToRecord.ONLY_RELEVANT else statesToRecord
|
||||
result.forEach {
|
||||
// For each transaction, verify it and insert it into the database. As we are iterating over them in a
|
||||
// depth-first order, we should not encounter any verification failures due to missing data. If we fail
|
||||
// half way through, it's no big deal, although it might result in us attempting to re-download data
|
||||
// redundantly next time we attempt verification.
|
||||
it.verify(serviceHub)
|
||||
serviceHub.recordTransactions(StatesToRecord.NONE, listOf(it))
|
||||
serviceHub.recordTransactions(usedStatesToRecord, listOf(it))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,18 +1,17 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.LinearPointer
|
||||
import net.corda.core.contracts.StatePointer
|
||||
import net.corda.core.contracts.StaticPointer
|
||||
import java.lang.reflect.Field
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Uses reflection to search for instances of [StatePointer] within a [ContractState].
|
||||
* TODO: Doesn't handle calculated properties. Add support for this.
|
||||
*/
|
||||
class StatePointerSearch(val state: ContractState) {
|
||||
// Classes in these packages should not be part of a search.
|
||||
private val blackListedPackages = setOf("java.", "javax.")
|
||||
private val blackListedPackages = setOf("java.", "javax.", "org.bouncycastle.", "net.i2p.crypto.")
|
||||
|
||||
// Type required for traversal.
|
||||
private data class FieldWithObject(val obj: Any, val field: Field)
|
||||
@ -21,14 +20,26 @@ class StatePointerSearch(val state: ContractState) {
|
||||
private val statePointers = mutableSetOf<StatePointer<*>>()
|
||||
|
||||
// Record seen objects to avoid getting stuck in loops.
|
||||
private val seenObjects = mutableSetOf<Any>().apply { add(state) }
|
||||
private val seenObjects = Collections.newSetFromMap(IdentityHashMap<Any, Boolean>()).apply { add(state) }
|
||||
|
||||
// Queue of fields to search.
|
||||
private val fieldQueue = ArrayDeque<FieldWithObject>().apply { addAllFields(state) }
|
||||
|
||||
// Get fields of class and all super-classes.
|
||||
private fun getAllFields(clazz: Class<*>): List<Field> {
|
||||
val fields = mutableListOf<Field>()
|
||||
var currentClazz = clazz
|
||||
while (currentClazz.superclass != null) {
|
||||
fields.addAll(currentClazz.declaredFields)
|
||||
currentClazz = currentClazz.superclass
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
// Helper for adding all fields to the queue.
|
||||
private fun ArrayDeque<FieldWithObject>.addAllFields(obj: Any) {
|
||||
val fields = obj::class.java.declaredFields
|
||||
val fields = getAllFields(obj::class.java)
|
||||
|
||||
val fieldsWithObjects = fields.mapNotNull { field ->
|
||||
// Ignore classes which have not been loaded.
|
||||
// Assumption: all required state classes are already loaded.
|
||||
@ -36,39 +47,44 @@ class StatePointerSearch(val state: ContractState) {
|
||||
if (packageName == null) {
|
||||
null
|
||||
} else {
|
||||
// Ignore JDK classes.
|
||||
val isBlacklistedPackage = blackListedPackages.any { packageName.startsWith(it) }
|
||||
if (isBlacklistedPackage) {
|
||||
null
|
||||
} else {
|
||||
FieldWithObject(obj, field)
|
||||
}
|
||||
FieldWithObject(obj, field)
|
||||
}
|
||||
}
|
||||
addAll(fieldsWithObjects)
|
||||
}
|
||||
|
||||
private fun handleField(obj: Any, field: Field) {
|
||||
when {
|
||||
// StatePointer. Handles nullable StatePointers too.
|
||||
field.type == LinearPointer::class.java -> statePointers.add(field.get(obj) as? LinearPointer<*> ?: return)
|
||||
field.type == StaticPointer::class.java -> statePointers.add(field.get(obj) as? StaticPointer<*> ?: return)
|
||||
// Not StatePointer.
|
||||
private fun handleIterable(iterable: Iterable<*>) {
|
||||
iterable.forEach { obj -> handleObject(obj) }
|
||||
}
|
||||
|
||||
private fun handleMap(map: Map<*, *>) {
|
||||
map.forEach { k, v ->
|
||||
handleObject(k)
|
||||
handleObject(v)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleObject(obj: Any?) {
|
||||
if (obj == null) return
|
||||
seenObjects.add(obj)
|
||||
when (obj) {
|
||||
is Map<*, *> -> handleMap(obj)
|
||||
is StatePointer<*> -> statePointers.add(obj)
|
||||
is Iterable<*> -> handleIterable(obj)
|
||||
else -> {
|
||||
val newObj = field.get(obj) ?: return
|
||||
|
||||
// Ignore nulls.
|
||||
if (newObj in seenObjects) {
|
||||
return
|
||||
}
|
||||
|
||||
// Recurse.
|
||||
fieldQueue.addAllFields(newObj)
|
||||
seenObjects.add(obj)
|
||||
val packageName = obj.javaClass.`package`.name
|
||||
val isBlackListed = blackListedPackages.any { packageName.startsWith(it) }
|
||||
if (isBlackListed.not()) fieldQueue.addAllFields(obj)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleField(obj: Any, field: Field) {
|
||||
val newObj = field.get(obj) ?: return
|
||||
if (newObj in seenObjects) return
|
||||
handleObject(newObj)
|
||||
}
|
||||
|
||||
fun search(): Set<StatePointer<*>> {
|
||||
while (fieldQueue.isNotEmpty()) {
|
||||
val (obj, field) = fieldQueue.pop()
|
||||
|
@ -70,7 +70,7 @@ fun <T : Any> deserialiseComponentGroup(componentGroups: List<ComponentGroup>,
|
||||
// If the componentGroup is a [LazyMappedList] it means that the original deserialized version is already available.
|
||||
val components = group.components
|
||||
if (!forceDeserialize && components is LazyMappedList<*, OpaqueBytes>) {
|
||||
return components.originalList as List<T>
|
||||
return uncheckedCast(components.originalList)
|
||||
}
|
||||
|
||||
return components.lazyMapped { component, internalIndex ->
|
||||
@ -166,7 +166,7 @@ fun FlowLogic<*>.checkParameterHash(networkParametersHash: SecureHash?) {
|
||||
if (serviceHub.networkParameters.minimumPlatformVersion < 4) return
|
||||
else throw IllegalArgumentException("Transaction for notarisation doesn't contain network parameters hash.")
|
||||
} else {
|
||||
serviceHub.networkParametersStorage.lookup(networkParametersHash) ?: throw IllegalArgumentException("Transaction for notarisation contains unknown parameters hash: $networkParametersHash")
|
||||
serviceHub.networkParametersService.lookup(networkParametersHash) ?: throw IllegalArgumentException("Transaction for notarisation contains unknown parameters hash: $networkParametersHash")
|
||||
}
|
||||
|
||||
// TODO: [ENT-2666] Implement network parameters fuzzy checking. By design in Corda network we have propagation time delay.
|
||||
|
@ -0,0 +1,408 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
import net.corda.core.DeleteForDJVM
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.contracts.TransactionVerificationException.TransactionContractConflictException
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.internal.cordapp.CordappImpl
|
||||
import net.corda.core.internal.rules.StateContractValidationEnforcementRule
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.utilities.contextLogger
|
||||
|
||||
@DeleteForDJVM
|
||||
interface TransactionVerifierServiceInternal {
|
||||
/**
|
||||
* Verifies the [transaction] but adds some [extraAttachments] to the classpath.
|
||||
* Required for transactions built with Corda 3.x that might miss some dependencies due to a bug in that version.
|
||||
*/
|
||||
fun verify(transaction: LedgerTransaction, extraAttachments: List<Attachment> ): CordaFuture<*>
|
||||
}
|
||||
|
||||
/**
|
||||
* Defined here for visibility reasons.
|
||||
*/
|
||||
fun LedgerTransaction.prepareVerify(extraAttachments: List<Attachment>) = this.internalPrepareVerify(extraAttachments)
|
||||
|
||||
/**
|
||||
* Because we create a separate [LedgerTransaction] onto which we need to perform verification, it becomes important we don't verify the
|
||||
* wrong object instance. This class helps avoid that.
|
||||
*/
|
||||
class Verifier(val ltx: LedgerTransaction, val transactionClassLoader: ClassLoader, private val inputStatesContractClassNameToMaxVersion: Map<ContractClassName, Version>) {
|
||||
private val inputStates: List<TransactionState<*>> = ltx.inputs.map { it.state }
|
||||
private val allStates: List<TransactionState<*>> = inputStates + ltx.references.map { it.state } + ltx.outputs
|
||||
private val contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>> = getContractAttachmentsByContract()
|
||||
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
}
|
||||
|
||||
fun verify() {
|
||||
// checkNoNotaryChange and checkEncumbrancesValid are called here, and not in the c'tor, as they need access to the "outputs"
|
||||
// list, the contents of which need to be deserialized under the correct classloader.
|
||||
checkNoNotaryChange()
|
||||
checkEncumbrancesValid()
|
||||
validateContractVersions()
|
||||
validatePackageOwnership()
|
||||
validateStatesAgainstContract()
|
||||
val hashToSignatureConstrainedContracts = verifyConstraintsValidity()
|
||||
verifyConstraints(hashToSignatureConstrainedContracts)
|
||||
verifyContracts()
|
||||
}
|
||||
|
||||
// TODO: revisit to include contract version information
|
||||
/**
|
||||
* This method may return more than one attachment for a given contract class.
|
||||
* Specifically, this is the case for transactions combining hash and signature constraints where the hash constrained contract jar
|
||||
* will be unsigned, and the signature constrained counterpart will be signed.
|
||||
*/
|
||||
private fun getContractAttachmentsByContract(): Map<ContractClassName, Set<ContractAttachment>> {
|
||||
val contractClasses = allStates.map { it.contract }.toSet()
|
||||
val result = mutableMapOf<ContractClassName, Set<ContractAttachment>>()
|
||||
|
||||
for (attachment in ltx.attachments) {
|
||||
if (attachment !is ContractAttachment) continue
|
||||
for (contract in contractClasses) {
|
||||
if (contract !in attachment.allContracts) continue
|
||||
result[contract] = result.getOrDefault(contract, setOf(attachment)).plus(attachment)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure the notary has stayed the same. As we can't tell how inputs and outputs connect, if there
|
||||
* are any inputs or reference inputs, all outputs must have the same notary.
|
||||
*
|
||||
* TODO: Is that the correct set of restrictions? May need to come back to this, see if we can be more
|
||||
* flexible on output notaries.
|
||||
*/
|
||||
private fun checkNoNotaryChange() {
|
||||
if (ltx.notary != null && (ltx.inputs.isNotEmpty() || ltx.references.isNotEmpty())) {
|
||||
ltx.outputs.forEach {
|
||||
if (it.notary != ltx.notary) {
|
||||
throw TransactionVerificationException.NotaryChangeInWrongTransactionType(ltx.id, ltx.notary, it.notary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkEncumbrancesValid() {
|
||||
// Validate that all encumbrances exist within the set of input states.
|
||||
ltx.inputs
|
||||
.filter { it.state.encumbrance != null }
|
||||
.forEach { (state, ref) -> checkInputEncumbranceStateExists(state, ref) }
|
||||
|
||||
// Check that in the outputs,
|
||||
// a) an encumbered state does not refer to itself as the encumbrance
|
||||
// b) the number of outputs can contain the encumbrance
|
||||
// c) the bi-directionality (full cycle) property is satisfied
|
||||
// d) encumbered output states are assigned to the same notary.
|
||||
val statesAndEncumbrance = ltx.outputs
|
||||
.withIndex()
|
||||
.filter { it.value.encumbrance != null }
|
||||
.map { Pair(it.index, it.value.encumbrance!!) }
|
||||
if (!statesAndEncumbrance.isEmpty()) {
|
||||
checkBidirectionalOutputEncumbrances(statesAndEncumbrance)
|
||||
checkNotariesOutputEncumbrance(statesAndEncumbrance)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkInputEncumbranceStateExists(state: TransactionState<ContractState>, ref: StateRef) {
|
||||
val encumbranceStateExists = ltx.inputs.any {
|
||||
it.ref.txhash == ref.txhash && it.ref.index == state.encumbrance
|
||||
}
|
||||
if (!encumbranceStateExists) {
|
||||
throw TransactionVerificationException.TransactionMissingEncumbranceException(
|
||||
ltx.id,
|
||||
state.encumbrance!!,
|
||||
TransactionVerificationException.Direction.INPUT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Using basic graph theory, a full cycle of encumbered (co-dependent) states should exist to achieve bi-directional
|
||||
// encumbrances. This property is important to ensure that no states involved in an encumbrance-relationship
|
||||
// can be spent on their own. Briefly, if any of the states is having more than one encumbrance references by
|
||||
// other states, a full cycle detection will fail. As a result, all of the encumbered states must be present
|
||||
// as "from" and "to" only once (or zero times if no encumbrance takes place). For instance,
|
||||
// a -> b
|
||||
// c -> b and a -> b
|
||||
// b -> a b -> c
|
||||
// do not satisfy the bi-directionality (full cycle) property.
|
||||
//
|
||||
// In the first example "b" appears twice in encumbrance ("to") list and "c" exists in the encumbered ("from") list only.
|
||||
// Due the above, one could consume "a" and "b" in the same transaction and then, because "b" is already consumed, "c" cannot be spent.
|
||||
//
|
||||
// Similarly, the second example does not form a full cycle because "a" and "c" exist in one of the lists only.
|
||||
// As a result, one can consume "b" and "c" in the same transactions, which will make "a" impossible to be spent.
|
||||
//
|
||||
// On other hand the following are valid constructions:
|
||||
// a -> b a -> c
|
||||
// b -> c and c -> b
|
||||
// c -> a b -> a
|
||||
// and form a full cycle, meaning that the bi-directionality property is satisfied.
|
||||
private fun checkBidirectionalOutputEncumbrances(statesAndEncumbrance: List<Pair<Int, Int>>) {
|
||||
// [Set] of "from" (encumbered states).
|
||||
val encumberedSet = mutableSetOf<Int>()
|
||||
// [Set] of "to" (encumbrance states).
|
||||
val encumbranceSet = mutableSetOf<Int>()
|
||||
// Update both [Set]s.
|
||||
statesAndEncumbrance.forEach { (statePosition, encumbrance) ->
|
||||
// Check it does not refer to itself.
|
||||
if (statePosition == encumbrance || encumbrance >= ltx.outputs.size) {
|
||||
throw TransactionVerificationException.TransactionMissingEncumbranceException(
|
||||
ltx.id,
|
||||
encumbrance,
|
||||
TransactionVerificationException.Direction.OUTPUT
|
||||
)
|
||||
} else {
|
||||
encumberedSet.add(statePosition) // Guaranteed to have unique elements.
|
||||
if (!encumbranceSet.add(encumbrance)) {
|
||||
throw TransactionVerificationException.TransactionDuplicateEncumbranceException(ltx.id, encumbrance)
|
||||
}
|
||||
}
|
||||
}
|
||||
// At this stage we have ensured that "from" and "to" [Set]s are equal in size, but we should check their
|
||||
// elements do indeed match. If they don't match, we return their symmetric difference (disjunctive union).
|
||||
val symmetricDifference = (encumberedSet union encumbranceSet).subtract(encumberedSet intersect encumbranceSet)
|
||||
if (symmetricDifference.isNotEmpty()) {
|
||||
// At least one encumbered state is not in the [encumbranceSet] and vice versa.
|
||||
throw TransactionVerificationException.TransactionNonMatchingEncumbranceException(ltx.id, symmetricDifference)
|
||||
}
|
||||
}
|
||||
|
||||
// Method to check if all encumbered states are assigned to the same notary Party.
|
||||
// This method should be invoked after [checkBidirectionalOutputEncumbrances], because it assumes that the
|
||||
// bi-directionality property is already satisfied.
|
||||
private fun checkNotariesOutputEncumbrance(statesAndEncumbrance: List<Pair<Int, Int>>) {
|
||||
// We only check for transactions in which notary is null (i.e., issuing transactions).
|
||||
// Note that if a notary is defined for a transaction, we already check if all outputs are assigned
|
||||
// to the same notary (transaction's notary) in [checkNoNotaryChange()].
|
||||
if (ltx.notary == null) {
|
||||
// indicesAlreadyChecked is used to bypass already checked indices and to avoid cycles.
|
||||
val indicesAlreadyChecked = HashSet<Int>()
|
||||
statesAndEncumbrance.forEach {
|
||||
checkNotary(it.first, indicesAlreadyChecked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private tailrec fun checkNotary(index: Int, indicesAlreadyChecked: HashSet<Int>) {
|
||||
if (indicesAlreadyChecked.add(index)) {
|
||||
val encumbranceIndex = ltx.outputs[index].encumbrance!!
|
||||
if (ltx.outputs[index].notary != ltx.outputs[encumbranceIndex].notary) {
|
||||
throw TransactionVerificationException.TransactionNotaryMismatchEncumbranceException(
|
||||
ltx.id,
|
||||
index,
|
||||
encumbranceIndex,
|
||||
ltx.outputs[index].notary,
|
||||
ltx.outputs[encumbranceIndex].notary
|
||||
)
|
||||
} else {
|
||||
checkNotary(encumbranceIndex, indicesAlreadyChecked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that contract class versions of output states are not lower that versions of relevant input states.
|
||||
*/
|
||||
private fun validateContractVersions() {
|
||||
contractAttachmentsByContract.forEach { contractClassName, attachments ->
|
||||
val outputVersion = attachments.signed?.version ?: attachments.unsigned?.version ?: CordappImpl.DEFAULT_CORDAPP_VERSION
|
||||
inputStatesContractClassNameToMaxVersion[contractClassName]?.let {
|
||||
if (it > outputVersion) {
|
||||
throw TransactionVerificationException.TransactionVerificationVersionException(ltx.id, contractClassName, "$it", "$outputVersion")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that for each contract the network wide package owner is respected.
|
||||
*
|
||||
* TODO - revisit once transaction contains network parameters. - UPDATE: It contains them, but because of the API stability and the fact that
|
||||
* LedgerTransaction was data class i.e. exposed constructors that shouldn't had been exposed, we still need to keep them nullable :/
|
||||
*/
|
||||
private fun validatePackageOwnership() {
|
||||
val contractsAndOwners = allStates.mapNotNull { transactionState ->
|
||||
val contractClassName = transactionState.contract
|
||||
ltx.networkParameters!!.getPackageOwnerOf(contractClassName)?.let { contractClassName to it }
|
||||
}.toMap()
|
||||
|
||||
contractsAndOwners.forEach { contract, owner ->
|
||||
contractAttachmentsByContract[contract]?.filter { it.isSigned }?.forEach { attachment ->
|
||||
if (!owner.isFulfilledBy(attachment.signerKeys))
|
||||
throw TransactionVerificationException.ContractAttachmentNotSignedByPackageOwnerException(ltx.id, attachment.id, contract)
|
||||
} ?: throw TransactionVerificationException.ContractAttachmentNotSignedByPackageOwnerException(ltx.id, ltx.id, contract)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For all input and output [TransactionState]s, validates that the wrapped [ContractState] matches up with the
|
||||
* wrapped [Contract], as declared by the [BelongsToContract] annotation on the [ContractState]'s class.
|
||||
*
|
||||
* If the target platform version of the current CorDapp is lower than 4.0, a warning will be written to the log
|
||||
* if any mismatch is detected. If it is 4.0 or later, then [TransactionContractConflictException] will be thrown.
|
||||
*/
|
||||
private fun validateStatesAgainstContract() = allStates.forEach(::validateStateAgainstContract)
|
||||
|
||||
private fun validateStateAgainstContract(state: TransactionState<ContractState>) {
|
||||
val shouldEnforce = StateContractValidationEnforcementRule.shouldEnforce(state.data)
|
||||
|
||||
val requiredContractClassName = state.data.requiredContractClassName
|
||||
?: if (shouldEnforce) throw TransactionVerificationException.TransactionRequiredContractUnspecifiedException(ltx.id, state) else return
|
||||
|
||||
if (state.contract != requiredContractClassName)
|
||||
if (shouldEnforce) {
|
||||
throw TransactionContractConflictException(ltx.id, state, requiredContractClassName)
|
||||
} else {
|
||||
logger.warnOnce("""
|
||||
State of class ${state.data::class.java.typeName} belongs to contract $requiredContractClassName, but
|
||||
is bundled in TransactionState with ${state.contract}.
|
||||
|
||||
For details see: https://docs.corda.net/api-contract-constraints.html#contract-state-agreement
|
||||
""".trimIndent().replace('\n', ' '))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforces the validity of the actual constraints.
|
||||
* * Constraints should be one of the valid supported ones.
|
||||
* * Constraints should propagate correctly if not marked otherwise.
|
||||
*
|
||||
* Returns set of contract classes that identify hash -> signature constraint switchover
|
||||
*/
|
||||
private fun verifyConstraintsValidity(): MutableSet<ContractClassName> {
|
||||
// First check that the constraints are valid.
|
||||
for (state in allStates) {
|
||||
checkConstraintValidity(state)
|
||||
}
|
||||
|
||||
// Group the inputs and outputs by contract, and for each contract verify the constraints propagation logic.
|
||||
// This is not required for reference states as there is nothing to propagate.
|
||||
val inputContractGroups = ltx.inputs.groupBy { it.state.contract }
|
||||
val outputContractGroups = ltx.outputs.groupBy { it.contract }
|
||||
|
||||
// identify any contract classes where input-output pair are transitioning from hash to signature constraints.
|
||||
val hashToSignatureConstrainedContracts = mutableSetOf<ContractClassName>()
|
||||
|
||||
for (contractClassName in (inputContractGroups.keys + outputContractGroups.keys)) {
|
||||
if (contractClassName.contractHasAutomaticConstraintPropagation(transactionClassLoader)) {
|
||||
// Verify that the constraints of output states have at least the same level of restriction as the constraints of the
|
||||
// corresponding input states.
|
||||
val inputConstraints = inputContractGroups[contractClassName]?.map { it.state.constraint }?.toSet()
|
||||
val outputConstraints = outputContractGroups[contractClassName]?.map { it.constraint }?.toSet()
|
||||
outputConstraints?.forEach { outputConstraint ->
|
||||
inputConstraints?.forEach { inputConstraint ->
|
||||
val constraintAttachment = resolveAttachment(contractClassName)
|
||||
if (!(outputConstraint.canBeTransitionedFrom(inputConstraint, constraintAttachment))) {
|
||||
throw TransactionVerificationException.ConstraintPropagationRejection(
|
||||
ltx.id,
|
||||
contractClassName,
|
||||
inputConstraint,
|
||||
outputConstraint
|
||||
)
|
||||
}
|
||||
// Hash to signature constraints auto-migration
|
||||
if (outputConstraint is SignatureAttachmentConstraint && inputConstraint is HashAttachmentConstraint)
|
||||
hashToSignatureConstrainedContracts.add(contractClassName)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
contractClassName.warnContractWithoutConstraintPropagation()
|
||||
}
|
||||
}
|
||||
return hashToSignatureConstrainedContracts
|
||||
}
|
||||
|
||||
private fun resolveAttachment(contractClassName: ContractClassName): AttachmentWithContext {
|
||||
val unsignedAttachment = contractAttachmentsByContract[contractClassName]!!.firstOrNull { !it.isSigned }
|
||||
val signedAttachment = contractAttachmentsByContract[contractClassName]!!.firstOrNull { it.isSigned }
|
||||
return when {
|
||||
(unsignedAttachment != null && signedAttachment != null) -> AttachmentWithContext(signedAttachment, contractClassName, ltx.networkParameters!!)
|
||||
(unsignedAttachment != null) -> AttachmentWithContext(unsignedAttachment, contractClassName, ltx.networkParameters!!)
|
||||
(signedAttachment != null) -> AttachmentWithContext(signedAttachment, contractClassName, ltx.networkParameters!!)
|
||||
else -> throw TransactionVerificationException.ContractConstraintRejection(ltx.id, contractClassName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that all contract constraints are passing before running any contract code.
|
||||
*
|
||||
* This check is running the [AttachmentConstraint.isSatisfiedBy] method for each corresponding [ContractAttachment].
|
||||
*
|
||||
* @throws TransactionVerificationException if the constraints fail to verify
|
||||
*/
|
||||
private fun verifyConstraints(hashToSignatureConstrainedContracts: MutableSet<ContractClassName>) {
|
||||
for (state in allStates) {
|
||||
if (state.constraint is SignatureAttachmentConstraint) {
|
||||
checkMinimumPlatformVersion(ltx.networkParameters!!.minimumPlatformVersion, 4, "Signature constraints")
|
||||
}
|
||||
|
||||
val constraintAttachment = if (state.contract in hashToSignatureConstrainedContracts) {
|
||||
// hash to to signature constraint migration logic:
|
||||
// pass the unsigned attachment when verifying the constraint of the input state, and the signed attachment when verifying
|
||||
// the constraint of the output state.
|
||||
val unsignedAttachment = contractAttachmentsByContract[state.contract].unsigned
|
||||
?: throw TransactionVerificationException.MissingAttachmentRejection(ltx.id, state.contract)
|
||||
val signedAttachment = contractAttachmentsByContract[state.contract].signed
|
||||
?: throw TransactionVerificationException.MissingAttachmentRejection(ltx.id, state.contract)
|
||||
when {
|
||||
// use unsigned attachment if hash-constrained input state
|
||||
state.data in ltx.inputStates -> AttachmentWithContext(unsignedAttachment, state.contract, ltx.networkParameters!!)
|
||||
// use signed attachment if signature-constrained output state
|
||||
state.data in ltx.outputStates -> AttachmentWithContext(signedAttachment, state.contract, ltx.networkParameters!!)
|
||||
else -> throw IllegalStateException("${state.contract} must use either signed or unsigned attachment in hash to signature constraints migration")
|
||||
}
|
||||
} else {
|
||||
// standard processing logic
|
||||
val contractAttachment = contractAttachmentsByContract[state.contract]?.firstOrNull()
|
||||
?: throw TransactionVerificationException.MissingAttachmentRejection(ltx.id, state.contract)
|
||||
AttachmentWithContext(contractAttachment, state.contract, ltx.networkParameters!!)
|
||||
}
|
||||
|
||||
if (!state.constraint.isSatisfiedBy(constraintAttachment)) {
|
||||
throw TransactionVerificationException.ContractConstraintRejection(ltx.id, state.contract)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the transaction is contract-valid by running the verify() for each input and output state contract.
|
||||
* If any contract fails to verify, the whole transaction is considered to be invalid.
|
||||
*/
|
||||
private fun verifyContracts() {
|
||||
val contractClasses = (inputStates + ltx.outputs).toSet()
|
||||
.map { it.contract to contractClassFor(it.contract, it.data.javaClass.classLoader) }
|
||||
|
||||
val contractInstances = contractClasses.map { (contractClassName, contractClass) ->
|
||||
try {
|
||||
contractClass.newInstance()
|
||||
} catch (e: Exception) {
|
||||
throw TransactionVerificationException.ContractCreationError(ltx.id, contractClassName, e)
|
||||
}
|
||||
}
|
||||
|
||||
contractInstances.forEach { contract ->
|
||||
try {
|
||||
contract.verify(ltx)
|
||||
} catch (e: Exception) {
|
||||
throw TransactionVerificationException.ContractRejection(ltx.id, contract, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun contractClassFor(className: ContractClassName, classLoader: ClassLoader): Class<out Contract> {
|
||||
return try {
|
||||
classLoader.loadClass(className).asSubclass(Contract::class.java)
|
||||
} catch (e: Exception) {
|
||||
throw TransactionVerificationException.ContractCreationError(ltx.id, className, e)
|
||||
}
|
||||
}
|
||||
|
||||
private val Set<ContractAttachment>?.unsigned: ContractAttachment? get() = this?.firstOrNull { !it.isSigned }
|
||||
private val Set<ContractAttachment>?.signed: ContractAttachment? get() = this?.firstOrNull { it.isSigned }
|
||||
}
|
@ -32,7 +32,7 @@ data class CordappImpl(
|
||||
override val jarHash: SecureHash.SHA256,
|
||||
override val minimumPlatformVersion: Int,
|
||||
override val targetPlatformVersion: Int,
|
||||
val notaryService: Class<out NotaryService>?,
|
||||
val notaryService: Class<out NotaryService>? = null,
|
||||
/** Indicates whether the CorDapp is loaded from external sources, or generated on node startup (virtual). */
|
||||
val isLoaded: Boolean = true
|
||||
) : Cordapp {
|
||||
|
@ -38,8 +38,8 @@ interface ServicesForResolution {
|
||||
/** Provides access to anything relating to cordapps including contract attachment resolution and app context */
|
||||
val cordappProvider: CordappProvider
|
||||
|
||||
/** Provides access to storage of historical network parameters that are used in transaction resolution */
|
||||
val networkParametersStorage: NetworkParametersStorage
|
||||
/** Provides access to historical network parameters that are used in transaction resolution. */
|
||||
val networkParametersService: NetworkParametersService
|
||||
|
||||
/** Returns the network parameters the node is operating under. */
|
||||
val networkParameters: NetworkParameters
|
||||
@ -359,7 +359,7 @@ interface ServiceHub : ServicesForResolution {
|
||||
* and thus queryable data will include everything committed as of the last checkpoint.
|
||||
*
|
||||
* @throws IllegalStateException if called outside of a transaction.
|
||||
* @return A new [Connection]
|
||||
* @return A [Connection]
|
||||
*/
|
||||
fun jdbcSession(): Connection
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
package net.corda.core.node.services
|
||||
|
||||
import net.corda.core.CordaInternal
|
||||
import net.corda.core.DoNotImplement
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.contracts.ContractAttachment
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.node.services.vault.AttachmentQueryCriteria
|
||||
import net.corda.core.node.services.vault.AttachmentSort
|
||||
|
@ -2,16 +2,14 @@ package net.corda.core.node.services
|
||||
|
||||
import net.corda.core.DoNotImplement
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.NotaryInfo
|
||||
|
||||
/**
|
||||
* Interface for handling network parameters storage used for resolving transactions according to parameters that were
|
||||
* Service for retrieving network parameters used for resolving transactions according to parameters that were
|
||||
* historically in force in the network.
|
||||
*/
|
||||
@DoNotImplement
|
||||
interface NetworkParametersStorage {
|
||||
interface NetworkParametersService {
|
||||
/**
|
||||
* Hash of the current parameters for the network.
|
||||
*/
|
||||
@ -23,8 +21,7 @@ interface NetworkParametersStorage {
|
||||
val defaultHash: SecureHash
|
||||
|
||||
/**
|
||||
* Return network parameters for the given hash. Null if there are no parameters for this hash in the storage and we are unable to
|
||||
* get them from network map.
|
||||
* Return the network parameters with the given hash, or null if it doesn't exist.
|
||||
*/
|
||||
fun lookup(hash: SecureHash): NetworkParameters?
|
||||
}
|
@ -3,6 +3,7 @@ package net.corda.core.node.services
|
||||
import net.corda.core.DeleteForDJVM
|
||||
import net.corda.core.DoNotImplement
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
|
||||
/**
|
||||
|
@ -390,8 +390,11 @@ interface VaultService {
|
||||
*
|
||||
* @throws VaultQueryException if the query cannot be executed for any reason.
|
||||
*
|
||||
* Notes: the snapshot part of the query adheres to the same behaviour as the [queryBy] function.
|
||||
* the [QueryCriteria] applies to both snapshot and deltas (streaming updates).
|
||||
* Notes:
|
||||
* - The snapshot part of the query adheres to the same behaviour as the [queryBy] function.
|
||||
* - The update part of the query currently only supports query criteria filtering by contract
|
||||
* type(s) and state status(es). CID-731 <https://r3-cev.atlassian.net/browse/CID-731> proposes
|
||||
* adding the complete set of [QueryCriteria] filtering.
|
||||
*/
|
||||
@Throws(VaultQueryException::class)
|
||||
fun <T : ContractState> _trackBy(criteria: QueryCriteria,
|
||||
@ -410,6 +413,10 @@ interface VaultService {
|
||||
return _queryBy(criteria, PageSpecification(), Sort(emptySet()), contractStateType)
|
||||
}
|
||||
|
||||
fun <T : ContractState> queryBy(contractStateType: Class<out T>, paging: PageSpecification): Vault.Page<T> {
|
||||
return _queryBy(QueryCriteria.VaultQueryCriteria(), paging, Sort(emptySet()), contractStateType)
|
||||
}
|
||||
|
||||
fun <T : ContractState> queryBy(contractStateType: Class<out T>, criteria: QueryCriteria, paging: PageSpecification): Vault.Page<T> {
|
||||
return _queryBy(criteria, paging, Sort(emptySet()), contractStateType)
|
||||
}
|
||||
@ -430,6 +437,10 @@ interface VaultService {
|
||||
return _trackBy(criteria, PageSpecification(), Sort(emptySet()), contractStateType)
|
||||
}
|
||||
|
||||
fun <T : ContractState> trackBy(contractStateType: Class<out T>, paging: PageSpecification): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
||||
return _trackBy(QueryCriteria.VaultQueryCriteria(), paging, Sort(emptySet()), contractStateType)
|
||||
}
|
||||
|
||||
fun <T : ContractState> trackBy(contractStateType: Class<out T>, criteria: QueryCriteria, paging: PageSpecification): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
||||
return _trackBy(criteria, paging, Sort(emptySet()), contractStateType)
|
||||
}
|
||||
@ -451,6 +462,10 @@ inline fun <reified T : ContractState> VaultService.queryBy(criteria: QueryCrite
|
||||
return _queryBy(criteria, PageSpecification(), Sort(emptySet()), T::class.java)
|
||||
}
|
||||
|
||||
inline fun <reified T : ContractState> VaultService.queryBy(paging: PageSpecification): Vault.Page<T> {
|
||||
return _queryBy(QueryCriteria.VaultQueryCriteria(), paging, Sort(emptySet()), T::class.java)
|
||||
}
|
||||
|
||||
inline fun <reified T : ContractState> VaultService.queryBy(criteria: QueryCriteria, paging: PageSpecification): Vault.Page<T> {
|
||||
return _queryBy(criteria, paging, Sort(emptySet()), T::class.java)
|
||||
}
|
||||
@ -467,6 +482,10 @@ inline fun <reified T : ContractState> VaultService.trackBy(): DataFeed<Vault.Pa
|
||||
return _trackBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), T::class.java)
|
||||
}
|
||||
|
||||
inline fun <reified T : ContractState> VaultService.trackBy(paging: PageSpecification): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
||||
return _trackBy(QueryCriteria.VaultQueryCriteria(), paging, Sort(emptySet()), T::class.java)
|
||||
}
|
||||
|
||||
inline fun <reified T : ContractState> VaultService.trackBy(criteria: QueryCriteria): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
||||
return _trackBy(criteria, PageSpecification(), Sort(emptySet()), T::class.java)
|
||||
}
|
||||
|
@ -94,12 +94,39 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
|
||||
val stateRefs: List<StateRef>? = null,
|
||||
val notary: List<AbstractParty>? = null,
|
||||
val softLockingCondition: SoftLockingCondition? = null,
|
||||
val timeCondition: TimeCondition? = null,
|
||||
override val relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL,
|
||||
override val constraintTypes: Set<Vault.ConstraintInfo.Type> = emptySet(),
|
||||
override val constraints: Set<Vault.ConstraintInfo> = emptySet(),
|
||||
override val participants: List<AbstractParty>? = null
|
||||
val timeCondition: TimeCondition? = null
|
||||
) : CommonQueryCriteria() {
|
||||
// These extra fields are handled this way to preserve Kotlin wire compatibility wrt additional parameters with default values.
|
||||
constructor(
|
||||
status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
contractStateTypes: Set<Class<out ContractState>>? = null,
|
||||
stateRefs: List<StateRef>? = null,
|
||||
notary: List<AbstractParty>? = null,
|
||||
softLockingCondition: SoftLockingCondition? = null,
|
||||
timeCondition: TimeCondition? = null,
|
||||
relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL,
|
||||
constraintTypes: Set<Vault.ConstraintInfo.Type> = emptySet(),
|
||||
constraints: Set<Vault.ConstraintInfo> = emptySet(),
|
||||
participants: List<AbstractParty>? = null
|
||||
) : this(status, contractStateTypes, stateRefs, notary, softLockingCondition, timeCondition) {
|
||||
this.relevancyStatus = relevancyStatus
|
||||
this.constraintTypes = constraintTypes
|
||||
this.constraints = constraints
|
||||
this.participants = participants
|
||||
}
|
||||
|
||||
override var relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL
|
||||
private set
|
||||
|
||||
override var constraintTypes: Set<Vault.ConstraintInfo.Type> = emptySet()
|
||||
private set
|
||||
|
||||
override var constraints: Set<Vault.ConstraintInfo> = emptySet()
|
||||
private set
|
||||
|
||||
override var participants: List<AbstractParty>? = null
|
||||
private set
|
||||
|
||||
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
|
||||
super.visit(parser)
|
||||
return parser.parseCriteria(this)
|
||||
@ -111,7 +138,11 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
|
||||
stateRefs: List<StateRef>? = this.stateRefs,
|
||||
notary: List<AbstractParty>? = this.notary,
|
||||
softLockingCondition: SoftLockingCondition? = this.softLockingCondition,
|
||||
timeCondition: TimeCondition? = this.timeCondition
|
||||
timeCondition: TimeCondition? = this.timeCondition,
|
||||
relevancyStatus: Vault.RelevancyStatus = this.relevancyStatus,
|
||||
constraintTypes: Set<Vault.ConstraintInfo.Type> = this.constraintTypes,
|
||||
constraints: Set<Vault.ConstraintInfo> = this.constraints,
|
||||
participants: List<AbstractParty>? = this.participants
|
||||
): VaultQueryCriteria {
|
||||
return VaultQueryCriteria(
|
||||
status,
|
||||
@ -119,7 +150,11 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
|
||||
stateRefs,
|
||||
notary,
|
||||
softLockingCondition,
|
||||
timeCondition
|
||||
timeCondition,
|
||||
relevancyStatus,
|
||||
constraintTypes,
|
||||
constraints,
|
||||
participants
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -238,9 +273,21 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
|
||||
data class VaultCustomQueryCriteria<L : StatePersistable> @JvmOverloads constructor(
|
||||
val expression: CriteriaExpression<L, Boolean>,
|
||||
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
override val contractStateTypes: Set<Class<out ContractState>>? = null,
|
||||
override val relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL
|
||||
override val contractStateTypes: Set<Class<out ContractState>>? = null
|
||||
) : CommonQueryCriteria() {
|
||||
// These extra field is handled this way to preserve Kotlin wire compatibility wrt additional parameters with default values.
|
||||
constructor(
|
||||
expression: CriteriaExpression<L, Boolean>,
|
||||
status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
contractStateTypes: Set<Class<out ContractState>>? = null,
|
||||
relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL
|
||||
) : this(expression, status, contractStateTypes) {
|
||||
this.relevancyStatus = relevancyStatus
|
||||
}
|
||||
|
||||
override var relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL
|
||||
private set
|
||||
|
||||
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
|
||||
super.visit(parser)
|
||||
return parser.parseCriteria(this)
|
||||
@ -249,12 +296,14 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
|
||||
fun copy(
|
||||
expression: CriteriaExpression<L, Boolean> = this.expression,
|
||||
status: Vault.StateStatus = this.status,
|
||||
contractStateTypes: Set<Class<out ContractState>>? = this.contractStateTypes
|
||||
contractStateTypes: Set<Class<out ContractState>>? = this.contractStateTypes,
|
||||
relevancyStatus: Vault.RelevancyStatus = this.relevancyStatus
|
||||
): VaultCustomQueryCriteria<L> {
|
||||
return VaultCustomQueryCriteria(
|
||||
expression,
|
||||
status,
|
||||
contractStateTypes
|
||||
contractStateTypes,
|
||||
relevancyStatus
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -212,7 +212,7 @@ data class Sort(val columns: Collection<SortColumn>) : BaseSort() {
|
||||
data class AttachmentSort(val columns: Collection<AttachmentSortColumn>) : BaseSort() {
|
||||
|
||||
enum class AttachmentSortAttribute(val columnName: String) {
|
||||
INSERTION_DATE("insertion_date"),
|
||||
INSERTION_DATE("insertionDate"),
|
||||
UPLOADER("uploader"),
|
||||
FILENAME("filename"),
|
||||
VERSION ("version")
|
||||
|
@ -1,6 +1,7 @@
|
||||
@file:KeepForDJVM
|
||||
package net.corda.core.serialization
|
||||
|
||||
import co.paralleluniverse.io.serialization.Serialization
|
||||
import net.corda.core.CordaInternal
|
||||
import net.corda.core.DeleteForDJVM
|
||||
import net.corda.core.DoNotImplement
|
||||
@ -160,6 +161,10 @@ interface SerializationContext {
|
||||
* The use case we are serializing or deserializing for. See [UseCase].
|
||||
*/
|
||||
val useCase: UseCase
|
||||
/**
|
||||
* Additional custom serializers that will be made available during (de)serialization.
|
||||
*/
|
||||
val customSerializers: Set<SerializationCustomSerializer<*, *>>
|
||||
|
||||
/**
|
||||
* Helper method to return a new context based on this context with the property added.
|
||||
@ -200,6 +205,11 @@ interface SerializationContext {
|
||||
*/
|
||||
fun withWhitelisted(clazz: Class<*>): SerializationContext
|
||||
|
||||
/**
|
||||
* Helper method to return a new context based on this context with the given serializers added.
|
||||
*/
|
||||
fun withCustomSerializers(serializers: Set<SerializationCustomSerializer<*, *>>): SerializationContext
|
||||
|
||||
/**
|
||||
* Helper method to return a new context based on this context but with serialization using the format this header sequence represents.
|
||||
*/
|
||||
@ -335,3 +345,15 @@ interface ClassWhitelist {
|
||||
interface EncodingWhitelist {
|
||||
fun acceptEncoding(encoding: SerializationEncoding): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to return a new context based on this context with the given list of classes specifically whitelisted.
|
||||
*/
|
||||
fun SerializationContext.withWhitelist(classes: List<Class<*>>): SerializationContext {
|
||||
var currentContext = this
|
||||
classes.forEach {
|
||||
clazz -> currentContext = currentContext.withWhitelisted(clazz)
|
||||
}
|
||||
|
||||
return currentContext
|
||||
}
|
||||
|
@ -1,17 +1,21 @@
|
||||
package net.corda.core.serialization.internal
|
||||
|
||||
import net.corda.core.CordaException
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.internal.loadClassesImplementing
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.contracts.ContractAttachment
|
||||
import net.corda.core.contracts.TransactionVerificationException.OverlappingAttachmentsException
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.internal.cordapp.targetPlatformVersion
|
||||
import net.corda.core.internal.createSimpleCache
|
||||
import net.corda.core.internal.isUploaderTrusted
|
||||
import net.corda.core.internal.toSynchronised
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.MissingAttachmentsException
|
||||
import net.corda.core.serialization.SerializationCustomSerializer
|
||||
import net.corda.core.serialization.SerializationFactory
|
||||
import net.corda.core.serialization.SerializationWhitelist
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.serialization.internal.AttachmentURLStreamHandlerFactory.toUrl
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
@ -19,6 +23,7 @@ import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.net.*
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* A custom ClassLoader that knows how to load classes from a set of attachments. The attachments themselves only
|
||||
@ -32,7 +37,7 @@ class AttachmentsClassLoader(attachments: List<Attachment>, parent: ClassLoader
|
||||
init {
|
||||
val untrusted = attachments.mapNotNull { it as? ContractAttachment }.filterNot { isUploaderTrusted(it.uploader) }.map(ContractAttachment::id)
|
||||
if(untrusted.isNotEmpty()) {
|
||||
throw MissingAttachmentsException(untrusted, "Attempting to load Contract Attachments downloaded from the network")
|
||||
throw UntrustedAttachmentsException(untrusted)
|
||||
}
|
||||
requireNoDuplicates(attachments)
|
||||
}
|
||||
@ -41,10 +46,11 @@ class AttachmentsClassLoader(attachments: List<Attachment>, parent: ClassLoader
|
||||
private val log = contextLogger()
|
||||
|
||||
init {
|
||||
// This is required to register the AttachmentURLStreamHandlerFactory.
|
||||
URL.setURLStreamHandlerFactory(AttachmentURLStreamHandlerFactory)
|
||||
// Apply our own URLStreamHandlerFactory to resolve attachments
|
||||
setOrDecorateURLStreamHandlerFactory()
|
||||
}
|
||||
|
||||
|
||||
// Jolokia and Json-simple are dependencies that were bundled by mistake within contract jars.
|
||||
// In the AttachmentsClassLoader we just ignore any class in those 2 packages.
|
||||
private val ignoreDirectories = listOf("org/jolokia/", "org/json/simple/")
|
||||
@ -115,6 +121,50 @@ class AttachmentsClassLoader(attachments: List<Attachment>, parent: ClassLoader
|
||||
return it.toByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply our custom factory either directly, if `URL.setURLStreamHandlerFactory` has not been called yet,
|
||||
* or use a decorator and reflection to bypass the single-call-per-JVM restriction otherwise.
|
||||
*/
|
||||
private fun setOrDecorateURLStreamHandlerFactory() {
|
||||
// Retrieve the `URL.factory` field
|
||||
val factoryField = URL::class.java.getDeclaredField("factory")
|
||||
// Make it accessible
|
||||
factoryField.isAccessible = true
|
||||
|
||||
// Check for preset factory, set directly if missing
|
||||
val existingFactory: URLStreamHandlerFactory? = factoryField.get(null) as URLStreamHandlerFactory?
|
||||
if (existingFactory == null) {
|
||||
URL.setURLStreamHandlerFactory(AttachmentURLStreamHandlerFactory)
|
||||
}
|
||||
// Otherwise, decorate the existing and replace via reflection
|
||||
// as calling `URL.setURLStreamHandlerFactory` again will throw an error
|
||||
else {
|
||||
log.warn("The URLStreamHandlerFactory was already set in the JVM. Please be aware that this is not recommended.")
|
||||
// Retrieve the field "streamHandlerLock" of the class URL that
|
||||
// is the lock used to synchronize access to the protocol handlers
|
||||
val lockField = URL::class.java.getDeclaredField("streamHandlerLock")
|
||||
// It is a private field so we need to make it accessible
|
||||
// Note: this will only work as-is in JDK8.
|
||||
lockField.isAccessible = true
|
||||
// Use the same lock to reset the factory
|
||||
synchronized(lockField.get(null)) {
|
||||
// Reset the value to prevent Error due to a factory already defined
|
||||
factoryField.set(null, null)
|
||||
// Set our custom factory and wrap the current one into it
|
||||
URL.setURLStreamHandlerFactory(
|
||||
// Set the factory to a decorator
|
||||
object : URLStreamHandlerFactory {
|
||||
// route between our own and the pre-existing factory
|
||||
override fun createURLStreamHandler(protocol: String): URLStreamHandler? {
|
||||
return AttachmentURLStreamHandlerFactory.createURLStreamHandler(protocol)
|
||||
?: existingFactory.createURLStreamHandler(protocol)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -130,34 +180,38 @@ class AttachmentsClassLoader(attachments: List<Attachment>, parent: ClassLoader
|
||||
}
|
||||
|
||||
/**
|
||||
* This is just a factory that provides a cache to avoid constructing expensive [AttachmentsClassLoader]s.
|
||||
* This is just a factory that provides caches to optimise expensive construction/loading of classloaders, serializers, whitelisted classes.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
internal object AttachmentsClassLoaderBuilder {
|
||||
|
||||
private const val ATTACHMENT_CLASSLOADER_CACHE_SIZE = 1000
|
||||
private const val CACHE_SIZE = 1000
|
||||
|
||||
// This runs in the DJVM so it can't use caffeine.
|
||||
private val cache: MutableMap<List<SecureHash>, AttachmentsClassLoader> = createSimpleCache<List<SecureHash>, AttachmentsClassLoader>(ATTACHMENT_CLASSLOADER_CACHE_SIZE)
|
||||
.toSynchronised()
|
||||
|
||||
fun build(attachments: List<Attachment>): AttachmentsClassLoader {
|
||||
return cache.computeIfAbsent(attachments.map { it.id }.sorted()) {
|
||||
AttachmentsClassLoader(attachments)
|
||||
}
|
||||
}
|
||||
private val cache: MutableMap<Set<SecureHash>, SerializationContext> = createSimpleCache(CACHE_SIZE)
|
||||
|
||||
fun <T> withAttachmentsClassloaderContext(attachments: List<Attachment>, block: (ClassLoader) -> T): T {
|
||||
val attachmentIds = attachments.map { it.id }.toSet()
|
||||
|
||||
// Create classloader from the attachments.
|
||||
val transactionClassLoader = AttachmentsClassLoaderBuilder.build(attachments)
|
||||
val serializationContext = cache.computeIfAbsent(attachmentIds) {
|
||||
// Create classloader and load serializers, whitelisted classes
|
||||
val transactionClassLoader = AttachmentsClassLoader(attachments)
|
||||
val serializers = loadClassesImplementing(transactionClassLoader, SerializationCustomSerializer::class.java)
|
||||
val whitelistedClasses = ServiceLoader.load(SerializationWhitelist::class.java, transactionClassLoader)
|
||||
.flatMap { it.whitelist }
|
||||
.toList()
|
||||
|
||||
// Create a new serializationContext for the current Transaction.
|
||||
val transactionSerializationContext = SerializationFactory.defaultFactory.defaultContext.withPreventDataLoss().withClassLoader(transactionClassLoader)
|
||||
// Create a new serializationContext for the current Transaction.
|
||||
SerializationFactory.defaultFactory.defaultContext
|
||||
.withPreventDataLoss()
|
||||
.withClassLoader(transactionClassLoader)
|
||||
.withWhitelist(whitelistedClasses)
|
||||
.withCustomSerializers(serializers)
|
||||
}
|
||||
|
||||
// Deserialize all relevant classes in the transaction classloader.
|
||||
return SerializationFactory.defaultFactory.withCurrentContext(transactionSerializationContext) {
|
||||
block(transactionClassLoader)
|
||||
return SerializationFactory.defaultFactory.withCurrentContext(serializationContext) {
|
||||
block(serializationContext.deserializationClassLoader)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -200,3 +254,12 @@ object AttachmentURLStreamHandlerFactory : URLStreamHandlerFactory {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Thrown during classloading upon encountering an untrusted attachment (eg. not in the [TRUSTED_UPLOADERS] list) */
|
||||
@KeepForDJVM
|
||||
@CordaSerializable
|
||||
class UntrustedAttachmentsException(val ids: List<SecureHash>) :
|
||||
CordaException("Attempting to load untrusted Contract Attachments: $ids" +
|
||||
"These may have been received over the p2p network from a remote node." +
|
||||
"Please follow the operational steps outlined in https://docs.corda.net/cordapp-build-systems.html#cordapp-contract-attachments to continue."
|
||||
)
|
||||
|
@ -111,8 +111,8 @@ data class ContractUpgradeWireTransaction(
|
||||
?: throw AttachmentResolutionException(legacyContractAttachmentId)
|
||||
val upgradedContractAttachment = services.attachments.openAttachment(upgradedContractAttachmentId)
|
||||
?: throw AttachmentResolutionException(upgradedContractAttachmentId)
|
||||
val hashToResolve = networkParametersHash ?: services.networkParametersStorage.defaultHash
|
||||
val resolvedNetworkParameters = services.networkParametersStorage.lookup(hashToResolve) ?: throw TransactionResolutionException(id)
|
||||
val hashToResolve = networkParametersHash ?: services.networkParametersService.defaultHash
|
||||
val resolvedNetworkParameters = services.networkParametersService.lookup(hashToResolve) ?: throw TransactionResolutionException(id)
|
||||
return ContractUpgradeLedgerTransaction(
|
||||
resolvedInputs,
|
||||
notary,
|
||||
|
@ -3,14 +3,9 @@ package net.corda.core.transactions
|
||||
import net.corda.core.CordaInternal
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.contracts.TransactionVerificationException.TransactionContractConflictException
|
||||
import net.corda.core.contracts.TransactionVerificationException.TransactionRequiredContractUnspecifiedException
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.internal.cordapp.CordappImpl.Companion.DEFAULT_CORDAPP_VERSION
|
||||
import net.corda.core.internal.rules.StateContractValidationEnforcementRule
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.serialization.ConstructorForDeserialization
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
@ -19,7 +14,6 @@ import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import java.util.*
|
||||
import java.util.function.Predicate
|
||||
import kotlin.collections.HashSet
|
||||
|
||||
/**
|
||||
* A LedgerTransaction is derived from a [WireTransaction]. It is the result of doing the following operations:
|
||||
@ -54,7 +48,7 @@ private constructor(
|
||||
/** Network parameters that were in force when the transaction was notarised. */
|
||||
override val networkParameters: NetworkParameters?,
|
||||
override val references: List<StateAndRef<ContractState>>,
|
||||
private val inputStatesContractClassNameToMaxVersion: Map<ContractClassName,Version>
|
||||
private val inputStatesContractClassNameToMaxVersion: Map<ContractClassName, Version>
|
||||
//DOCEND 1
|
||||
) : FullTransaction() {
|
||||
// These are not part of the c'tor above as that defines LedgerTransaction's serialisation format
|
||||
@ -66,8 +60,6 @@ private constructor(
|
||||
checkBaseInvariants()
|
||||
if (timeWindow != null) check(notary != null) { "Transactions with time-windows must be notarised" }
|
||||
checkNotaryWhitelisted()
|
||||
checkNoNotaryChange()
|
||||
checkEncumbrancesValid()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@ -101,9 +93,6 @@ private constructor(
|
||||
val inputStates: List<ContractState> get() = inputs.map { it.state.data }
|
||||
val referenceStates: List<ContractState> get() = references.map { it.state.data }
|
||||
|
||||
private val inputAndOutputStates = inputs.map { it.state } + outputs
|
||||
private val allStates = inputAndOutputStates + references.map { it.state }
|
||||
|
||||
/**
|
||||
* Returns the typed input StateAndRef at the specified index
|
||||
* @param index The index into the inputs.
|
||||
@ -129,216 +118,16 @@ private constructor(
|
||||
logger.warn("Network parameters on the LedgerTransaction with id: $id are null. Please don't use deprecated constructors of the LedgerTransaction. " +
|
||||
"Use WireTransaction.toLedgerTransaction instead. The result of the verify method might not be accurate.")
|
||||
}
|
||||
val contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>> = getContractAttachmentsByContract(allStates.map { it.contract }.toSet())
|
||||
|
||||
AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(this.attachments) { transactionClassLoader ->
|
||||
|
||||
val internalTx = createLtxForVerification()
|
||||
|
||||
validateContractVersions(contractAttachmentsByContract)
|
||||
validatePackageOwnership(contractAttachmentsByContract)
|
||||
validateStatesAgainstContract(internalTx)
|
||||
val hashToSignatureConstrainedContracts = verifyConstraintsValidity(internalTx, contractAttachmentsByContract, transactionClassLoader)
|
||||
verifyConstraints(internalTx, contractAttachmentsByContract, hashToSignatureConstrainedContracts)
|
||||
verifyContracts(internalTx)
|
||||
}
|
||||
val verifier = internalPrepareVerify(emptyList())
|
||||
verifier.verify()
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that contract class versions of output states are not lower that versions of relevant input states.
|
||||
* This method has to be called in a context where it has access to the database.
|
||||
*/
|
||||
@Throws(TransactionVerificationException::class)
|
||||
private fun validateContractVersions(contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>>) {
|
||||
contractAttachmentsByContract.forEach { contractClassName, attachments ->
|
||||
val outputVersion = attachments.signed?.version ?: attachments.unsigned?.version ?: DEFAULT_CORDAPP_VERSION
|
||||
inputStatesContractClassNameToMaxVersion[contractClassName]?.let {
|
||||
if (it > outputVersion) {
|
||||
throw TransactionVerificationException.TransactionVerificationVersionException(this.id, contractClassName, "$it", "$outputVersion")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For all input and output [TransactionState]s, validates that the wrapped [ContractState] matches up with the
|
||||
* wrapped [Contract], as declared by the [BelongsToContract] annotation on the [ContractState]'s class.
|
||||
*
|
||||
* If the target platform version of the current CorDapp is lower than 4.0, a warning will be written to the log
|
||||
* if any mismatch is detected. If it is 4.0 or later, then [TransactionContractConflictException] will be thrown.
|
||||
*/
|
||||
private fun validateStatesAgainstContract(internalTx: LedgerTransaction) =
|
||||
internalTx.allStates.forEach(::validateStateAgainstContract)
|
||||
|
||||
private fun validateStateAgainstContract(state: TransactionState<ContractState>) {
|
||||
val shouldEnforce = StateContractValidationEnforcementRule.shouldEnforce(state.data)
|
||||
|
||||
val requiredContractClassName = state.data.requiredContractClassName ?:
|
||||
if (shouldEnforce) throw TransactionRequiredContractUnspecifiedException(id, state)
|
||||
else return
|
||||
|
||||
if (state.contract != requiredContractClassName)
|
||||
if (shouldEnforce) {
|
||||
throw TransactionContractConflictException(id, state, requiredContractClassName)
|
||||
} else {
|
||||
logger.warnOnce("""
|
||||
State of class ${state.data::class.java.typeName} belongs to contract $requiredContractClassName, but
|
||||
is bundled in TransactionState with ${state.contract}.
|
||||
|
||||
For details see: https://docs.corda.net/api-contract-constraints.html#contract-state-agreement
|
||||
""".trimIndent().replace('\n', ' '))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that for each contract the network wide package owner is respected.
|
||||
*
|
||||
* TODO - revisit once transaction contains network parameters. - UPDATE: It contains them, but because of the API stability and the fact that
|
||||
* LedgerTransaction was data class i.e. exposed constructors that shouldn't had been exposed, we still need to keep them nullable :/
|
||||
*/
|
||||
private fun validatePackageOwnership(contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>>) {
|
||||
val contractsAndOwners = allStates.mapNotNull { transactionState ->
|
||||
val contractClassName = transactionState.contract
|
||||
networkParameters!!.getPackageOwnerOf(contractClassName)?.let { contractClassName to it }
|
||||
}.toMap()
|
||||
|
||||
contractsAndOwners.forEach { contract, owner ->
|
||||
contractAttachmentsByContract[contract]?.filter { it.isSigned }?.forEach { attachment ->
|
||||
if (!owner.isFulfilledBy(attachment.signerKeys))
|
||||
throw TransactionVerificationException.ContractAttachmentNotSignedByPackageOwnerException(this.id, id, contract)
|
||||
} ?: throw TransactionVerificationException.ContractAttachmentNotSignedByPackageOwnerException(this.id, id, contract)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforces the validity of the actual constraints.
|
||||
* * Constraints should be one of the valid supported ones.
|
||||
* * Constraints should propagate correctly if not marked otherwise.
|
||||
*
|
||||
* Returns set of contract classes that identify hash -> signature constraint switchover
|
||||
*/
|
||||
private fun verifyConstraintsValidity(internalTx: LedgerTransaction, contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>>, transactionClassLoader: ClassLoader): MutableSet<ContractClassName> {
|
||||
// First check that the constraints are valid.
|
||||
for (state in internalTx.allStates) {
|
||||
checkConstraintValidity(state)
|
||||
}
|
||||
|
||||
// Group the inputs and outputs by contract, and for each contract verify the constraints propagation logic.
|
||||
// This is not required for reference states as there is nothing to propagate.
|
||||
val inputContractGroups = internalTx.inputs.groupBy { it.state.contract }
|
||||
val outputContractGroups = internalTx.outputs.groupBy { it.contract }
|
||||
|
||||
// identify any contract classes where input-output pair are transitioning from hash to signature constraints.
|
||||
val hashToSignatureConstrainedContracts = mutableSetOf<ContractClassName>()
|
||||
|
||||
for (contractClassName in (inputContractGroups.keys + outputContractGroups.keys)) {
|
||||
if (contractClassName.contractHasAutomaticConstraintPropagation(transactionClassLoader)) {
|
||||
// Verify that the constraints of output states have at least the same level of restriction as the constraints of the corresponding input states.
|
||||
val inputConstraints = inputContractGroups[contractClassName]?.map { it.state.constraint }?.toSet()
|
||||
val outputConstraints = outputContractGroups[contractClassName]?.map { it.constraint }?.toSet()
|
||||
outputConstraints?.forEach { outputConstraint ->
|
||||
inputConstraints?.forEach { inputConstraint ->
|
||||
val constraintAttachment = resolveAttachment(contractClassName, contractAttachmentsByContract)
|
||||
if (!(outputConstraint.canBeTransitionedFrom(inputConstraint, constraintAttachment))) {
|
||||
throw TransactionVerificationException.ConstraintPropagationRejection(id, contractClassName, inputConstraint, outputConstraint)
|
||||
}
|
||||
// Hash to signature constraints auto-migration
|
||||
if (outputConstraint is SignatureAttachmentConstraint && inputConstraint is HashAttachmentConstraint)
|
||||
hashToSignatureConstrainedContracts.add(contractClassName)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
contractClassName.warnContractWithoutConstraintPropagation()
|
||||
}
|
||||
}
|
||||
return hashToSignatureConstrainedContracts
|
||||
}
|
||||
|
||||
private fun resolveAttachment(contractClassName: ContractClassName, contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>>): AttachmentWithContext {
|
||||
val unsignedAttachment = contractAttachmentsByContract[contractClassName]!!.filter { !it.isSigned }.firstOrNull()
|
||||
val signedAttachment = contractAttachmentsByContract[contractClassName]!!.filter { it.isSigned }.firstOrNull()
|
||||
return when {
|
||||
(unsignedAttachment != null && signedAttachment != null) -> AttachmentWithContext(signedAttachment, contractClassName, networkParameters!!)
|
||||
(unsignedAttachment != null) -> AttachmentWithContext(unsignedAttachment, contractClassName, networkParameters!!)
|
||||
(signedAttachment != null) -> AttachmentWithContext(signedAttachment, contractClassName, networkParameters!!)
|
||||
else -> throw TransactionVerificationException.ContractConstraintRejection(id, contractClassName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that all contract constraints are passing before running any contract code.
|
||||
*
|
||||
* This check is running the [AttachmentConstraint.isSatisfiedBy] method for each corresponding [ContractAttachment].
|
||||
*
|
||||
* @throws TransactionVerificationException if the constraints fail to verify
|
||||
*/
|
||||
private fun verifyConstraints(internalTx: LedgerTransaction, contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>>, hashToSignatureConstrainedContracts: MutableSet<ContractClassName>) {
|
||||
for (state in internalTx.allStates) {
|
||||
if (state.constraint is SignatureAttachmentConstraint)
|
||||
checkMinimumPlatformVersion(networkParameters!!.minimumPlatformVersion, 4, "Signature constraints")
|
||||
|
||||
val constraintAttachment =
|
||||
// hash to to signature constraint migration logic:
|
||||
// pass the unsigned attachment when verifying the constraint of the input state, and the signed attachment when verifying the constraint of the output state.
|
||||
if (state.contract in hashToSignatureConstrainedContracts) {
|
||||
val unsignedAttachment = contractAttachmentsByContract[state.contract].unsigned
|
||||
?: throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract)
|
||||
val signedAttachment = contractAttachmentsByContract[state.contract].signed
|
||||
?: throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract)
|
||||
when {
|
||||
// use unsigned attachment if hash-constrained input state
|
||||
state.data in inputStates -> AttachmentWithContext(unsignedAttachment, state.contract, networkParameters!!)
|
||||
// use signed attachment if signature-constrained output state
|
||||
state.data in outputStates -> AttachmentWithContext(signedAttachment, state.contract, networkParameters!!)
|
||||
else -> throw IllegalStateException("${state.contract} must use either signed or unsigned attachment in hash to signature constraints migration")
|
||||
}
|
||||
}
|
||||
// standard processing logic
|
||||
else {
|
||||
val contractAttachment = contractAttachmentsByContract[state.contract]?.firstOrNull()
|
||||
?: throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract)
|
||||
AttachmentWithContext(contractAttachment, state.contract, networkParameters!!)
|
||||
}
|
||||
|
||||
if (!state.constraint.isSatisfiedBy(constraintAttachment)) {
|
||||
throw TransactionVerificationException.ContractConstraintRejection(id, state.contract)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val Set<ContractAttachment>?.unsigned: ContractAttachment?
|
||||
get() {
|
||||
return this?.filter { !it.isSigned }?.firstOrNull()
|
||||
}
|
||||
|
||||
private val Set<ContractAttachment>?.signed: ContractAttachment?
|
||||
get() {
|
||||
return this?.filter { it.isSigned }?.firstOrNull()
|
||||
}
|
||||
|
||||
// TODO: revisit to include contract version information
|
||||
/**
|
||||
* This method may return more than one attachment for a given contract class.
|
||||
* Specifically, this is the case for transactions combining hash and signature constraints where the hash constrained contract jar
|
||||
* will be unsigned, and the signature constrained counterpart will be signed.
|
||||
*/
|
||||
private fun getContractAttachmentsByContract(contractClasses: Set<ContractClassName>): Map<ContractClassName, Set<ContractAttachment>> {
|
||||
val result = mutableMapOf<ContractClassName, Set<ContractAttachment>>()
|
||||
|
||||
for (attachment in attachments) {
|
||||
if (attachment !is ContractAttachment) continue
|
||||
for (contract in contractClasses) {
|
||||
if (!attachment.allContracts.contains(contract)) continue
|
||||
result[contract] = result.getOrDefault(contract, setOf(attachment)).plus(attachment)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private fun contractClassFor(className: ContractClassName, classLoader: ClassLoader): Class<out Contract> = try {
|
||||
classLoader.loadClass(className).asSubclass(Contract::class.java)
|
||||
} catch (e: Exception) {
|
||||
throw TransactionVerificationException.ContractCreationError(id, className, e)
|
||||
@CordaInternal
|
||||
internal fun internalPrepareVerify(extraAttachments: List<Attachment>) = AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(this.attachments + extraAttachments) { transactionClassLoader ->
|
||||
Verifier(createLtxForVerification(), transactionClassLoader, inputStatesContractClassNameToMaxVersion)
|
||||
}
|
||||
|
||||
private fun createLtxForVerification(): LedgerTransaction {
|
||||
@ -368,7 +157,7 @@ private constructor(
|
||||
privacySalt = this.privacySalt,
|
||||
networkParameters = this.networkParameters,
|
||||
references = deserializedReferences,
|
||||
inputStatesContractClassNameToMaxVersion = emptyMap()
|
||||
inputStatesContractClassNameToMaxVersion = this.inputStatesContractClassNameToMaxVersion
|
||||
)
|
||||
} else {
|
||||
// This branch is only present for backwards compatibility.
|
||||
@ -378,156 +167,6 @@ private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the transaction is contract-valid by running the verify() for each input and output state contract.
|
||||
* If any contract fails to verify, the whole transaction is considered to be invalid.
|
||||
*/
|
||||
private fun verifyContracts(internalTx: LedgerTransaction) {
|
||||
val contractClasses = (internalTx.inputs.map { it.state } + internalTx.outputs).toSet()
|
||||
.map { it.contract to contractClassFor(it.contract, it.data.javaClass.classLoader) }
|
||||
|
||||
val contractInstances = contractClasses.map { (contractClassName, contractClass) ->
|
||||
try {
|
||||
contractClass.newInstance()
|
||||
} catch (e: Exception) {
|
||||
throw TransactionVerificationException.ContractCreationError(id, contractClassName, e)
|
||||
}
|
||||
}
|
||||
|
||||
contractInstances.forEach { contract ->
|
||||
try {
|
||||
contract.verify(internalTx)
|
||||
} catch (e: Exception) {
|
||||
throw TransactionVerificationException.ContractRejection(id, contract, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure the notary has stayed the same. As we can't tell how inputs and outputs connect, if there
|
||||
* are any inputs or reference inputs, all outputs must have the same notary.
|
||||
*
|
||||
* TODO: Is that the correct set of restrictions? May need to come back to this, see if we can be more
|
||||
* flexible on output notaries.
|
||||
*/
|
||||
private fun checkNoNotaryChange() {
|
||||
if (notary != null && (inputs.isNotEmpty() || references.isNotEmpty())) {
|
||||
outputs.forEach {
|
||||
if (it.notary != notary) {
|
||||
throw TransactionVerificationException.NotaryChangeInWrongTransactionType(id, notary, it.notary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkEncumbrancesValid() {
|
||||
// Validate that all encumbrances exist within the set of input states.
|
||||
inputs.filter { it.state.encumbrance != null }
|
||||
.forEach { (state, ref) -> checkInputEncumbranceStateExists(state, ref) }
|
||||
|
||||
// Check that in the outputs,
|
||||
// a) an encumbered state does not refer to itself as the encumbrance
|
||||
// b) the number of outputs can contain the encumbrance
|
||||
// c) the bi-directionality (full cycle) property is satisfied
|
||||
// d) encumbered output states are assigned to the same notary.
|
||||
val statesAndEncumbrance = outputs.withIndex().filter { it.value.encumbrance != null }
|
||||
.map { Pair(it.index, it.value.encumbrance!!) }
|
||||
if (!statesAndEncumbrance.isEmpty()) {
|
||||
checkBidirectionalOutputEncumbrances(statesAndEncumbrance)
|
||||
checkNotariesOutputEncumbrance(statesAndEncumbrance)
|
||||
}
|
||||
}
|
||||
|
||||
// Method to check if all encumbered states are assigned to the same notary Party.
|
||||
// This method should be invoked after [checkBidirectionalOutputEncumbrances], because it assumes that the
|
||||
// bi-directionality property is already satisfied.
|
||||
private fun checkNotariesOutputEncumbrance(statesAndEncumbrance: List<Pair<Int, Int>>) {
|
||||
// We only check for transactions in which notary is null (i.e., issuing transactions).
|
||||
// Note that if a notary is defined for a transaction, we already check if all outputs are assigned
|
||||
// to the same notary (transaction's notary) in [checkNoNotaryChange()].
|
||||
if (notary == null) {
|
||||
// indicesAlreadyChecked is used to bypass already checked indices and to avoid cycles.
|
||||
val indicesAlreadyChecked = HashSet<Int>()
|
||||
statesAndEncumbrance.forEach {
|
||||
checkNotary(it.first, indicesAlreadyChecked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private tailrec fun checkNotary(index: Int, indicesAlreadyChecked: HashSet<Int>) {
|
||||
if (indicesAlreadyChecked.add(index)) {
|
||||
val encumbranceIndex = outputs[index].encumbrance!!
|
||||
if (outputs[index].notary != outputs[encumbranceIndex].notary) {
|
||||
throw TransactionVerificationException.TransactionNotaryMismatchEncumbranceException(id, index, encumbranceIndex, outputs[index].notary, outputs[encumbranceIndex].notary)
|
||||
} else {
|
||||
checkNotary(encumbranceIndex, indicesAlreadyChecked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkInputEncumbranceStateExists(state: TransactionState<ContractState>, ref: StateRef) {
|
||||
val encumbranceStateExists = inputs.any {
|
||||
it.ref.txhash == ref.txhash && it.ref.index == state.encumbrance
|
||||
}
|
||||
if (!encumbranceStateExists) {
|
||||
throw TransactionVerificationException.TransactionMissingEncumbranceException(
|
||||
id,
|
||||
state.encumbrance!!,
|
||||
TransactionVerificationException.Direction.INPUT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Using basic graph theory, a full cycle of encumbered (co-dependent) states should exist to achieve bi-directional
|
||||
// encumbrances. This property is important to ensure that no states involved in an encumbrance-relationship
|
||||
// can be spent on their own. Briefly, if any of the states is having more than one encumbrance references by
|
||||
// other states, a full cycle detection will fail. As a result, all of the encumbered states must be present
|
||||
// as "from" and "to" only once (or zero times if no encumbrance takes place). For instance,
|
||||
// a -> b
|
||||
// c -> b and a -> b
|
||||
// b -> a b -> c
|
||||
// do not satisfy the bi-directionality (full cycle) property.
|
||||
//
|
||||
// In the first example "b" appears twice in encumbrance ("to") list and "c" exists in the encumbered ("from") list only.
|
||||
// Due the above, one could consume "a" and "b" in the same transaction and then, because "b" is already consumed, "c" cannot be spent.
|
||||
//
|
||||
// Similarly, the second example does not form a full cycle because "a" and "c" exist in one of the lists only.
|
||||
// As a result, one can consume "b" and "c" in the same transactions, which will make "a" impossible to be spent.
|
||||
//
|
||||
// On other hand the following are valid constructions:
|
||||
// a -> b a -> c
|
||||
// b -> c and c -> b
|
||||
// c -> a b -> a
|
||||
// and form a full cycle, meaning that the bi-directionality property is satisfied.
|
||||
private fun checkBidirectionalOutputEncumbrances(statesAndEncumbrance: List<Pair<Int, Int>>) {
|
||||
// [Set] of "from" (encumbered states).
|
||||
val encumberedSet = mutableSetOf<Int>()
|
||||
// [Set] of "to" (encumbrance states).
|
||||
val encumbranceSet = mutableSetOf<Int>()
|
||||
// Update both [Set]s.
|
||||
statesAndEncumbrance.forEach { (statePosition, encumbrance) ->
|
||||
// Check it does not refer to itself.
|
||||
if (statePosition == encumbrance || encumbrance >= outputs.size) {
|
||||
throw TransactionVerificationException.TransactionMissingEncumbranceException(
|
||||
id,
|
||||
encumbrance,
|
||||
TransactionVerificationException.Direction.OUTPUT)
|
||||
} else {
|
||||
encumberedSet.add(statePosition) // Guaranteed to have unique elements.
|
||||
if (!encumbranceSet.add(encumbrance)) {
|
||||
throw TransactionVerificationException.TransactionDuplicateEncumbranceException(id, encumbrance)
|
||||
}
|
||||
}
|
||||
}
|
||||
// At this stage we have ensured that "from" and "to" [Set]s are equal in size, but we should check their
|
||||
// elements do indeed match. If they don't match, we return their symmetric difference (disjunctive union).
|
||||
val symmetricDifference = (encumberedSet union encumbranceSet).subtract(encumberedSet intersect encumbranceSet)
|
||||
if (symmetricDifference.isNotEmpty()) {
|
||||
// At least one encumbered state is not in the [encumbranceSet] and vice versa.
|
||||
throw TransactionVerificationException.TransactionNonMatchingEncumbranceException(id, symmetricDifference)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a type and a function that returns a grouping key, associates inputs and outputs together so that they
|
||||
* can be processed as one. The grouping key is any arbitrary object that can act as a map key (so must implement
|
||||
|
@ -78,8 +78,8 @@ data class NotaryChangeWireTransaction(
|
||||
@DeleteForDJVM
|
||||
fun resolve(services: ServicesForResolution, sigs: List<TransactionSignature>): NotaryChangeLedgerTransaction {
|
||||
val resolvedInputs = services.loadStates(inputs.toSet()).toList()
|
||||
val hashToResolve = networkParametersHash ?: services.networkParametersStorage.defaultHash
|
||||
val resolvedNetworkParameters = services.networkParametersStorage.lookup(hashToResolve)
|
||||
val hashToResolve = networkParametersHash ?: services.networkParametersService.defaultHash
|
||||
val resolvedNetworkParameters = services.networkParametersService.lookup(hashToResolve)
|
||||
?: throw TransactionResolutionException(id)
|
||||
return NotaryChangeLedgerTransaction.create(resolvedInputs, notary, newNotary, id, sigs, resolvedNetworkParameters)
|
||||
}
|
||||
|
@ -7,13 +7,16 @@ import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.TransactionVerifierServiceInternal
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
import net.corda.core.internal.internalFindTrustedAttachmentForClass
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
@ -200,8 +203,29 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
|
||||
@DeleteForDJVM
|
||||
private fun verifyRegularTransaction(services: ServiceHub, checkSufficientSignatures: Boolean) {
|
||||
val ltx = toLedgerTransaction(services, checkSufficientSignatures)
|
||||
// TODO: allow non-blocking verification.
|
||||
services.transactionVerifierService.verify(ltx).getOrThrow()
|
||||
try {
|
||||
// TODO: allow non-blocking verification.
|
||||
services.transactionVerifierService.verify(ltx).getOrThrow()
|
||||
} catch (e: NoClassDefFoundError) {
|
||||
// Transactions created before Corda 4 can be missing dependencies on other cordapps.
|
||||
// This code attempts to find the missing dependency in the attachment storage among the trusted contract attachments.
|
||||
// When it finds one, it instructs the verifier to use it to create the transaction classloader.
|
||||
// TODO - add check that transaction was created before Corda 4.
|
||||
|
||||
// TODO - should this be a [TransactionVerificationException]?
|
||||
val missingClass = requireNotNull(e.message) { "Transaction $ltx is incorrectly formed." }
|
||||
|
||||
val attachment = requireNotNull(services.attachments.internalFindTrustedAttachmentForClass(missingClass)) {
|
||||
"Transaction $ltx is incorrectly formed. Could not find local dependency for class: $missingClass."
|
||||
}
|
||||
|
||||
log.warn("""Detected that transaction ${this.id} does not contain all cordapp dependencies.
|
||||
|This may be the result of a bug in a previous version of Corda.
|
||||
|Attempting to verify using the additional dependency: $attachment.
|
||||
|Please check with the originator that this is a valid transaction.""".trimMargin())
|
||||
|
||||
(services.transactionVerifierService as TransactionVerifierServiceInternal).verify(ltx, listOf(attachment)).getOrThrow()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -219,7 +243,6 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Resolves the underlying transaction with signatures and then returns it, handling any special case transactions
|
||||
* such as [NotaryChangeWireTransaction].
|
||||
@ -272,6 +295,8 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
|
||||
"keys: ${missing.joinToString { it.toStringShort() }}, " +
|
||||
"by signers: ${descriptions.joinToString()} "
|
||||
}
|
||||
|
||||
private val log = contextLogger()
|
||||
}
|
||||
|
||||
@KeepForDJVM
|
||||
|
@ -8,6 +8,7 @@ import net.corda.core.crypto.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.internal.cordapp.CordappImpl.Companion.DEFAULT_CORDAPP_VERSION
|
||||
import net.corda.core.internal.cordapp.CordappResolver
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
@ -55,6 +56,8 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
private fun defaultReferencesList(): MutableList<StateRef> = arrayListOf()
|
||||
|
||||
private fun defaultServiceHub(): ServiceHub? = (Strand.currentStrand() as? FlowStateMachine<*>)?.serviceHub
|
||||
|
||||
private const val CORDA_VERSION_THAT_INTRODUCED_FLATTENED_COMMANDS = 4
|
||||
}
|
||||
|
||||
constructor(
|
||||
@ -146,7 +149,7 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
checkConstraintValidity(state)
|
||||
}
|
||||
|
||||
return SerializationFactory.defaultFactory.withCurrentContext(serializationContext) {
|
||||
val wireTx = SerializationFactory.defaultFactory.withCurrentContext(serializationContext) {
|
||||
WireTransaction(
|
||||
createComponentGroups(
|
||||
inputStates(),
|
||||
@ -156,10 +159,51 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
notary,
|
||||
window,
|
||||
referenceStates,
|
||||
services.networkParametersStorage.currentHash),
|
||||
services.networkParametersService.currentHash),
|
||||
privacySalt
|
||||
)
|
||||
}
|
||||
|
||||
// Check the transaction for missing dependencies, and attempt to add them.
|
||||
// This is a workaround as the current version of Corda does not support cordapp dependencies.
|
||||
// It works by running transaction validation and then scan the attachment storage for missing classes.
|
||||
// TODO - remove once proper support for cordapp dependencies is added.
|
||||
val addedDependency = addMissingDependency(services, wireTx)
|
||||
|
||||
return if (addedDependency)
|
||||
toWireTransactionWithContext(services, serializationContext)
|
||||
else
|
||||
wireTx
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if a new dependency was successfully added.
|
||||
*/
|
||||
private fun addMissingDependency(services: ServicesForResolution, wireTx: WireTransaction): Boolean {
|
||||
try {
|
||||
wireTx.toLedgerTransaction(services).verify()
|
||||
} catch (e: NoClassDefFoundError) {
|
||||
val missingClass = e.message
|
||||
requireNotNull(missingClass) { "Transaction is incorrectly formed." }
|
||||
|
||||
val attachment = services.attachments.internalFindTrustedAttachmentForClass(missingClass!!)
|
||||
?: throw IllegalArgumentException("Attempted to find dependent attachment for class $missingClass, but could not find a suitable candidate.")
|
||||
|
||||
log.warnOnce("""The transaction currently built is missing an attachment for class: $missingClass.
|
||||
Automatically attaching contract dependency $attachment.
|
||||
It is strongly recommended to check that this is the desired attachment, and to manually add it to the transaction builder.
|
||||
""".trimIndent())
|
||||
|
||||
addAttachment(attachment.id)
|
||||
return true
|
||||
// Ignore these exceptions as they will break unit tests.
|
||||
// The point here is only to detect missing dependencies. The other exceptions are irrelevant.
|
||||
} catch (tve: TransactionVerificationException) {
|
||||
} catch (tre: TransactionResolutionException) {
|
||||
} catch (ise: IllegalStateException) {
|
||||
} catch (ise: IllegalArgumentException) {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
@ -185,7 +229,8 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
|
||||
val explicitAttachmentContractsMap: Map<ContractClassName, SecureHash> = explicitAttachmentContracts.toMap()
|
||||
|
||||
val inputContractGroups: Map<ContractClassName, List<TransactionState<ContractState>>> = inputsWithTransactionState.map {it.state}.groupBy { it.contract }
|
||||
val inputContractGroups: Map<ContractClassName, List<TransactionState<ContractState>>> = inputsWithTransactionState.map { it.state }
|
||||
.groupBy { it.contract }
|
||||
val outputContractGroups: Map<ContractClassName, List<TransactionState<ContractState>>> = outputs.groupBy { it.contract }
|
||||
|
||||
val allContracts: Set<ContractClassName> = inputContractGroups.keys + outputContractGroups.keys
|
||||
@ -199,7 +244,8 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
selectAttachmentThatSatisfiesConstraints(true, refStateEntry.key, refStateEntry.value, emptySet(), services)
|
||||
}
|
||||
|
||||
val contractClassNameToInputStateRef : Map<ContractClassName, Set<StateRef>> = inputsWithTransactionState.map { Pair(it.state.contract,it.ref) }.groupBy { it.first }.mapValues { it.value.map { e -> e.second }.toSet() }
|
||||
val contractClassNameToInputStateRef: Map<ContractClassName, Set<StateRef>> = inputsWithTransactionState.map { Pair(it.state.contract, it.ref) }
|
||||
.groupBy { it.first }.mapValues { it.value.map { e -> e.second }.toSet() }
|
||||
|
||||
// For each contract, resolve the AutomaticPlaceholderConstraint, and select the attachment.
|
||||
val contractAttachmentsAndResolvedOutputStates: List<Pair<Set<AttachmentId>, List<TransactionState<ContractState>>?>> = allContracts.toSet()
|
||||
@ -255,20 +301,21 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
if (inputsHashConstraints.isNotEmpty() && (outputHashConstraints.isNotEmpty() || outputSignatureConstraints.isNotEmpty())) {
|
||||
val attachmentIds = services.attachments.getContractAttachments(contractClassName)
|
||||
// only switchover if we have both signed and unsigned attachments for the given contract class name
|
||||
if (attachmentIds.isNotEmpty() && attachmentIds.size == 2) {
|
||||
if (attachmentIds.isNotEmpty() && attachmentIds.size == 2) {
|
||||
val attachmentsToUse = attachmentIds.map {
|
||||
services.attachments.openAttachment(it)?.let { it as ContractAttachment }
|
||||
?: throw IllegalArgumentException("Contract attachment $it for $contractClassName is missing.")
|
||||
}
|
||||
val signedAttachment = attachmentsToUse.filter { it.isSigned }.firstOrNull() ?: throw IllegalArgumentException("Signed contract attachment for $contractClassName is missing.")
|
||||
val signedAttachment = attachmentsToUse.filter { it.isSigned }.firstOrNull()
|
||||
?: throw IllegalArgumentException("Signed contract attachment for $contractClassName is missing.")
|
||||
val outputConstraints =
|
||||
if (outputHashConstraints.isNotEmpty()) {
|
||||
log.warn("Switching output states from hash to signed constraints using signers in signed contract attachment given by ${signedAttachment.id}")
|
||||
val outputsSignatureConstraints = outputHashConstraints.map { it.copy(constraint = SignatureAttachmentConstraint(signedAttachment.signerKeys.first())) }
|
||||
outputs.addAll(outputsSignatureConstraints)
|
||||
outputs.removeAll(outputHashConstraints)
|
||||
outputsSignatureConstraints
|
||||
} else outputSignatureConstraints
|
||||
if (outputHashConstraints.isNotEmpty()) {
|
||||
log.warn("Switching output states from hash to signed constraints using signers in signed contract attachment given by ${signedAttachment.id}")
|
||||
val outputsSignatureConstraints = outputHashConstraints.map { it.copy(constraint = SignatureAttachmentConstraint(signedAttachment.signerKeys.first())) }
|
||||
outputs.addAll(outputsSignatureConstraints)
|
||||
outputs.removeAll(outputHashConstraints)
|
||||
outputsSignatureConstraints
|
||||
} else outputSignatureConstraints
|
||||
return Pair(attachmentIds.toSet(), outputConstraints)
|
||||
}
|
||||
}
|
||||
@ -643,8 +690,15 @@ with @BelongsToContract, or supply an explicit contract parameter to addOutputSt
|
||||
/** Returns an immutable list of output [TransactionState]s. */
|
||||
fun outputStates(): List<TransactionState<*>> = ArrayList(outputs)
|
||||
|
||||
/** Returns an immutable list of [Command]s, grouping by [CommandData] and joining signers. */
|
||||
fun commands(): List<Command<*>> = commands.groupBy { cmd -> cmd.value }.entries.map { (data, cmds) -> Command(data, cmds.flatMap(Command<*>::signers).toSet().toList()) }
|
||||
/** Returns an immutable list of [Command]s, grouping by [CommandData] and joining signers (from v4, v3 and below return all commands with duplicates for different signers). */
|
||||
fun commands(): List<Command<*>> {
|
||||
return if (CordappResolver.currentTargetVersion >= CORDA_VERSION_THAT_INTRODUCED_FLATTENED_COMMANDS) {
|
||||
commands.groupBy { cmd -> cmd.value }
|
||||
.entries.map { (data, cmds) -> Command(data, cmds.flatMap(Command<*>::signers).toSet().toList()) }
|
||||
} else {
|
||||
ArrayList(commands)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign the built transaction and return it. This is an internal function for use by the service hub, please use
|
||||
|
@ -105,8 +105,8 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
resolveAttachment = { services.attachments.openAttachment(it) },
|
||||
resolveStateRefAsSerialized = { resolveStateRefBinaryComponent(it, services) },
|
||||
resolveParameters = {
|
||||
val hashToResolve = it ?: services.networkParametersStorage.defaultHash
|
||||
services.networkParametersStorage.lookup(hashToResolve)
|
||||
val hashToResolve = it ?: services.networkParametersService.defaultHash
|
||||
services.networkParametersService.lookup(hashToResolve)
|
||||
},
|
||||
resolveContractAttachment = { services.loadContractAttachment(it) }
|
||||
)
|
||||
@ -130,18 +130,37 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
*/
|
||||
@Deprecated("Use toLedgerTransaction(ServicesForTransaction) instead")
|
||||
@Throws(AttachmentResolutionException::class, TransactionResolutionException::class)
|
||||
@JvmOverloads
|
||||
fun toLedgerTransaction(
|
||||
resolveIdentity: (PublicKey) -> Party?,
|
||||
resolveAttachment: (SecureHash) -> Attachment?,
|
||||
resolveStateRef: (StateRef) -> TransactionState<*>?,
|
||||
@Suppress("UNUSED_PARAMETER") resolveContractAttachment: (TransactionState<ContractState>) -> AttachmentId?,
|
||||
resolveParameters: (SecureHash?) -> NetworkParameters? = { null } // TODO This { null } is left here only because of API stability. It doesn't make much sense anymore as it will fail on transaction verification.
|
||||
@Suppress("UNUSED_PARAMETER") resolveContractAttachment: (TransactionState<ContractState>) -> AttachmentId?
|
||||
): LedgerTransaction {
|
||||
// This reverts to serializing the resolved transaction state.
|
||||
return toLedgerTransactionInternal(resolveIdentity, resolveAttachment, { stateRef -> resolveStateRef(stateRef)?.serialize() }, resolveParameters,
|
||||
return toLedgerTransactionInternal(
|
||||
resolveIdentity,
|
||||
resolveAttachment,
|
||||
{ stateRef -> resolveStateRef(stateRef)?.serialize() },
|
||||
{ null },
|
||||
// Returning a dummy `missingAttachment` Attachment allows this deprecated method to work and it disables "contract version no downgrade rule" as a dummy Attachment returns version 1
|
||||
{ it -> resolveAttachment(it.txhash) ?: missingAttachment })
|
||||
{ it -> resolveAttachment(it.txhash) ?: missingAttachment }
|
||||
)
|
||||
}
|
||||
|
||||
// Especially crafted for TransactionVerificationRequest
|
||||
@CordaInternal
|
||||
internal fun toLtxDjvmInternalBridge(
|
||||
resolveAttachment: (SecureHash) -> Attachment?,
|
||||
resolveStateRef: (StateRef) -> TransactionState<*>?,
|
||||
resolveParameters: (SecureHash?) -> NetworkParameters?
|
||||
): LedgerTransaction {
|
||||
return toLedgerTransactionInternal(
|
||||
{ null },
|
||||
resolveAttachment,
|
||||
{ stateRef -> resolveStateRef(stateRef)?.serialize() },
|
||||
resolveParameters,
|
||||
{ it -> resolveAttachment(it.txhash) ?: missingAttachment }
|
||||
)
|
||||
}
|
||||
|
||||
private fun toLedgerTransactionInternal(
|
||||
|
@ -287,13 +287,6 @@ class ProgressTracker(vararg inputSteps: Step) {
|
||||
|
||||
/** Returns true if the progress tracker has ended, either by reaching the [DONE] step or prematurely with an error */
|
||||
val hasEnded: Boolean get() = _changes.hasCompleted() || _changes.hasThrowable()
|
||||
|
||||
companion object {
|
||||
val DEFAULT_TRACKER = { ProgressTracker() }
|
||||
}
|
||||
}
|
||||
// TODO: Expose the concept of errors.
|
||||
// TODO: It'd be helpful if this class was at least partly thread safe.
|
||||
|
||||
|
||||
|
||||
|
@ -3,6 +3,7 @@ package net.corda.core.utilities
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.util.function.Consumer
|
||||
|
||||
/**
|
||||
* Representation of an operation that has either succeeded with a result (represented by [Success]) or failed with an
|
||||
@ -12,15 +13,16 @@ import net.corda.core.serialization.CordaSerializable
|
||||
sealed class Try<out A> {
|
||||
companion object {
|
||||
/**
|
||||
* Executes the given block of code and returns a [Success] capturing the result, or a [Failure] if an exception
|
||||
* is thrown.
|
||||
* Executes the given block of code and returns a [Success] capturing the result, or a [Failure] if a [Throwable] is thrown.
|
||||
*
|
||||
* It is recommended this be chained with [throwError] to ensure critial [Error]s are thrown and not captured.
|
||||
*/
|
||||
@JvmStatic
|
||||
inline fun <T> on(body: () -> T): Try<T> {
|
||||
return try {
|
||||
Success(body())
|
||||
} catch (e: Exception) {
|
||||
Failure(e)
|
||||
} catch (t: Throwable) {
|
||||
Failure(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -34,6 +36,15 @@ sealed class Try<out A> {
|
||||
/** Returns the value if a [Success] otherwise throws the exception if a [Failure]. */
|
||||
abstract fun getOrThrow(): A
|
||||
|
||||
/** If this is a [Failure] wrapping an [Error] then throw it, otherwise return `this` for chaining. */
|
||||
fun throwError(): Try<A> {
|
||||
if (this is Failure && exception is Error) {
|
||||
throw exception
|
||||
} else {
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
/** Maps the given function to the value from this [Success], or returns `this` if this is a [Failure]. */
|
||||
inline fun <B> map(function: (A) -> B): Try<B> = when (this) {
|
||||
is Success -> Success(function(value))
|
||||
@ -59,34 +70,21 @@ sealed class Try<out A> {
|
||||
}
|
||||
|
||||
/** Applies the given action to the value if [Success], or does nothing if [Failure]. Returns `this` for chaining. */
|
||||
fun doOnSuccess(action: (A) -> Unit): Try<A> {
|
||||
when (this) {
|
||||
is Success -> action.invoke(value)
|
||||
is Failure -> {}
|
||||
fun doOnSuccess(action: Consumer<in A>): Try<A> {
|
||||
if (this is Success) {
|
||||
action.accept(value)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/** Applies the given action to the error if [Failure], or does nothing if [Success]. Returns `this` for chaining. */
|
||||
fun doOnFailure(action: (Throwable) -> Unit): Try<A> {
|
||||
when (this) {
|
||||
is Success -> {}
|
||||
is Failure -> action.invoke(exception)
|
||||
fun doOnFailure(action: Consumer<Throwable>): Try<A> {
|
||||
if (this is Failure) {
|
||||
action.accept(exception)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/** Applies the given action to the exception if [Failure], rethrowing [Error]s. Does nothing if [Success]. Returns `this` for chaining. */
|
||||
fun doOnException(action: (Exception) -> Unit): Try<A> {
|
||||
return doOnFailure { error ->
|
||||
if (error is Exception) {
|
||||
action.invoke(error)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@KeepForDJVM
|
||||
data class Success<out A>(val value: A) : Try<A>() {
|
||||
override val isSuccess: Boolean get() = true
|
||||
|
@ -1,11 +1,11 @@
|
||||
package net.corda.core.flows;
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.primitives.Primitives;
|
||||
import net.corda.core.identity.Party;
|
||||
import net.corda.testing.core.TestConstants;
|
||||
import net.corda.testing.node.MockNetwork;
|
||||
import net.corda.testing.node.MockNetworkParameters;
|
||||
import net.corda.testing.node.StartedMockNode;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
@ -15,11 +15,14 @@ import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import static net.corda.testing.core.TestUtils.singleIdentity;
|
||||
import static net.corda.testing.node.internal.InternalTestUtilsKt.cordappsForPackages;
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class FlowsInJavaTest {
|
||||
private final MockNetwork mockNet = new MockNetwork(ImmutableList.of("net.corda.core.flows"));
|
||||
private final MockNetwork mockNet = new MockNetwork(
|
||||
new MockNetworkParameters().withCordappsForAllNodes(cordappsForPackages("net.corda.core.flows"))
|
||||
);
|
||||
private StartedMockNode aliceNode;
|
||||
private StartedMockNode bobNode;
|
||||
private Party bob;
|
||||
|
@ -83,7 +83,7 @@ class ConstraintsPropagationTests {
|
||||
ledgerServices = object : MockServices(
|
||||
cordappPackages = listOf("net.corda.finance.contracts.asset"),
|
||||
initialIdentity = ALICE,
|
||||
identityService = rigorousMock<IdentityServiceInternal>().also {
|
||||
identityService = mock<IdentityServiceInternal>().also {
|
||||
doReturn(ALICE_PARTY).whenever(it).partyFromKey(ALICE_PUBKEY)
|
||||
doReturn(BOB_PARTY).whenever(it).partyFromKey(BOB_PUBKEY)
|
||||
},
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SecureHash
|
||||
@ -40,7 +41,7 @@ class PackageOwnershipVerificationTests {
|
||||
private val ledgerServices = MockServices(
|
||||
cordappPackages = listOf("net.corda.finance.contracts.asset"),
|
||||
initialIdentity = ALICE,
|
||||
identityService = rigorousMock<IdentityServiceInternal>().also {
|
||||
identityService = mock<IdentityServiceInternal>().also {
|
||||
doReturn(ALICE_PARTY).whenever(it).partyFromKey(ALICE_PUBKEY)
|
||||
doReturn(BOB_PARTY).whenever(it).partyFromKey(BOB_PUBKEY)
|
||||
},
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash.Companion.zeroHash
|
||||
@ -68,7 +69,7 @@ class PartialMerkleTreeTest {
|
||||
testLedger = MockServices(
|
||||
cordappPackages = emptyList(),
|
||||
initialIdentity = TestIdentity(MEGA_CORP.name),
|
||||
identityService = rigorousMock<IdentityServiceInternal>().also {
|
||||
identityService = mock<IdentityServiceInternal>().also {
|
||||
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
|
||||
},
|
||||
networkParameters = testNetworkParameters(minimumPlatformVersion = 4, notaries = listOf(NotaryInfo(DUMMY_NOTARY, true)))
|
||||
|
@ -3,6 +3,7 @@ package net.corda.core.flows
|
||||
import com.natpryce.hamkrest.and
|
||||
import com.natpryce.hamkrest.assertion.assert
|
||||
import net.corda.core.flows.mixins.WithFinality
|
||||
import net.corda.core.flows.mixins.WithFinality.FinalityInvoker
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.cordapp.CordappResolver
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
@ -75,15 +76,29 @@ class FinalityFlowTests : WithFinality {
|
||||
|
||||
@Test
|
||||
fun `allow use of the old API if the CorDapp target version is 3`() {
|
||||
// We need Bob to load at least one old CorDapp so that its FinalityHandler is enabled
|
||||
val bob = createBob(cordapps = listOf(cordappWithPackages("com.template").copy(targetPlatformVersion = 3)))
|
||||
val stx = aliceNode.issuesCashTo(bob)
|
||||
val oldBob = createBob(cordapps = listOf(tokenOldCordapp()))
|
||||
val stx = aliceNode.issuesCashTo(oldBob)
|
||||
val resultFuture = CordappResolver.withCordapp(targetPlatformVersion = 3) {
|
||||
@Suppress("DEPRECATION")
|
||||
aliceNode.startFlowAndRunNetwork(FinalityFlow(stx)).resultFuture
|
||||
}
|
||||
resultFuture.getOrThrow()
|
||||
assertThat(bob.services.validatedTransactions.getTransaction(stx.id)).isNotNull()
|
||||
assertThat(oldBob.services.validatedTransactions.getTransaction(stx.id)).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `broadcasting to both new and old participants`() {
|
||||
val newCharlie = mockNet.createNode(InternalMockNodeParameters(legalName = CHARLIE_NAME))
|
||||
val oldBob = createBob(cordapps = listOf(tokenOldCordapp()))
|
||||
val stx = aliceNode.issuesCashTo(oldBob)
|
||||
val resultFuture = aliceNode.startFlowAndRunNetwork(FinalityInvoker(
|
||||
stx,
|
||||
newRecipients = setOf(newCharlie.info.singleIdentity()),
|
||||
oldRecipients = setOf(oldBob.info.singleIdentity())
|
||||
)).resultFuture
|
||||
resultFuture.getOrThrow()
|
||||
assertThat(newCharlie.services.validatedTransactions.getTransaction(stx.id)).isNotNull()
|
||||
assertThat(oldBob.services.validatedTransactions.getTransaction(stx.id)).isNotNull()
|
||||
}
|
||||
|
||||
private fun createBob(cordapps: List<TestCordappInternal> = emptyList()): TestStartedNode {
|
||||
@ -100,4 +115,7 @@ class FinalityFlowTests : WithFinality {
|
||||
Cash().generateIssue(builder, amount, other, notary)
|
||||
return services.signInitialTransaction(builder)
|
||||
}
|
||||
|
||||
/** "Old" CorDapp which will force its node to keep its FinalityHandler enabled */
|
||||
private fun tokenOldCordapp() = cordappWithPackages("com.template").copy(targetPlatformVersion = 3)
|
||||
}
|
||||
|
@ -149,7 +149,7 @@ class WithReferencedStatesFlowTests {
|
||||
val updatedRefState = updatedRefTx.tx.outRefsOfType<RefState.State>().single()
|
||||
|
||||
// 4. Try to use the old reference state. This will throw a NotaryException.
|
||||
val useRefTx = nodes[1].services.startFlow(WithReferencedStatesFlow(UseRefState(newRefState.state.data.linearId))).resultFuture
|
||||
val useRefTx = nodes[1].services.startFlow(WithReferencedStatesFlow { UseRefState(newRefState.state.data.linearId) }).resultFuture
|
||||
|
||||
// 5. Share the update reference state.
|
||||
nodes[0].services.startFlow(ShareRefState.Initiator(updatedRefState)).resultFuture.getOrThrow()
|
||||
|
@ -17,7 +17,7 @@ import net.corda.testing.node.internal.TestStartedNode
|
||||
interface WithFinality : WithMockNet {
|
||||
//region Operations
|
||||
fun TestStartedNode.finalise(stx: SignedTransaction, vararg recipients: Party): FlowStateMachine<SignedTransaction> {
|
||||
return startFlowAndRunNetwork(FinalityInvoker(stx, recipients.toSet()))
|
||||
return startFlowAndRunNetwork(FinalityInvoker(stx, recipients.toSet(), emptySet()))
|
||||
}
|
||||
|
||||
fun TestStartedNode.getValidatedTransaction(stx: SignedTransaction): SignedTransaction {
|
||||
@ -25,7 +25,7 @@ interface WithFinality : WithMockNet {
|
||||
}
|
||||
|
||||
fun CordaRPCOps.finalise(stx: SignedTransaction, vararg recipients: Party): FlowHandle<SignedTransaction> {
|
||||
return startFlow(::FinalityInvoker, stx, recipients.toSet()).andRunNetwork()
|
||||
return startFlow(::FinalityInvoker, stx, recipients.toSet(), emptySet()).andRunNetwork()
|
||||
}
|
||||
//endregion
|
||||
|
||||
@ -41,11 +41,13 @@ interface WithFinality : WithMockNet {
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
class FinalityInvoker(private val transaction: SignedTransaction,
|
||||
private val recipients: Set<Party>) : FlowLogic<SignedTransaction>() {
|
||||
private val newRecipients: Set<Party>,
|
||||
private val oldRecipients: Set<Party>) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val sessions = recipients.map(::initiateFlow)
|
||||
return subFlow(FinalityFlow(transaction, sessions))
|
||||
val sessions = newRecipients.map(::initiateFlow)
|
||||
@Suppress("DEPRECATION")
|
||||
return subFlow(FinalityFlow(transaction, sessions, oldRecipients, FinalityFlow.tracker()))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,34 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Test
|
||||
import java.lang.IllegalArgumentException
|
||||
|
||||
class ClassLoadingUtilsTest {
|
||||
|
||||
interface BaseInterface {}
|
||||
|
||||
interface BaseInterface2 {}
|
||||
|
||||
class ConcreteClassWithEmptyConstructor: BaseInterface {}
|
||||
|
||||
abstract class AbstractClass: BaseInterface
|
||||
|
||||
class ConcreteClassWithNonEmptyConstructor(private val someData: Int): BaseInterface2 {}
|
||||
|
||||
@Test
|
||||
fun predicateClassAreLoadedSuccessfully() {
|
||||
val classes = loadClassesImplementing(BaseInterface::class.java.classLoader, BaseInterface::class.java)
|
||||
|
||||
val classNames = classes.map { it.javaClass.name }
|
||||
|
||||
assertThat(classNames).contains(ConcreteClassWithEmptyConstructor::class.java.name)
|
||||
assertThat(classNames).doesNotContain(AbstractClass::class.java.name)
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun throwsExceptionWhenClassDoesNotContainProperConstructors() {
|
||||
val classes = loadClassesImplementing(BaseInterface::class.java.classLoader, BaseInterface2::class.java)
|
||||
}
|
||||
|
||||
}
|
@ -13,7 +13,9 @@ import net.corda.core.utilities.unwrap
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.MockNetworkParameters
|
||||
import net.corda.testing.node.StartedMockNode
|
||||
import net.corda.testing.node.internal.cordappsForPackages
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@ -36,11 +38,9 @@ class ResolveTransactionsFlowTest {
|
||||
private lateinit var miniCorp: Party
|
||||
private lateinit var notary: Party
|
||||
|
||||
private lateinit var rootTx: SignedTransaction
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts", "net.corda.core.internal"))
|
||||
mockNet = MockNetwork(MockNetworkParameters(cordappsForAllNodes = cordappsForPackages("net.corda.testing.contracts", javaClass.packageName)))
|
||||
notaryNode = mockNet.defaultNotaryNode
|
||||
megaCorpNode = mockNet.createPartyNode(CordaX500Name("MegaCorp", "London", "GB"))
|
||||
miniCorpNode = mockNet.createPartyNode(CordaX500Name("MiniCorp", "London", "GB"))
|
||||
|
@ -0,0 +1,77 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.NullKeys
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class StatePointerSearchTests {
|
||||
|
||||
private val partyAndRef = PartyAndReference(AnonymousParty(NullKeys.NullPublicKey), OpaqueBytes.of(0))
|
||||
|
||||
private data class StateWithGeneric(val amount: Amount<Issued<LinearPointer<LinearState>>>) : ContractState {
|
||||
override val participants: List<AbstractParty> get() = listOf()
|
||||
}
|
||||
|
||||
private data class StateWithList(val pointerList: List<LinearPointer<LinearState>>) : ContractState {
|
||||
override val participants: List<AbstractParty> get() = listOf()
|
||||
}
|
||||
|
||||
private data class StateWithMap(val pointerMap: Map<Any, Any>) : ContractState {
|
||||
override val participants: List<AbstractParty> get() = listOf()
|
||||
}
|
||||
|
||||
private data class StateWithSet(val pointerSet: Set<LinearPointer<LinearState>>) : ContractState {
|
||||
override val participants: List<AbstractParty> get() = listOf()
|
||||
}
|
||||
|
||||
private data class StateWithListOfList(val pointerSet: List<List<LinearPointer<LinearState>>>) : ContractState {
|
||||
override val participants: List<AbstractParty> get() = listOf()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `find pointer in state with generic type`() {
|
||||
val linearPointer = LinearPointer(UniqueIdentifier(), LinearState::class.java)
|
||||
val testState = StateWithGeneric(Amount(100L, Issued(partyAndRef, linearPointer)))
|
||||
val results = StatePointerSearch(testState).search()
|
||||
assertEquals(results, setOf(linearPointer))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `find pointers which are inside a list`() {
|
||||
val linearPointerOne = LinearPointer(UniqueIdentifier(), LinearState::class.java)
|
||||
val linearPointerTwo = LinearPointer(UniqueIdentifier(), LinearState::class.java)
|
||||
val testState = StateWithList(listOf(linearPointerOne, linearPointerTwo))
|
||||
val results = StatePointerSearch(testState).search()
|
||||
assertEquals(results, setOf(linearPointerOne, linearPointerTwo))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `find pointers which are inside a map`() {
|
||||
val linearPointerOne = LinearPointer(UniqueIdentifier(), LinearState::class.java)
|
||||
val linearPointerTwo = LinearPointer(UniqueIdentifier(), LinearState::class.java)
|
||||
val testState = StateWithMap(mapOf(linearPointerOne to 1, 2 to linearPointerTwo))
|
||||
val results = StatePointerSearch(testState).search()
|
||||
assertEquals(results, setOf(linearPointerOne, linearPointerTwo))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `find pointers which are inside a set`() {
|
||||
val linearPointer = LinearPointer(UniqueIdentifier(), LinearState::class.java)
|
||||
val testState = StateWithSet(setOf(linearPointer))
|
||||
val results = StatePointerSearch(testState).search()
|
||||
assertEquals(results, setOf(linearPointer))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `find pointers which are inside nested iterables`() {
|
||||
val linearPointer = LinearPointer(UniqueIdentifier(), LinearState::class.java)
|
||||
val testState = StateWithListOfList(listOf(listOf(linearPointer)))
|
||||
val results = StatePointerSearch(testState).search()
|
||||
assertEquals(results, setOf(linearPointer))
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
package net.corda.core.node
|
||||
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.internal.getPackageOwnerOf
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
@ -7,16 +9,15 @@ import net.corda.core.utilities.days
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.flows.CashIssueFlow
|
||||
import net.corda.node.services.config.NotaryConfig
|
||||
import net.corda.nodeapi.internal.network.NetworkParametersCopier
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.node.MockNetNotaryConfig
|
||||
import net.corda.testing.node.MockNetworkNotarySpec
|
||||
import net.corda.testing.node.MockNetworkParameters
|
||||
import net.corda.testing.node.MockNodeConfigOverrides
|
||||
import net.corda.testing.node.internal.InternalMockNetwork
|
||||
import net.corda.testing.node.internal.InternalMockNodeParameters
|
||||
import net.corda.testing.node.internal.MOCK_VERSION_INFO
|
||||
@ -66,8 +67,14 @@ class NetworkParametersTest {
|
||||
// Notaries tests
|
||||
@Test
|
||||
fun `choosing notary not specified in network parameters will fail`() {
|
||||
val fakeNotary = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME,
|
||||
configOverrides = MockNodeConfigOverrides(notary = MockNetNotaryConfig(validating = false))))
|
||||
val fakeNotary = mockNet.createNode(
|
||||
InternalMockNodeParameters(
|
||||
legalName = BOB_NAME,
|
||||
configOverrides = {
|
||||
doReturn(NotaryConfig(validating = false)).whenever(it).notary
|
||||
}
|
||||
)
|
||||
)
|
||||
val fakeNotaryId = fakeNotary.info.singleIdentity()
|
||||
val alice = mockNet.createPartyNode(ALICE_NAME)
|
||||
assertThat(alice.services.networkMapCache.notaryIdentities).doesNotContain(fakeNotaryId)
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.core.serialization
|
||||
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.identity.AbstractParty
|
||||
@ -69,7 +70,7 @@ class TransactionSerializationTests {
|
||||
val outputState = TransactionState(TestCash.State(depositRef, 600.POUNDS, MEGA_CORP), TEST_CASH_PROGRAM_ID, DUMMY_NOTARY)
|
||||
val changeState = TransactionState(TestCash.State(depositRef, 400.POUNDS, MEGA_CORP), TEST_CASH_PROGRAM_ID, DUMMY_NOTARY)
|
||||
|
||||
val megaCorpServices = object : MockServices(listOf("net.corda.core.serialization"), MEGA_CORP.name, rigorousMock(), testNetworkParameters(notaries = listOf(NotaryInfo(DUMMY_NOTARY, true))), MEGA_CORP_KEY) {
|
||||
val megaCorpServices = object : MockServices(listOf("net.corda.core.serialization"), MEGA_CORP.name, mock(), testNetworkParameters(notaries = listOf(NotaryInfo(DUMMY_NOTARY, true))), MEGA_CORP_KEY) {
|
||||
//override mock implementation with a real one
|
||||
override fun loadContractAttachment(stateRef: StateRef, forContractClassName: ContractClassName?): Attachment = servicesForResolution.loadContractAttachment(stateRef, forContractClassName)
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.nodeapi.DummyContractBackdoor
|
||||
import net.corda.isolated.contracts.DummyContractBackdoor
|
||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import net.corda.testing.core.TestIdentity
|
||||
@ -26,15 +26,15 @@ import kotlin.test.assertFailsWith
|
||||
class AttachmentsClassLoaderSerializationTests {
|
||||
|
||||
companion object {
|
||||
val ISOLATED_CONTRACTS_JAR_PATH: URL = AttachmentsClassLoaderSerializationTests::class.java.getResource("isolated.jar")
|
||||
private const val ISOLATED_CONTRACT_CLASS_NAME = "net.corda.finance.contracts.isolated.AnotherDummyContract"
|
||||
val ISOLATED_CONTRACTS_JAR_PATH: URL = AttachmentsClassLoaderSerializationTests::class.java.getResource("/isolated.jar")
|
||||
private const val ISOLATED_CONTRACT_CLASS_NAME = "net.corda.isolated.contracts.AnotherDummyContract"
|
||||
}
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
|
||||
val storage = MockAttachmentStorage()
|
||||
private val storage = MockAttachmentStorage()
|
||||
|
||||
@Test
|
||||
fun `Can serialize and deserialize with an attachment classloader`() {
|
||||
|
@ -4,35 +4,38 @@ import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.contracts.Contract
|
||||
import net.corda.core.contracts.TransactionVerificationException
|
||||
import net.corda.core.internal.declaredField
|
||||
import net.corda.core.internal.inputStream
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.serialization.internal.AttachmentsClassLoader
|
||||
import net.corda.testing.core.internal.ContractJarTestUtils.signContractJar
|
||||
import net.corda.testing.internal.fakeAttachment
|
||||
import net.corda.testing.node.internal.FINANCE_CONTRACTS_CORDAPP
|
||||
import net.corda.testing.services.MockAttachmentStorage
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import java.net.URL
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class AttachmentsClassLoaderTests {
|
||||
|
||||
companion object {
|
||||
val ISOLATED_CONTRACTS_JAR_PATH: URL = AttachmentsClassLoaderTests::class.java.getResource("isolated.jar")
|
||||
// TODO Update this test to use the new isolated.jar
|
||||
val ISOLATED_CONTRACTS_JAR_PATH: URL = AttachmentsClassLoaderTests::class.java.getResource("old-isolated.jar")
|
||||
val ISOLATED_CONTRACTS_JAR_PATH_V4: URL = AttachmentsClassLoaderTests::class.java.getResource("isolated-4.0.jar")
|
||||
val FINANCE_JAR_PATH: URL = AttachmentsClassLoaderTests::class.java.getResource("finance.jar")
|
||||
private const val ISOLATED_CONTRACT_CLASS_NAME = "net.corda.finance.contracts.isolated.AnotherDummyContract"
|
||||
|
||||
private fun readAttachment(attachment: Attachment, filepath: String): ByteArray {
|
||||
ByteArrayOutputStream().use {
|
||||
return ByteArrayOutputStream().let {
|
||||
attachment.extractFile(filepath, it)
|
||||
return it.toByteArray()
|
||||
it.toByteArray()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val storage = MockAttachmentStorage()
|
||||
private val storage = MockAttachmentStorage()
|
||||
|
||||
@Test
|
||||
fun `Loading AnotherDummyContract without using the AttachmentsClassLoader fails`() {
|
||||
@ -43,7 +46,7 @@ class AttachmentsClassLoaderTests {
|
||||
|
||||
@Test
|
||||
fun `Dynamically load AnotherDummyContract from isolated contracts jar using the AttachmentsClassLoader`() {
|
||||
val isolatedId = storage.importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
|
||||
val isolatedId = importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
|
||||
|
||||
val classloader = AttachmentsClassLoader(listOf(storage.openAttachment(isolatedId)!!))
|
||||
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, classloader)
|
||||
@ -53,8 +56,8 @@ class AttachmentsClassLoaderTests {
|
||||
|
||||
@Test
|
||||
fun `Test non-overlapping contract jar`() {
|
||||
val att1 = storage.importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
|
||||
val att2 = storage.importAttachment(ISOLATED_CONTRACTS_JAR_PATH_V4.openStream(), "app", "isolated-4.0.jar")
|
||||
val att1 = importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
|
||||
val att2 = importAttachment(ISOLATED_CONTRACTS_JAR_PATH_V4.openStream(), "app", "isolated-4.0.jar")
|
||||
|
||||
assertFailsWith(TransactionVerificationException.OverlappingAttachmentsException::class) {
|
||||
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
|
||||
@ -63,9 +66,9 @@ class AttachmentsClassLoaderTests {
|
||||
|
||||
@Test
|
||||
fun `Test valid overlapping contract jar`() {
|
||||
val isolatedId = storage.importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
|
||||
val isolatedId = importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
|
||||
val signedJar = signContractJar(ISOLATED_CONTRACTS_JAR_PATH, copyFirst = true)
|
||||
val isolatedSignedId = storage.importAttachment(signedJar.first.toUri().toURL().openStream(), "app", "isolated-signed.jar")
|
||||
val isolatedSignedId = importAttachment(signedJar.first.toUri().toURL().openStream(), "app", "isolated-signed.jar")
|
||||
|
||||
// does not throw OverlappingAttachments exception
|
||||
AttachmentsClassLoader(arrayOf(isolatedId, isolatedSignedId).map { storage.openAttachment(it)!! })
|
||||
@ -73,8 +76,8 @@ class AttachmentsClassLoaderTests {
|
||||
|
||||
@Test
|
||||
fun `Test non-overlapping different contract jars`() {
|
||||
val att1 = storage.importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
|
||||
val att2 = storage.importAttachment(FINANCE_JAR_PATH.openStream(), "app", "finance.jar")
|
||||
val att1 = importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
|
||||
val att2 = importAttachment(FINANCE_CONTRACTS_CORDAPP.jarFile.inputStream(), "app", "finance.jar")
|
||||
|
||||
// does not throw OverlappingAttachments exception
|
||||
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
|
||||
@ -82,8 +85,8 @@ class AttachmentsClassLoaderTests {
|
||||
|
||||
@Test
|
||||
fun `Load text resources from AttachmentsClassLoader`() {
|
||||
val att1 = storage.importAttachment(fakeAttachment("file1.txt", "some data").inputStream(), "app", "file1.jar")
|
||||
val att2 = storage.importAttachment(fakeAttachment("file2.txt", "some other data").inputStream(), "app", "file2.jar")
|
||||
val att1 = importAttachment(fakeAttachment("file1.txt", "some data").inputStream(), "app", "file1.jar")
|
||||
val att2 = importAttachment(fakeAttachment("file2.txt", "some other data").inputStream(), "app", "file2.jar")
|
||||
|
||||
val cl = AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
|
||||
val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"), Charsets.UTF_8.name())
|
||||
@ -95,8 +98,8 @@ class AttachmentsClassLoaderTests {
|
||||
|
||||
@Test
|
||||
fun `Test valid overlapping file condition`() {
|
||||
val att1 = storage.importAttachment(fakeAttachment("file1.txt", "same data").inputStream(), "app", "file1.jar")
|
||||
val att2 = storage.importAttachment(fakeAttachment("file1.txt", "same data").inputStream(), "app", "file2.jar")
|
||||
val att1 = importAttachment(fakeAttachment("file1.txt", "same data").inputStream(), "app", "file1.jar")
|
||||
val att2 = importAttachment(fakeAttachment("file1.txt", "same data").inputStream(), "app", "file2.jar")
|
||||
|
||||
val cl = AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
|
||||
val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"), Charsets.UTF_8.name())
|
||||
@ -106,8 +109,8 @@ class AttachmentsClassLoaderTests {
|
||||
@Test
|
||||
fun `No overlapping exception thrown on certain META-INF files`() {
|
||||
listOf("meta-inf/manifest.mf", "meta-inf/license", "meta-inf/test.dsa", "meta-inf/test.sf").forEach { path ->
|
||||
val att1 = storage.importAttachment(fakeAttachment(path, "some data").inputStream(), "app", "file1.jar")
|
||||
val att2 = storage.importAttachment(fakeAttachment(path, "some other data").inputStream(), "app", "file2.jar")
|
||||
val att1 = importAttachment(fakeAttachment(path, "some data").inputStream(), "app", "file1.jar")
|
||||
val att2 = importAttachment(fakeAttachment(path, "some other data").inputStream(), "app", "file2.jar")
|
||||
|
||||
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
|
||||
}
|
||||
@ -115,10 +118,8 @@ class AttachmentsClassLoaderTests {
|
||||
|
||||
@Test
|
||||
fun `Check platform independent path handling in attachment jars`() {
|
||||
val storage = MockAttachmentStorage()
|
||||
|
||||
val att1 = storage.importAttachment(fakeAttachment("/folder1/foldera/file1.txt", "some data").inputStream(), "app", "file1.jar")
|
||||
val att2 = storage.importAttachment(fakeAttachment("\\folder1\\folderb\\file2.txt", "some other data").inputStream(), "app", "file2.jar")
|
||||
val att1 = importAttachment(fakeAttachment("/folder1/foldera/file1.txt", "some data").inputStream(), "app", "file1.jar")
|
||||
val att2 = importAttachment(fakeAttachment("\\folder1\\folderb\\file2.txt", "some other data").inputStream(), "app", "file2.jar")
|
||||
|
||||
val data1a = readAttachment(storage.openAttachment(att1)!!, "/folder1/foldera/file1.txt")
|
||||
assertArrayEquals("some data".toByteArray(), data1a)
|
||||
@ -132,4 +133,8 @@ class AttachmentsClassLoaderTests {
|
||||
val data2b = readAttachment(storage.openAttachment(att2)!!, "/folder1/folderb/file2.txt")
|
||||
assertArrayEquals("some other data".toByteArray(), data2b)
|
||||
}
|
||||
|
||||
private fun importAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId {
|
||||
return jar.use { storage.importAttachment(jar, uploader, filename) }
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.core.transactions
|
||||
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
@ -34,7 +35,7 @@ class LedgerTransactionQueryTests {
|
||||
private val services = MockServices(
|
||||
listOf("net.corda.testing.contracts"),
|
||||
TestIdentity(CordaX500Name("MegaCorp", "London", "GB"), keyPair),
|
||||
rigorousMock<IdentityServiceInternal>().also {
|
||||
mock<IdentityServiceInternal>().also {
|
||||
doReturn(null).whenever(it).partyFromKey(keyPair.public)
|
||||
},
|
||||
testNetworkParameters(notaries = listOf(NotaryInfo(DUMMY_NOTARY, true))),
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.core.transactions
|
||||
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.contracts.Requirements.using
|
||||
@ -48,7 +49,7 @@ class ReferenceStateTests {
|
||||
private val ledgerServices = MockServices(
|
||||
cordappPackages = listOf("net.corda.core.transactions", "net.corda.finance.contracts.asset"),
|
||||
initialIdentity = ALICE,
|
||||
identityService = rigorousMock<IdentityServiceInternal>().also {
|
||||
identityService = mock<IdentityServiceInternal>().also {
|
||||
doReturn(ALICE_PARTY).whenever(it).partyFromKey(ALICE_PUBKEY)
|
||||
doReturn(BOB_PARTY).whenever(it).partyFromKey(BOB_PUBKEY)
|
||||
},
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.core.transactions
|
||||
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.cordapp.CordappProvider
|
||||
@ -9,12 +10,11 @@ import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.AbstractAttachment
|
||||
import net.corda.core.internal.PLATFORM_VERSION
|
||||
import net.corda.core.internal.RPC_UPLOADER
|
||||
import net.corda.core.internal.cordapp.CordappImpl.Companion.DEFAULT_CORDAPP_VERSION
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.node.ZoneVersionTooLowException
|
||||
import net.corda.core.node.services.AttachmentStorage
|
||||
import net.corda.core.node.services.NetworkParametersStorage
|
||||
import net.corda.core.node.services.NetworkParametersService
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
@ -39,14 +39,14 @@ class TransactionBuilderTest {
|
||||
private val services = rigorousMock<ServicesForResolution>()
|
||||
private val contractAttachmentId = SecureHash.randomSHA256()
|
||||
private val attachments = rigorousMock<AttachmentStorage>()
|
||||
private val networkParametersStorage = rigorousMock<NetworkParametersStorage>()
|
||||
private val networkParametersService = mock<NetworkParametersService>()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
val cordappProvider = rigorousMock<CordappProvider>()
|
||||
val networkParameters = testNetworkParameters(minimumPlatformVersion = PLATFORM_VERSION)
|
||||
doReturn(networkParametersStorage).whenever(services).networkParametersStorage
|
||||
doReturn(networkParameters.serialize().hash).whenever(networkParametersStorage).currentHash
|
||||
doReturn(networkParametersService).whenever(services).networkParametersService
|
||||
doReturn(networkParameters.serialize().hash).whenever(networkParametersService).currentHash
|
||||
doReturn(cordappProvider).whenever(services).cordappProvider
|
||||
doReturn(contractAttachmentId).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID)
|
||||
doReturn(networkParameters).whenever(services).networkParameters
|
||||
@ -77,7 +77,7 @@ class TransactionBuilderTest {
|
||||
val wtx = builder.toWireTransaction(services)
|
||||
assertThat(wtx.outputs).containsOnly(outputState)
|
||||
assertThat(wtx.commands).containsOnly(Command(DummyCommandData, notary.owningKey))
|
||||
assertThat(wtx.networkParametersHash).isEqualTo(networkParametersStorage.currentHash)
|
||||
assertThat(wtx.networkParametersHash).isEqualTo(networkParametersService.currentHash)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -109,6 +109,7 @@ class TransactionBuilderTest {
|
||||
.hasMessageContaining("Reference states")
|
||||
|
||||
doReturn(testNetworkParameters(minimumPlatformVersion = 4)).whenever(services).networkParameters
|
||||
doReturn(referenceState).whenever(services).loadState(referenceStateRef)
|
||||
val wtx = builder.toWireTransaction(services)
|
||||
assertThat(wtx.references).containsOnly(referenceStateRef)
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.core.transactions
|
||||
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.identity.AbstractParty
|
||||
@ -17,7 +18,7 @@ import net.corda.testing.core.TestIdentity
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.ledger
|
||||
import org.assertj.core.api.AssertionsForClassTypes
|
||||
import org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.time.Instant
|
||||
@ -56,7 +57,7 @@ class TransactionEncumbranceTests {
|
||||
val ledgerServices = MockServices(
|
||||
listOf("net.corda.core.transactions", "net.corda.finance.contracts.asset"),
|
||||
MEGA_CORP.name,
|
||||
rigorousMock<IdentityServiceInternal>().also {
|
||||
mock<IdentityServiceInternal>().also {
|
||||
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
|
||||
},
|
||||
testNetworkParameters(notaries = listOf(NotaryInfo(DUMMY_NOTARY, true)))
|
||||
@ -330,12 +331,13 @@ class TransactionEncumbranceTests {
|
||||
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY2, 0, AutomaticHashConstraint)
|
||||
.addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey)
|
||||
.toLedgerTransaction(ledgerServices)
|
||||
.verify()
|
||||
}
|
||||
|
||||
// More complex encumbrance (full cycle of size 4) where one of the encumbered states is assigned to a different notary.
|
||||
// 0 -> 1, 1 -> 3, 3 -> 2, 2 -> 0
|
||||
// We expect that state at index 3 cannot be encumbered with the state at index 2, due to mismatched notaries.
|
||||
AssertionsForClassTypes.assertThatExceptionOfType(TransactionVerificationException.TransactionNotaryMismatchEncumbranceException::class.java)
|
||||
assertThatExceptionOfType(TransactionVerificationException.TransactionNotaryMismatchEncumbranceException::class.java)
|
||||
.isThrownBy {
|
||||
TransactionBuilder()
|
||||
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 1, AutomaticHashConstraint)
|
||||
@ -344,13 +346,15 @@ class TransactionEncumbranceTests {
|
||||
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 2, AutomaticHashConstraint)
|
||||
.addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey)
|
||||
.toLedgerTransaction(ledgerServices)
|
||||
.verify()
|
||||
}
|
||||
.withMessageContaining("index 3 is assigned to notary [O=Notary Service, L=Zurich, C=CH], while its encumbrance with index 2 is assigned to notary [O=Notary Service2, L=Zurich, C=CH]")
|
||||
.withMessageContaining("index 3 is assigned to notary [O=Notary Service, L=Zurich, C=CH], while its encumbrance with " +
|
||||
"index 2 is assigned to notary [O=Notary Service2, L=Zurich, C=CH]")
|
||||
|
||||
// Two different encumbrance chains, where only one fails due to mismatched notary.
|
||||
// 0 -> 1, 1 -> 0, 2 -> 3, 3 -> 2 where encumbered states with indices 2 and 3, respectively, are assigned
|
||||
// to different notaries.
|
||||
AssertionsForClassTypes.assertThatExceptionOfType(TransactionVerificationException.TransactionNotaryMismatchEncumbranceException::class.java)
|
||||
assertThatExceptionOfType(TransactionVerificationException.TransactionNotaryMismatchEncumbranceException::class.java)
|
||||
.isThrownBy {
|
||||
TransactionBuilder()
|
||||
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 1, AutomaticHashConstraint)
|
||||
@ -359,7 +363,9 @@ class TransactionEncumbranceTests {
|
||||
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY2, 2, AutomaticHashConstraint)
|
||||
.addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey)
|
||||
.toLedgerTransaction(ledgerServices)
|
||||
.verify()
|
||||
}
|
||||
.withMessageContaining("index 2 is assigned to notary [O=Notary Service, L=Zurich, C=CH], while its encumbrance with index 3 is assigned to notary [O=Notary Service2, L=Zurich, C=CH]")
|
||||
.withMessageContaining("index 2 is assigned to notary [O=Notary Service, L=Zurich, C=CH], while its encumbrance with " +
|
||||
"index 3 is assigned to notary [O=Notary Service2, L=Zurich, C=CH]")
|
||||
}
|
||||
}
|
||||
|
@ -170,6 +170,7 @@ class TransactionTests {
|
||||
val id = SecureHash.randomSHA256()
|
||||
val timeWindow: TimeWindow? = null
|
||||
val privacySalt = PrivacySalt()
|
||||
|
||||
fun buildTransaction() = LedgerTransaction.create(
|
||||
inputs,
|
||||
outputs,
|
||||
@ -184,7 +185,7 @@ class TransactionTests {
|
||||
inputStatesContractClassNameToMaxVersion = emptyMap()
|
||||
)
|
||||
|
||||
assertFailsWith<TransactionVerificationException.NotaryChangeInWrongTransactionType> { buildTransaction() }
|
||||
assertFailsWith<TransactionVerificationException.NotaryChangeInWrongTransactionType> { buildTransaction().verify() }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -1,4 +1,4 @@
|
||||
package net.corda.nodeapi
|
||||
package net.corda.isolated.contracts
|
||||
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.PartyAndReference
|
||||
@ -6,7 +6,7 @@ import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
|
||||
/**
|
||||
* This interface deliberately mirrors the one in the finance:isolated module.
|
||||
* This interface deliberately mirrors the one in the isolated module.
|
||||
* We will actually link [AnotherDummyContract] against this interface rather
|
||||
* than the one inside isolated.jar, which means we won't need to use reflection
|
||||
* to execute the contract's generateInitial() method.
|
BIN
core/src/test/resources/isolated.jar
Normal file
BIN
core/src/test/resources/isolated.jar
Normal file
Binary file not shown.
Binary file not shown.
@ -4,6 +4,7 @@ import org.assertj.core.api.Assertions.*
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
import sandbox.java.lang.sandbox
|
||||
import java.text.DecimalFormatSymbols
|
||||
|
||||
class DJVMTest : TestBase() {
|
||||
|
||||
@ -41,7 +42,8 @@ class DJVMTest : TestBase() {
|
||||
stringClass.getMethod("format", stringClass, Array<Any>::class.java)
|
||||
.invoke(null, stringOf("%9.4f"), arrayOf(doubleOf(1234.5678))).toString()
|
||||
}
|
||||
assertEquals("1234.5678", result)
|
||||
val sep = DecimalFormatSymbols.getInstance().decimalSeparator
|
||||
assertEquals("1234${sep}5678", result)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -50,7 +52,8 @@ class DJVMTest : TestBase() {
|
||||
stringClass.getMethod("format", stringClass, Array<Any>::class.java)
|
||||
.invoke(null, stringOf("%7.2f"), arrayOf(floatOf(1234.5678f))).toString()
|
||||
}
|
||||
assertEquals("1234.57", result)
|
||||
val sep = DecimalFormatSymbols.getInstance().decimalSeparator
|
||||
assertEquals("1234${sep}57", result)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -230,6 +230,25 @@ The contract attachment non-downgrade rule is enforced in two locations:
|
||||
A version number is stored in the manifest information of the enclosing JAR file. This version identifier should be a whole number starting
|
||||
from 1. This information should be set using the Gradle cordapp plugin, or manually, as described in :doc:`versioning`.
|
||||
|
||||
|
||||
Uniqueness requirement Contract and Version for Signature Constraint
|
||||
--------------------------------------------------------------------
|
||||
|
||||
CorDapps in Corda 4 may be signed (to use new signature constraints functionality) or unsigned, and versioned.
|
||||
The following controls are enforced for these different types of jars within the attachment store of a node:
|
||||
|
||||
- Signed contract JARs must be uniquely versioned per contract class (or group of).
|
||||
At runtime the node will throw a `DuplicateContractClassException`` exception if this condition is violated.
|
||||
|
||||
- Unsigned contract JARs: there should not exist multiple instances of the same contract jar.
|
||||
When a whitelisted JARs is imported and it doesn't contain a version number, the version will be copied from the position (counting from 1)
|
||||
of this JAR in the whilelist. The same JAR can be present in many lists (if it contains many contracts),
|
||||
in such case the version will be equal to the highest position of the JAR in all lists.
|
||||
The new whitelist needs to be distributed to the node before the JAR is imported, otherwise it will receive default version.
|
||||
At run-time the node will warn of duplicates encountered.
|
||||
The most recent version given by insertionDate into the attachment storage will be used upon transaction building/resolution.
|
||||
|
||||
|
||||
Issues when using the HashAttachmentConstraint
|
||||
----------------------------------------------
|
||||
|
||||
@ -311,70 +330,6 @@ For example:
|
||||
For Contracts that are annotated with ``@NoConstraintPropagation``, the platform requires that the Transaction Builder specifies
|
||||
an actual constraint for the output states (the ``AutomaticPlaceholderConstraint`` can't be used) .
|
||||
|
||||
|
||||
Testing
|
||||
-------
|
||||
|
||||
Since all tests involving transactions now require attachments it is also required to load the correct attachments
|
||||
for tests. Unit test environments in JVM ecosystems tend to use class directories rather than JARs, and so CorDapp JARs
|
||||
typically aren't built for testing. Requiring this would add significant complexity to the build systems of Corda
|
||||
and CorDapps, so the test suite has a set of convenient functions to generate CorDapps from package names or
|
||||
to specify JAR URLs in the case that the CorDapp(s) involved in testing already exist. You can also just use
|
||||
``AlwaysAcceptAttachmentConstraint`` in your tests to disable the constraints mechanism.
|
||||
|
||||
MockNetwork/MockNode
|
||||
********************
|
||||
|
||||
The simplest way to ensure that a vanilla instance of a MockNode generates the correct CorDapps is to use the
|
||||
``cordappPackages`` constructor parameter (Kotlin) or the ``setCordappPackages`` method on ``MockNetworkParameters`` (Java)
|
||||
when creating the MockNetwork. This will cause the ``AbstractNode`` to use the named packages as sources for CorDapps. All files
|
||||
within those packages will be zipped into a JAR and added to the attachment store and loaded as CorDapps by the
|
||||
``CordappLoader``.
|
||||
|
||||
An example of this usage would be:
|
||||
|
||||
.. sourcecode:: java
|
||||
|
||||
class SomeTestClass {
|
||||
MockNetwork network = null;
|
||||
|
||||
@Before
|
||||
void setup() {
|
||||
network = new MockNetwork(new MockNetworkParameters().setCordappPackages(Arrays.asList("com.domain.cordapp")))
|
||||
}
|
||||
|
||||
... // Your tests go here
|
||||
}
|
||||
|
||||
|
||||
MockServices
|
||||
************
|
||||
|
||||
If your test uses a ``MockServices`` directly you can instantiate it using a constructor that takes a list of packages
|
||||
to use as CorDapps using the ``cordappPackages`` parameter.
|
||||
|
||||
.. sourcecode:: java
|
||||
|
||||
MockServices mockServices = new MockServices(Arrays.asList("com.domain.cordapp"))
|
||||
|
||||
However - there is an easier way! If your unit tests are in the same package as the contract code itself, then you
|
||||
can use the no-args constructor of ``MockServices``. The package to be scanned for CorDapps will be the same as the
|
||||
the package of the class that constructed the object. This is a convenient default.
|
||||
|
||||
Driver
|
||||
******
|
||||
|
||||
The driver takes a parameter called ``extraCordappPackagesToScan`` which is a list of packages to use as CorDapps.
|
||||
|
||||
.. sourcecode:: java
|
||||
|
||||
driver(new DriverParameters().setExtraCordappPackagesToScan(Arrays.asList("com.domain.cordapp"))) ...
|
||||
|
||||
Full Nodes
|
||||
**********
|
||||
|
||||
When testing against full nodes simply place your CorDapp into the cordapps directory of the node.
|
||||
|
||||
Debugging
|
||||
---------
|
||||
If an attachment constraint cannot be resolved, a ``MissingContractAttachments`` exception is thrown. There are two
|
||||
|
@ -1,3 +1,9 @@
|
||||
.. highlight:: kotlin
|
||||
.. raw:: html
|
||||
|
||||
<script type="text/javascript" src="_static/jquery.js"></script>
|
||||
<script type="text/javascript" src="_static/codesets.js"></script>
|
||||
|
||||
API: Core types
|
||||
===============
|
||||
|
||||
|
@ -1,3 +1,9 @@
|
||||
.. highlight:: kotlin
|
||||
.. raw:: html
|
||||
|
||||
<script type="text/javascript" src="_static/jquery.js"></script>
|
||||
<script type="text/javascript" src="_static/codesets.js"></script>
|
||||
|
||||
API: Identity
|
||||
=============
|
||||
|
||||
|
@ -9,24 +9,26 @@ API: Persistence
|
||||
|
||||
.. contents::
|
||||
|
||||
Corda offers developers the option to expose all or some part of a contract state to an *Object Relational Mapping*
|
||||
(ORM) tool to be persisted in a RDBMS. The purpose of this is to assist *vault* development by effectively indexing
|
||||
persisted contract states held in the vault for the purpose of running queries over them and to allow relational joins
|
||||
between Corda data and private data local to the organisation owning a node.
|
||||
Corda offers developers the option to expose all or some parts of a contract state to an *Object Relational Mapping*
|
||||
(ORM) tool to be persisted in a *Relational Database Management System* (RDBMS).
|
||||
|
||||
The ORM mapping is specified using the `Java Persistence API <https://en.wikipedia.org/wiki/Java_Persistence_API>`_
|
||||
(JPA) as annotations and is converted to database table rows by the node automatically every time a state is recorded
|
||||
in the node's local vault as part of a transaction.
|
||||
The purpose of this, is to assist `vault <https://docs.corda.net/vault.html>`_
|
||||
development and allow for the persistence of state data to a custom database table. Persisted states held in the
|
||||
vault are indexed for the purposes of executing queries. This also allows for relational joins between Corda tables
|
||||
and the organization's existing data.
|
||||
|
||||
.. note:: Presently the node includes an instance of the H2 database but any database that supports JDBC is a
|
||||
candidate and the node will in the future support a range of database implementations via their JDBC drivers. Much
|
||||
of the node internal state is also persisted there. You can access the internal H2 database via JDBC, please see the
|
||||
info in ":doc:`node-administration`" for details.
|
||||
The Object Relational Mapping is specified using `Java Persistence API <https://en.wikipedia.org/wiki/Java_Persistence_API>`_
|
||||
(JPA) annotations. This mapping is persisted to the database as a table row (a single, implicitly structured data item) by the node
|
||||
automatically every time a state is recorded in the node's local vault as part of a transaction.
|
||||
|
||||
.. note:: By default, nodes use an H2 database which is accessed using *Java Database Connectivity* JDBC. Any database
|
||||
with a JDBC driver is a candidate and several integrations have been contributed to by the community.
|
||||
Please see the info in ":doc:`node-database`" for details.
|
||||
|
||||
Schemas
|
||||
-------
|
||||
Every ``ContractState`` can implement the ``QueryableState`` interface if it wishes to be inserted into the node's local
|
||||
database and accessible using SQL.
|
||||
Every ``ContractState`` may implement the ``QueryableState`` interface if it wishes to be inserted into a custom table in the node's
|
||||
database and made accessible using SQL.
|
||||
|
||||
.. literalinclude:: ../../core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt
|
||||
:language: kotlin
|
||||
@ -34,12 +36,12 @@ database and accessible using SQL.
|
||||
:end-before: DOCEND QueryableState
|
||||
|
||||
The ``QueryableState`` interface requires the state to enumerate the different relational schemas it supports, for
|
||||
instance in cases where the schema has evolved, with each one being represented by a ``MappedSchema`` object return
|
||||
by the ``supportedSchemas()`` method. Once a schema is selected it must generate that representation when requested
|
||||
via the ``generateMappedObject()`` method which is then passed to the ORM.
|
||||
instance in situations where the schema has evolved. Each relational schema is represented as a ``MappedSchema``
|
||||
object returned by the state's ``supportedSchemas`` method.
|
||||
|
||||
Nodes have an internal ``SchemaService`` which decides what to persist and what not by selecting the ``MappedSchema``
|
||||
to use.
|
||||
Nodes have an internal ``SchemaService`` which decides what data to persist by selecting the ``MappedSchema`` to use.
|
||||
Once a ``MappedSchema`` is selected, the ``SchemaService`` will delegate to the ``QueryableState`` to generate a corresponding
|
||||
representation (mapped object) via the ``generateMappedObject`` method, the output of which is then passed to the *ORM*.
|
||||
|
||||
.. literalinclude:: ../../node/src/main/kotlin/net/corda/node/services/api/SchemaService.kt
|
||||
:language: kotlin
|
||||
@ -51,13 +53,10 @@ to use.
|
||||
:start-after: DOCSTART MappedSchema
|
||||
:end-before: DOCEND MappedSchema
|
||||
|
||||
The ``SchemaService`` can be configured by a node administrator to select the schemas used by each app. In this way the
|
||||
relational view of ledger states can evolve in a controlled fashion in lock-step with internal systems or other
|
||||
integration points and not necessarily with every upgrade to the contract code. It can select from the
|
||||
``MappedSchema`` offered by a ``QueryableState``, automatically upgrade to a later version of a schema or even
|
||||
provide a ``MappedSchema`` not originally offered by the ``QueryableState``.
|
||||
With this framework, the relational view of ledger states can evolve in a controlled fashion in lock-step with internal systems or other
|
||||
integration points and is not dependant on changes to the contract code.
|
||||
|
||||
It is expected that multiple different contract state implementations might provide mappings within a single schema.
|
||||
It is expected that multiple contract state implementations might provide mappings within a single schema.
|
||||
For example an Interest Rate Swap contract and an Equity OTC Option contract might both provide a mapping to
|
||||
a Derivative contract within the same schema. The schemas should typically not be part of the contract itself and should exist independently
|
||||
to encourage re-use of a common set within a particular business area or Cordapp.
|
||||
@ -71,19 +70,19 @@ class name of a *schema family* class that is constant across versions, allowing
|
||||
preferred version of a schema.
|
||||
|
||||
The ``SchemaService`` is also responsible for the ``SchemaOptions`` that can be configured for a particular
|
||||
``MappedSchema`` which allow the configuration of a database schema or table name prefixes to avoid any clash with
|
||||
``MappedSchema``. These allow the configuration of database schemas or table name prefixes to avoid clashes with
|
||||
other ``MappedSchema``.
|
||||
|
||||
.. note:: It is intended that there should be plugin support for the ``SchemaService`` to offer the version upgrading
|
||||
and additional schemas as part of Cordapps, and that the active schemas be configurable. However the present
|
||||
implementation offers none of this and simply results in all versions of all schemas supported by a
|
||||
``QueryableState`` being persisted. This will change in due course. Similarly, it does not currently support
|
||||
.. note:: It is intended that there should be plugin support for the ``SchemaService`` to offer version upgrading, implementation
|
||||
of additional schemas, and enable active schemas as being configurable. The present implementation does not include these features
|
||||
and simply results in all versions of all schemas supported by a ``QueryableState`` being persisted.
|
||||
This will change in due course. Similarly, the service does not currently support
|
||||
configuring ``SchemaOptions`` but will do so in the future.
|
||||
|
||||
Custom schema registration
|
||||
--------------------------
|
||||
Custom contract schemas are automatically registered at startup time for CorDapps. The node bootstrap process will scan
|
||||
for schemas (any class that extends the ``MappedSchema`` interface) in the `plugins` configuration directory in your CorDapp jar.
|
||||
Custom contract schemas are automatically registered at startup time for CorDapps. The node bootstrap process will scan for states that implement
|
||||
the Queryable state interface. Tables are then created as specified by the ``MappedSchema``s identified by each states ``supportedSchemas`` method.
|
||||
|
||||
For testing purposes it is necessary to manually register the packages containing custom schemas as follows:
|
||||
|
||||
@ -94,16 +93,16 @@ For testing purposes it is necessary to manually register the packages containin
|
||||
|
||||
Object relational mapping
|
||||
-------------------------
|
||||
The persisted representation of a ``QueryableState`` should be an instance of a ``PersistentState`` subclass,
|
||||
constructed either by the state itself or a plugin to the ``SchemaService``. This allows the ORM layer to always
|
||||
To facilitate the ORM, the persisted representation of a ``QueryableState`` should be an instance of a ``PersistentState`` subclass,
|
||||
constructed either by the state itself or a plugin to the ``SchemaService``. This allows the ORM layer to always
|
||||
associate a ``StateRef`` with a persisted representation of a ``ContractState`` and allows joining with the set of
|
||||
unconsumed states in the vault.
|
||||
|
||||
The ``PersistentState`` subclass should be marked up as a JPA 2.1 *Entity* with a defined table name and having
|
||||
properties (in Kotlin, getters/setters in Java) annotated to map to the appropriate columns and SQL types. Additional
|
||||
entities can be included to model these properties where they are more complex, for example collections, so the mapping
|
||||
does not have to be *flat*. The ``MappedSchema`` must provide a list of all of the JPA entity classes for that schema
|
||||
in order to initialise the ORM layer.
|
||||
entities can be included to model these properties where they are more complex, for example collections (:ref:`Persisting Hierarchical Data<persisting-hierarchical-data>`), so
|
||||
the mapping does not have to be *flat*. The ``MappedSchema`` constructor accepts a list of all JPA entity classes for that schema in
|
||||
the ``MappedTypes`` parameter. It must provide this list in order to initialise the ORM layer.
|
||||
|
||||
Several examples of entities and mappings are provided in the codebase, including ``Cash.State`` and
|
||||
``CommercialPaper.State``. For example, here's the first version of the cash schema.
|
||||
@ -116,6 +115,218 @@ Several examples of entities and mappings are provided in the codebase, includin
|
||||
Ensure that table and column names are compatible with the naming convention of the database vendors for which the Cordapp will be deployed,
|
||||
e.g. for Oracle database, prior to version 12.2 the maximum length of table/column name is 30 bytes (the exact number of characters depends on the database encoding).
|
||||
|
||||
Persisting Hierarchical Data
|
||||
----------------------------
|
||||
|
||||
You may wish to persist hierarchical relationships within states using multiple database tables
|
||||
|
||||
You may wish to persist hierarchical relationships within state data using multiple database tables. In order to facillitate this, multiple ``PersistentState``
|
||||
subclasses may be implemented. The relationship between these classes is defined using JPA annotations. It is important to note that the ``MappedSchema``
|
||||
constructor requires a list of *all* of these subclasses.
|
||||
|
||||
An example Schema implementing hierarchical relationships with JPA annotations has been implemented below. This Schema will cause ``parent_data`` and ``child_data` tables to be
|
||||
created.
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: java
|
||||
|
||||
@CordaSerializable
|
||||
public class SchemaV1 extends MappedSchema {
|
||||
|
||||
/**
|
||||
* This class must extend the MappedSchema class. Its name is based on the SchemaFamily name and the associated version number abbreviation (V1, V2... Vn).
|
||||
* In the constructor, use the super keyword to call the constructor of MappedSchema with the following arguments: a class literal representing the schema family,
|
||||
* a version number and a collection of mappedTypes (class literals) which represent JPA entity classes that the ORM layer needs to be configured with for this schema.
|
||||
*/
|
||||
|
||||
public SchemaV1() {
|
||||
super(Schema.class, 1, ImmutableList.of(PersistentParentToken.class, PersistentChildToken.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* The @entity annotation signifies that the specified POJO class' non-transient fields should be persisted to a relational database using the services
|
||||
* of an entity manager. The @table annotation specifies properties of the table that will be created to contain the persisted data, in this case we have
|
||||
* specified a name argument which will be used the table's title.
|
||||
*/
|
||||
|
||||
@Entity
|
||||
@Table(name = "parent_data")
|
||||
public static class PersistentParentToken extends PersistentState {
|
||||
|
||||
/**
|
||||
* The @Column annotations specify the columns that will comprise the inserted table and specify the shape of the fields and associated
|
||||
* data types of each database entry.
|
||||
*/
|
||||
|
||||
@Column(name = "owner") private final String owner;
|
||||
@Column(name = "issuer") private final String issuer;
|
||||
@Column(name = "amount") private final int amount;
|
||||
@Column(name = "linear_id") public final UUID linearId;
|
||||
|
||||
/**
|
||||
* The @OneToMany annotation specifies a one-to-many relationship between this class and a collection included as a field.
|
||||
* The @JoinColumn and @JoinColumns annotations specify on which columns these tables will be joined on.
|
||||
*/
|
||||
|
||||
@OneToMany(cascade = CascadeType.PERSIST)
|
||||
@JoinColumns({
|
||||
@JoinColumn(name = "output_index", referencedColumnName = "output_index"),
|
||||
@JoinColumn(name = "transaction_id", referencedColumnName = "transaction_id"),
|
||||
})
|
||||
private final List<PersistentChildToken> listOfPersistentChildTokens;
|
||||
|
||||
public PersistentParentToken(String owner, String issuer, int amount, UUID linearId, List<PersistentChildToken> listOfPersistentChildTokens) {
|
||||
this.owner = owner;
|
||||
this.issuer = issuer;
|
||||
this.amount = amount;
|
||||
this.linearId = linearId;
|
||||
this.listOfPersistentChildTokens = listOfPersistentChildTokens;
|
||||
}
|
||||
|
||||
// Default constructor required by hibernate.
|
||||
public PersistentParentToken() {
|
||||
this.owner = "";
|
||||
this.issuer = "";
|
||||
this.amount = 0;
|
||||
this.linearId = UUID.randomUUID();
|
||||
this.listOfPersistentChildTokens = null;
|
||||
}
|
||||
|
||||
public String getOwner() {
|
||||
return owner;
|
||||
}
|
||||
|
||||
public String getIssuer() {
|
||||
return issuer;
|
||||
}
|
||||
|
||||
public int getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public UUID getLinearId() {
|
||||
return linearId;
|
||||
}
|
||||
|
||||
public List<PersistentChildToken> getChildTokens() { return listOfPersistentChildTokens; }
|
||||
}
|
||||
|
||||
@Entity
|
||||
@CordaSerializable
|
||||
@Table(name = "child_data")
|
||||
public static class PersistentChildToken {
|
||||
// The @Id annotation marks this field as the primary key of the persisted entity.
|
||||
@Id
|
||||
private final UUID Id;
|
||||
@Column(name = "owner")
|
||||
private final String owner;
|
||||
@Column(name = "issuer")
|
||||
private final String issuer;
|
||||
@Column(name = "amount")
|
||||
private final int amount;
|
||||
|
||||
/**
|
||||
* The @ManyToOne annotation specifies that this class will be present as a member of a collection on a parent class and that it should
|
||||
* be persisted with the joining columns specified in the parent class. It is important to note the targetEntity parameter which should correspond
|
||||
* to a class literal of the parent class.
|
||||
*/
|
||||
|
||||
@ManyToOne(targetEntity = PersistentParentToken.class)
|
||||
private final TokenState persistentParentToken;
|
||||
|
||||
|
||||
public PersistentChildToken(String owner, String issuer, int amount) {
|
||||
this.Id = UUID.randomUUID();
|
||||
this.owner = owner;
|
||||
this.issuer = issuer;
|
||||
this.amount = amount;
|
||||
this.persistentParentToken = null;
|
||||
}
|
||||
|
||||
// Default constructor required by hibernate.
|
||||
public PersistentChildToken() {
|
||||
this.Id = UUID.randomUUID();
|
||||
this.owner = "";
|
||||
this.issuer = "";
|
||||
this.amount = 0;
|
||||
this.persistentParentToken = null;
|
||||
}
|
||||
|
||||
public UUID getId() {
|
||||
return Id;
|
||||
}
|
||||
|
||||
public String getOwner() {
|
||||
return owner;
|
||||
}
|
||||
|
||||
public String getIssuer() {
|
||||
return issuer;
|
||||
}
|
||||
|
||||
public int getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public TokenState getPersistentToken() {
|
||||
return persistentToken;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
@CordaSerializable
|
||||
object SchemaV1 : MappedSchema(schemaFamily = Schema::class.java, version = 1, mappedTypes = listOf(PersistentParentToken::class.java, PersistentChildToken::class.java)) {
|
||||
|
||||
@Entity
|
||||
@Table(name = "parent_data")
|
||||
class PersistentParentToken(
|
||||
@Column(name = "owner")
|
||||
var owner: String,
|
||||
|
||||
@Column(name = "issuer")
|
||||
var issuer: String,
|
||||
|
||||
@Column(name = "amount")
|
||||
var currency: Int,
|
||||
|
||||
@Column(name = "linear_id")
|
||||
var linear_id: UUID,
|
||||
|
||||
@JoinColumns(JoinColumn(name = "transaction_id", referencedColumnName = "transaction_id"), JoinColumn(name = "output_index", referencedColumnName = "output_index"))
|
||||
|
||||
var listOfPersistentChildTokens: MutableList<PersistentChildToken>
|
||||
) : PersistentState()
|
||||
|
||||
@Entity
|
||||
@CordaSerializable
|
||||
@Table(name = "child_data")
|
||||
class PersistentChildToken(
|
||||
@Id
|
||||
var Id: UUID = UUID.randomUUID(),
|
||||
|
||||
@Column(name = "owner")
|
||||
var owner: String,
|
||||
|
||||
@Column(name = "issuer")
|
||||
var issuer: String,
|
||||
|
||||
@Column(name = "amount")
|
||||
var currency: Int,
|
||||
|
||||
@Column(name = "linear_id")
|
||||
var linear_id: UUID,
|
||||
|
||||
@ManyToOne(targetEntity = PersistentParentToken::class)
|
||||
var persistentParentToken: TokenState
|
||||
|
||||
) : PersistentState()
|
||||
|
||||
|
||||
Identity mapping
|
||||
----------------
|
||||
Schema entity attributes defined by identity types (``AbstractParty``, ``Party``, ``AnonymousParty``) are automatically
|
||||
@ -161,8 +372,8 @@ In addition to ``jdbcSession``, ``ServiceHub`` also exposes the Java Persistence
|
||||
method. This method can be used to persist and query entities which inherit from ``MappedSchema``. This is particularly
|
||||
useful if off-ledger data must be maintained in conjunction with on-ledger state data.
|
||||
|
||||
.. note:: Your entity must be included as a mappedType in as part of a MappedSchema for it to be added to Hibernate
|
||||
as a custom schema. See Samples below.
|
||||
.. note:: Your entity must be included as a mappedType as part of a ``MappedSchema`` for it to be added to Hibernate
|
||||
as a custom schema. If it's not included as a mappedType, a corresponding table will not be created. See Samples below.
|
||||
|
||||
The code snippet below defines a ``PersistentFoo`` type inside ``FooSchemaV1``. Note that ``PersistentFoo`` is added to
|
||||
a list of mapped types which is passed to ``MappedSchema``. This is exactly how state schemas are defined, except that
|
||||
@ -201,7 +412,7 @@ the entity in this case should not subclass ``PersistentState`` (as it is not a
|
||||
class PersistentFoo(@Id @Column(name = "foo_id") var fooId: String, @Column(name = "foo_data") var fooData: String) : Serializable
|
||||
}
|
||||
|
||||
Instances of ``PersistentFoo`` can be persisted inside a flow as follows:
|
||||
Instances of ``PersistentFoo`` can be manually persisted inside a flow as follows:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
|
@ -1,3 +1,9 @@
|
||||
.. highlight:: kotlin
|
||||
.. raw:: html
|
||||
|
||||
<script type="text/javascript" src="_static/jquery.js"></script>
|
||||
<script type="text/javascript" src="_static/codesets.js"></script>
|
||||
|
||||
API: States
|
||||
===========
|
||||
|
||||
|
@ -22,108 +22,20 @@ A ``MockNetwork`` is created as follows:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/MockNetworkTestsTutorial.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 1
|
||||
:end-before: DOCEND 1
|
||||
|
||||
class FlowTests {
|
||||
private lateinit var mockNet: MockNetwork
|
||||
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/MockNetworkTestsTutorial.java
|
||||
:language: java
|
||||
:start-after: DOCSTART 1
|
||||
:end-before: DOCEND 1
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
network = MockNetwork(listOf("my.cordapp.package", "my.other.cordapp.package"))
|
||||
}
|
||||
}
|
||||
The ``MockNetwork`` requires at a minimum a list of CorDapps to be installed on each ``StartedMockNode``. The CorDapps are looked up on the
|
||||
classpath by package name, using ``TestCordapp.findCordapp``.
|
||||
|
||||
|
||||
.. sourcecode:: java
|
||||
|
||||
public class IOUFlowTests {
|
||||
private MockNetwork network;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
network = new MockNetwork(ImmutableList.of("my.cordapp.package", "my.other.cordapp.package"));
|
||||
}
|
||||
}
|
||||
|
||||
The ``MockNetwork`` requires at a minimum a list of packages. Each package is packaged into a CorDapp JAR and installed
|
||||
as a CorDapp on each ``StartedMockNode``.
|
||||
|
||||
Configuring the ``MockNetwork``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The ``MockNetwork`` is configured automatically. You can tweak its configuration using a ``MockNetworkParameters``
|
||||
object, or by using named parameters in Kotlin:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
val network = MockNetwork(
|
||||
// A list of packages to scan. Any contracts, flows and Corda services within these
|
||||
// packages will be automatically available to any nodes within the mock network
|
||||
cordappPackages = listOf("my.cordapp.package", "my.other.cordapp.package"),
|
||||
// If true then each node will be run in its own thread. This can result in race conditions in your
|
||||
// code if not carefully written, but is more realistic and may help if you have flows in your app that
|
||||
// do long blocking operations.
|
||||
threadPerNode = false,
|
||||
// The notaries to use on the mock network. By default you get one mock notary and that is usually
|
||||
// sufficient.
|
||||
notarySpecs = listOf(MockNetworkNotarySpec(DUMMY_NOTARY_NAME)),
|
||||
// If true then messages will not be routed from sender to receiver until you use the
|
||||
// [MockNetwork.runNetwork] method. This is useful for writing single-threaded unit test code that can
|
||||
// examine the state of the mock network before and after a message is sent, without races and without
|
||||
// the receiving node immediately sending a response.
|
||||
networkSendManuallyPumped = false,
|
||||
// How traffic is allocated in the case where multiple nodes share a single identity, which happens for
|
||||
// notaries in a cluster. You don't normally ever need to change this: it is mostly useful for testing
|
||||
// notary implementations.
|
||||
servicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random())
|
||||
|
||||
val network2 = MockNetwork(
|
||||
// A list of packages to scan. Any contracts, flows and Corda services within these
|
||||
// packages will be automatically available to any nodes within the mock network
|
||||
listOf("my.cordapp.package", "my.other.cordapp.package"), MockNetworkParameters(
|
||||
// If true then each node will be run in its own thread. This can result in race conditions in your
|
||||
// code if not carefully written, but is more realistic and may help if you have flows in your app that
|
||||
// do long blocking operations.
|
||||
threadPerNode = false,
|
||||
// The notaries to use on the mock network. By default you get one mock notary and that is usually
|
||||
// sufficient.
|
||||
notarySpecs = listOf(MockNetworkNotarySpec(DUMMY_NOTARY_NAME)),
|
||||
// If true then messages will not be routed from sender to receiver until you use the
|
||||
// [MockNetwork.runNetwork] method. This is useful for writing single-threaded unit test code that can
|
||||
// examine the state of the mock network before and after a message is sent, without races and without
|
||||
// the receiving node immediately sending a response.
|
||||
networkSendManuallyPumped = false,
|
||||
// How traffic is allocated in the case where multiple nodes share a single identity, which happens for
|
||||
// notaries in a cluster. You don't normally ever need to change this: it is mostly useful for testing
|
||||
// notary implementations.
|
||||
servicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random())
|
||||
)
|
||||
|
||||
.. sourcecode:: java
|
||||
|
||||
MockNetwork network = MockNetwork(
|
||||
// A list of packages to scan. Any contracts, flows and Corda services within these
|
||||
// packages will be automatically available to any nodes within the mock network
|
||||
ImmutableList.of("my.cordapp.package", "my.other.cordapp.package"),
|
||||
new MockNetworkParameters()
|
||||
// If true then each node will be run in its own thread. This can result in race conditions in
|
||||
// your code if not carefully written, but is more realistic and may help if you have flows in
|
||||
// your app that do long blocking operations.
|
||||
.setThreadPerNode(false)
|
||||
// The notaries to use on the mock network. By default you get one mock notary and that is
|
||||
// usually sufficient.
|
||||
.setNotarySpecs(ImmutableList.of(new MockNetworkNotarySpec(DUMMY_NOTARY_NAME)))
|
||||
// If true then messages will not be routed from sender to receiver until you use the
|
||||
// [MockNetwork.runNetwork] method. This is useful for writing single-threaded unit test code
|
||||
// that can examine the state of the mock network before and after a message is sent, without
|
||||
// races and without the receiving node immediately sending a response.
|
||||
.setNetworkSendManuallyPumped(false)
|
||||
// How traffic is allocated in the case where multiple nodes share a single identity, which
|
||||
// happens for notaries in a cluster. You don't normally ever need to change this: it is mostly
|
||||
// useful for testing notary implementations.
|
||||
.setServicePeerAllocationStrategy(new InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random()));
|
||||
``MockNetworkParameters`` provides other properties for the network which can be tweaked. They default to sensible values if not specified.
|
||||
|
||||
Adding nodes to the network
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
@ -132,73 +44,21 @@ Nodes are created on the ``MockNetwork`` using:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/MockNetworkTestsTutorial.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 2
|
||||
:end-before: DOCEND 2
|
||||
|
||||
class FlowTests {
|
||||
private lateinit var mockNet: MockNetwork
|
||||
lateinit var nodeA: StartedMockNode
|
||||
lateinit var nodeB: StartedMockNode
|
||||
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/MockNetworkTestsTutorial.java
|
||||
:language: java
|
||||
:start-after: DOCSTART 2
|
||||
:end-before: DOCEND 2
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
network = MockNetwork(listOf("my.cordapp.package", "my.other.cordapp.package"))
|
||||
nodeA = network.createPartyNode()
|
||||
// We can optionally give the node a name.
|
||||
nodeB = network.createPartyNode(CordaX500Name("Bank B", "London", "GB"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.. sourcecode:: java
|
||||
|
||||
public class IOUFlowTests {
|
||||
private MockNetwork network;
|
||||
private StartedMockNode a;
|
||||
private StartedMockNode b;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
network = new MockNetwork(ImmutableList.of("my.cordapp.package", "my.other.cordapp.package"));
|
||||
nodeA = network.createPartyNode(null);
|
||||
// We can optionally give the node a name.
|
||||
nodeB = network.createPartyNode(new CordaX500Name("Bank B", "London", "GB"));
|
||||
}
|
||||
}
|
||||
|
||||
Nodes added using ``createPartyNode`` are provided a default set of node parameters. However, it is also possible to
|
||||
provide different parameters to each node using the following methods on ``MockNetwork``:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
/**
|
||||
* Create a started node with the given parameters.
|
||||
*
|
||||
* @param legalName The node's legal name.
|
||||
* @param forcedID A unique identifier for the node.
|
||||
* @param entropyRoot The initial entropy value to use when generating keys. Defaults to an (insecure) random value,
|
||||
* but can be overridden to cause nodes to have stable or colliding identity/service keys.
|
||||
* @param configOverrides Add/override the default configuration/behaviour of the node
|
||||
* @param extraCordappPackages Extra CorDapp packages to add for this node.
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun createNode(legalName: CordaX500Name? = null,
|
||||
forcedID: Int? = null,
|
||||
entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()),
|
||||
configOverrides: MockNodeConfigOverrides? = null,
|
||||
extraCordappPackages: List<String> = emptyList()
|
||||
): StartedMockNode
|
||||
|
||||
/** Create a started node with the given parameters. **/
|
||||
fun createNode(parameters: MockNodeParameters = MockNodeParameters()): StartedMockNode
|
||||
|
||||
As you can see above, parameters can be added individually or encapsulated within a ``MockNodeParameters`` object. Of
|
||||
particular interest are ``configOverrides`` which allow you to override some of the default node
|
||||
configuration options. Please refer to the ``MockNodeConfigOverrides`` class for details what can currently be overridden.
|
||||
Also, the ``extraCordappPackages`` parameter allows you to add extra CorDapps to a
|
||||
specific node. This is useful when you wish for all nodes to load a common CorDapp but for a subset of nodes to load
|
||||
CorDapps specific to their role in the network.
|
||||
Nodes added using ``createNode`` are provided a default set of node parameters. However, it is also possible to
|
||||
provide different parameters to each node using ``MockNodeParameters``. Of particular interest are ``configOverrides`` which allow you to
|
||||
override some of the default node configuration options. Please refer to the ``MockNodeConfigOverrides`` class for details what can currently
|
||||
be overridden. Also, the ``additionalCordapps`` parameter allows you to add extra CorDapps to a specific node. This is useful when you wish
|
||||
for all nodes to load a common CorDapp but for a subset of nodes to load CorDapps specific to their role in the network.
|
||||
|
||||
Running the network
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
@ -94,25 +94,24 @@ to "walk the chain" and verify that each input was generated through a valid seq
|
||||
Reference input states
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A reference input state is added to a transaction as a ``ReferencedStateAndRef``. A ``ReferencedStateAndRef`` can be
|
||||
obtained from a ``StateAndRef`` by calling the ``StateAndRef.referenced()`` method which returns a
|
||||
``ReferencedStateAndRef``.
|
||||
|
||||
.. warning:: Reference states are only available on Corda networks with a minimum platform version >= 4.
|
||||
|
||||
A reference input state is added to a transaction as a ``ReferencedStateAndRef``. A ``ReferencedStateAndRef`` can be
|
||||
obtained from a ``StateAndRef`` by calling the ``StateAndRef.referenced()`` method which returns a ``ReferencedStateAndRef``.
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 55
|
||||
:end-before: DOCEND 55
|
||||
:dedent: 8
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 55
|
||||
:end-before: DOCEND 55
|
||||
:dedent: 8
|
||||
|
||||
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java
|
||||
:language: java
|
||||
:start-after: DOCSTART 55
|
||||
:end-before: DOCEND 55
|
||||
:dedent: 12
|
||||
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java
|
||||
:language: java
|
||||
:start-after: DOCSTART 55
|
||||
:end-before: DOCEND 55
|
||||
:dedent: 12
|
||||
|
||||
**Handling of update races:**
|
||||
|
||||
|
@ -64,7 +64,8 @@ filter criteria:
|
||||
- Use ``queryBy`` to obtain a current snapshot of data (for a given ``QueryCriteria``)
|
||||
- Use ``trackBy`` to obtain both a current snapshot and a future stream of updates (for a given ``QueryCriteria``)
|
||||
|
||||
.. note:: Streaming updates are only filtered based on contract type and state status (UNCONSUMED, CONSUMED, ALL)
|
||||
.. note:: Streaming updates are only filtered based on contract type and state status (UNCONSUMED, CONSUMED, ALL).
|
||||
They will not respect any other criteria that the initial query has been filtered by.
|
||||
|
||||
Simple pagination (page number and size) and sorting (directional ordering using standard or custom property
|
||||
attributes) is also specifiable. Defaults are defined for paging (pageNumber = 1, pageSize = 200) and sorting
|
||||
|
@ -28,7 +28,7 @@ Alter the versions you depend on in your Gradle file like so:
|
||||
.. sourcecode:: groovy
|
||||
|
||||
ext.corda_release_version = '4.0'
|
||||
ext.corda_gradle_plugins_version = '4.0.37'
|
||||
ext.corda_gradle_plugins_version = '4.0.38'
|
||||
ext.kotlin_version = '1.2.71'
|
||||
ext.quasar_version = '0.7.10'
|
||||
|
||||
@ -121,10 +121,13 @@ away to the new API, as otherwise things like business network membership checks
|
||||
|
||||
This is a three step process:
|
||||
|
||||
1. Change the flow that calls ``FinalityFlow``
|
||||
1. Change the flow that calls ``FinalityFlow``.
|
||||
2. Change or create the flow that will receive the finalised transaction.
|
||||
3. Make sure your application's minimum and target version numbers are both set to 4 (see step 2).
|
||||
|
||||
Upgrading a non-initiating flow
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
As an example, let's take a very simple flow that finalises a transaction without the involvement of a counterpart flow:
|
||||
|
||||
.. container:: codeset
|
||||
@ -140,7 +143,7 @@ As an example, let's take a very simple flow that finalises a transaction withou
|
||||
:end-before: DOCEND SimpleFlowUsingOldApi
|
||||
:dedent: 4
|
||||
|
||||
To use the new API, this flow needs to be annotated with ``InitiatingFlow`` and a ``FlowSession`` to the participant of the transaction must be
|
||||
To use the new API, this flow needs to be annotated with ``InitiatingFlow`` and a ``FlowSession`` to the participant(s) of the transaction must be
|
||||
passed to ``FinalityFlow`` :
|
||||
|
||||
.. container:: codeset
|
||||
@ -175,10 +178,17 @@ to record the finalised transaction:
|
||||
:end-before: DOCEND SimpleNewResponderFlow
|
||||
:dedent: 4
|
||||
|
||||
For flows which are already initiating counterpart flows then it's a simple matter of using the existing flow session.
|
||||
.. note:: All the nodes in your business network will need the new CorDapp, otherwise they won't know how to receive the transaction. **This
|
||||
includes nodes which previously didn't have the old CorDapp.** If a node is sent a transaction and it doesn't have the new CorDapp loaded
|
||||
then simply restart it with the CorDapp and the transaction will be recorded.
|
||||
|
||||
Upgrading an initiating flow
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
For flows which are already initiating counterpart flows then it's a matter of using the existing flow session.
|
||||
Note however, the new ``FinalityFlow`` is inlined and so the sequence of sends and receives between the two flows will
|
||||
change and will be incompatible with your current flows. You can use the flow version API to write your flows in a
|
||||
backwards compatible way.
|
||||
backwards compatible manner.
|
||||
|
||||
Here's what an upgraded initiating flow may look like:
|
||||
|
||||
@ -212,8 +222,9 @@ finalised transaction. If the initiator is written in a backwards compatible way
|
||||
:end-before: DOCEND ExistingResponderFlow
|
||||
:dedent: 12
|
||||
|
||||
The responder flow may be waiting for the finalised transaction to appear in the local node's vault using ``waitForLedgerCommit``.
|
||||
This is no longer necessary with ``ReceiveFinalityFlow`` and the call to ``waitForLedgerCommit`` can be removed.
|
||||
You may already be using ``waitForLedgerCommit`` in your responder flow for the finalised transaction to appear in the local node's vault.
|
||||
Now that it's calling ``ReceiveFinalityFlow``, which effectively does the same thing, this is no longer necessary. The call to
|
||||
``waitForLedgerCommit`` should be removed.
|
||||
|
||||
Step 4. Security: Upgrade your use of SwapIdentitiesFlow
|
||||
--------------------------------------------------------
|
||||
|
@ -52,18 +52,8 @@ Version 4.0
|
||||
|
||||
* Fixed a problem that was preventing `Cash.generateSpend` to be used more than once per transaction (https://github.com/corda/corda/issues/4110).
|
||||
|
||||
* ``SwapIdentitiesFlow``, from the experimental confidential-identities module, is now an inlined flow. Instead of passing in a ``Party`` with
|
||||
whom to exchange the anonymous identity, a ``FlowSession`` to that party is required instead. The flow running on the other side must
|
||||
also call ``SwapIdentitiesFlow``. This change was required as the previous API allowed any counterparty to generate anonoymous identities
|
||||
with a node at will with no checks.
|
||||
|
||||
The result type has changed to a simple wrapper class, instead of a Map, to make extracting the identities easier. Also, the wire protocol
|
||||
of the flow has slightly changed.
|
||||
|
||||
.. note:: V3 and V4 of confidential-identities are not compatible and confidential-identities V3 will not work with a V4 Corda node. CorDapps
|
||||
in such scenarios using confidential-identities must be updated.
|
||||
|
||||
The ``confidential-identities`` dependency in your CorDapp must now be ``compile`` and not ``cordaCompile``.
|
||||
* The experimental confidential-identities is now a separate CorDapp and must now be loaded onto the node alongside any CorDapp that needs it.
|
||||
This also means your gradle dependency for it should be ``cordapp`` and not ``cordaCompile``.
|
||||
|
||||
* Fixed a bug resulting in poor vault query performance and incorrect results when sorting.
|
||||
|
||||
@ -86,6 +76,9 @@ Version 4.0
|
||||
* Introduced new optional network bootstrapper command line options (--register-package-owner, --unregister-package-owner)
|
||||
to register/unregister a java package namespace with an associated owner in the network parameter packageOwnership whitelist.
|
||||
|
||||
* BFT-Smart and Raft notary implementations have been move to the ``net.corda.notary.experimental`` package to emphasise
|
||||
their experimental nature.
|
||||
|
||||
* New "validate-configuration" sub-command to `corda.jar`, allowing to validate the actual node configuration without starting the node.
|
||||
|
||||
* CorDapps now have the ability to specify a minimum platform version in their MANIFEST.MF to prevent old nodes from loading them.
|
||||
@ -99,39 +92,16 @@ Version 4.0
|
||||
|
||||
* ``FinalityFlow`` is now an inlined flow and requires ``FlowSession`` s to each party intended to receive the transaction. This is to fix the
|
||||
security problem with the old API that required every node to accept any transaction it received without any checks. Existing CorDapp
|
||||
binaries relying on this old behaviour will continue to function as previously. However, it is strongly recommended that CorDapps switch to
|
||||
binaries relying on this old behaviour will continue to function as previously. However, it is strongly recommended CorDapps switch to
|
||||
this new API. See :doc:`app-upgrade-notes` for further details.
|
||||
|
||||
* For similar reasons, ``SwapIdentitiesFlow``, from confidential-identities, is also now an inlined flow. The old API has been preserved but
|
||||
it is strongly recommended CorDapps switch to this new API. See :doc:`app-upgrade-notes` for further details.
|
||||
|
||||
* Introduced new optional network bootstrapper command line option (--minimum-platform-version) to set as a network parameter
|
||||
|
||||
* BFT-Smart and Raft notary implementations have been extracted out of node into ``experimental`` CorDapps to emphasise
|
||||
their experimental nature. Moreover, the BFT-Smart notary will only work in dev mode due to its use of Java serialization.
|
||||
|
||||
* Vault storage of contract state constraints metadata and associated vault query functions to retrieve and sort by constraint type.
|
||||
|
||||
* UPGRADE REQUIRED: changes have been made to how notary implementations are configured and loaded.
|
||||
No upgrade steps are required for the single-node notary (both validating and non-validating variants).
|
||||
Other notary implementations have been moved out of the Corda node into individual Cordapps, and require configuration
|
||||
file updates.
|
||||
|
||||
To run a notary you will now need to include the appropriate notary CorDapp in the ``cordapps/`` directory:
|
||||
|
||||
* ``corda-notary-raft`` for the Raft notary.
|
||||
* ``corda-notary-bft-smart`` for the BFT-Smart notary.
|
||||
|
||||
It is now required to specify the fully qualified notary service class name, ``className``, and the legal name of
|
||||
the notary service in case of distributed notaries: ``serviceLegalName``.
|
||||
|
||||
Implementation-specific configuration values have been moved to the ``extraConfig`` configuration block.
|
||||
|
||||
Example configuration changes for the Raft notary:
|
||||
|
||||
.. image:: resources/notary-config-update.png
|
||||
|
||||
Example configuration changes for the BFT-Smart notary:
|
||||
|
||||
.. image:: resources/notary-config-update-bft.png
|
||||
|
||||
* New overload for ``CordaRPCClient.start()`` method allowing to specify target legal identity to use for RPC call.
|
||||
|
||||
* Case insensitive vault queries can be specified via a boolean on applicable SQL criteria builder operators. By default
|
||||
@ -145,7 +115,8 @@ Version 4.0
|
||||
|
||||
* Removed experimental feature ``CordformDefinition``
|
||||
|
||||
* Added ``registerResponderFlow`` method to ``StartedMockNode``, to support isolated testing of responder flow behaviour.
|
||||
* Added new overload of ``StartedMockNode.registerInitiatedFlow`` which allows registering custom initiating-responder flow pairs, which
|
||||
can be useful for testing error cases.
|
||||
|
||||
* "app", "rpc", "p2p" and "unknown" are no longer allowed as uploader values when importing attachments. These are used
|
||||
internally in security sensitive code.
|
||||
@ -340,6 +311,9 @@ Version 4.0
|
||||
* Finance CorDapp is now build as a sealed and signed JAR file.
|
||||
Custom classes can no longer be placed in the packages defined in Finance Cordapp or access it's non-public members.
|
||||
|
||||
* Finance CorDapp was split into two separate apps: ``corda-finance-contracts`` and ``corda-finance-workflows``,
|
||||
``corda-finance`` is kept for backward compatibility, it is recommended to use separated jars.
|
||||
|
||||
* The format of the shell commands' output can now be customized via the node shell, using the ``output-format`` command.
|
||||
|
||||
* The ``node_transaction_mapping`` database table has been folded into the ``node_transactions`` database table as an additional column.
|
||||
|
@ -21,6 +21,8 @@ you.
|
||||
`CordaRPCClient`_ class. You can find an example of how to do this using the popular Spring Boot server
|
||||
`here <https://github.com/corda/spring-webserver>`_.
|
||||
|
||||
.. _clientrpc_connect_ref:
|
||||
|
||||
Connecting to a node via RPC
|
||||
----------------------------
|
||||
To use `CordaRPCClient`_, you must add ``net.corda:corda-rpc:$corda_release_version`` as a ``cordaCompile`` dependency
|
||||
|
@ -50,7 +50,7 @@ Corda incubating modules
|
||||
The following modules don't yet have a completely stable API, but we will do our best to minimise disruption to
|
||||
developers using them until we are able to graduate them into the public API:
|
||||
|
||||
* **net.corda.confidential.identities**: experimental support for confidential identities on the ledger
|
||||
* **net.corda.confidential**: experimental support for confidential identities on the ledger
|
||||
* **net.corda.finance**: a range of elementary contracts (and associated schemas) and protocols, such as abstract fungible assets, cash, obligation and commercial paper
|
||||
* **net.corda.client.jfx**: support for Java FX UI
|
||||
* **net.corda.client.mock**: client mock utilities
|
||||
|
@ -1,545 +0,0 @@
|
||||
Corda Network Foundation : Governance Guidelines
|
||||
================================================
|
||||
|
||||
23 October 2018
|
||||
|
||||
Version 0.95
|
||||
|
||||
DRAFT for discussion
|
||||
|
||||
This is a set of governance guidelines for the Corda Network Foundation. It provides a framework for the Foundation’s
|
||||
Board, to steer and govern Corda Network effectively to realise its potential. It is not a set of binding legal obligations.
|
||||
|
||||
1 Background to Corda and the Network
|
||||
-------------------------------------
|
||||
|
||||
Corda allows multiple independent applications and private networks to coexist, each with their own business models and
|
||||
membership criteria, yet linked by the same underlying network (‘Corda Network’). This Network enables ‘interoperability’,
|
||||
the exchange of data or assets via a secure, efficient ‘internet layer’, in a way that isn’t possible with competing
|
||||
permissioned distributed ledger technologies or legacy systems.
|
||||
|
||||
Corda Network operates the protocol of Corda currently, and is always expected to. The protocol is currently
|
||||
specified in the Corda Open Source Project codebase, but later may be formalised in a protocol specification document,
|
||||
which then will become canonical.
|
||||
|
||||
1.1 Reason for a Corda Network Foundation
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
R3 has set up and governs by default Corda Network currently (along with Corda). This includes making key decisions
|
||||
around establishing, maintaining and updating standards, policies, and procedures for participation in, and use of,
|
||||
Corda Network.
|
||||
|
||||
However, it is critically important that a commercial entity should not control Corda Network going forwards. It should
|
||||
be governed transparently (to its Participants), with a fair and stable structure. Analysis and feedback show
|
||||
that Corda Network will be most effectively governed via a Foundation, a not-for-profit, independent entity in which
|
||||
Network Participants elect, and can be elected to, a governing Board.
|
||||
|
||||
A Foundation will enable Network Participants to be involved with, and also understand, how decisions are made (including
|
||||
around issues of identity and permission), building trust and engagement from a wide range of stakeholders. This will
|
||||
bring about the best decisions and outcomes for the Network’s long-term success.
|
||||
|
||||
In other words, to achieve the community's objective of Corda ubiquity, it is necessary to put in place a governance
|
||||
structure which explicitly limits R3’s control of Corda Network, and enables this ubiquity.
|
||||
|
||||
2 The Corda Network Foundation
|
||||
------------------------------
|
||||
|
||||
2.1 Mission and Values
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
Following on from the Corda introduction and technical white papers, we see the mission of the Corda Network Foundation
|
||||
to achieve the vision of Corda - whereby the state of transactions and agreements of business partners can be recorded
|
||||
in a single global database, ending the need for costly reconciliation and error correction, while maintaining privacy.
|
||||
Further details of the vision are laid out in the
|
||||
[Corda Introductory White Paper](https://www.corda.net/content/corda-platform-whitepaper.pdf).
|
||||
|
||||
Achieving this vision in its full ubiquity will involve running and maintaining a stable and secure Network with open
|
||||
and fair governance, while also promoting the Network so as to ensure its more widespread use.
|
||||
|
||||
Following on this, the Corda Network Foundation shall embody the following qualities in executing its mission:
|
||||
|
||||
* Fairness and openness – Participants can join the Network and make up the Network’s governing board, elected through a
|
||||
straightforward voting process.
|
||||
* Democracy and transparency – Key decisions and rationale are shared openly with Participants.
|
||||
* Stability (with a long-term view) with flexibility – Board directors’ terms will last three years with a staggered
|
||||
board, and the governance model will be flexible to adapt where required.
|
||||
* Efficiency – Staying a lean organisation, sufficient to commission and monitor an Operator to run any services,
|
||||
physical infrastructure, or operational processes that may be needed to achieve the vision of Corda. Provide adequate
|
||||
support, through advisory committees.
|
||||
* Cost effectiveness - Funding received through participation fees pays for an Operator to run the Network securely, and
|
||||
the Foundation shall not be a profit-making entity.
|
||||
* Independence – Corda Network Foundation makes its own decisions (within the law), and is not following another
|
||||
entity’s rules.
|
||||
|
||||
More specifically, the Foundation shall focus on the following commitments over the long-term:
|
||||
|
||||
* Maintain the long-term standards, services and open governance model of the Network, ensuring it continues to be
|
||||
updated to the current Corda protocol version.
|
||||
* Hold the Trust Root for the Network, used for creation of operational certificates, independently of the Operator.
|
||||
* Commission the provision and operation of infrastructure and services for the Network, both of technical services, and
|
||||
infrastructure needed for meetings, events and collaborative discussions, and provide structure around the business and
|
||||
technical governance of the Network.
|
||||
* Facilitate a diverse and vibrant community of industry experts, Corda contributors, users and services, including
|
||||
developers, service and solution providers and end users.
|
||||
* Set minimum standards for the external provision of notary and oracle services.
|
||||
* Enable the ubiquity and utility of Corda throughout all applicable industries and commercial use cases.
|
||||
* Balance the divergent interests of a wide range of stakeholders, including business network operators, Corda
|
||||
customers, open source developers, and R3’s shareholders.
|
||||
|
||||
2.2 Structure of the Foundation
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
The Foundation shall be a not-for-profit entity created exclusively to execute the mission set out in this Constitution.
|
||||
With the advice of international lawyers, this is a ‘Stichting’ domiciled in Holland – a legal entity suited for
|
||||
governance activities, able to act commercially, with limited liability but no shareholders, capital or dividends.
|
||||
|
||||
The Foundation is defined in a set of Articles of Association and By-laws.
|
||||
|
||||
The Foundation governance bodies shall include:
|
||||
|
||||
1. A **Governing Board** (‘the Board’) of 11 representatives (‘Directors’) with privileges and responsibilities as set out
|
||||
in section 3.
|
||||
2. A **Technical Advisory Committee** (‘the TAC’), comprised of representatives of Participant organisations with
|
||||
responsibilities set out in section 6.2.
|
||||
3. A **Governance Advisory Committee**, comprised of representatives of Participant organisations with responsibilities
|
||||
set out in section 6.3.
|
||||
4. A **Network Operator** (‘the Operator’), charging the Foundation reasonable costs for providing network and
|
||||
administration services, paid by the Foundation through membership funds, and accountable directly to the Board, set
|
||||
out in section 7.
|
||||
|
||||
Operating on behalf of:
|
||||
|
||||
* A **General Membership** (‘the Participant Community’), which is open to any organisation participating in Corda Network,
|
||||
and with privileges and responsibilities as set out in section 6.
|
||||
|
||||
Any change to the structure of the Foundation is a constitutional change, described in section 5.1.
|
||||
|
||||
3 Governing Board
|
||||
-----------------
|
||||
|
||||
3.1 Role of the Board
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
The goal of the Board is primarily to ensure the stable and secure operation of the Network, as well as to achieve the
|
||||
vision of Corda laid out in section 2.1. The fundamental responsibility of directors appointed to the Board is to
|
||||
exercise their business judgement to act in what they believe to be the best interests of the Network, taking account
|
||||
of the interests of the Network community as a whole (rather than any one individual interest).
|
||||
|
||||
Directors are expected to comply with the Conflict of Interest policy, which includes a responsibility to disclose
|
||||
promptly any conflicts that may arise, and meet the expected standards specified in the Code of Conduct Guidelines for
|
||||
ethical conduct and breach reporting.
|
||||
|
||||
The Board is the formal decision-making authority of the Foundation, and actions of the Board reflect its collective
|
||||
decision making.
|
||||
|
||||
3.2 Relationship of the Board with the Operator
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
It is the duty of the Board to monitor the Operator’s performance to ensure that the Network operates in an effective,
|
||||
efficient and ethical manner. The Board will also be responsible for overseeing the Operator in the development of the
|
||||
Network’s strategic and tactical plans, ensuring that they will result in broad and open adoption of Corda. The Operator
|
||||
is responsible to the Board for the execution of day to day operations, and the implementation of strategic and tactical
|
||||
change.
|
||||
|
||||
3.3 Composition and Establishment of the Board
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
### 3.3.1 Size
|
||||
The Board shall consist of 11 voting members (‘Directors’) in total, to allow broad representation but maintain an agile
|
||||
decision-making ability. The selection process (using the Participant Community) is intended to ensure that the Board is
|
||||
composed of members who display diversity in geography, culture, skills, experience and perspective, and that the
|
||||
abilities and interests of Directors are aligned with those of Corda.
|
||||
|
||||
R3 shall have the ongoing right to appoint two Directors, as the firm which developed Corda and funded the initial
|
||||
construction of the Network. It represents the interests of its large and diverse alliance of commercial organisations,
|
||||
financial institutions, and regulatory bodies. Similarly to the rest of the board, the Directors will have three-year
|
||||
terms (unless the director resigns or leaves for another reason) and can be re-appointed without limit. Appointment will
|
||||
be effective immediately.
|
||||
|
||||
The Chair of the board will be elected for a one year term by a vote of the Directors of the Board, at the first Board
|
||||
meeting following the Board election.
|
||||
|
||||
#### 3.3.2 Participation Criteria
|
||||
Participants shall be directed to vote to ensure that the Board is composed of Directors who in the aggregate produce
|
||||
the broadest diversity on the Board, consistent with meeting the other criteria. In addition, the Board is to be
|
||||
comprised of individuals who can demonstrate to Participants they meet the following requirements:
|
||||
|
||||
* Hold an understanding and appreciation of the Corda protocol and community purpose.
|
||||
* Have an awareness of cultural and geographic perspectives.
|
||||
* Demonstrate integrity, intelligence and objectivity.
|
||||
* Can work and communicate in written and spoken English.
|
||||
|
||||
To promote diversity, the following guidelines are adopted, in particular for steady-state governance (recognising that
|
||||
these may not be possible to fulfil during the transition period):
|
||||
|
||||
* No corporate group of participants may have more than one Director. In the event of votes for two different candidates
|
||||
representing the same overall corporate group, the candidate with the most votes shall be considered.
|
||||
* Of the nine Directors, there should not be more than three Directors from any broad industry classification, according to
|
||||
company classification data.
|
||||
* Of the nine Directors, there should not be more than three Directors from any continent (one must be based in the Americas,
|
||||
Europe/Africa and Asia, to ensure geographic diversity.
|
||||
* Of the nine Directors, there should not be more than three Directors representing any Corporate Group with more than
|
||||
100,000 employees.
|
||||
* There is no restriction of re-election of Directors or the Chair of the Board.
|
||||
|
||||
### 3.3.3 Establishment of the Board
|
||||
#### 3.3.3.1 Pre-Foundation
|
||||
Initially R3 shall govern the Network to ensure rapid progress on its implementation. Once the Foundation is set-up and
|
||||
at least three business networks are conducting live transactions using the network with at least three Participants each, the
|
||||
'transition period' of one year will commence.
|
||||
#### 3.3.3.2 Transition: Initial set-up of Foundation Board:
|
||||
For the transition year, the first three business networks shall have the right to choose three Participants, to
|
||||
represent the interests of the business network. One of each of these must be based in the Americas, Europe/Africa and
|
||||
Asia, to ensure geographic diversity, if the pool Participants allows. Each selected Participant will appoint a
|
||||
Director, to sit on the Board, making nine Directors in addition to the two Directors from R3.
|
||||
|
||||
After this start-up period, there will be a vote for Board Directors.
|
||||
|
||||
For the first election only, of the nine vacant seats, three will be for a duration of one year, three for two years, and
|
||||
three for three years. This will introduce a staggered board, so there is greater continuity at the end of each term.
|
||||
Candidates with the most votes will fill the three-year seats first, followed by two-year and then one-year seats. In all other
|
||||
respects, the first election will follow the steady state process.
|
||||
#### 3.3.3.3 Steady-State
|
||||
1. Participants may nominate candidates for Director election. Appointments to the nine rotating seats of the Board will be
|
||||
by vote of the Participants, with three seats up for election each year. Any seats vacated mid-term will also be
|
||||
elected at the same time. R3 may not put forward candidates for the nine rotating seats, and these may not be held by
|
||||
R3 employees.
|
||||
2. Candidates will create a personal statement and short biography, which will be shared with all Participants.
|
||||
3. Participants may each cast up to three votes for three separate candidates.
|
||||
4. Subject to meeting certain criteria (including diversity of geography and industry), the most popular candidates
|
||||
will be appointed as Directors.
|
||||
5. Candidates will be considered in sequence from most popular to least, and if a seat is vacant according to the
|
||||
diversity criteria in section 3.3.2, the candidate will be allocated to it. This may mean that occasionally a less
|
||||
popular candidate fills a seat instead of a more popular one.
|
||||
6. R3 shall appoint Directors to the two remaining seats, when appropriate.
|
||||
#### 3.3.4 Removal from the Board and Re-election.
|
||||
Apart from the three-year expiry, Directors can otherwise leave the Board by resignation, prolonged non-attendance of
|
||||
board meetings of more than six months, death, or if necessary, removal by a Mandatory Governance Event. In any case, a
|
||||
vacant seat will be contested at the next annual election.
|
||||
|
||||
3.4 Conduct of Board Meetings
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Attendance may be in person or by video-conference.
|
||||
|
||||
The Board shall meet not less than every six months, and may meet on the request of any Director, but not more than every
|
||||
two months. At least two weeks’ notice shall be given to Directors. By exception, the Chair may convene an emergency
|
||||
meeting with minimal notice, appropriate to the situation in the Chair's judgement.
|
||||
|
||||
The Board shall consider all Governance Events proposed since the previous meeting, except for an emergency convening.
|
||||
|
||||
Board meetings shall be limited to the Board representatives, and shall follow the requirements for quorum and voting
|
||||
outlined in this Constitution.
|
||||
|
||||
The Board may decide whether to allow one named representative to attend as an alternate, and typically these shall be
|
||||
allowed.
|
||||
|
||||
The Board meetings shall be conducted in private, but in the interest of transparency, public minutes shall be
|
||||
published within two weeks following their approval by the Board.
|
||||
|
||||
Participants who do not have representation on the Board may request an observer to be present at a Board meeting.
|
||||
This is subject to a lottery held one week prior to the meeting, a limit of 20 observer places, and a limit of one
|
||||
observer per unrepresented Participant. Observers may participate in discussions but shall not participate in any Board
|
||||
vote, and may be asked to join by video-conference if there are logistical constraints.
|
||||
|
||||
4 Relation of the Foundation to Business Networks
|
||||
---------------------------------------------------
|
||||
|
||||
The global Network shall support the operation of any business networks which may be formed by industry-specific
|
||||
operators on top of the Network. The Board shall ensure that there is a clear separation between areas of governance
|
||||
for the Foundation and Network outlined in this document, and for individual business networks.
|
||||
|
||||
Additionally, the structure and control processes defined for the Foundation shall be documented and made available
|
||||
under a Creative Commons license, both for reuse by business network operators if business networks need a similar
|
||||
governance structure, and so that such governance layers are complementary and not contradictory.
|
||||
|
||||
5 Governance Events
|
||||
---------------------
|
||||
|
||||
All formal changes to the Network and the Foundation shall be controlled through a formal Governance Event process, and
|
||||
the right to initiate this shall be held by all Directors and Participants. In the event of disruptive behaviour by an
|
||||
individual Participant or group of Participants, this right may be curtailed, as described in 5.2.5.
|
||||
|
||||
5.1 Types of Governance Events
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
There are three types of change that affect the Network and the Foundation, which require a controlled change process
|
||||
and a vote described in 5.5, and are defined as Governance Events:
|
||||
|
||||
1. All changes to the Stichting Articles of Association and By-laws are defined as Constitutional Governance Events.
|
||||
2. All changes to Network participation criteria, charges, budgets, change management process and other business areas not
|
||||
defined in Articles of Association or By-laws are defined as Mandatory Governance Events in section 5.2. The Board shall
|
||||
vote to accept or reject all such Mandatory Governance Events, and the outcomes are binding on Participants and the
|
||||
Operator for implementation.
|
||||
3. All changes to technical parameters and notary criteria, which affect the nodes of participants, are defined as
|
||||
Advisory Governance Events in section 5.3. While the Operator can implement these without Board approval, it may ask the
|
||||
Board to provide an advisory (non-binding) vote. Conversely, the Board may require that it is given the opportunity to
|
||||
provide an advisory vote.
|
||||
|
||||
Any other changes in the day to day internal implementation of network services by the Operator, which do not require
|
||||
changes to be implemented on the nodes of participants, are out of scope as Governance Events.
|
||||
|
||||
All Constitutional, Mandatory and Advisory Governance Events shall be supported by a formal proposal, using standard
|
||||
structured documents and containing all relevant background information, to create an efficient process both for the
|
||||
submitter and the Board.
|
||||
Depending on the content of the Governance Event proposal, the Board or Operator may rely on the Governance or Technical
|
||||
Advisory Committee to provide due diligence and make a recommendation for implementation.
|
||||
|
||||
For all Governance Events, decisions and the rationale for the decision shall be published transparently.
|
||||
|
||||
5.2 Mandatory Governance Events
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
### 5.2.1 Access Standards
|
||||
The Corda system can be accessed by using software which implements the set of technical protocols which define
|
||||
compatibility (see 5.3.1) above). The reference implementation of this software is open source and freely accessible at
|
||||
[www.corda.net](https://www.corda.net).
|
||||
|
||||
To join the Network, a participant running Corda compatible software also needs a unique and real-world identity. The
|
||||
Foundation shall enforce this access requirement through the issuance of PKI certificates. Corda has a primary objective
|
||||
to facilitate automation of real-world contracts between real-world parties, and has a particular requirement to ensure
|
||||
that identities on the Network are unique and that all participants understand the basis on which they have been issued.
|
||||
The Foundation shall govern the operation of the technical infrastructure to enable a good level of service for identity
|
||||
issuance.
|
||||
|
||||
The access criteria for proving real world identity shall be defined by the Board and shall be transparent and objective.
|
||||
Any party which can demonstrate they meet these criteria will be issued a certificate without prejudice. The goal to
|
||||
ensure that the Operator which manages the issuance of the network certificates cannot act arbitrarily or
|
||||
discriminatory. But lawful requests from regulatory authorities of the Foundation’s jurisdiction shall be accepted.
|
||||
|
||||
These criteria may be subject to change over time to deal with changing circumstances, like regulatory requirements.
|
||||
However, the changes shall be subject to Mandatory Governance Events. In this way the Network is able to provide its
|
||||
participants with a strong and fair identity framework.
|
||||
|
||||
Arbitration, suspension, and in extreme circumstances, revocation (for example for illegal behaviour or when a
|
||||
participant no longer meets the standards set forth) shall be managed through an Emergency Governance Event, set out in
|
||||
5.4.
|
||||
|
||||
#### 5.2.2 Budget, Expenditure and Participation Fees
|
||||
The Board shall annually prepare and approve a budget for the operations of the Foundation, taking into account the
|
||||
not-for-profit status of the Foundation and the mission to promote the Corda Ecosystem.
|
||||
|
||||
The Foundation shall charge a fee for Membership, as described in section 6.1.
|
||||
|
||||
The Operator shall charge the Foundation for services that the Operator provides under the requirements of the contract
|
||||
with the Foundation, including management of Participants, Network participation and access services, Network map
|
||||
and Operator-provided notary services. The Operator may also provide fee-based services that are supplementary to those
|
||||
needed to participate on the Network.
|
||||
|
||||
#### 5.2.3 Change of Network Operator
|
||||
For three years upon establishment of the Foundation, R3 will undertake the role of Operator. Annually thereafter, the
|
||||
Board will approve the appointment of the Operator, which may be changed with a Mandatory Governance Event and vote.
|
||||
As noted, the Foundation shall hold the Trust Root, and the Operator and any services they operate shall be provisioned
|
||||
with intermediate certificates that derive from this. The Operator must enter into an escrow arrangement upon
|
||||
appointment, so that existing certificates continue to work, certificate revocation lists continue to be published, and
|
||||
there is no disruption to Participants if the Operator is changed.
|
||||
|
||||
#### 5.2.4 Change Management Process
|
||||
The Network will periodically require participating nodes to implement change. A change notification and management
|
||||
process shall be defined and communicated; and any change to the change management process shall be the subject of a
|
||||
Mandatory Governance Event.
|
||||
|
||||
#### 5.2.5 Other Mandatory Governance Events
|
||||
Restrictions on individual Participants or a group to initiate Governance Events; in the event of disruptive behaviour.
|
||||
|
||||
Audit: the Board may request an audit of the activities and services provided by the Operator, no more frequently than
|
||||
every year, unless an emergency audit is authorised through a Mandatory Governance Event.
|
||||
|
||||
Marketing, Trademark and Branding: R3 shall commit to license the Corda trademark to the Foundation. The Foundation
|
||||
shall manage its own brand and any trademarks created.
|
||||
|
||||
Certifications: Where the Foundation provides standards for certification of organisations, individuals or technologies,
|
||||
the Board shall approve the standards and processes for certification.
|
||||
|
||||
Change to the arbitration and dispute resolution process shall be the subject of a Mandatory Governance Event.
|
||||
|
||||
Policies covering areas of operation not covered by the Constitution (e.g. code of conduct for Board Directors).
|
||||
|
||||
5.3 Advisory Governance Events
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
#### 5.3.1 Technical Standards
|
||||
There is a set of technical standards, such as ‘network parameters’, which all Corda Network nodes need to comply with
|
||||
in order to guarantee technical compatibility to other nodes and services within the Network. While Corda has stability
|
||||
and backwards compatibility as key design goals, there may be circumstances under which these standards will need to
|
||||
change. Where these changes require participants in the Network to update to remain compatible, these changes will be
|
||||
subject to Governance Events.
|
||||
|
||||
Changes to technical standards, such as some network parameters, shall require formal design processes, and the Operator may
|
||||
choose to delegate technical due diligence to the Technical Advisory Committee prior to formally accepting a change to
|
||||
the technical standards.
|
||||
|
||||
The Corda open source software is the reference implementation for the core technical standards adopted for the Network.
|
||||
Corda implementations and distributions can vary in their internal details, but their core interfaces and Corda protocol
|
||||
implementation must conform to this standard to be compatible with the Network.
|
||||
|
||||
#### 5.3.2 Consensus Standards
|
||||
The Foundation shall set minimum standards for notary clusters, to allow their use across different business
|
||||
applications. The Operator shall ensure that standards are followed by notary service providers, and shall operate a
|
||||
framework of audit and assessment, review, feedback, and certification, covering the following:
|
||||
|
||||
1. Technical standards, such as meeting strict requirements for high-availability and data replication/security and
|
||||
performance.
|
||||
2. Compliance with necessary laws and regulations (for example privacy and data retention regulations) in the
|
||||
jurisdictions in which they operate.
|
||||
3. Availability for independent audits upon request by the Board.
|
||||
|
||||
Additionally, the Operator shall manage a reference distributed notary service for the Network, using a Board-approved
|
||||
crash or Byzantine fault tolerant (BFT) consensus algorithm, with nodes provided by a minimum number of identified and
|
||||
independent entities.
|
||||
|
||||
#### 5.3.3 Dispute Resolution Process
|
||||
Disputes between Participants arising from the operation of a Corda application are anticipated to be resolved by the
|
||||
business network operator, or directly if no business network is involved. If necessary, Participants may escalate to
|
||||
the Board by creating an Advisory Governance Event.
|
||||
|
||||
5.4 Emergency Governance Events
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Certain Network incidents, which could impact one or more Network participants and that would be the subject of
|
||||
Mandatory or Advisory Governance Events, shall require immediate resolution. In these cases, the Operator may make
|
||||
emergency changes, but these shall be subject to post-event evaluation and standard Governance Event processing. Areas
|
||||
of control that are the subject of Mandatory Governance Events are not expected to require emergency remediation, but
|
||||
the Operator shall be entitled to make emergency changes to preserve the stability and integrity of the Network.
|
||||
|
||||
5.5 Voting
|
||||
^^^^^^^^^^
|
||||
All Constitutional, Mandatory and Advisory Governance Events outlined in sections 5.2 and 5.3 shall be presented to the
|
||||
Board for voting. The representatives of the Board shall vote on a one vote per Director basis to approve or reject the
|
||||
Governance Event.
|
||||
|
||||
Quorum for the Board shall require two thirds of the Directors to vote. Abstention is not a vote. The Board may continue
|
||||
to meet if quorum is not met, but shall be prevented from making any decisions at the meeting. Decisions by electronic
|
||||
vote without a meeting shall require a vote by two thirds majority of all Directors.
|
||||
|
||||
Provided quorum is met, Constitutional Governance Events shall require a three quarters majority vote, and Mandatory
|
||||
Governance Events shall require a two thirds majority vote.
|
||||
|
||||
In the event of a tied vote (the odd number of representatives is intended to avoid tied votes) the vote of the Chair
|
||||
shall carry the vote. If the Chair does not vote in the case of a tied vote, the Event will not be passed.
|
||||
|
||||
All Governance Events proposed for consideration by the Board at a meeting shall be circulated in draft form to the
|
||||
members of the Board at least one week prior to the date of the meeting, and the text of such draft events may be altered
|
||||
at the meeting.
|
||||
|
||||
The Foundation may chose to implement the tracking and voting for Governance Events using an on-ledger Corda application
|
||||
in an attempt to simplify governance, provide transparency and lower costs, provided the application has been tested
|
||||
thoroughly and has sufficient manual override controls.
|
||||
|
||||
6 Participation
|
||||
-----------------
|
||||
|
||||
6.1 General Membership
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
Participation is open to any potential participant on the Network, subject to meeting normal Network access conditions
|
||||
described in section 5.2.1, and paying a nominal annual participation fee to cover both the operational costs of Network
|
||||
services and the Foundation, and to ensure that its activities are sufficiently resourced.
|
||||
|
||||
The Participant Community have the right to:
|
||||
|
||||
1. Propose a formal Governance Event to the Board for voting. This must meet the appropriate standards and formats.
|
||||
2. Request observer representation at a board meeting subject to logistical constraints.
|
||||
3. Utilise any brand and marketing materials that may be provided by the Foundation to Participants.
|
||||
4. Identify themselves as participants of the Foundation.
|
||||
5. Vote in the periodic election of a new Board.
|
||||
6. Participate in conferences, projects and initiatives of the Foundation. Numbers of participants and any additional
|
||||
costs will depend on the individual event.
|
||||
7. Receive an identity necessary to operate a Corda node on the Network.
|
||||
8. Use the Network for live business activities running 'in production'.
|
||||
|
||||
6.2 Technical Advisory Committee
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
The Technical Advisory Committee shall have limited participants appointed directly by the Board. Its mandate and
|
||||
charter will be set by the Board. It shall act directly on the instructions of the Board or the Operator, which shall
|
||||
set expected deliverables and timelines. It shall focus on specific technical topics and may have responsibility for
|
||||
the following:
|
||||
|
||||
1. Advise on technical decisions for the Operator.
|
||||
2. Advising the Board in technical matters.
|
||||
3. Provide feedback on the technical roadmap for Corda, from real-world and practical experience gained from observing
|
||||
the operation of the Network.
|
||||
4. Conducting open design reviews and soliciting public input for technical proposals.
|
||||
5. Contributing to the Corda open source community from a Network perspective, to ensure that Corda retains a coherent,
|
||||
elegant and practical system design
|
||||
|
||||
6.3 Governance Advisory Committee
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
The Governance Advisory Committee shall have limited participants appointed directly by the board. Its purpose is to
|
||||
recommend actions to the Board for approval on non-technical matters, where additional support is helpful. This may
|
||||
include decisions on:
|
||||
|
||||
1. Operator Due Diligence
|
||||
2. Identity and Permissions
|
||||
3. Risks and Escalations
|
||||
4. Interacting with Regulators
|
||||
5. Complaints and Whistle-blowing
|
||||
|
||||
7 The Corda Network Operator
|
||||
----------------------------
|
||||
|
||||
In order to pursue the mission of the Foundation as set out in section 1, there will need to be a set of operational
|
||||
activities, including technical activities such as hosting services, marketing activities, community management and
|
||||
promotion. These activities shall be funded through the participation fees and overseen by the Board, and they will
|
||||
require operational staffing by the Operator. It is not envisaged (at least during the first year) that the Corda
|
||||
Network Foundation will need separate staff. Administrative operations and meeting facilities will be provided by the
|
||||
Operator.
|
||||
|
||||
The Operator shall invoice the Foundation for the costs of operating the Network and minor administrative expenses,
|
||||
initially on a cost-plus basis, and subject to annual review. Corda Network identity and map technical services
|
||||
have been designed to be highly cacheable, and low-cost in operation.
|
||||
|
||||
For the first three years, R3 shall act as the Operator.
|
||||
|
||||
8 Costs and Participation Fees
|
||||
------------------------------
|
||||
|
||||
8.1 Costs
|
||||
^^^^^^^^^
|
||||
In line with the mission and values of the Foundation, the Network Foundation is not a profit seeking entity. But the
|
||||
Foundation needs to provide governance and technical services, and these will incur costs. The Foundation maintains these
|
||||
cost principles, as ideals but not contractual standards:
|
||||
|
||||
1. Costs for all services should be borne by all users of those services.
|
||||
2. One group of participants should not subsidise another.
|
||||
3. The costs shall be tightly managed, and the Foundation shall seek to provide the most cost-effective implementation
|
||||
of all of its own administration, governance and technical services.
|
||||
4. Costs of one service should not be subsidised by another.
|
||||
5. The Foundation's cost model should be public, to demonstrate that the costs could not reasonably be lower.
|
||||
|
||||
8.2 Participation Fee
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
The Foundation shall meet costs by levying a participation fee and notary fee for all Participants. The participation
|
||||
fee will be independent of organisation size and number of transactions on Corda, to reflect the underlying cost of
|
||||
identity issuance.
|
||||
|
||||
The fee shall be based on the number of Participants divided by an estimate of the cost of running the Foundation,
|
||||
which is set out in section 7. There may be variance in the fee depending on whether the Participant is indirectly using a
|
||||
Corda Network-powered application, and therefore the services which the Participant is able to access.
|
||||
|
||||
Such fees shall be detailed in a separate schedule to be updated annually and approved by the Board by a Mandatory
|
||||
Governance Event.
|
||||
|
||||
The Operator may agree to provide the Foundation with a start-up commercial loan, in order to allow the Foundation to
|
||||
cap fees for Participants initially. This will allow early widespread adoption, when early participant numbers will not
|
||||
offset fixed operating costs. In this case, the fees will not fall to steady-state levels until the loan has been repaid.
|
||||
|
||||
Subsidiaries of large organisations shall apply for membership separately, since the model for Corda usage is for one
|
||||
identity per legal entity, unless varied by Mandatory Governance Event. The fee and voting right shall apply to each
|
||||
subsidiary individually.
|
||||
|
||||
The fee applies even if the Participants chooses not to operate a Corda node on the Network. Therefore, Participants
|
||||
can be potential or active participants.
|
||||
|
||||
8.3 Notary Fee
|
||||
^^^^^^^^^^^^^^
|
||||
Transaction notary fees will be charged separately, on a per-use basis. This reflects the variable cost of providing
|
||||
notary services, with a wide orders-of-magnitude disparity between frequent and infrequent participant transaction
|
||||
volumes. As a principle, notary fees shall not subsidise participation fees, nor vice versa.
|
||||
|
||||
9 Community
|
||||
-----------
|
||||
Corda is a collaborative effort, and part of the Foundation’s mission is to help create and foster a technical community
|
||||
that will benefit all Corda solution providers and users. As such, the Foundation will work to encourage further
|
||||
participation of leading Participants of the ecosystem, including developers, service and solution providers and end
|
||||
users. This community shall work towards furthering the adoption of Corda, and contribute to the specific capabilities
|
||||
identified in the technical white paper.
|
||||
|
||||
The Corda technical community should be broad and open, encouraging participation and active conversations on the
|
||||
technology and applications, but this cannot be mandated by the Foundation.
|
||||
|
||||
9.1 Non-Discrimination
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
The Foundation will welcome any organization able to meet the Participation criteria, regardless of competitive
|
||||
interests with other Participants. The Board shall not seek to exclude any Participant for any reasons other than those
|
||||
that are reasonable, explicit and applied on a non-discriminatory basis to all Participants.
|
||||
|
||||
END
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user