mirror of
https://github.com/corda/corda.git
synced 2025-05-18 16:33:26 +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 int hashCode()
|
||||||
public String toString()
|
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
|
public final class net.corda.testing.node.StartedMockNode extends java.lang.Object
|
||||||
@NotNull
|
@NotNull
|
||||||
public final java.util.List<kotlin.Pair<F, net.corda.core.concurrent.CordaFuture<?>>> findStateMachines(Class<F>)
|
public final java.util.List<kotlin.Pair<F, net.corda.core.concurrent.CordaFuture<?>>> findStateMachines(Class<F>)
|
||||||
|
@ -44,10 +44,12 @@ $newAbstracts
|
|||||||
EOF
|
EOF
|
||||||
`
|
`
|
||||||
|
|
||||||
#Get a list of any methods that expose classes in .internal. namespaces, and any classes which extend/implement
|
# Get a list of any methods that expose internal classes, which includes:
|
||||||
#an internal class
|
# - 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
|
#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\.")
|
newNodeExposures=$(echo "$userDiffContents" | grep "^+" | grep "net\.corda\.node\.")
|
||||||
|
|
||||||
internalCount=`grep -v "^$" <<EOF | wc -l
|
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_integrationTest" target="1.8" />
|
||||||
<module name="corda-core_smokeTest" target="1.8" />
|
<module name="corda-core_smokeTest" target="1.8" />
|
||||||
<module name="corda-finance_integrationTest" 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_main" target="1.8" />
|
||||||
<module name="corda-project_buildSrc_test" target="1.8" />
|
<module name="corda-project_buildSrc_test" target="1.8" />
|
||||||
<module name="corda-project_canonicalizer_main" 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-blobinspector_test" target="1.8" />
|
||||||
<module name="net.corda-finance-contracts-states_main" 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-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_main" target="1.8" />
|
||||||
<module name="net.corda-verifier_test" target="1.8" />
|
<module name="net.corda-verifier_test" target="1.8" />
|
||||||
<module name="net.corda_buildSrc_main" 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)
|
* Ajitha Thayaharan (BCS Technology International)
|
||||||
* Alberto Arri (R3)
|
* Alberto Arri (R3)
|
||||||
* amiracam
|
* amiracam
|
||||||
|
* Amol Pednekar
|
||||||
* Andras Slemmer (R3)
|
* Andras Slemmer (R3)
|
||||||
* Andrius Dagys (R3)
|
* Andrius Dagys (R3)
|
||||||
* Andrzej Cichocki (R3)
|
* Andrzej Cichocki (R3)
|
||||||
@ -79,6 +80,7 @@ see changes to this list.
|
|||||||
* Emanuel Russo (NTT DATA Italy)
|
* Emanuel Russo (NTT DATA Italy)
|
||||||
* Farzad Pezeshkpour (RBS)
|
* Farzad Pezeshkpour (RBS)
|
||||||
* fracting
|
* fracting
|
||||||
|
* Francisco Spadafora (NTT DATA Italy)
|
||||||
* Frederic Dalibard (Natixis)
|
* Frederic Dalibard (Natixis)
|
||||||
* Garrett Macey (Wells Fargo)
|
* Garrett Macey (Wells Fargo)
|
||||||
* gary-rowe
|
* gary-rowe
|
||||||
@ -212,6 +214,7 @@ see changes to this list.
|
|||||||
* verymahler
|
* verymahler
|
||||||
* Viktor Kolomeyko (R3)
|
* Viktor Kolomeyko (R3)
|
||||||
* Vipin Bharathan
|
* Vipin Bharathan
|
||||||
|
* Vitalij Reicherdt (Commerzbank AG)
|
||||||
* Wawrzek Niewodniczanski (R3)
|
* Wawrzek Niewodniczanski (R3)
|
||||||
* Wei Wu Zhang (Commonwealth Bank of Australia)
|
* Wei Wu Zhang (Commonwealth Bank of Australia)
|
||||||
* Zabrina Smith (Northern Trust)
|
* 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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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
|
## 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
|
## License
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ buildscript {
|
|||||||
file("$projectDir/constants.properties").withInputStream { constants.load(it) }
|
file("$projectDir/constants.properties").withInputStream { constants.load(it) }
|
||||||
|
|
||||||
// Our version: bump this on release.
|
// 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.corda_platform_version = constants.getProperty("platformVersion")
|
||||||
ext.gradle_plugins_version = constants.getProperty("gradlePluginsVersion")
|
ext.gradle_plugins_version = constants.getProperty("gradlePluginsVersion")
|
||||||
|
|
||||||
@ -383,8 +383,6 @@ bintrayConfig {
|
|||||||
'corda-tools-explorer',
|
'corda-tools-explorer',
|
||||||
'corda-tools-network-bootstrapper',
|
'corda-tools-network-bootstrapper',
|
||||||
'corda-tools-cliutils',
|
'corda-tools-cliutils',
|
||||||
'corda-notary-raft',
|
|
||||||
'corda-notary-bft-smart',
|
|
||||||
'corda-common-configuration-parsing',
|
'corda-common-configuration-parsing',
|
||||||
'corda-common-validation'
|
'corda-common-validation'
|
||||||
]
|
]
|
||||||
@ -474,4 +472,4 @@ wrapper {
|
|||||||
buildScan {
|
buildScan {
|
||||||
termsOfServiceUrl = 'https://gradle.com/terms-of-service'
|
termsOfServiceUrl = 'https://gradle.com/terms-of-service'
|
||||||
termsOfServiceAgree = 'yes'
|
termsOfServiceAgree = 'yes'
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
|
|||||||
import com.fasterxml.jackson.module.kotlin.convertValue
|
import com.fasterxml.jackson.module.kotlin.convertValue
|
||||||
import com.nhaarman.mockito_kotlin.any
|
import com.nhaarman.mockito_kotlin.any
|
||||||
import com.nhaarman.mockito_kotlin.doReturn
|
import com.nhaarman.mockito_kotlin.doReturn
|
||||||
|
import com.nhaarman.mockito_kotlin.mock
|
||||||
import com.nhaarman.mockito_kotlin.whenever
|
import com.nhaarman.mockito_kotlin.whenever
|
||||||
import net.corda.client.jackson.internal.childrenAs
|
import net.corda.client.jackson.internal.childrenAs
|
||||||
import net.corda.client.jackson.internal.valueAs
|
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.NodeInfo
|
||||||
import net.corda.core.node.ServiceHub
|
import net.corda.core.node.ServiceHub
|
||||||
import net.corda.core.node.services.AttachmentStorage
|
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.CordaSerializable
|
||||||
import net.corda.core.serialization.SerializedBytes
|
import net.corda.core.serialization.SerializedBytes
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
@ -94,10 +96,10 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
|
|||||||
services = rigorousMock()
|
services = rigorousMock()
|
||||||
cordappProvider = rigorousMock()
|
cordappProvider = rigorousMock()
|
||||||
val networkParameters = testNetworkParameters(minimumPlatformVersion = 4)
|
val networkParameters = testNetworkParameters(minimumPlatformVersion = 4)
|
||||||
val networkParametersStorage = rigorousMock<NetworkParametersStorage>().also {
|
val networkParametersService = rigorousMock<NetworkParametersService>().also {
|
||||||
doReturn(networkParameters.serialize().hash).whenever(it).currentHash
|
doReturn(networkParameters.serialize().hash).whenever(it).currentHash
|
||||||
}
|
}
|
||||||
doReturn(networkParametersStorage).whenever(services).networkParametersStorage
|
doReturn(networkParametersService).whenever(services).networkParametersService
|
||||||
doReturn(cordappProvider).whenever(services).cordappProvider
|
doReturn(cordappProvider).whenever(services).cordappProvider
|
||||||
doReturn(networkParameters).whenever(services).networkParameters
|
doReturn(networkParameters).whenever(services).networkParameters
|
||||||
doReturn(attachments).whenever(services).attachments
|
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)
|
doReturn(attachmentId).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID)
|
||||||
val attachmentStorage = rigorousMock<AttachmentStorage>()
|
val attachmentStorage = rigorousMock<AttachmentStorage>()
|
||||||
doReturn(attachmentStorage).whenever(services).attachments
|
doReturn(attachmentStorage).whenever(services).attachments
|
||||||
|
doReturn(mock<TransactionStorage>()).whenever(services).validatedTransactions
|
||||||
val attachment = rigorousMock<ContractAttachment>()
|
val attachment = rigorousMock<ContractAttachment>()
|
||||||
doReturn(attachment).whenever(attachmentStorage).openAttachment(attachmentId)
|
doReturn(attachment).whenever(attachmentStorage).openAttachment(attachmentId)
|
||||||
doReturn(attachmentId).whenever(attachment).id
|
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.driver.driver
|
||||||
import net.corda.testing.internal.chooseIdentity
|
import net.corda.testing.internal.chooseIdentity
|
||||||
import net.corda.testing.node.User
|
import net.corda.testing.node.User
|
||||||
|
import net.corda.testing.node.internal.FINANCE_CORDAPPS
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
@ -49,7 +50,7 @@ class NodeMonitorModelTest {
|
|||||||
private lateinit var newNode: (CordaX500Name) -> NodeInfo
|
private lateinit var newNode: (CordaX500Name) -> NodeInfo
|
||||||
|
|
||||||
private fun setup(runTest: () -> Unit) {
|
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 cashUser = User("user1", "test", permissions = setOf(all()))
|
||||||
val aliceNodeHandle = startNode(providedName = ALICE_NAME, rpcUsers = listOf(cashUser)).getOrThrow()
|
val aliceNodeHandle = startNode(providedName = ALICE_NAME, rpcUsers = listOf(cashUser)).getOrThrow()
|
||||||
aliceNode = aliceNodeHandle.nodeInfo
|
aliceNode = aliceNodeHandle.nodeInfo
|
||||||
|
@ -57,10 +57,13 @@ processSmokeTestResources {
|
|||||||
rename 'corda-(.*)', 'corda.jar'
|
rename 'corda-(.*)', 'corda.jar'
|
||||||
}
|
}
|
||||||
from(project(':finance:workflows').tasks['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']) {
|
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(':smoke-test-utils')
|
||||||
smokeTestCompile project(':finance:contracts')
|
smokeTestCompile project(':finance:contracts')
|
||||||
smokeTestCompile project(':finance:workflows')
|
smokeTestCompile project(':finance:workflows')
|
||||||
|
smokeTestCompile project(':confidential-identities')
|
||||||
smokeTestCompile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
|
smokeTestCompile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
|
||||||
smokeTestCompile "org.apache.logging.log4j:log4j-core:$log4j_version"
|
smokeTestCompile "org.apache.logging.log4j:log4j-core:$log4j_version"
|
||||||
smokeTestCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_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 com.github.benmanes.caffeine.cache.Caffeine
|
||||||
import net.corda.client.rpc.internal.RPCClient
|
import net.corda.client.rpc.internal.RPCClient
|
||||||
import net.corda.client.rpc.internal.serialization.amqp.AMQPClientSerializationScheme
|
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.Actor
|
||||||
import net.corda.core.context.Trace
|
import net.corda.core.context.Trace
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.internal.PLATFORM_VERSION
|
import net.corda.core.internal.PLATFORM_VERSION
|
||||||
import net.corda.core.messaging.ClientRpcSslOptions
|
import net.corda.core.messaging.ClientRpcSslOptions
|
||||||
import net.corda.core.messaging.CordaRPCOps
|
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.serialization.internal.effectiveSerializationEnv
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
import net.corda.core.utilities.days
|
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.SerializationFactoryCacheKey
|
||||||
import net.corda.serialization.internal.amqp.SerializerFactory
|
import net.corda.serialization.internal.amqp.SerializerFactory
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
|
import java.util.ServiceLoader
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is essentially just a wrapper for an RPCConnection<CordaRPCOps> and can be treated identically.
|
* 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 configuration An optional configuration used to tweak client behaviour.
|
||||||
* @param sslConfiguration An optional [ClientRpcSslOptions] used to enable secure communication with the server.
|
* @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 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
|
* 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.
|
* 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
|
@JvmOverloads
|
||||||
constructor(hostAndPort: NetworkHostAndPort,
|
constructor(hostAndPort: NetworkHostAndPort,
|
||||||
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT)
|
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT,
|
||||||
: this(hostAndPort, configuration, null)
|
classLoader: ClassLoader? = null)
|
||||||
|
: this(hostAndPort, configuration, null, classLoader = classLoader)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param haAddressPool A list of [NetworkHostAndPort] representing the addresses of servers in HA mode.
|
* @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,
|
sslConfiguration: ClientRpcSslOptions? = null,
|
||||||
classLoader: ClassLoader? = null
|
classLoader: ClassLoader? = null
|
||||||
): CordaRPCClient {
|
): CordaRPCClient {
|
||||||
return CordaRPCClient(hostAndPort, configuration, sslConfiguration, classLoader)
|
return CordaRPCClient(hostAndPort, configuration, sslConfiguration, classLoader = classLoader)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -296,7 +302,14 @@ class CordaRPCClient private constructor(
|
|||||||
effectiveSerializationEnv
|
effectiveSerializationEnv
|
||||||
} catch (e: IllegalStateException) {
|
} catch (e: IllegalStateException) {
|
||||||
try {
|
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) {
|
} catch (e: IllegalStateException) {
|
||||||
// Race e.g. two of these constructed in parallel, ignore.
|
// 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
|
* Thrown to indicate that the calling user does not have permission for something they have requested (for example
|
||||||
* calling a method).
|
* 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
|
||||||
import net.corda.core.serialization.SerializationContext.UseCase
|
import net.corda.core.serialization.SerializationContext.UseCase
|
||||||
import net.corda.core.serialization.SerializationCustomSerializer
|
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.SerializationEnvironment
|
||||||
import net.corda.core.serialization.internal.nodeSerializationEnv
|
import net.corda.core.serialization.internal.nodeSerializationEnv
|
||||||
import net.corda.serialization.internal.*
|
import net.corda.serialization.internal.*
|
||||||
@ -17,24 +18,25 @@ import net.corda.serialization.internal.amqp.custom.RxNotificationSerializer
|
|||||||
*/
|
*/
|
||||||
class AMQPClientSerializationScheme(
|
class AMQPClientSerializationScheme(
|
||||||
cordappCustomSerializers: Set<SerializationCustomSerializer<*,*>>,
|
cordappCustomSerializers: Set<SerializationCustomSerializer<*,*>>,
|
||||||
|
cordappSerializationWhitelists: Set<SerializationWhitelist>,
|
||||||
serializerFactoriesForContexts: MutableMap<SerializationFactoryCacheKey, SerializerFactory>
|
serializerFactoriesForContexts: MutableMap<SerializationFactoryCacheKey, SerializerFactory>
|
||||||
) : AbstractAMQPSerializationScheme(cordappCustomSerializers, serializerFactoriesForContexts) {
|
) : AbstractAMQPSerializationScheme(cordappCustomSerializers, cordappSerializationWhitelists, serializerFactoriesForContexts) {
|
||||||
constructor(cordapps: List<Cordapp>) : this(cordapps.customSerializers, AccessOrderLinkedHashMap<SerializationFactoryCacheKey, SerializerFactory>(128).toSynchronised())
|
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, serializerFactoriesForContexts)
|
constructor(cordapps: List<Cordapp>, serializerFactoriesForContexts: MutableMap<SerializationFactoryCacheKey, SerializerFactory>) : this(cordapps.customSerializers, cordapps.serializationWhitelists, serializerFactoriesForContexts)
|
||||||
|
|
||||||
@Suppress("UNUSED")
|
@Suppress("UNUSED")
|
||||||
constructor() : this(emptySet(), AccessOrderLinkedHashMap<SerializationFactoryCacheKey, SerializerFactory>(128).toSynchronised())
|
constructor() : this(emptySet(), emptySet(), AccessOrderLinkedHashMap<SerializationFactoryCacheKey, SerializerFactory>(128).toSynchronised())
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** Call from main only. */
|
/** Call from main only. */
|
||||||
fun initialiseSerialization(classLoader: ClassLoader? = null, serializerFactoriesForContexts: MutableMap<SerializationFactoryCacheKey, SerializerFactory> = AccessOrderLinkedHashMap<SerializationFactoryCacheKey, SerializerFactory>(128).toSynchronised()) {
|
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, serializerFactoriesForContexts)
|
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(
|
return SerializationEnvironment.with(
|
||||||
SerializationFactoryImpl().apply {
|
SerializationFactoryImpl().apply {
|
||||||
registerScheme(AMQPClientSerializationScheme(emptyList(), serializerFactoriesForContexts))
|
registerScheme(AMQPClientSerializationScheme(customSerializers, serializationWhitelists, serializerFactoriesForContexts))
|
||||||
},
|
},
|
||||||
storageContext = AMQP_STORAGE_CONTEXT,
|
storageContext = AMQP_STORAGE_CONTEXT,
|
||||||
p2pContext = if (classLoader != null) AMQP_P2P_CONTEXT.withClassLoader(classLoader) else AMQP_P2P_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;
|
import static net.corda.finance.contracts.GetBalances.getCashBalance;
|
||||||
|
|
||||||
public class StandaloneCordaRPCJavaClientTest {
|
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 List<String> perms = Collections.singletonList("ALL");
|
||||||
private Set<String> permSet = new HashSet<>(perms);
|
private Set<String> permSet = new HashSet<>(perms);
|
||||||
private User rpcUser = new User("user1", "test", permSet);
|
private User rpcUser = new User("user1", "test", permSet);
|
||||||
@ -55,7 +76,7 @@ public class StandaloneCordaRPCJavaClientTest {
|
|||||||
@Before
|
@Before
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
factory = new NodeProcess.Factory();
|
factory = new NodeProcess.Factory();
|
||||||
copyFinanceCordapp();
|
copyCordapps(factory, notaryConfig);
|
||||||
notary = factory.create(notaryConfig);
|
notary = factory.create(notaryConfig);
|
||||||
connection = notary.connect();
|
connection = notary.connect();
|
||||||
rpcProxy = connection.getProxy();
|
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
|
@Test
|
||||||
public void testCashBalances() throws NoSuchFieldException, ExecutionException, InterruptedException {
|
public void testCashBalances() throws NoSuchFieldException, ExecutionException, InterruptedException {
|
||||||
Amount<Currency> dollars123 = new Amount<>(123, Currency.getInstance("USD"));
|
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.crypto.SecureHash
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.identity.Party
|
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.messaging.*
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.node.services.Vault
|
import net.corda.core.node.services.Vault
|
||||||
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.DOLLARS
|
||||||
import net.corda.finance.POUNDS
|
import net.corda.finance.POUNDS
|
||||||
import net.corda.finance.SWISS_FRANCS
|
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.contracts.getCashBalances
|
||||||
import net.corda.finance.flows.CashIssueFlow
|
import net.corda.finance.flows.CashIssueFlow
|
||||||
import net.corda.finance.flows.CashPaymentFlow
|
import net.corda.finance.flows.CashPaymentFlow
|
||||||
|
import net.corda.java.rpc.StandaloneCordaRPCJavaClientTest
|
||||||
import net.corda.nodeapi.internal.config.User
|
import net.corda.nodeapi.internal.config.User
|
||||||
import net.corda.smoketesting.NodeConfig
|
import net.corda.smoketesting.NodeConfig
|
||||||
import net.corda.smoketesting.NodeProcess
|
import net.corda.smoketesting.NodeProcess
|
||||||
@ -31,11 +35,9 @@ import org.junit.Ignore
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.io.FilterInputStream
|
import java.io.FilterInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.nio.file.Paths
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import kotlin.streams.toList
|
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertFalse
|
import kotlin.test.assertFalse
|
||||||
import kotlin.test.assertNotEquals
|
import kotlin.test.assertNotEquals
|
||||||
@ -69,7 +71,7 @@ class StandaloneCordaRPClientTest {
|
|||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
factory = NodeProcess.Factory()
|
factory = NodeProcess.Factory()
|
||||||
copyFinanceCordapp()
|
StandaloneCordaRPCJavaClientTest.copyCordapps(factory, notaryConfig)
|
||||||
notary = factory.create(notaryConfig)
|
notary = factory.create(notaryConfig)
|
||||||
connection = notary.connect()
|
connection = notary.connect()
|
||||||
rpcProxy = connection.proxy
|
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
|
@Test
|
||||||
fun `test attachments`() {
|
fun `test attachments`() {
|
||||||
|
@ -6,12 +6,13 @@ apply plugin: 'kotlin'
|
|||||||
apply plugin: CanonicalizerPlugin
|
apply plugin: CanonicalizerPlugin
|
||||||
apply plugin: 'net.corda.plugins.publish-utils'
|
apply plugin: 'net.corda.plugins.publish-utils'
|
||||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||||
|
apply plugin: 'net.corda.plugins.cordapp'
|
||||||
apply plugin: 'com.jfrog.artifactory'
|
apply plugin: 'com.jfrog.artifactory'
|
||||||
|
|
||||||
description 'Corda Experimental Confidential Identities'
|
description 'Corda Experimental Confidential Identities'
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile project(':core')
|
cordaCompile project(':core')
|
||||||
|
|
||||||
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||||
testCompile "junit:junit:$junit_version"
|
testCompile "junit:junit:$junit_version"
|
||||||
@ -26,6 +27,17 @@ dependencies {
|
|||||||
testCompile "org.assertj:assertj-core:$assertj_version"
|
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 {
|
jar {
|
||||||
baseName 'corda-confidential-identities'
|
baseName 'corda-confidential-identities'
|
||||||
}
|
}
|
||||||
|
@ -4,29 +4,49 @@ import co.paralleluniverse.fibers.Suspendable
|
|||||||
import net.corda.core.CordaInternal
|
import net.corda.core.CordaInternal
|
||||||
import net.corda.core.crypto.DigitalSignature
|
import net.corda.core.crypto.DigitalSignature
|
||||||
import net.corda.core.crypto.verify
|
import net.corda.core.crypto.verify
|
||||||
import net.corda.core.flows.FlowException
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.flows.FlowLogic
|
|
||||||
import net.corda.core.flows.FlowSession
|
|
||||||
import net.corda.core.identity.AnonymousParty
|
import net.corda.core.identity.AnonymousParty
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.identity.PartyAndCertificate
|
import net.corda.core.identity.PartyAndCertificate
|
||||||
import net.corda.core.internal.VisibleForTesting
|
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.node.ServiceHub
|
||||||
import net.corda.core.serialization.CordaSerializable
|
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.serialization.serialize
|
||||||
import net.corda.core.utilities.ProgressTracker
|
import net.corda.core.utilities.ProgressTracker
|
||||||
import net.corda.core.utilities.unwrap
|
import net.corda.core.utilities.unwrap
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.security.SignatureException
|
import java.security.SignatureException
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Very basic flow which generates new confidential identities for parties in a transaction and exchanges the transaction
|
* 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
|
* 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.
|
* 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,
|
@InitiatingFlow
|
||||||
override val progressTracker: ProgressTracker = tracker()) : FlowLogic<SwapIdentitiesFlow.AnonymousResult>() {
|
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 {
|
companion object {
|
||||||
object GENERATING_IDENTITY : ProgressTracker.Step("Generating our anonymous identity")
|
object GENERATING_IDENTITY : ProgressTracker.Step("Generating our anonymous identity")
|
||||||
object SIGNING_IDENTITY : ProgressTracker.Step("Signing our anonymous identity")
|
object SIGNING_IDENTITY : ProgressTracker.Step("Signing our anonymous identity")
|
||||||
@ -69,38 +89,49 @@ class SwapIdentitiesFlow @JvmOverloads constructor(private val otherSideSession:
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@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
|
progressTracker.currentStep = GENERATING_IDENTITY
|
||||||
val ourAnonymousIdentity = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, false)
|
val ourAnonymousIdentity = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, false)
|
||||||
val data = buildDataToSign(ourAnonymousIdentity)
|
val data = buildDataToSign(ourAnonymousIdentity)
|
||||||
progressTracker.currentStep = SIGNING_IDENTITY
|
progressTracker.currentStep = SIGNING_IDENTITY
|
||||||
val signature = serviceHub.keyManagementService.sign(data, ourAnonymousIdentity.owningKey).withoutKey()
|
val signature = serviceHub.keyManagementService.sign(data, ourAnonymousIdentity.owningKey).withoutKey()
|
||||||
val ourIdentWithSig = IdentityWithSignature(ourAnonymousIdentity, signature)
|
val ourIdentWithSig = IdentityWithSignature(ourAnonymousIdentity.serialize(), signature)
|
||||||
progressTracker.currentStep = AWAITING_IDENTITY
|
progressTracker.currentStep = AWAITING_IDENTITY
|
||||||
val theirAnonymousIdentity = otherSideSession.sendAndReceive<IdentityWithSignature>(ourIdentWithSig).unwrap { theirIdentWithSig ->
|
val theirAnonymousIdentity = session.sendAndReceive<IdentityWithSignature>(ourIdentWithSig).unwrap { theirIdentWithSig ->
|
||||||
progressTracker.currentStep = VERIFYING_IDENTITY
|
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
|
@CordaSerializable
|
||||||
data class AnonymousResult(val ourIdentity: AnonymousParty, val theirIdentity: AnonymousParty)
|
data class IdentityWithSignature(val identity: SerializedBytes<PartyAndCertificate>, val signature: DigitalSignature)
|
||||||
|
|
||||||
@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 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)
|
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
|
@InitiatingFlow
|
||||||
private class SwapIdentitiesInitiator(private val otherSide: Party) : FlowLogic<Map<Party, AnonymousParty>>() {
|
private class SwapIdentitiesInitiator(private val otherSide: Party) : FlowLogic<Map<Party, AnonymousParty>>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call(): Map<Party, AnonymousParty> {
|
override fun call(): Map<Party, AnonymousParty> = subFlow(SwapIdentitiesFlow(initiateFlow(otherSide)))
|
||||||
val (anonymousUs, anonymousThem) = subFlow(SwapIdentitiesFlow(initiateFlow(otherSide)))
|
|
||||||
return mapOf(ourIdentity to anonymousUs, otherSide to anonymousThem)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@InitiatedBy(SwapIdentitiesInitiator::class)
|
@InitiatedBy(SwapIdentitiesInitiator::class)
|
||||||
|
@ -10,27 +10,59 @@
|
|||||||
</Properties>
|
</Properties>
|
||||||
|
|
||||||
<Appenders>
|
<Appenders>
|
||||||
<Console name="Console-Appender" target="SYSTEM_OUT">
|
<ScriptAppenderSelector name="Console-Selector">
|
||||||
<PatternLayout>
|
<Script language="nashorn"><![CDATA[
|
||||||
<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}">
|
var System = Java.type('java.lang.System');
|
||||||
<Script name="MDCSelector" language="javascript"><![CDATA[
|
var level = System.getProperty("consoleLogLevel");
|
||||||
result = null;
|
var enabled = System.getProperty("consoleLoggingEnabled");
|
||||||
if (!logEvent.getContextData().size() == 0) {
|
enabled == "true" && (level == "debug" || level == "trace") ? "Console-Debug-Appender" : "Console-Appender";
|
||||||
result = "WithMDC";
|
]]></Script>
|
||||||
} else {
|
<AppenderSet>
|
||||||
result = null;
|
|
||||||
}
|
<!-- The default console appender - prints no exception information -->
|
||||||
result;
|
<Console name="Console-Appender" target="SYSTEM_OUT">
|
||||||
]]>
|
<PatternLayout>
|
||||||
</Script>
|
<ScriptPatternSelector
|
||||||
<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}"/>
|
defaultPattern="%highlight{[%level{length=5}] %date{HH:mm:ssZ} [%t] %c{2}.%method - %msg%n%throwable{0}}{INFO=white,WARN=red,FATAL=bright red}">
|
||||||
</ScriptPatternSelector>
|
<Script name="MDCSelector" language="javascript"><![CDATA[
|
||||||
</PatternLayout>
|
result = null;
|
||||||
</Console>
|
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 -->
|
<!-- Required for printBasicInfo -->
|
||||||
<Console name="Console-Appender-Println" target="SYSTEM_OUT">
|
<Console name="Console-Appender-Println" target="SYSTEM_OUT">
|
||||||
<PatternLayout pattern="%msg%n%throwable{short.message}" />
|
<PatternLayout pattern="%msg%n%throwable{0}" />
|
||||||
</Console>
|
</Console>
|
||||||
|
|
||||||
<!-- Will generate up to 100 log files for a given day. During every rollover it will delete
|
<!-- Will generate up to 100 log files for a given day. During every rollover it will delete
|
||||||
@ -73,8 +105,8 @@
|
|||||||
|
|
||||||
</RollingRandomAccessFile>
|
</RollingRandomAccessFile>
|
||||||
|
|
||||||
<Rewrite name="Console-ErrorCode-Appender">
|
<Rewrite name="Console-ErrorCode-Selector">
|
||||||
<AppenderRef ref="Console-Appender"/>
|
<AppenderRef ref="Console-Selector"/>
|
||||||
<ErrorCodeRewritePolicy/>
|
<ErrorCodeRewritePolicy/>
|
||||||
</Rewrite>
|
</Rewrite>
|
||||||
|
|
||||||
@ -91,7 +123,7 @@
|
|||||||
|
|
||||||
<Loggers>
|
<Loggers>
|
||||||
<Root level="${defaultLogLevel}">
|
<Root level="${defaultLogLevel}">
|
||||||
<AppenderRef ref="Console-ErrorCode-Appender" level="${consoleLogLevel}"/>
|
<AppenderRef ref="Console-ErrorCode-Selector" level="${consoleLogLevel}"/>
|
||||||
<AppenderRef ref="RollingFile-ErrorCode-Appender"/>
|
<AppenderRef ref="RollingFile-ErrorCode-Appender"/>
|
||||||
</Root>
|
</Root>
|
||||||
<Logger name="BasicInfo" additivity="false">
|
<Logger name="BasicInfo" additivity="false">
|
||||||
@ -99,11 +131,11 @@
|
|||||||
<AppenderRef ref="RollingFile-ErrorCode-Appender"/>
|
<AppenderRef ref="RollingFile-ErrorCode-Appender"/>
|
||||||
</Logger>
|
</Logger>
|
||||||
<Logger name="org.hibernate.SQL" level="info" additivity="false">
|
<Logger name="org.hibernate.SQL" level="info" additivity="false">
|
||||||
<AppenderRef ref="Console-ErrorCode-Appender"/>
|
<AppenderRef ref="Console-ErrorCode-Selector"/>
|
||||||
<AppenderRef ref="RollingFile-ErrorCode-Appender"/>
|
<AppenderRef ref="RollingFile-ErrorCode-Appender"/>
|
||||||
</Logger>
|
</Logger>
|
||||||
<Logger name="org.apache.activemq.artemis.core.server" level="error" additivity="false">
|
<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"/>
|
<AppenderRef ref="RollingFile-ErrorCode-Appender"/>
|
||||||
</Logger>
|
</Logger>
|
||||||
<Logger name="org.jolokia" additivity="true" level="warn">
|
<Logger name="org.jolokia" additivity="true" level="warn">
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
gradlePluginsVersion=4.0.37
|
gradlePluginsVersion=4.0.38
|
||||||
kotlinVersion=1.2.71
|
kotlinVersion=1.2.71
|
||||||
# ***************************************************************#
|
# ***************************************************************#
|
||||||
# When incrementing platformVersion make sure to update #
|
# When incrementing platformVersion make sure to update #
|
||||||
# net.corda.core.internal.CordaUtilsKt.PLATFORM_VERSION as well. #
|
# net.corda.core.internal.CordaUtilsKt.PLATFORM_VERSION as well. #
|
||||||
# ***************************************************************#
|
# ***************************************************************#
|
||||||
platformVersion=4
|
platformVersion=5
|
||||||
guavaVersion=25.1-jre
|
guavaVersion=25.1-jre
|
||||||
proguardVersion=6.0.3
|
proguardVersion=6.0.3
|
||||||
bouncycastleVersion=1.60
|
bouncycastleVersion=1.60
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.corda.deterministic.data
|
package net.corda.deterministic.data
|
||||||
|
|
||||||
import com.nhaarman.mockito_kotlin.doReturn
|
import com.nhaarman.mockito_kotlin.doReturn
|
||||||
|
import com.nhaarman.mockito_kotlin.mock
|
||||||
import com.nhaarman.mockito_kotlin.whenever
|
import com.nhaarman.mockito_kotlin.whenever
|
||||||
import net.corda.core.crypto.entropyToKeyPair
|
import net.corda.core.crypto.entropyToKeyPair
|
||||||
import net.corda.core.identity.AnonymousParty
|
import net.corda.core.identity.AnonymousParty
|
||||||
@ -39,7 +40,7 @@ object TransactionGenerator {
|
|||||||
private val MEGA_CORP_PUBKEY: PublicKey = megaCorp.keyPair.public
|
private val MEGA_CORP_PUBKEY: PublicKey = megaCorp.keyPair.public
|
||||||
private val MINI_CORP_PUBKEY: PublicKey = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")).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(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
|
||||||
doReturn(DUMMY_CASH_ISSUER.party).whenever(it).partyFromKey(DUMMY_CASH_ISSUER_KEY.public)
|
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
|
||||||
import net.corda.core.serialization.SerializationContext.UseCase.P2P
|
import net.corda.core.serialization.SerializationContext.UseCase.P2P
|
||||||
import net.corda.core.serialization.SerializationCustomSerializer
|
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.SerializationEnvironment
|
||||||
import net.corda.core.serialization.internal._contextSerializationEnv
|
import net.corda.core.serialization.internal._contextSerializationEnv
|
||||||
import net.corda.serialization.internal.*
|
import net.corda.serialization.internal.*
|
||||||
@ -57,15 +58,16 @@ class LocalSerializationRule(private val label: String) : TestRule {
|
|||||||
|
|
||||||
private fun createTestSerializationEnv(): SerializationEnvironment {
|
private fun createTestSerializationEnv(): SerializationEnvironment {
|
||||||
val factory = SerializationFactoryImpl(mutableMapOf()).apply {
|
val factory = SerializationFactoryImpl(mutableMapOf()).apply {
|
||||||
registerScheme(AMQPSerializationScheme(emptySet(), AccessOrderLinkedHashMap(128)))
|
registerScheme(AMQPSerializationScheme(emptySet(), emptySet(), AccessOrderLinkedHashMap(128)))
|
||||||
}
|
}
|
||||||
return SerializationEnvironment.with(factory, AMQP_P2P_CONTEXT)
|
return SerializationEnvironment.with(factory, AMQP_P2P_CONTEXT)
|
||||||
}
|
}
|
||||||
|
|
||||||
private class AMQPSerializationScheme(
|
private class AMQPSerializationScheme(
|
||||||
cordappCustomSerializers: Set<SerializationCustomSerializer<*, *>>,
|
cordappCustomSerializers: Set<SerializationCustomSerializer<*, *>>,
|
||||||
|
cordappSerializationWhitelists: Set<SerializationWhitelist>,
|
||||||
serializerFactoriesForContexts: AccessOrderLinkedHashMap<SerializationFactoryCacheKey, SerializerFactory>
|
serializerFactoriesForContexts: AccessOrderLinkedHashMap<SerializationFactoryCacheKey, SerializerFactory>
|
||||||
) : AbstractAMQPSerializationScheme(cordappCustomSerializers, serializerFactoriesForContexts) {
|
) : AbstractAMQPSerializationScheme(cordappCustomSerializers, cordappSerializationWhitelists, serializerFactoriesForContexts) {
|
||||||
override fun rpcServerSerializerFactory(context: SerializationContext): SerializerFactory {
|
override fun rpcServerSerializerFactory(context: SerializationContext): SerializerFactory {
|
||||||
throw UnsupportedOperationException()
|
throw UnsupportedOperationException()
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,8 @@ package net.corda.deterministic.verifier
|
|||||||
|
|
||||||
import net.corda.core.contracts.Attachment
|
import net.corda.core.contracts.Attachment
|
||||||
import net.corda.core.contracts.ContractAttachment
|
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.DEPLOYED_CORDAPP_UPLOADER
|
||||||
|
import net.corda.core.internal.toLtxDjvmInternal
|
||||||
import net.corda.core.node.NetworkParameters
|
import net.corda.core.node.NetworkParameters
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.serialization.SerializedBytes
|
import net.corda.core.serialization.SerializedBytes
|
||||||
@ -25,13 +25,10 @@ class TransactionVerificationRequest(val wtxToVerify: SerializedBytes<WireTransa
|
|||||||
val attachmentMap = attachments
|
val attachmentMap = attachments
|
||||||
.mapNotNull { it as? MockContractAttachment }
|
.mapNotNull { it as? MockContractAttachment }
|
||||||
.associateBy(Attachment::id) { ContractAttachment(it, it.contract, uploader = DEPLOYED_CORDAPP_UPLOADER) }
|
.associateBy(Attachment::id) { ContractAttachment(it, it.contract, uploader = DEPLOYED_CORDAPP_UPLOADER) }
|
||||||
val contractAttachmentMap = emptyMap<ContractClassName, ContractAttachment>()
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
return wtxToVerify.deserialize().toLedgerTransaction(
|
return wtxToVerify.deserialize().toLtxDjvmInternal(
|
||||||
resolveIdentity = { null },
|
|
||||||
resolveAttachment = { attachmentMap[it] },
|
resolveAttachment = { attachmentMap[it] },
|
||||||
resolveStateRef = { deps[it.txhash]?.outputs?.get(it.index) },
|
resolveStateRef = { deps[it.txhash]?.outputs?.get(it.index) },
|
||||||
resolveContractAttachment = { contractAttachmentMap[it.contract]?.id },
|
|
||||||
resolveParameters = { networkParameters.deserialize() }
|
resolveParameters = { networkParameters.deserialize() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -111,6 +111,8 @@ dependencies {
|
|||||||
|
|
||||||
// required to use @Type annotation
|
// required to use @Type annotation
|
||||||
compile "org.hibernate:hibernate-core:$hibernate_version"
|
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)
|
// TODO Consider moving it to quasar-utils in the future (introduced with PR-1388)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.corda.core.contracts
|
package net.corda.core.contracts
|
||||||
|
|
||||||
import kotlin.reflect.KClass
|
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
|
* 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
|
* 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.
|
* 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.
|
* @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> {
|
override fun resolve(ltx: LedgerTransaction): StateAndRef<T> {
|
||||||
return ltx.referenceInputRefsOfType(type).single { pointer == it.ref }
|
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> {
|
override fun resolve(ltx: LedgerTransaction): StateAndRef<T> {
|
||||||
return ltx.referenceInputRefsOfType(type).single { pointer == it.state.data.linearId }
|
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>(
|
data class CommandWithParties<out T : CommandData>(
|
||||||
val signers: List<PublicKey>,
|
val signers: List<PublicKey>,
|
||||||
/** If any public keys were recognised, the looked up institutions are available here */
|
/** 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 signingParties: List<Party>,
|
||||||
val value: T
|
val value: T
|
||||||
)
|
)
|
||||||
|
@ -7,11 +7,13 @@ import net.corda.core.identity.Party
|
|||||||
import net.corda.core.identity.groupAbstractPartyByWellKnownParty
|
import net.corda.core.identity.groupAbstractPartyByWellKnownParty
|
||||||
import net.corda.core.internal.cordapp.CordappResolver
|
import net.corda.core.internal.cordapp.CordappResolver
|
||||||
import net.corda.core.internal.pushToLoggingContext
|
import net.corda.core.internal.pushToLoggingContext
|
||||||
|
import net.corda.core.internal.warnOnce
|
||||||
import net.corda.core.node.StatesToRecord
|
import net.corda.core.node.StatesToRecord
|
||||||
import net.corda.core.node.StatesToRecord.ONLY_RELEVANT
|
import net.corda.core.node.StatesToRecord.ONLY_RELEVANT
|
||||||
import net.corda.core.transactions.LedgerTransaction
|
import net.corda.core.transactions.LedgerTransaction
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.utilities.ProgressTracker
|
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
|
* 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.
|
* 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].
|
* 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.
|
// 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
|
// 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.
|
// presence of @InitiatingFlow is checked). So the new API is inlined simply because that code path doesn't call initiateFlow.
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
class FinalityFlow private constructor(val transaction: SignedTransaction,
|
class FinalityFlow private constructor(val transaction: SignedTransaction,
|
||||||
private val extraRecipients: Set<Party>,
|
private val oldParticipants: Collection<Party>,
|
||||||
override val progressTracker: ProgressTracker,
|
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)
|
@Deprecated(DEPRECATION_MSG)
|
||||||
constructor(transaction: SignedTransaction, extraRecipients: Set<Party>, progressTracker: ProgressTracker) : this(
|
constructor(transaction: SignedTransaction, extraRecipients: Set<Party>, progressTracker: ProgressTracker) : this(
|
||||||
transaction, extraRecipients, progressTracker, null
|
transaction, extraRecipients, progressTracker, emptyList(), false
|
||||||
)
|
)
|
||||||
@Deprecated(DEPRECATION_MSG)
|
@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)
|
@Deprecated(DEPRECATION_MSG)
|
||||||
constructor(transaction: SignedTransaction) : this(transaction, emptySet(), tracker(), null)
|
constructor(transaction: SignedTransaction) : this(transaction, emptySet(), tracker(), emptyList(), false)
|
||||||
@Deprecated(DEPRECATION_MSG)
|
@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
|
* 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.
|
||||||
constructor(transaction: SignedTransaction, sessions: Collection<FlowSession>) : this(
|
*
|
||||||
transaction, emptySet(), tracker(), sessions
|
* @param transaction What to commit.
|
||||||
)
|
*/
|
||||||
constructor(transaction: SignedTransaction, firstSession: FlowSession, vararg restSessions: FlowSession) : this(
|
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 {
|
companion object {
|
||||||
private const val DEPRECATION_MSG = "It is unsafe to use this constructor as it requires nodes to automatically " +
|
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 " +
|
"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") {
|
object NOTARISING : ProgressTracker.Step("Requesting signature by notary service") {
|
||||||
override fun childProgressTracker() = NotaryFlow.Client.tracker()
|
override fun childProgressTracker() = NotaryFlow.Client.tracker()
|
||||||
@ -81,13 +113,13 @@ class FinalityFlow private constructor(val transaction: SignedTransaction,
|
|||||||
@Suspendable
|
@Suspendable
|
||||||
@Throws(NotaryException::class)
|
@Throws(NotaryException::class)
|
||||||
override fun call(): SignedTransaction {
|
override fun call(): SignedTransaction {
|
||||||
if (sessions == null) {
|
if (!newApi) {
|
||||||
require(CordappResolver.currentTargetVersion < 4) {
|
require(CordappResolver.currentTargetVersion < 4) {
|
||||||
"A flow session for each external participant to the transaction must be provided. If you wish to continue " +
|
"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."
|
"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 " +
|
logger.warnOnce("The current usage of FinalityFlow is unsafe. Please consider upgrading your CorDapp to use " +
|
||||||
"FinalityFlow with FlowSessions.")
|
"FinalityFlow with FlowSessions. (${CordappResolver.currentCordapp?.info})")
|
||||||
} else {
|
} else {
|
||||||
require(sessions.none { serviceHub.myInfo.isLegalIdentity(it.counterparty) }) {
|
require(sessions.none { serviceHub.myInfo.isLegalIdentity(it.counterparty) }) {
|
||||||
"Do not provide flow sessions for the local node. FinalityFlow will record the notarised transaction locally."
|
"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()
|
transaction.pushToLoggingContext()
|
||||||
logCommandData()
|
logCommandData()
|
||||||
val ledgerTransaction = verifyTx()
|
val ledgerTransaction = verifyTx()
|
||||||
val externalParticipants = extractExternalParticipants(ledgerTransaction)
|
val externalTxParticipants = extractExternalParticipants(ledgerTransaction)
|
||||||
|
|
||||||
if (sessions != null) {
|
if (newApi) {
|
||||||
val missingRecipients = externalParticipants - sessions.map { it.counterparty }
|
val sessionParties = sessions.map { it.counterparty }
|
||||||
|
val missingRecipients = externalTxParticipants - sessionParties - oldParticipants
|
||||||
require(missingRecipients.isEmpty()) {
|
require(missingRecipients.isEmpty()) {
|
||||||
"Flow sessions were not provided for the following transaction participants: $missingRecipients"
|
"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()
|
val notarised = notariseAndRecord()
|
||||||
|
|
||||||
// Each transaction has its own set of recipients, but extra recipients get them all.
|
|
||||||
progressTracker.currentStep = BROADCASTING
|
progressTracker.currentStep = BROADCASTING
|
||||||
|
|
||||||
if (sessions == null) {
|
if (newApi) {
|
||||||
val recipients = externalParticipants + (extraRecipients - serviceHub.myInfo.legalIdentities)
|
oldV3Broadcast(notarised, oldParticipants.toSet())
|
||||||
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 {
|
|
||||||
for (session in sessions) {
|
for (session in sessions) {
|
||||||
try {
|
try {
|
||||||
subFlow(SendTransactionFlow(session, notarised))
|
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.")
|
logger.info("All parties received the transaction successfully.")
|
||||||
@ -147,6 +176,18 @@ class FinalityFlow private constructor(val transaction: SignedTransaction,
|
|||||||
return notarised
|
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() {
|
private fun logCommandData() {
|
||||||
if (logger.isDebugEnabled) {
|
if (logger.isDebugEnabled) {
|
||||||
val commandDataTypes = transaction.tx.commands.asSequence().mapNotNull { it.value::class.qualifiedName }.distinct()
|
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)
|
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
|
* Note that this has to return a tracker before the flow is invoked. You can't change your mind half way
|
||||||
* through.
|
* through.
|
||||||
*/
|
*/
|
||||||
open val progressTracker: ProgressTracker? = ProgressTracker.DEFAULT_TRACKER()
|
open val progressTracker: ProgressTracker? = DEFAULT_TRACKER()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is where you fill out your business logic.
|
* This is where you fill out your business logic.
|
||||||
|
@ -34,7 +34,7 @@ class NotaryChangeFlow<out T : ContractState>(
|
|||||||
inputs.map { it.ref },
|
inputs.map { it.ref },
|
||||||
originalState.state.notary,
|
originalState.state.notary,
|
||||||
modification,
|
modification,
|
||||||
serviceHub.networkParametersStorage.currentHash
|
serviceHub.networkParametersService.currentHash
|
||||||
).build()
|
).build()
|
||||||
|
|
||||||
val participantKeys = inputs.flatMap { it.state.data.participants }.map { it.owningKey }.toSet()
|
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. */
|
/** Specifies which states have already been consumed in another transaction. */
|
||||||
val consumedStates: Map<StateRef, StateConsumptionDetails>
|
val consumedStates: Map<StateRef, StateConsumptionDetails>
|
||||||
) : NotaryError() {
|
) : 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" +
|
"${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."
|
"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?
|
// TODO: include notary timestamp?
|
||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
data class StateConsumptionDetails(
|
data class StateConsumptionDetails(
|
||||||
val hashOfTransactionId: SecureHash,
|
val hashOfTransactionId: SecureHash,
|
||||||
val type: ConsumedStateType
|
val type: ConsumedStateType
|
||||||
) {
|
) {
|
||||||
|
@ -10,7 +10,7 @@ import net.corda.core.crypto.TransactionSignature
|
|||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.BackpressureAwareTimedFlow
|
import net.corda.core.internal.BackpressureAwareTimedFlow
|
||||||
import net.corda.core.internal.FetchDataFlow
|
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.generateSignature
|
||||||
import net.corda.core.internal.notary.validateSignatures
|
import net.corda.core.internal.notary.validateSignatures
|
||||||
import net.corda.core.internal.pushToLoggingContext
|
import net.corda.core.internal.pushToLoggingContext
|
||||||
@ -107,7 +107,7 @@ class NotaryFlow {
|
|||||||
check(stx.coreTransaction is NotaryChangeWireTransaction) {
|
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"
|
"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.")
|
?: throw IllegalStateException("The notary party $notaryParty specified by transaction ${stx.id}, is not recognised as a current or historic notary.")
|
||||||
historicNotary.validating
|
historicNotary.validating
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ open class ReceiveTransactionFlow @JvmOverloads constructor(private val otherSid
|
|||||||
it.pushToLoggingContext()
|
it.pushToLoggingContext()
|
||||||
logger.info("Received transaction acknowledgement request from party ${otherSideSession.counterparty}.")
|
logger.info("Received transaction acknowledgement request from party ${otherSideSession.counterparty}.")
|
||||||
checkParameterHash(it.networkParametersHash)
|
checkParameterHash(it.networkParametersHash)
|
||||||
subFlow(ResolveTransactionsFlow(it, otherSideSession))
|
subFlow(ResolveTransactionsFlow(it, otherSideSession, statesToRecord))
|
||||||
logger.info("Transaction dependencies resolution completed.")
|
logger.info("Transaction dependencies resolution completed.")
|
||||||
try {
|
try {
|
||||||
it.verify(serviceHub, checkSufficientSignatures)
|
it.verify(serviceHub, checkSufficientSignatures)
|
||||||
|
@ -4,11 +4,10 @@ import co.paralleluniverse.fibers.Suspendable
|
|||||||
import net.corda.core.contracts.StateRef
|
import net.corda.core.contracts.StateRef
|
||||||
import net.corda.core.internal.uncheckedCast
|
import net.corda.core.internal.uncheckedCast
|
||||||
import net.corda.core.utilities.ProgressTracker
|
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.
|
* 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 it will be suspended until the
|
* 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:
|
* 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
|
* 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
|
* 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.
|
* reasonable, especially if some economics transaction depends upon it.
|
||||||
*
|
*
|
||||||
* @param flowLogic a flow which uses reference states.
|
|
||||||
* @param progressTracker a progress tracker instance.
|
* @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>(
|
class WithReferencedStatesFlow<T : Any> @JvmOverloads constructor(
|
||||||
val flowLogic: FlowLogic<T>,
|
override val progressTracker: ProgressTracker = tracker(),
|
||||||
override val progressTracker: ProgressTracker = WithReferencedStatesFlow.tracker()
|
private val flowLogicProducer: () -> FlowLogic<T>
|
||||||
) : FlowLogic<T>() {
|
) : FlowLogic<T>() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val logger = contextLogger()
|
|
||||||
|
|
||||||
object ATTEMPT : ProgressTracker.Step("Attempting to run flow which uses reference states.")
|
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 RETRYING : ProgressTracker.Step("Reference states are out of date! Waiting for updated states...")
|
||||||
object SUCCESS : ProgressTracker.Step("Flow ran successfully.")
|
object SUCCESS : ProgressTracker.Step("Flow ran successfully.")
|
||||||
@ -40,10 +38,10 @@ class WithReferencedStatesFlow<T : Any>(
|
|||||||
fun tracker() = ProgressTracker(ATTEMPT, RETRYING, SUCCESS)
|
fun tracker() = ProgressTracker(ATTEMPT, RETRYING, SUCCESS)
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class FlowResult {
|
// This is not a sealed data class as that requires exposing Success and Conflict
|
||||||
data class Success<T : Any>(val value: T) : FlowResult()
|
private interface FlowResult
|
||||||
data class Conflict(val stateRefs: Set<StateRef>) : 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
|
* 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 {
|
val conflictingReferenceStateRefs = error.consumedStates.filter {
|
||||||
it.value.type == StateConsumptionDetails.ConsumedStateType.REFERENCE_INPUT_STATE
|
it.value.type == StateConsumptionDetails.ConsumedStateType.REFERENCE_INPUT_STATE
|
||||||
}.map { it.key }.toSet()
|
}.map { it.key }.toSet()
|
||||||
FlowResult.Conflict(conflictingReferenceStateRefs)
|
Conflict(conflictingReferenceStateRefs)
|
||||||
} else {
|
} else {
|
||||||
throw result
|
throw result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is FlowException -> 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
|
// Loop until the flow successfully completes. We need to
|
||||||
// do this because there might be consecutive update races.
|
// do this because there might be consecutive update races.
|
||||||
while (true) {
|
while (true) {
|
||||||
|
val flowLogic = flowLogicProducer()
|
||||||
// Return a successful flow result or a FlowException.
|
// Return a successful flow result or a FlowException.
|
||||||
logger.info("Attempting to run the supplied flow ${flowLogic.javaClass.canonicalName}.")
|
logger.info("Attempting to run the supplied flow ${flowLogic.javaClass.canonicalName}.")
|
||||||
val result = try {
|
val result = try {
|
||||||
@ -91,12 +90,12 @@ class WithReferencedStatesFlow<T : Any>(
|
|||||||
// states have been updated.
|
// states have been updated.
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
when (processedResult) {
|
when (processedResult) {
|
||||||
is FlowResult.Success<*> -> {
|
is Success<*> -> {
|
||||||
logger.info("Flow ${flowLogic.javaClass.canonicalName} completed successfully.")
|
logger.info("Flow ${flowLogic.javaClass.canonicalName} completed successfully.")
|
||||||
progressTracker.currentStep = SUCCESS
|
progressTracker.currentStep = SUCCESS
|
||||||
return uncheckedCast(processedResult.value)
|
return uncheckedCast(processedResult.value)
|
||||||
}
|
}
|
||||||
is FlowResult.Conflict -> {
|
is Conflict -> {
|
||||||
val conflicts = processedResult.stateRefs
|
val conflicts = processedResult.stateRefs
|
||||||
logger.info("Flow ${flowLogic.javaClass.name} failed due to reference state conflicts: $conflicts.")
|
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)
|
else -> getContractAttachmentId(stateAndRef.state.contract, services)
|
||||||
}
|
}
|
||||||
val upgradedContractAttachmentId = getContractAttachmentId(upgradedContractClass.name, services)
|
val upgradedContractAttachmentId = getContractAttachmentId(upgradedContractClass.name, services)
|
||||||
val networkParametersHash = services.networkParametersStorage.currentHash
|
val networkParametersHash = services.networkParametersService.currentHash
|
||||||
|
|
||||||
val inputs = listOf(stateAndRef.ref)
|
val inputs = listOf(stateAndRef.ref)
|
||||||
return ContractUpgradeTransactionBuilder(
|
return ContractUpgradeTransactionBuilder(
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.corda.core.internal
|
package net.corda.core.internal
|
||||||
|
|
||||||
import net.corda.core.DeleteForDJVM
|
import net.corda.core.DeleteForDJVM
|
||||||
|
import net.corda.core.contracts.ContractAttachment
|
||||||
import net.corda.core.contracts.ContractClassName
|
import net.corda.core.contracts.ContractClassName
|
||||||
import net.corda.core.cordapp.Cordapp
|
import net.corda.core.cordapp.Cordapp
|
||||||
import net.corda.core.cordapp.CordappConfig
|
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.NetworkParameters
|
||||||
import net.corda.core.node.ServicesForResolution
|
import net.corda.core.node.ServicesForResolution
|
||||||
import net.corda.core.node.ZoneVersionTooLowException
|
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.CordaSerializable
|
||||||
import net.corda.core.serialization.SerializationContext
|
import net.corda.core.serialization.SerializationContext
|
||||||
|
import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
|
||||||
import net.corda.core.transactions.LedgerTransaction
|
import net.corda.core.transactions.LedgerTransaction
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.core.transactions.WireTransaction
|
import net.corda.core.transactions.WireTransaction
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import org.slf4j.MDC
|
import org.slf4j.MDC
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
import java.util.jar.JarEntry
|
||||||
|
import java.util.jar.JarInputStream
|
||||||
|
|
||||||
// *Internal* Corda-specific utilities.
|
// *Internal* Corda-specific utilities.
|
||||||
|
|
||||||
const val PLATFORM_VERSION = 4
|
const val PLATFORM_VERSION = 5
|
||||||
|
|
||||||
fun ServicesForResolution.ensureMinimumPlatformVersion(requiredMinPlatformVersion: Int, feature: String) {
|
fun ServicesForResolution.ensureMinimumPlatformVersion(requiredMinPlatformVersion: Int, feature: String) {
|
||||||
checkMinimumPlatformVersion(networkParameters.minimumPlatformVersion, requiredMinPlatformVersion, feature)
|
checkMinimumPlatformVersion(networkParameters.minimumPlatformVersion, requiredMinPlatformVersion, feature)
|
||||||
@ -116,3 +127,34 @@ internal fun NetworkParameters.getPackageOwnerOf(contractClassNames: Set<Contrac
|
|||||||
fun noPackageOverlap(packages: Collection<String>): Boolean {
|
fun noPackageOverlap(packages: Collection<String>): Boolean {
|
||||||
return packages.all { outer -> packages.none { inner -> inner != outer && inner.startsWith("$outer.") } }
|
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.identity.Party
|
||||||
import net.corda.core.node.NotaryInfo
|
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.
|
* 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 */
|
/** @see Files.size */
|
||||||
inline val Path.size: Long get() = Files.size(this)
|
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 */
|
/** @see Files.getLastModifiedTime */
|
||||||
fun Path.lastModifiedTime(vararg options: LinkOption): FileTime = Files.getLastModifiedTime(this, *options)
|
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].
|
* 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.
|
* 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
|
@DeleteForDJVM
|
||||||
class ResolveTransactionsFlow(txHashesArg: Set<SecureHash>,
|
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.
|
// Need it ordered in terms of iteration. Needs to be a variable for the check-pointing logic to work.
|
||||||
private val txHashes = txHashesArg.toList()
|
private val txHashes = txHashesArg.toList()
|
||||||
@ -40,6 +39,10 @@ class ResolveTransactionsFlow(txHashesArg: Set<SecureHash>,
|
|||||||
this.signedTransaction = signedTransaction
|
this.signedTransaction = signedTransaction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor(signedTransaction: SignedTransaction, otherSide: FlowSession, statesToRecord: StatesToRecord) : this(dependencyIDs(signedTransaction), otherSide, statesToRecord) {
|
||||||
|
this.signedTransaction = signedTransaction
|
||||||
|
}
|
||||||
|
|
||||||
@DeleteForDJVM
|
@DeleteForDJVM
|
||||||
companion object {
|
companion object {
|
||||||
private fun dependencyIDs(stx: SignedTransaction) = stx.inputs.map { it.txhash }.toSet() + stx.references.map { it.txhash }.toSet()
|
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.
|
// Finish fetching data.
|
||||||
|
|
||||||
val result = topologicalSort(newTxns)
|
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 {
|
result.forEach {
|
||||||
// For each transaction, verify it and insert it into the database. As we are iterating over them in a
|
// 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
|
// 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
|
// 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.
|
// redundantly next time we attempt verification.
|
||||||
it.verify(serviceHub)
|
it.verify(serviceHub)
|
||||||
serviceHub.recordTransactions(StatesToRecord.NONE, listOf(it))
|
serviceHub.recordTransactions(usedStatesToRecord, listOf(it))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
package net.corda.core.internal
|
package net.corda.core.internal
|
||||||
|
|
||||||
import net.corda.core.contracts.ContractState
|
import net.corda.core.contracts.ContractState
|
||||||
import net.corda.core.contracts.LinearPointer
|
|
||||||
import net.corda.core.contracts.StatePointer
|
import net.corda.core.contracts.StatePointer
|
||||||
import net.corda.core.contracts.StaticPointer
|
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uses reflection to search for instances of [StatePointer] within a [ContractState].
|
* 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) {
|
class StatePointerSearch(val state: ContractState) {
|
||||||
// Classes in these packages should not be part of a search.
|
// 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.
|
// Type required for traversal.
|
||||||
private data class FieldWithObject(val obj: Any, val field: Field)
|
private data class FieldWithObject(val obj: Any, val field: Field)
|
||||||
@ -21,14 +20,26 @@ class StatePointerSearch(val state: ContractState) {
|
|||||||
private val statePointers = mutableSetOf<StatePointer<*>>()
|
private val statePointers = mutableSetOf<StatePointer<*>>()
|
||||||
|
|
||||||
// Record seen objects to avoid getting stuck in loops.
|
// 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.
|
// Queue of fields to search.
|
||||||
private val fieldQueue = ArrayDeque<FieldWithObject>().apply { addAllFields(state) }
|
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.
|
// Helper for adding all fields to the queue.
|
||||||
private fun ArrayDeque<FieldWithObject>.addAllFields(obj: Any) {
|
private fun ArrayDeque<FieldWithObject>.addAllFields(obj: Any) {
|
||||||
val fields = obj::class.java.declaredFields
|
val fields = getAllFields(obj::class.java)
|
||||||
|
|
||||||
val fieldsWithObjects = fields.mapNotNull { field ->
|
val fieldsWithObjects = fields.mapNotNull { field ->
|
||||||
// Ignore classes which have not been loaded.
|
// Ignore classes which have not been loaded.
|
||||||
// Assumption: all required state classes are already loaded.
|
// Assumption: all required state classes are already loaded.
|
||||||
@ -36,39 +47,44 @@ class StatePointerSearch(val state: ContractState) {
|
|||||||
if (packageName == null) {
|
if (packageName == null) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
// Ignore JDK classes.
|
FieldWithObject(obj, field)
|
||||||
val isBlacklistedPackage = blackListedPackages.any { packageName.startsWith(it) }
|
|
||||||
if (isBlacklistedPackage) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
FieldWithObject(obj, field)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
addAll(fieldsWithObjects)
|
addAll(fieldsWithObjects)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleField(obj: Any, field: Field) {
|
private fun handleIterable(iterable: Iterable<*>) {
|
||||||
when {
|
iterable.forEach { obj -> handleObject(obj) }
|
||||||
// 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)
|
private fun handleMap(map: Map<*, *>) {
|
||||||
// Not StatePointer.
|
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 -> {
|
else -> {
|
||||||
val newObj = field.get(obj) ?: return
|
val packageName = obj.javaClass.`package`.name
|
||||||
|
val isBlackListed = blackListedPackages.any { packageName.startsWith(it) }
|
||||||
// Ignore nulls.
|
if (isBlackListed.not()) fieldQueue.addAllFields(obj)
|
||||||
if (newObj in seenObjects) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recurse.
|
|
||||||
fieldQueue.addAllFields(newObj)
|
|
||||||
seenObjects.add(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<*>> {
|
fun search(): Set<StatePointer<*>> {
|
||||||
while (fieldQueue.isNotEmpty()) {
|
while (fieldQueue.isNotEmpty()) {
|
||||||
val (obj, field) = fieldQueue.pop()
|
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.
|
// If the componentGroup is a [LazyMappedList] it means that the original deserialized version is already available.
|
||||||
val components = group.components
|
val components = group.components
|
||||||
if (!forceDeserialize && components is LazyMappedList<*, OpaqueBytes>) {
|
if (!forceDeserialize && components is LazyMappedList<*, OpaqueBytes>) {
|
||||||
return components.originalList as List<T>
|
return uncheckedCast(components.originalList)
|
||||||
}
|
}
|
||||||
|
|
||||||
return components.lazyMapped { component, internalIndex ->
|
return components.lazyMapped { component, internalIndex ->
|
||||||
@ -166,7 +166,7 @@ fun FlowLogic<*>.checkParameterHash(networkParametersHash: SecureHash?) {
|
|||||||
if (serviceHub.networkParameters.minimumPlatformVersion < 4) return
|
if (serviceHub.networkParameters.minimumPlatformVersion < 4) return
|
||||||
else throw IllegalArgumentException("Transaction for notarisation doesn't contain network parameters hash.")
|
else throw IllegalArgumentException("Transaction for notarisation doesn't contain network parameters hash.")
|
||||||
} else {
|
} 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.
|
// 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 jarHash: SecureHash.SHA256,
|
||||||
override val minimumPlatformVersion: Int,
|
override val minimumPlatformVersion: Int,
|
||||||
override val targetPlatformVersion: 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). */
|
/** Indicates whether the CorDapp is loaded from external sources, or generated on node startup (virtual). */
|
||||||
val isLoaded: Boolean = true
|
val isLoaded: Boolean = true
|
||||||
) : Cordapp {
|
) : Cordapp {
|
||||||
|
@ -38,8 +38,8 @@ interface ServicesForResolution {
|
|||||||
/** Provides access to anything relating to cordapps including contract attachment resolution and app context */
|
/** Provides access to anything relating to cordapps including contract attachment resolution and app context */
|
||||||
val cordappProvider: CordappProvider
|
val cordappProvider: CordappProvider
|
||||||
|
|
||||||
/** Provides access to storage of historical network parameters that are used in transaction resolution */
|
/** Provides access to historical network parameters that are used in transaction resolution. */
|
||||||
val networkParametersStorage: NetworkParametersStorage
|
val networkParametersService: NetworkParametersService
|
||||||
|
|
||||||
/** Returns the network parameters the node is operating under. */
|
/** Returns the network parameters the node is operating under. */
|
||||||
val networkParameters: NetworkParameters
|
val networkParameters: NetworkParameters
|
||||||
@ -359,7 +359,7 @@ interface ServiceHub : ServicesForResolution {
|
|||||||
* and thus queryable data will include everything committed as of the last checkpoint.
|
* and thus queryable data will include everything committed as of the last checkpoint.
|
||||||
*
|
*
|
||||||
* @throws IllegalStateException if called outside of a transaction.
|
* @throws IllegalStateException if called outside of a transaction.
|
||||||
* @return A new [Connection]
|
* @return A [Connection]
|
||||||
*/
|
*/
|
||||||
fun jdbcSession(): Connection
|
fun jdbcSession(): Connection
|
||||||
|
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
package net.corda.core.node.services
|
package net.corda.core.node.services
|
||||||
|
|
||||||
|
import net.corda.core.CordaInternal
|
||||||
import net.corda.core.DoNotImplement
|
import net.corda.core.DoNotImplement
|
||||||
import net.corda.core.contracts.Attachment
|
import net.corda.core.contracts.Attachment
|
||||||
|
import net.corda.core.contracts.ContractAttachment
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.node.services.vault.AttachmentQueryCriteria
|
import net.corda.core.node.services.vault.AttachmentQueryCriteria
|
||||||
import net.corda.core.node.services.vault.AttachmentSort
|
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.DoNotImplement
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.identity.Party
|
|
||||||
import net.corda.core.node.NetworkParameters
|
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.
|
* historically in force in the network.
|
||||||
*/
|
*/
|
||||||
@DoNotImplement
|
@DoNotImplement
|
||||||
interface NetworkParametersStorage {
|
interface NetworkParametersService {
|
||||||
/**
|
/**
|
||||||
* Hash of the current parameters for the network.
|
* Hash of the current parameters for the network.
|
||||||
*/
|
*/
|
||||||
@ -23,8 +21,7 @@ interface NetworkParametersStorage {
|
|||||||
val defaultHash: SecureHash
|
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
|
* Return the network parameters with the given hash, or null if it doesn't exist.
|
||||||
* get them from network map.
|
|
||||||
*/
|
*/
|
||||||
fun lookup(hash: SecureHash): NetworkParameters?
|
fun lookup(hash: SecureHash): NetworkParameters?
|
||||||
}
|
}
|
@ -3,6 +3,7 @@ package net.corda.core.node.services
|
|||||||
import net.corda.core.DeleteForDJVM
|
import net.corda.core.DeleteForDJVM
|
||||||
import net.corda.core.DoNotImplement
|
import net.corda.core.DoNotImplement
|
||||||
import net.corda.core.concurrent.CordaFuture
|
import net.corda.core.concurrent.CordaFuture
|
||||||
|
import net.corda.core.contracts.Attachment
|
||||||
import net.corda.core.transactions.LedgerTransaction
|
import net.corda.core.transactions.LedgerTransaction
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -390,8 +390,11 @@ interface VaultService {
|
|||||||
*
|
*
|
||||||
* @throws VaultQueryException if the query cannot be executed for any reason.
|
* @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.
|
* Notes:
|
||||||
* the [QueryCriteria] applies to both snapshot and deltas (streaming updates).
|
* - 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)
|
@Throws(VaultQueryException::class)
|
||||||
fun <T : ContractState> _trackBy(criteria: QueryCriteria,
|
fun <T : ContractState> _trackBy(criteria: QueryCriteria,
|
||||||
@ -410,6 +413,10 @@ interface VaultService {
|
|||||||
return _queryBy(criteria, PageSpecification(), Sort(emptySet()), contractStateType)
|
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> {
|
fun <T : ContractState> queryBy(contractStateType: Class<out T>, criteria: QueryCriteria, paging: PageSpecification): Vault.Page<T> {
|
||||||
return _queryBy(criteria, paging, Sort(emptySet()), contractStateType)
|
return _queryBy(criteria, paging, Sort(emptySet()), contractStateType)
|
||||||
}
|
}
|
||||||
@ -430,6 +437,10 @@ interface VaultService {
|
|||||||
return _trackBy(criteria, PageSpecification(), Sort(emptySet()), contractStateType)
|
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>> {
|
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)
|
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)
|
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> {
|
inline fun <reified T : ContractState> VaultService.queryBy(criteria: QueryCriteria, paging: PageSpecification): Vault.Page<T> {
|
||||||
return _queryBy(criteria, paging, Sort(emptySet()), T::class.java)
|
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)
|
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>> {
|
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)
|
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 stateRefs: List<StateRef>? = null,
|
||||||
val notary: List<AbstractParty>? = null,
|
val notary: List<AbstractParty>? = null,
|
||||||
val softLockingCondition: SoftLockingCondition? = null,
|
val softLockingCondition: SoftLockingCondition? = null,
|
||||||
val timeCondition: TimeCondition? = 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
|
|
||||||
) : CommonQueryCriteria() {
|
) : 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> {
|
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
|
||||||
super.visit(parser)
|
super.visit(parser)
|
||||||
return parser.parseCriteria(this)
|
return parser.parseCriteria(this)
|
||||||
@ -111,7 +138,11 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
|
|||||||
stateRefs: List<StateRef>? = this.stateRefs,
|
stateRefs: List<StateRef>? = this.stateRefs,
|
||||||
notary: List<AbstractParty>? = this.notary,
|
notary: List<AbstractParty>? = this.notary,
|
||||||
softLockingCondition: SoftLockingCondition? = this.softLockingCondition,
|
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 {
|
): VaultQueryCriteria {
|
||||||
return VaultQueryCriteria(
|
return VaultQueryCriteria(
|
||||||
status,
|
status,
|
||||||
@ -119,7 +150,11 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
|
|||||||
stateRefs,
|
stateRefs,
|
||||||
notary,
|
notary,
|
||||||
softLockingCondition,
|
softLockingCondition,
|
||||||
timeCondition
|
timeCondition,
|
||||||
|
relevancyStatus,
|
||||||
|
constraintTypes,
|
||||||
|
constraints,
|
||||||
|
participants
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -238,9 +273,21 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
|
|||||||
data class VaultCustomQueryCriteria<L : StatePersistable> @JvmOverloads constructor(
|
data class VaultCustomQueryCriteria<L : StatePersistable> @JvmOverloads constructor(
|
||||||
val expression: CriteriaExpression<L, Boolean>,
|
val expression: CriteriaExpression<L, Boolean>,
|
||||||
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
||||||
override val contractStateTypes: Set<Class<out ContractState>>? = null,
|
override val contractStateTypes: Set<Class<out ContractState>>? = null
|
||||||
override val relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL
|
|
||||||
) : CommonQueryCriteria() {
|
) : 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> {
|
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
|
||||||
super.visit(parser)
|
super.visit(parser)
|
||||||
return parser.parseCriteria(this)
|
return parser.parseCriteria(this)
|
||||||
@ -249,12 +296,14 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
|
|||||||
fun copy(
|
fun copy(
|
||||||
expression: CriteriaExpression<L, Boolean> = this.expression,
|
expression: CriteriaExpression<L, Boolean> = this.expression,
|
||||||
status: Vault.StateStatus = this.status,
|
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> {
|
): VaultCustomQueryCriteria<L> {
|
||||||
return VaultCustomQueryCriteria(
|
return VaultCustomQueryCriteria(
|
||||||
expression,
|
expression,
|
||||||
status,
|
status,
|
||||||
contractStateTypes
|
contractStateTypes,
|
||||||
|
relevancyStatus
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -212,7 +212,7 @@ data class Sort(val columns: Collection<SortColumn>) : BaseSort() {
|
|||||||
data class AttachmentSort(val columns: Collection<AttachmentSortColumn>) : BaseSort() {
|
data class AttachmentSort(val columns: Collection<AttachmentSortColumn>) : BaseSort() {
|
||||||
|
|
||||||
enum class AttachmentSortAttribute(val columnName: String) {
|
enum class AttachmentSortAttribute(val columnName: String) {
|
||||||
INSERTION_DATE("insertion_date"),
|
INSERTION_DATE("insertionDate"),
|
||||||
UPLOADER("uploader"),
|
UPLOADER("uploader"),
|
||||||
FILENAME("filename"),
|
FILENAME("filename"),
|
||||||
VERSION ("version")
|
VERSION ("version")
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
@file:KeepForDJVM
|
@file:KeepForDJVM
|
||||||
package net.corda.core.serialization
|
package net.corda.core.serialization
|
||||||
|
|
||||||
|
import co.paralleluniverse.io.serialization.Serialization
|
||||||
import net.corda.core.CordaInternal
|
import net.corda.core.CordaInternal
|
||||||
import net.corda.core.DeleteForDJVM
|
import net.corda.core.DeleteForDJVM
|
||||||
import net.corda.core.DoNotImplement
|
import net.corda.core.DoNotImplement
|
||||||
@ -160,6 +161,10 @@ interface SerializationContext {
|
|||||||
* The use case we are serializing or deserializing for. See [UseCase].
|
* The use case we are serializing or deserializing for. See [UseCase].
|
||||||
*/
|
*/
|
||||||
val useCase: 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.
|
* 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
|
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.
|
* 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 {
|
interface EncodingWhitelist {
|
||||||
fun acceptEncoding(encoding: SerializationEncoding): Boolean
|
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
|
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.Attachment
|
||||||
import net.corda.core.contracts.ContractAttachment
|
import net.corda.core.contracts.ContractAttachment
|
||||||
import net.corda.core.contracts.TransactionVerificationException.OverlappingAttachmentsException
|
import net.corda.core.contracts.TransactionVerificationException.OverlappingAttachmentsException
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.sha256
|
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.cordapp.targetPlatformVersion
|
||||||
import net.corda.core.internal.createSimpleCache
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.internal.isUploaderTrusted
|
|
||||||
import net.corda.core.internal.toSynchronised
|
|
||||||
import net.corda.core.serialization.MissingAttachmentsException
|
import net.corda.core.serialization.MissingAttachmentsException
|
||||||
|
import net.corda.core.serialization.SerializationCustomSerializer
|
||||||
import net.corda.core.serialization.SerializationFactory
|
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.serialization.internal.AttachmentURLStreamHandlerFactory.toUrl
|
||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import net.corda.core.utilities.debug
|
import net.corda.core.utilities.debug
|
||||||
@ -19,6 +23,7 @@ import java.io.ByteArrayOutputStream
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.net.*
|
import java.net.*
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A custom ClassLoader that knows how to load classes from a set of attachments. The attachments themselves only
|
* 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 {
|
init {
|
||||||
val untrusted = attachments.mapNotNull { it as? ContractAttachment }.filterNot { isUploaderTrusted(it.uploader) }.map(ContractAttachment::id)
|
val untrusted = attachments.mapNotNull { it as? ContractAttachment }.filterNot { isUploaderTrusted(it.uploader) }.map(ContractAttachment::id)
|
||||||
if(untrusted.isNotEmpty()) {
|
if(untrusted.isNotEmpty()) {
|
||||||
throw MissingAttachmentsException(untrusted, "Attempting to load Contract Attachments downloaded from the network")
|
throw UntrustedAttachmentsException(untrusted)
|
||||||
}
|
}
|
||||||
requireNoDuplicates(attachments)
|
requireNoDuplicates(attachments)
|
||||||
}
|
}
|
||||||
@ -41,10 +46,11 @@ class AttachmentsClassLoader(attachments: List<Attachment>, parent: ClassLoader
|
|||||||
private val log = contextLogger()
|
private val log = contextLogger()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// This is required to register the AttachmentURLStreamHandlerFactory.
|
// Apply our own URLStreamHandlerFactory to resolve attachments
|
||||||
URL.setURLStreamHandlerFactory(AttachmentURLStreamHandlerFactory)
|
setOrDecorateURLStreamHandlerFactory()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Jolokia and Json-simple are dependencies that were bundled by mistake within contract jars.
|
// 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.
|
// In the AttachmentsClassLoader we just ignore any class in those 2 packages.
|
||||||
private val ignoreDirectories = listOf("org/jolokia/", "org/json/simple/")
|
private val ignoreDirectories = listOf("org/jolokia/", "org/json/simple/")
|
||||||
@ -115,6 +121,50 @@ class AttachmentsClassLoader(attachments: List<Attachment>, parent: ClassLoader
|
|||||||
return it.toByteArray()
|
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
|
@VisibleForTesting
|
||||||
internal object AttachmentsClassLoaderBuilder {
|
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.
|
// 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)
|
private val cache: MutableMap<Set<SecureHash>, SerializationContext> = createSimpleCache(CACHE_SIZE)
|
||||||
.toSynchronised()
|
|
||||||
|
|
||||||
fun build(attachments: List<Attachment>): AttachmentsClassLoader {
|
|
||||||
return cache.computeIfAbsent(attachments.map { it.id }.sorted()) {
|
|
||||||
AttachmentsClassLoader(attachments)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> withAttachmentsClassloaderContext(attachments: List<Attachment>, block: (ClassLoader) -> T): T {
|
fun <T> withAttachmentsClassloaderContext(attachments: List<Attachment>, block: (ClassLoader) -> T): T {
|
||||||
|
val attachmentIds = attachments.map { it.id }.toSet()
|
||||||
|
|
||||||
// Create classloader from the attachments.
|
val serializationContext = cache.computeIfAbsent(attachmentIds) {
|
||||||
val transactionClassLoader = AttachmentsClassLoaderBuilder.build(attachments)
|
// 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.
|
// Create a new serializationContext for the current Transaction.
|
||||||
val transactionSerializationContext = SerializationFactory.defaultFactory.defaultContext.withPreventDataLoss().withClassLoader(transactionClassLoader)
|
SerializationFactory.defaultFactory.defaultContext
|
||||||
|
.withPreventDataLoss()
|
||||||
|
.withClassLoader(transactionClassLoader)
|
||||||
|
.withWhitelist(whitelistedClasses)
|
||||||
|
.withCustomSerializers(serializers)
|
||||||
|
}
|
||||||
|
|
||||||
// Deserialize all relevant classes in the transaction classloader.
|
// Deserialize all relevant classes in the transaction classloader.
|
||||||
return SerializationFactory.defaultFactory.withCurrentContext(transactionSerializationContext) {
|
return SerializationFactory.defaultFactory.withCurrentContext(serializationContext) {
|
||||||
block(transactionClassLoader)
|
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)
|
?: throw AttachmentResolutionException(legacyContractAttachmentId)
|
||||||
val upgradedContractAttachment = services.attachments.openAttachment(upgradedContractAttachmentId)
|
val upgradedContractAttachment = services.attachments.openAttachment(upgradedContractAttachmentId)
|
||||||
?: throw AttachmentResolutionException(upgradedContractAttachmentId)
|
?: throw AttachmentResolutionException(upgradedContractAttachmentId)
|
||||||
val hashToResolve = networkParametersHash ?: services.networkParametersStorage.defaultHash
|
val hashToResolve = networkParametersHash ?: services.networkParametersService.defaultHash
|
||||||
val resolvedNetworkParameters = services.networkParametersStorage.lookup(hashToResolve) ?: throw TransactionResolutionException(id)
|
val resolvedNetworkParameters = services.networkParametersService.lookup(hashToResolve) ?: throw TransactionResolutionException(id)
|
||||||
return ContractUpgradeLedgerTransaction(
|
return ContractUpgradeLedgerTransaction(
|
||||||
resolvedInputs,
|
resolvedInputs,
|
||||||
notary,
|
notary,
|
||||||
|
@ -3,14 +3,9 @@ package net.corda.core.transactions
|
|||||||
import net.corda.core.CordaInternal
|
import net.corda.core.CordaInternal
|
||||||
import net.corda.core.KeepForDJVM
|
import net.corda.core.KeepForDJVM
|
||||||
import net.corda.core.contracts.*
|
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.SecureHash
|
||||||
import net.corda.core.crypto.isFulfilledBy
|
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.*
|
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.node.NetworkParameters
|
||||||
import net.corda.core.serialization.ConstructorForDeserialization
|
import net.corda.core.serialization.ConstructorForDeserialization
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
@ -19,7 +14,6 @@ import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
|
|||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.function.Predicate
|
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:
|
* 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. */
|
/** Network parameters that were in force when the transaction was notarised. */
|
||||||
override val networkParameters: NetworkParameters?,
|
override val networkParameters: NetworkParameters?,
|
||||||
override val references: List<StateAndRef<ContractState>>,
|
override val references: List<StateAndRef<ContractState>>,
|
||||||
private val inputStatesContractClassNameToMaxVersion: Map<ContractClassName,Version>
|
private val inputStatesContractClassNameToMaxVersion: Map<ContractClassName, Version>
|
||||||
//DOCEND 1
|
//DOCEND 1
|
||||||
) : FullTransaction() {
|
) : FullTransaction() {
|
||||||
// These are not part of the c'tor above as that defines LedgerTransaction's serialisation format
|
// These are not part of the c'tor above as that defines LedgerTransaction's serialisation format
|
||||||
@ -66,8 +60,6 @@ private constructor(
|
|||||||
checkBaseInvariants()
|
checkBaseInvariants()
|
||||||
if (timeWindow != null) check(notary != null) { "Transactions with time-windows must be notarised" }
|
if (timeWindow != null) check(notary != null) { "Transactions with time-windows must be notarised" }
|
||||||
checkNotaryWhitelisted()
|
checkNotaryWhitelisted()
|
||||||
checkNoNotaryChange()
|
|
||||||
checkEncumbrancesValid()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -101,9 +93,6 @@ private constructor(
|
|||||||
val inputStates: List<ContractState> get() = inputs.map { it.state.data }
|
val inputStates: List<ContractState> get() = inputs.map { it.state.data }
|
||||||
val referenceStates: List<ContractState> get() = references.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
|
* Returns the typed input StateAndRef at the specified index
|
||||||
* @param index The index into the inputs.
|
* @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. " +
|
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.")
|
"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())
|
val verifier = internalPrepareVerify(emptyList())
|
||||||
|
verifier.verify()
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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)
|
@CordaInternal
|
||||||
private fun validateContractVersions(contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>>) {
|
internal fun internalPrepareVerify(extraAttachments: List<Attachment>) = AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(this.attachments + extraAttachments) { transactionClassLoader ->
|
||||||
contractAttachmentsByContract.forEach { contractClassName, attachments ->
|
Verifier(createLtxForVerification(), transactionClassLoader, inputStatesContractClassNameToMaxVersion)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createLtxForVerification(): LedgerTransaction {
|
private fun createLtxForVerification(): LedgerTransaction {
|
||||||
@ -368,7 +157,7 @@ private constructor(
|
|||||||
privacySalt = this.privacySalt,
|
privacySalt = this.privacySalt,
|
||||||
networkParameters = this.networkParameters,
|
networkParameters = this.networkParameters,
|
||||||
references = deserializedReferences,
|
references = deserializedReferences,
|
||||||
inputStatesContractClassNameToMaxVersion = emptyMap()
|
inputStatesContractClassNameToMaxVersion = this.inputStatesContractClassNameToMaxVersion
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// This branch is only present for backwards compatibility.
|
// 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
|
* 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
|
* 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
|
@DeleteForDJVM
|
||||||
fun resolve(services: ServicesForResolution, sigs: List<TransactionSignature>): NotaryChangeLedgerTransaction {
|
fun resolve(services: ServicesForResolution, sigs: List<TransactionSignature>): NotaryChangeLedgerTransaction {
|
||||||
val resolvedInputs = services.loadStates(inputs.toSet()).toList()
|
val resolvedInputs = services.loadStates(inputs.toSet()).toList()
|
||||||
val hashToResolve = networkParametersHash ?: services.networkParametersStorage.defaultHash
|
val hashToResolve = networkParametersHash ?: services.networkParametersService.defaultHash
|
||||||
val resolvedNetworkParameters = services.networkParametersStorage.lookup(hashToResolve)
|
val resolvedNetworkParameters = services.networkParametersService.lookup(hashToResolve)
|
||||||
?: throw TransactionResolutionException(id)
|
?: throw TransactionResolutionException(id)
|
||||||
return NotaryChangeLedgerTransaction.create(resolvedInputs, notary, newNotary, id, sigs, resolvedNetworkParameters)
|
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.contracts.*
|
||||||
import net.corda.core.crypto.*
|
import net.corda.core.crypto.*
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.internal.TransactionVerifierServiceInternal
|
||||||
import net.corda.core.internal.VisibleForTesting
|
import net.corda.core.internal.VisibleForTesting
|
||||||
|
import net.corda.core.internal.internalFindTrustedAttachmentForClass
|
||||||
import net.corda.core.node.ServiceHub
|
import net.corda.core.node.ServiceHub
|
||||||
import net.corda.core.node.ServicesForResolution
|
import net.corda.core.node.ServicesForResolution
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.serialization.SerializedBytes
|
import net.corda.core.serialization.SerializedBytes
|
||||||
import net.corda.core.serialization.deserialize
|
import net.corda.core.serialization.deserialize
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
|
import net.corda.core.utilities.contextLogger
|
||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
@ -200,8 +203,29 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
|
|||||||
@DeleteForDJVM
|
@DeleteForDJVM
|
||||||
private fun verifyRegularTransaction(services: ServiceHub, checkSufficientSignatures: Boolean) {
|
private fun verifyRegularTransaction(services: ServiceHub, checkSufficientSignatures: Boolean) {
|
||||||
val ltx = toLedgerTransaction(services, checkSufficientSignatures)
|
val ltx = toLedgerTransaction(services, checkSufficientSignatures)
|
||||||
// TODO: allow non-blocking verification.
|
try {
|
||||||
services.transactionVerifierService.verify(ltx).getOrThrow()
|
// 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
|
* Resolves the underlying transaction with signatures and then returns it, handling any special case transactions
|
||||||
* such as [NotaryChangeWireTransaction].
|
* such as [NotaryChangeWireTransaction].
|
||||||
@ -272,6 +295,8 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
|
|||||||
"keys: ${missing.joinToString { it.toStringShort() }}, " +
|
"keys: ${missing.joinToString { it.toStringShort() }}, " +
|
||||||
"by signers: ${descriptions.joinToString()} "
|
"by signers: ${descriptions.joinToString()} "
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val log = contextLogger()
|
||||||
}
|
}
|
||||||
|
|
||||||
@KeepForDJVM
|
@KeepForDJVM
|
||||||
|
@ -8,6 +8,7 @@ import net.corda.core.crypto.*
|
|||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.*
|
import net.corda.core.internal.*
|
||||||
import net.corda.core.internal.cordapp.CordappImpl.Companion.DEFAULT_CORDAPP_VERSION
|
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.NetworkParameters
|
||||||
import net.corda.core.node.ServiceHub
|
import net.corda.core.node.ServiceHub
|
||||||
import net.corda.core.node.ServicesForResolution
|
import net.corda.core.node.ServicesForResolution
|
||||||
@ -55,6 +56,8 @@ open class TransactionBuilder @JvmOverloads constructor(
|
|||||||
private fun defaultReferencesList(): MutableList<StateRef> = arrayListOf()
|
private fun defaultReferencesList(): MutableList<StateRef> = arrayListOf()
|
||||||
|
|
||||||
private fun defaultServiceHub(): ServiceHub? = (Strand.currentStrand() as? FlowStateMachine<*>)?.serviceHub
|
private fun defaultServiceHub(): ServiceHub? = (Strand.currentStrand() as? FlowStateMachine<*>)?.serviceHub
|
||||||
|
|
||||||
|
private const val CORDA_VERSION_THAT_INTRODUCED_FLATTENED_COMMANDS = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -146,7 +149,7 @@ open class TransactionBuilder @JvmOverloads constructor(
|
|||||||
checkConstraintValidity(state)
|
checkConstraintValidity(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
return SerializationFactory.defaultFactory.withCurrentContext(serializationContext) {
|
val wireTx = SerializationFactory.defaultFactory.withCurrentContext(serializationContext) {
|
||||||
WireTransaction(
|
WireTransaction(
|
||||||
createComponentGroups(
|
createComponentGroups(
|
||||||
inputStates(),
|
inputStates(),
|
||||||
@ -156,10 +159,51 @@ open class TransactionBuilder @JvmOverloads constructor(
|
|||||||
notary,
|
notary,
|
||||||
window,
|
window,
|
||||||
referenceStates,
|
referenceStates,
|
||||||
services.networkParametersStorage.currentHash),
|
services.networkParametersService.currentHash),
|
||||||
privacySalt
|
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 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 outputContractGroups: Map<ContractClassName, List<TransactionState<ContractState>>> = outputs.groupBy { it.contract }
|
||||||
|
|
||||||
val allContracts: Set<ContractClassName> = inputContractGroups.keys + outputContractGroups.keys
|
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)
|
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.
|
// For each contract, resolve the AutomaticPlaceholderConstraint, and select the attachment.
|
||||||
val contractAttachmentsAndResolvedOutputStates: List<Pair<Set<AttachmentId>, List<TransactionState<ContractState>>?>> = allContracts.toSet()
|
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())) {
|
if (inputsHashConstraints.isNotEmpty() && (outputHashConstraints.isNotEmpty() || outputSignatureConstraints.isNotEmpty())) {
|
||||||
val attachmentIds = services.attachments.getContractAttachments(contractClassName)
|
val attachmentIds = services.attachments.getContractAttachments(contractClassName)
|
||||||
// only switchover if we have both signed and unsigned attachments for the given contract class name
|
// 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 {
|
val attachmentsToUse = attachmentIds.map {
|
||||||
services.attachments.openAttachment(it)?.let { it as ContractAttachment }
|
services.attachments.openAttachment(it)?.let { it as ContractAttachment }
|
||||||
?: throw IllegalArgumentException("Contract attachment $it for $contractClassName is missing.")
|
?: 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 =
|
val outputConstraints =
|
||||||
if (outputHashConstraints.isNotEmpty()) {
|
if (outputHashConstraints.isNotEmpty()) {
|
||||||
log.warn("Switching output states from hash to signed constraints using signers in signed contract attachment given by ${signedAttachment.id}")
|
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())) }
|
val outputsSignatureConstraints = outputHashConstraints.map { it.copy(constraint = SignatureAttachmentConstraint(signedAttachment.signerKeys.first())) }
|
||||||
outputs.addAll(outputsSignatureConstraints)
|
outputs.addAll(outputsSignatureConstraints)
|
||||||
outputs.removeAll(outputHashConstraints)
|
outputs.removeAll(outputHashConstraints)
|
||||||
outputsSignatureConstraints
|
outputsSignatureConstraints
|
||||||
} else outputSignatureConstraints
|
} else outputSignatureConstraints
|
||||||
return Pair(attachmentIds.toSet(), outputConstraints)
|
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. */
|
/** Returns an immutable list of output [TransactionState]s. */
|
||||||
fun outputStates(): List<TransactionState<*>> = ArrayList(outputs)
|
fun outputStates(): List<TransactionState<*>> = ArrayList(outputs)
|
||||||
|
|
||||||
/** Returns an immutable list of [Command]s, grouping by [CommandData] and joining signers. */
|
/** 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<*>> = commands.groupBy { cmd -> cmd.value }.entries.map { (data, cmds) -> Command(data, cmds.flatMap(Command<*>::signers).toSet().toList()) }
|
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
|
* 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) },
|
resolveAttachment = { services.attachments.openAttachment(it) },
|
||||||
resolveStateRefAsSerialized = { resolveStateRefBinaryComponent(it, services) },
|
resolveStateRefAsSerialized = { resolveStateRefBinaryComponent(it, services) },
|
||||||
resolveParameters = {
|
resolveParameters = {
|
||||||
val hashToResolve = it ?: services.networkParametersStorage.defaultHash
|
val hashToResolve = it ?: services.networkParametersService.defaultHash
|
||||||
services.networkParametersStorage.lookup(hashToResolve)
|
services.networkParametersService.lookup(hashToResolve)
|
||||||
},
|
},
|
||||||
resolveContractAttachment = { services.loadContractAttachment(it) }
|
resolveContractAttachment = { services.loadContractAttachment(it) }
|
||||||
)
|
)
|
||||||
@ -130,18 +130,37 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
|||||||
*/
|
*/
|
||||||
@Deprecated("Use toLedgerTransaction(ServicesForTransaction) instead")
|
@Deprecated("Use toLedgerTransaction(ServicesForTransaction) instead")
|
||||||
@Throws(AttachmentResolutionException::class, TransactionResolutionException::class)
|
@Throws(AttachmentResolutionException::class, TransactionResolutionException::class)
|
||||||
@JvmOverloads
|
|
||||||
fun toLedgerTransaction(
|
fun toLedgerTransaction(
|
||||||
resolveIdentity: (PublicKey) -> Party?,
|
resolveIdentity: (PublicKey) -> Party?,
|
||||||
resolveAttachment: (SecureHash) -> Attachment?,
|
resolveAttachment: (SecureHash) -> Attachment?,
|
||||||
resolveStateRef: (StateRef) -> TransactionState<*>?,
|
resolveStateRef: (StateRef) -> TransactionState<*>?,
|
||||||
@Suppress("UNUSED_PARAMETER") resolveContractAttachment: (TransactionState<ContractState>) -> AttachmentId?,
|
@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.
|
|
||||||
): LedgerTransaction {
|
): LedgerTransaction {
|
||||||
// This reverts to serializing the resolved transaction state.
|
// 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
|
// 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(
|
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 */
|
/** 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()
|
val hasEnded: Boolean get() = _changes.hasCompleted() || _changes.hasThrowable()
|
||||||
|
|
||||||
companion object {
|
|
||||||
val DEFAULT_TRACKER = { ProgressTracker() }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// TODO: Expose the concept of errors.
|
// TODO: Expose the concept of errors.
|
||||||
// TODO: It'd be helpful if this class was at least partly thread safe.
|
// 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.KeepForDJVM
|
||||||
import net.corda.core.internal.uncheckedCast
|
import net.corda.core.internal.uncheckedCast
|
||||||
import net.corda.core.serialization.CordaSerializable
|
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
|
* 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> {
|
sealed class Try<out A> {
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* Executes the given block of code and returns a [Success] capturing the result, or a [Failure] if an exception
|
* Executes the given block of code and returns a [Success] capturing the result, or a [Failure] if a [Throwable] is thrown.
|
||||||
* is thrown.
|
*
|
||||||
|
* It is recommended this be chained with [throwError] to ensure critial [Error]s are thrown and not captured.
|
||||||
*/
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
inline fun <T> on(body: () -> T): Try<T> {
|
inline fun <T> on(body: () -> T): Try<T> {
|
||||||
return try {
|
return try {
|
||||||
Success(body())
|
Success(body())
|
||||||
} catch (e: Exception) {
|
} catch (t: Throwable) {
|
||||||
Failure(e)
|
Failure(t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -34,6 +36,15 @@ sealed class Try<out A> {
|
|||||||
/** Returns the value if a [Success] otherwise throws the exception if a [Failure]. */
|
/** Returns the value if a [Success] otherwise throws the exception if a [Failure]. */
|
||||||
abstract fun getOrThrow(): A
|
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]. */
|
/** 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) {
|
inline fun <B> map(function: (A) -> B): Try<B> = when (this) {
|
||||||
is Success -> Success(function(value))
|
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. */
|
/** 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> {
|
fun doOnSuccess(action: Consumer<in A>): Try<A> {
|
||||||
when (this) {
|
if (this is Success) {
|
||||||
is Success -> action.invoke(value)
|
action.accept(value)
|
||||||
is Failure -> {}
|
|
||||||
}
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Applies the given action to the error if [Failure], or does nothing if [Success]. Returns `this` for chaining. */
|
/** 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> {
|
fun doOnFailure(action: Consumer<Throwable>): Try<A> {
|
||||||
when (this) {
|
if (this is Failure) {
|
||||||
is Success -> {}
|
action.accept(exception)
|
||||||
is Failure -> action.invoke(exception)
|
|
||||||
}
|
}
|
||||||
return this
|
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
|
@KeepForDJVM
|
||||||
data class Success<out A>(val value: A) : Try<A>() {
|
data class Success<out A>(val value: A) : Try<A>() {
|
||||||
override val isSuccess: Boolean get() = true
|
override val isSuccess: Boolean get() = true
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
package net.corda.core.flows;
|
package net.corda.core.flows;
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable;
|
import co.paralleluniverse.fibers.Suspendable;
|
||||||
import com.google.common.collect.ImmutableList;
|
|
||||||
import com.google.common.primitives.Primitives;
|
import com.google.common.primitives.Primitives;
|
||||||
import net.corda.core.identity.Party;
|
import net.corda.core.identity.Party;
|
||||||
import net.corda.testing.core.TestConstants;
|
import net.corda.testing.core.TestConstants;
|
||||||
import net.corda.testing.node.MockNetwork;
|
import net.corda.testing.node.MockNetwork;
|
||||||
|
import net.corda.testing.node.MockNetworkParameters;
|
||||||
import net.corda.testing.node.StartedMockNode;
|
import net.corda.testing.node.StartedMockNode;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
@ -15,11 +15,14 @@ import java.util.concurrent.ExecutionException;
|
|||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
import static net.corda.testing.core.TestUtils.singleIdentity;
|
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.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
public class FlowsInJavaTest {
|
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 aliceNode;
|
||||||
private StartedMockNode bobNode;
|
private StartedMockNode bobNode;
|
||||||
private Party bob;
|
private Party bob;
|
||||||
|
@ -83,7 +83,7 @@ class ConstraintsPropagationTests {
|
|||||||
ledgerServices = object : MockServices(
|
ledgerServices = object : MockServices(
|
||||||
cordappPackages = listOf("net.corda.finance.contracts.asset"),
|
cordappPackages = listOf("net.corda.finance.contracts.asset"),
|
||||||
initialIdentity = ALICE,
|
initialIdentity = ALICE,
|
||||||
identityService = rigorousMock<IdentityServiceInternal>().also {
|
identityService = mock<IdentityServiceInternal>().also {
|
||||||
doReturn(ALICE_PARTY).whenever(it).partyFromKey(ALICE_PUBKEY)
|
doReturn(ALICE_PARTY).whenever(it).partyFromKey(ALICE_PUBKEY)
|
||||||
doReturn(BOB_PARTY).whenever(it).partyFromKey(BOB_PUBKEY)
|
doReturn(BOB_PARTY).whenever(it).partyFromKey(BOB_PUBKEY)
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.corda.core.contracts
|
package net.corda.core.contracts
|
||||||
|
|
||||||
import com.nhaarman.mockito_kotlin.doReturn
|
import com.nhaarman.mockito_kotlin.doReturn
|
||||||
|
import com.nhaarman.mockito_kotlin.mock
|
||||||
import com.nhaarman.mockito_kotlin.whenever
|
import com.nhaarman.mockito_kotlin.whenever
|
||||||
import net.corda.core.crypto.Crypto
|
import net.corda.core.crypto.Crypto
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
@ -40,7 +41,7 @@ class PackageOwnershipVerificationTests {
|
|||||||
private val ledgerServices = MockServices(
|
private val ledgerServices = MockServices(
|
||||||
cordappPackages = listOf("net.corda.finance.contracts.asset"),
|
cordappPackages = listOf("net.corda.finance.contracts.asset"),
|
||||||
initialIdentity = ALICE,
|
initialIdentity = ALICE,
|
||||||
identityService = rigorousMock<IdentityServiceInternal>().also {
|
identityService = mock<IdentityServiceInternal>().also {
|
||||||
doReturn(ALICE_PARTY).whenever(it).partyFromKey(ALICE_PUBKEY)
|
doReturn(ALICE_PARTY).whenever(it).partyFromKey(ALICE_PUBKEY)
|
||||||
doReturn(BOB_PARTY).whenever(it).partyFromKey(BOB_PUBKEY)
|
doReturn(BOB_PARTY).whenever(it).partyFromKey(BOB_PUBKEY)
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.corda.core.crypto
|
package net.corda.core.crypto
|
||||||
|
|
||||||
import com.nhaarman.mockito_kotlin.doReturn
|
import com.nhaarman.mockito_kotlin.doReturn
|
||||||
|
import com.nhaarman.mockito_kotlin.mock
|
||||||
import com.nhaarman.mockito_kotlin.whenever
|
import com.nhaarman.mockito_kotlin.whenever
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.crypto.SecureHash.Companion.zeroHash
|
import net.corda.core.crypto.SecureHash.Companion.zeroHash
|
||||||
@ -68,7 +69,7 @@ class PartialMerkleTreeTest {
|
|||||||
testLedger = MockServices(
|
testLedger = MockServices(
|
||||||
cordappPackages = emptyList(),
|
cordappPackages = emptyList(),
|
||||||
initialIdentity = TestIdentity(MEGA_CORP.name),
|
initialIdentity = TestIdentity(MEGA_CORP.name),
|
||||||
identityService = rigorousMock<IdentityServiceInternal>().also {
|
identityService = mock<IdentityServiceInternal>().also {
|
||||||
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
|
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
|
||||||
},
|
},
|
||||||
networkParameters = testNetworkParameters(minimumPlatformVersion = 4, notaries = listOf(NotaryInfo(DUMMY_NOTARY, true)))
|
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.and
|
||||||
import com.natpryce.hamkrest.assertion.assert
|
import com.natpryce.hamkrest.assertion.assert
|
||||||
import net.corda.core.flows.mixins.WithFinality
|
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.identity.Party
|
||||||
import net.corda.core.internal.cordapp.CordappResolver
|
import net.corda.core.internal.cordapp.CordappResolver
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
@ -75,15 +76,29 @@ class FinalityFlowTests : WithFinality {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `allow use of the old API if the CorDapp target version is 3`() {
|
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 oldBob = createBob(cordapps = listOf(tokenOldCordapp()))
|
||||||
val bob = createBob(cordapps = listOf(cordappWithPackages("com.template").copy(targetPlatformVersion = 3)))
|
val stx = aliceNode.issuesCashTo(oldBob)
|
||||||
val stx = aliceNode.issuesCashTo(bob)
|
|
||||||
val resultFuture = CordappResolver.withCordapp(targetPlatformVersion = 3) {
|
val resultFuture = CordappResolver.withCordapp(targetPlatformVersion = 3) {
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
aliceNode.startFlowAndRunNetwork(FinalityFlow(stx)).resultFuture
|
aliceNode.startFlowAndRunNetwork(FinalityFlow(stx)).resultFuture
|
||||||
}
|
}
|
||||||
resultFuture.getOrThrow()
|
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 {
|
private fun createBob(cordapps: List<TestCordappInternal> = emptyList()): TestStartedNode {
|
||||||
@ -100,4 +115,7 @@ class FinalityFlowTests : WithFinality {
|
|||||||
Cash().generateIssue(builder, amount, other, notary)
|
Cash().generateIssue(builder, amount, other, notary)
|
||||||
return services.signInitialTransaction(builder)
|
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()
|
val updatedRefState = updatedRefTx.tx.outRefsOfType<RefState.State>().single()
|
||||||
|
|
||||||
// 4. Try to use the old reference state. This will throw a NotaryException.
|
// 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.
|
// 5. Share the update reference state.
|
||||||
nodes[0].services.startFlow(ShareRefState.Initiator(updatedRefState)).resultFuture.getOrThrow()
|
nodes[0].services.startFlow(ShareRefState.Initiator(updatedRefState)).resultFuture.getOrThrow()
|
||||||
|
@ -17,7 +17,7 @@ import net.corda.testing.node.internal.TestStartedNode
|
|||||||
interface WithFinality : WithMockNet {
|
interface WithFinality : WithMockNet {
|
||||||
//region Operations
|
//region Operations
|
||||||
fun TestStartedNode.finalise(stx: SignedTransaction, vararg recipients: Party): FlowStateMachine<SignedTransaction> {
|
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 {
|
fun TestStartedNode.getValidatedTransaction(stx: SignedTransaction): SignedTransaction {
|
||||||
@ -25,7 +25,7 @@ interface WithFinality : WithMockNet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun CordaRPCOps.finalise(stx: SignedTransaction, vararg recipients: Party): FlowHandle<SignedTransaction> {
|
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
|
//endregion
|
||||||
|
|
||||||
@ -41,11 +41,13 @@ interface WithFinality : WithMockNet {
|
|||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
@StartableByRPC
|
@StartableByRPC
|
||||||
class FinalityInvoker(private val transaction: SignedTransaction,
|
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
|
@Suspendable
|
||||||
override fun call(): SignedTransaction {
|
override fun call(): SignedTransaction {
|
||||||
val sessions = recipients.map(::initiateFlow)
|
val sessions = newRecipients.map(::initiateFlow)
|
||||||
return subFlow(FinalityFlow(transaction, sessions))
|
@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.contracts.DummyContract
|
||||||
import net.corda.testing.core.singleIdentity
|
import net.corda.testing.core.singleIdentity
|
||||||
import net.corda.testing.node.MockNetwork
|
import net.corda.testing.node.MockNetwork
|
||||||
|
import net.corda.testing.node.MockNetworkParameters
|
||||||
import net.corda.testing.node.StartedMockNode
|
import net.corda.testing.node.StartedMockNode
|
||||||
|
import net.corda.testing.node.internal.cordappsForPackages
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@ -36,11 +38,9 @@ class ResolveTransactionsFlowTest {
|
|||||||
private lateinit var miniCorp: Party
|
private lateinit var miniCorp: Party
|
||||||
private lateinit var notary: Party
|
private lateinit var notary: Party
|
||||||
|
|
||||||
private lateinit var rootTx: SignedTransaction
|
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
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
|
notaryNode = mockNet.defaultNotaryNode
|
||||||
megaCorpNode = mockNet.createPartyNode(CordaX500Name("MegaCorp", "London", "GB"))
|
megaCorpNode = mockNet.createPartyNode(CordaX500Name("MegaCorp", "London", "GB"))
|
||||||
miniCorpNode = mockNet.createPartyNode(CordaX500Name("MiniCorp", "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
|
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.crypto.generateKeyPair
|
||||||
import net.corda.core.internal.getPackageOwnerOf
|
import net.corda.core.internal.getPackageOwnerOf
|
||||||
import net.corda.core.utilities.OpaqueBytes
|
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.core.utilities.getOrThrow
|
||||||
import net.corda.finance.DOLLARS
|
import net.corda.finance.DOLLARS
|
||||||
import net.corda.finance.flows.CashIssueFlow
|
import net.corda.finance.flows.CashIssueFlow
|
||||||
|
import net.corda.node.services.config.NotaryConfig
|
||||||
import net.corda.nodeapi.internal.network.NetworkParametersCopier
|
import net.corda.nodeapi.internal.network.NetworkParametersCopier
|
||||||
import net.corda.testing.common.internal.testNetworkParameters
|
import net.corda.testing.common.internal.testNetworkParameters
|
||||||
import net.corda.testing.core.ALICE_NAME
|
import net.corda.testing.core.ALICE_NAME
|
||||||
import net.corda.testing.core.BOB_NAME
|
import net.corda.testing.core.BOB_NAME
|
||||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||||
import net.corda.testing.core.singleIdentity
|
import net.corda.testing.core.singleIdentity
|
||||||
import net.corda.testing.node.MockNetNotaryConfig
|
|
||||||
import net.corda.testing.node.MockNetworkNotarySpec
|
import net.corda.testing.node.MockNetworkNotarySpec
|
||||||
import net.corda.testing.node.MockNetworkParameters
|
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.InternalMockNetwork
|
||||||
import net.corda.testing.node.internal.InternalMockNodeParameters
|
import net.corda.testing.node.internal.InternalMockNodeParameters
|
||||||
import net.corda.testing.node.internal.MOCK_VERSION_INFO
|
import net.corda.testing.node.internal.MOCK_VERSION_INFO
|
||||||
@ -66,8 +67,14 @@ class NetworkParametersTest {
|
|||||||
// Notaries tests
|
// Notaries tests
|
||||||
@Test
|
@Test
|
||||||
fun `choosing notary not specified in network parameters will fail`() {
|
fun `choosing notary not specified in network parameters will fail`() {
|
||||||
val fakeNotary = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME,
|
val fakeNotary = mockNet.createNode(
|
||||||
configOverrides = MockNodeConfigOverrides(notary = MockNetNotaryConfig(validating = false))))
|
InternalMockNodeParameters(
|
||||||
|
legalName = BOB_NAME,
|
||||||
|
configOverrides = {
|
||||||
|
doReturn(NotaryConfig(validating = false)).whenever(it).notary
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
val fakeNotaryId = fakeNotary.info.singleIdentity()
|
val fakeNotaryId = fakeNotary.info.singleIdentity()
|
||||||
val alice = mockNet.createPartyNode(ALICE_NAME)
|
val alice = mockNet.createPartyNode(ALICE_NAME)
|
||||||
assertThat(alice.services.networkMapCache.notaryIdentities).doesNotContain(fakeNotaryId)
|
assertThat(alice.services.networkMapCache.notaryIdentities).doesNotContain(fakeNotaryId)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package net.corda.core.serialization
|
package net.corda.core.serialization
|
||||||
|
|
||||||
|
import com.nhaarman.mockito_kotlin.mock
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.crypto.*
|
import net.corda.core.crypto.*
|
||||||
import net.corda.core.identity.AbstractParty
|
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 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 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 mock implementation with a real one
|
||||||
override fun loadContractAttachment(stateRef: StateRef, forContractClassName: ContractClassName?): Attachment = servicesForResolution.loadContractAttachment(stateRef, forContractClassName)
|
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.serialization.serialize
|
||||||
import net.corda.core.utilities.ByteSequence
|
import net.corda.core.utilities.ByteSequence
|
||||||
import net.corda.core.utilities.OpaqueBytes
|
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.DUMMY_NOTARY_NAME
|
||||||
import net.corda.testing.core.SerializationEnvironmentRule
|
import net.corda.testing.core.SerializationEnvironmentRule
|
||||||
import net.corda.testing.core.TestIdentity
|
import net.corda.testing.core.TestIdentity
|
||||||
@ -26,15 +26,15 @@ import kotlin.test.assertFailsWith
|
|||||||
class AttachmentsClassLoaderSerializationTests {
|
class AttachmentsClassLoaderSerializationTests {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val ISOLATED_CONTRACTS_JAR_PATH: URL = AttachmentsClassLoaderSerializationTests::class.java.getResource("isolated.jar")
|
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"
|
private const val ISOLATED_CONTRACT_CLASS_NAME = "net.corda.isolated.contracts.AnotherDummyContract"
|
||||||
}
|
}
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
@JvmField
|
@JvmField
|
||||||
val testSerialization = SerializationEnvironmentRule()
|
val testSerialization = SerializationEnvironmentRule()
|
||||||
|
|
||||||
val storage = MockAttachmentStorage()
|
private val storage = MockAttachmentStorage()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Can serialize and deserialize with an attachment classloader`() {
|
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.Contract
|
||||||
import net.corda.core.contracts.TransactionVerificationException
|
import net.corda.core.contracts.TransactionVerificationException
|
||||||
import net.corda.core.internal.declaredField
|
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.core.serialization.internal.AttachmentsClassLoader
|
||||||
import net.corda.testing.core.internal.ContractJarTestUtils.signContractJar
|
import net.corda.testing.core.internal.ContractJarTestUtils.signContractJar
|
||||||
import net.corda.testing.internal.fakeAttachment
|
import net.corda.testing.internal.fakeAttachment
|
||||||
|
import net.corda.testing.node.internal.FINANCE_CONTRACTS_CORDAPP
|
||||||
import net.corda.testing.services.MockAttachmentStorage
|
import net.corda.testing.services.MockAttachmentStorage
|
||||||
import org.apache.commons.io.IOUtils
|
import org.apache.commons.io.IOUtils
|
||||||
import org.junit.Assert.assertArrayEquals
|
import org.junit.Assert.assertArrayEquals
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.InputStream
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import kotlin.test.assertFailsWith
|
import kotlin.test.assertFailsWith
|
||||||
|
|
||||||
class AttachmentsClassLoaderTests {
|
class AttachmentsClassLoaderTests {
|
||||||
|
|
||||||
companion object {
|
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 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 const val ISOLATED_CONTRACT_CLASS_NAME = "net.corda.finance.contracts.isolated.AnotherDummyContract"
|
||||||
|
|
||||||
private fun readAttachment(attachment: Attachment, filepath: String): ByteArray {
|
private fun readAttachment(attachment: Attachment, filepath: String): ByteArray {
|
||||||
ByteArrayOutputStream().use {
|
return ByteArrayOutputStream().let {
|
||||||
attachment.extractFile(filepath, it)
|
attachment.extractFile(filepath, it)
|
||||||
return it.toByteArray()
|
it.toByteArray()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val storage = MockAttachmentStorage()
|
private val storage = MockAttachmentStorage()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Loading AnotherDummyContract without using the AttachmentsClassLoader fails`() {
|
fun `Loading AnotherDummyContract without using the AttachmentsClassLoader fails`() {
|
||||||
@ -43,7 +46,7 @@ class AttachmentsClassLoaderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Dynamically load AnotherDummyContract from isolated contracts jar using the AttachmentsClassLoader`() {
|
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 classloader = AttachmentsClassLoader(listOf(storage.openAttachment(isolatedId)!!))
|
||||||
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, classloader)
|
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, classloader)
|
||||||
@ -53,8 +56,8 @@ class AttachmentsClassLoaderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Test non-overlapping contract jar`() {
|
fun `Test non-overlapping contract jar`() {
|
||||||
val att1 = storage.importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
|
val att1 = 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 att2 = importAttachment(ISOLATED_CONTRACTS_JAR_PATH_V4.openStream(), "app", "isolated-4.0.jar")
|
||||||
|
|
||||||
assertFailsWith(TransactionVerificationException.OverlappingAttachmentsException::class) {
|
assertFailsWith(TransactionVerificationException.OverlappingAttachmentsException::class) {
|
||||||
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
|
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
|
||||||
@ -63,9 +66,9 @@ class AttachmentsClassLoaderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Test valid overlapping contract jar`() {
|
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 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
|
// does not throw OverlappingAttachments exception
|
||||||
AttachmentsClassLoader(arrayOf(isolatedId, isolatedSignedId).map { storage.openAttachment(it)!! })
|
AttachmentsClassLoader(arrayOf(isolatedId, isolatedSignedId).map { storage.openAttachment(it)!! })
|
||||||
@ -73,8 +76,8 @@ class AttachmentsClassLoaderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Test non-overlapping different contract jars`() {
|
fun `Test non-overlapping different contract jars`() {
|
||||||
val att1 = storage.importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
|
val att1 = importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
|
||||||
val att2 = storage.importAttachment(FINANCE_JAR_PATH.openStream(), "app", "finance.jar")
|
val att2 = importAttachment(FINANCE_CONTRACTS_CORDAPP.jarFile.inputStream(), "app", "finance.jar")
|
||||||
|
|
||||||
// does not throw OverlappingAttachments exception
|
// does not throw OverlappingAttachments exception
|
||||||
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
|
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
|
||||||
@ -82,8 +85,8 @@ class AttachmentsClassLoaderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Load text resources from AttachmentsClassLoader`() {
|
fun `Load text resources from AttachmentsClassLoader`() {
|
||||||
val att1 = storage.importAttachment(fakeAttachment("file1.txt", "some data").inputStream(), "app", "file1.jar")
|
val att1 = 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 att2 = importAttachment(fakeAttachment("file2.txt", "some other data").inputStream(), "app", "file2.jar")
|
||||||
|
|
||||||
val cl = AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
|
val cl = AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
|
||||||
val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"), Charsets.UTF_8.name())
|
val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"), Charsets.UTF_8.name())
|
||||||
@ -95,8 +98,8 @@ class AttachmentsClassLoaderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Test valid overlapping file condition`() {
|
fun `Test valid overlapping file condition`() {
|
||||||
val att1 = storage.importAttachment(fakeAttachment("file1.txt", "same data").inputStream(), "app", "file1.jar")
|
val att1 = importAttachment(fakeAttachment("file1.txt", "same data").inputStream(), "app", "file1.jar")
|
||||||
val att2 = storage.importAttachment(fakeAttachment("file1.txt", "same data").inputStream(), "app", "file2.jar")
|
val att2 = importAttachment(fakeAttachment("file1.txt", "same data").inputStream(), "app", "file2.jar")
|
||||||
|
|
||||||
val cl = AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
|
val cl = AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
|
||||||
val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"), Charsets.UTF_8.name())
|
val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"), Charsets.UTF_8.name())
|
||||||
@ -106,8 +109,8 @@ class AttachmentsClassLoaderTests {
|
|||||||
@Test
|
@Test
|
||||||
fun `No overlapping exception thrown on certain META-INF files`() {
|
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 ->
|
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 att1 = importAttachment(fakeAttachment(path, "some data").inputStream(), "app", "file1.jar")
|
||||||
val att2 = storage.importAttachment(fakeAttachment(path, "some other data").inputStream(), "app", "file2.jar")
|
val att2 = importAttachment(fakeAttachment(path, "some other data").inputStream(), "app", "file2.jar")
|
||||||
|
|
||||||
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
|
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
|
||||||
}
|
}
|
||||||
@ -115,10 +118,8 @@ class AttachmentsClassLoaderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Check platform independent path handling in attachment jars`() {
|
fun `Check platform independent path handling in attachment jars`() {
|
||||||
val storage = MockAttachmentStorage()
|
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 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 data1a = readAttachment(storage.openAttachment(att1)!!, "/folder1/foldera/file1.txt")
|
val data1a = readAttachment(storage.openAttachment(att1)!!, "/folder1/foldera/file1.txt")
|
||||||
assertArrayEquals("some data".toByteArray(), data1a)
|
assertArrayEquals("some data".toByteArray(), data1a)
|
||||||
@ -132,4 +133,8 @@ class AttachmentsClassLoaderTests {
|
|||||||
val data2b = readAttachment(storage.openAttachment(att2)!!, "/folder1/folderb/file2.txt")
|
val data2b = readAttachment(storage.openAttachment(att2)!!, "/folder1/folderb/file2.txt")
|
||||||
assertArrayEquals("some other data".toByteArray(), data2b)
|
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
|
package net.corda.core.transactions
|
||||||
|
|
||||||
import com.nhaarman.mockito_kotlin.doReturn
|
import com.nhaarman.mockito_kotlin.doReturn
|
||||||
|
import com.nhaarman.mockito_kotlin.mock
|
||||||
import com.nhaarman.mockito_kotlin.whenever
|
import com.nhaarman.mockito_kotlin.whenever
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.crypto.generateKeyPair
|
import net.corda.core.crypto.generateKeyPair
|
||||||
@ -34,7 +35,7 @@ class LedgerTransactionQueryTests {
|
|||||||
private val services = MockServices(
|
private val services = MockServices(
|
||||||
listOf("net.corda.testing.contracts"),
|
listOf("net.corda.testing.contracts"),
|
||||||
TestIdentity(CordaX500Name("MegaCorp", "London", "GB"), keyPair),
|
TestIdentity(CordaX500Name("MegaCorp", "London", "GB"), keyPair),
|
||||||
rigorousMock<IdentityServiceInternal>().also {
|
mock<IdentityServiceInternal>().also {
|
||||||
doReturn(null).whenever(it).partyFromKey(keyPair.public)
|
doReturn(null).whenever(it).partyFromKey(keyPair.public)
|
||||||
},
|
},
|
||||||
testNetworkParameters(notaries = listOf(NotaryInfo(DUMMY_NOTARY, true))),
|
testNetworkParameters(notaries = listOf(NotaryInfo(DUMMY_NOTARY, true))),
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.corda.core.transactions
|
package net.corda.core.transactions
|
||||||
|
|
||||||
import com.nhaarman.mockito_kotlin.doReturn
|
import com.nhaarman.mockito_kotlin.doReturn
|
||||||
|
import com.nhaarman.mockito_kotlin.mock
|
||||||
import com.nhaarman.mockito_kotlin.whenever
|
import com.nhaarman.mockito_kotlin.whenever
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.contracts.Requirements.using
|
import net.corda.core.contracts.Requirements.using
|
||||||
@ -48,7 +49,7 @@ class ReferenceStateTests {
|
|||||||
private val ledgerServices = MockServices(
|
private val ledgerServices = MockServices(
|
||||||
cordappPackages = listOf("net.corda.core.transactions", "net.corda.finance.contracts.asset"),
|
cordappPackages = listOf("net.corda.core.transactions", "net.corda.finance.contracts.asset"),
|
||||||
initialIdentity = ALICE,
|
initialIdentity = ALICE,
|
||||||
identityService = rigorousMock<IdentityServiceInternal>().also {
|
identityService = mock<IdentityServiceInternal>().also {
|
||||||
doReturn(ALICE_PARTY).whenever(it).partyFromKey(ALICE_PUBKEY)
|
doReturn(ALICE_PARTY).whenever(it).partyFromKey(ALICE_PUBKEY)
|
||||||
doReturn(BOB_PARTY).whenever(it).partyFromKey(BOB_PUBKEY)
|
doReturn(BOB_PARTY).whenever(it).partyFromKey(BOB_PUBKEY)
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.corda.core.transactions
|
package net.corda.core.transactions
|
||||||
|
|
||||||
import com.nhaarman.mockito_kotlin.doReturn
|
import com.nhaarman.mockito_kotlin.doReturn
|
||||||
|
import com.nhaarman.mockito_kotlin.mock
|
||||||
import com.nhaarman.mockito_kotlin.whenever
|
import com.nhaarman.mockito_kotlin.whenever
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.cordapp.CordappProvider
|
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.identity.Party
|
||||||
import net.corda.core.internal.AbstractAttachment
|
import net.corda.core.internal.AbstractAttachment
|
||||||
import net.corda.core.internal.PLATFORM_VERSION
|
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.internal.cordapp.CordappImpl.Companion.DEFAULT_CORDAPP_VERSION
|
||||||
import net.corda.core.node.ServicesForResolution
|
import net.corda.core.node.ServicesForResolution
|
||||||
import net.corda.core.node.ZoneVersionTooLowException
|
import net.corda.core.node.ZoneVersionTooLowException
|
||||||
import net.corda.core.node.services.AttachmentStorage
|
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.core.serialization.serialize
|
||||||
import net.corda.testing.common.internal.testNetworkParameters
|
import net.corda.testing.common.internal.testNetworkParameters
|
||||||
import net.corda.testing.contracts.DummyContract
|
import net.corda.testing.contracts.DummyContract
|
||||||
@ -39,14 +39,14 @@ class TransactionBuilderTest {
|
|||||||
private val services = rigorousMock<ServicesForResolution>()
|
private val services = rigorousMock<ServicesForResolution>()
|
||||||
private val contractAttachmentId = SecureHash.randomSHA256()
|
private val contractAttachmentId = SecureHash.randomSHA256()
|
||||||
private val attachments = rigorousMock<AttachmentStorage>()
|
private val attachments = rigorousMock<AttachmentStorage>()
|
||||||
private val networkParametersStorage = rigorousMock<NetworkParametersStorage>()
|
private val networkParametersService = mock<NetworkParametersService>()
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
val cordappProvider = rigorousMock<CordappProvider>()
|
val cordappProvider = rigorousMock<CordappProvider>()
|
||||||
val networkParameters = testNetworkParameters(minimumPlatformVersion = PLATFORM_VERSION)
|
val networkParameters = testNetworkParameters(minimumPlatformVersion = PLATFORM_VERSION)
|
||||||
doReturn(networkParametersStorage).whenever(services).networkParametersStorage
|
doReturn(networkParametersService).whenever(services).networkParametersService
|
||||||
doReturn(networkParameters.serialize().hash).whenever(networkParametersStorage).currentHash
|
doReturn(networkParameters.serialize().hash).whenever(networkParametersService).currentHash
|
||||||
doReturn(cordappProvider).whenever(services).cordappProvider
|
doReturn(cordappProvider).whenever(services).cordappProvider
|
||||||
doReturn(contractAttachmentId).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID)
|
doReturn(contractAttachmentId).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID)
|
||||||
doReturn(networkParameters).whenever(services).networkParameters
|
doReturn(networkParameters).whenever(services).networkParameters
|
||||||
@ -77,7 +77,7 @@ class TransactionBuilderTest {
|
|||||||
val wtx = builder.toWireTransaction(services)
|
val wtx = builder.toWireTransaction(services)
|
||||||
assertThat(wtx.outputs).containsOnly(outputState)
|
assertThat(wtx.outputs).containsOnly(outputState)
|
||||||
assertThat(wtx.commands).containsOnly(Command(DummyCommandData, notary.owningKey))
|
assertThat(wtx.commands).containsOnly(Command(DummyCommandData, notary.owningKey))
|
||||||
assertThat(wtx.networkParametersHash).isEqualTo(networkParametersStorage.currentHash)
|
assertThat(wtx.networkParametersHash).isEqualTo(networkParametersService.currentHash)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -109,6 +109,7 @@ class TransactionBuilderTest {
|
|||||||
.hasMessageContaining("Reference states")
|
.hasMessageContaining("Reference states")
|
||||||
|
|
||||||
doReturn(testNetworkParameters(minimumPlatformVersion = 4)).whenever(services).networkParameters
|
doReturn(testNetworkParameters(minimumPlatformVersion = 4)).whenever(services).networkParameters
|
||||||
|
doReturn(referenceState).whenever(services).loadState(referenceStateRef)
|
||||||
val wtx = builder.toWireTransaction(services)
|
val wtx = builder.toWireTransaction(services)
|
||||||
assertThat(wtx.references).containsOnly(referenceStateRef)
|
assertThat(wtx.references).containsOnly(referenceStateRef)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.corda.core.transactions
|
package net.corda.core.transactions
|
||||||
|
|
||||||
import com.nhaarman.mockito_kotlin.doReturn
|
import com.nhaarman.mockito_kotlin.doReturn
|
||||||
|
import com.nhaarman.mockito_kotlin.mock
|
||||||
import com.nhaarman.mockito_kotlin.whenever
|
import com.nhaarman.mockito_kotlin.whenever
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.identity.AbstractParty
|
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.internal.rigorousMock
|
||||||
import net.corda.testing.node.MockServices
|
import net.corda.testing.node.MockServices
|
||||||
import net.corda.testing.node.ledger
|
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.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@ -56,7 +57,7 @@ class TransactionEncumbranceTests {
|
|||||||
val ledgerServices = MockServices(
|
val ledgerServices = MockServices(
|
||||||
listOf("net.corda.core.transactions", "net.corda.finance.contracts.asset"),
|
listOf("net.corda.core.transactions", "net.corda.finance.contracts.asset"),
|
||||||
MEGA_CORP.name,
|
MEGA_CORP.name,
|
||||||
rigorousMock<IdentityServiceInternal>().also {
|
mock<IdentityServiceInternal>().also {
|
||||||
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
|
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
|
||||||
},
|
},
|
||||||
testNetworkParameters(notaries = listOf(NotaryInfo(DUMMY_NOTARY, true)))
|
testNetworkParameters(notaries = listOf(NotaryInfo(DUMMY_NOTARY, true)))
|
||||||
@ -330,12 +331,13 @@ class TransactionEncumbranceTests {
|
|||||||
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY2, 0, AutomaticHashConstraint)
|
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY2, 0, AutomaticHashConstraint)
|
||||||
.addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey)
|
.addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey)
|
||||||
.toLedgerTransaction(ledgerServices)
|
.toLedgerTransaction(ledgerServices)
|
||||||
|
.verify()
|
||||||
}
|
}
|
||||||
|
|
||||||
// More complex encumbrance (full cycle of size 4) where one of the encumbered states is assigned to a different notary.
|
// 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
|
// 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.
|
// 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 {
|
.isThrownBy {
|
||||||
TransactionBuilder()
|
TransactionBuilder()
|
||||||
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 1, AutomaticHashConstraint)
|
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 1, AutomaticHashConstraint)
|
||||||
@ -344,13 +346,15 @@ class TransactionEncumbranceTests {
|
|||||||
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 2, AutomaticHashConstraint)
|
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 2, AutomaticHashConstraint)
|
||||||
.addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey)
|
.addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey)
|
||||||
.toLedgerTransaction(ledgerServices)
|
.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.
|
// 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
|
// 0 -> 1, 1 -> 0, 2 -> 3, 3 -> 2 where encumbered states with indices 2 and 3, respectively, are assigned
|
||||||
// to different notaries.
|
// to different notaries.
|
||||||
AssertionsForClassTypes.assertThatExceptionOfType(TransactionVerificationException.TransactionNotaryMismatchEncumbranceException::class.java)
|
assertThatExceptionOfType(TransactionVerificationException.TransactionNotaryMismatchEncumbranceException::class.java)
|
||||||
.isThrownBy {
|
.isThrownBy {
|
||||||
TransactionBuilder()
|
TransactionBuilder()
|
||||||
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 1, AutomaticHashConstraint)
|
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 1, AutomaticHashConstraint)
|
||||||
@ -359,7 +363,9 @@ class TransactionEncumbranceTests {
|
|||||||
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY2, 2, AutomaticHashConstraint)
|
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY2, 2, AutomaticHashConstraint)
|
||||||
.addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey)
|
.addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey)
|
||||||
.toLedgerTransaction(ledgerServices)
|
.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 id = SecureHash.randomSHA256()
|
||||||
val timeWindow: TimeWindow? = null
|
val timeWindow: TimeWindow? = null
|
||||||
val privacySalt = PrivacySalt()
|
val privacySalt = PrivacySalt()
|
||||||
|
|
||||||
fun buildTransaction() = LedgerTransaction.create(
|
fun buildTransaction() = LedgerTransaction.create(
|
||||||
inputs,
|
inputs,
|
||||||
outputs,
|
outputs,
|
||||||
@ -184,7 +185,7 @@ class TransactionTests {
|
|||||||
inputStatesContractClassNameToMaxVersion = emptyMap()
|
inputStatesContractClassNameToMaxVersion = emptyMap()
|
||||||
)
|
)
|
||||||
|
|
||||||
assertFailsWith<TransactionVerificationException.NotaryChangeInWrongTransactionType> { buildTransaction() }
|
assertFailsWith<TransactionVerificationException.NotaryChangeInWrongTransactionType> { buildTransaction().verify() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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.ContractState
|
||||||
import net.corda.core.contracts.PartyAndReference
|
import net.corda.core.contracts.PartyAndReference
|
||||||
@ -6,7 +6,7 @@ import net.corda.core.identity.Party
|
|||||||
import net.corda.core.transactions.TransactionBuilder
|
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
|
* We will actually link [AnotherDummyContract] against this interface rather
|
||||||
* than the one inside isolated.jar, which means we won't need to use reflection
|
* than the one inside isolated.jar, which means we won't need to use reflection
|
||||||
* to execute the contract's generateInitial() method.
|
* 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.Assert.*
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import sandbox.java.lang.sandbox
|
import sandbox.java.lang.sandbox
|
||||||
|
import java.text.DecimalFormatSymbols
|
||||||
|
|
||||||
class DJVMTest : TestBase() {
|
class DJVMTest : TestBase() {
|
||||||
|
|
||||||
@ -41,7 +42,8 @@ class DJVMTest : TestBase() {
|
|||||||
stringClass.getMethod("format", stringClass, Array<Any>::class.java)
|
stringClass.getMethod("format", stringClass, Array<Any>::class.java)
|
||||||
.invoke(null, stringOf("%9.4f"), arrayOf(doubleOf(1234.5678))).toString()
|
.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
|
@Test
|
||||||
@ -50,7 +52,8 @@ class DJVMTest : TestBase() {
|
|||||||
stringClass.getMethod("format", stringClass, Array<Any>::class.java)
|
stringClass.getMethod("format", stringClass, Array<Any>::class.java)
|
||||||
.invoke(null, stringOf("%7.2f"), arrayOf(floatOf(1234.5678f))).toString()
|
.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
|
@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
|
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`.
|
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
|
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
|
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) .
|
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
|
Debugging
|
||||||
---------
|
---------
|
||||||
If an attachment constraint cannot be resolved, a ``MissingContractAttachments`` exception is thrown. There are two
|
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
|
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
|
API: Identity
|
||||||
=============
|
=============
|
||||||
|
|
||||||
|
@ -9,24 +9,26 @@ API: Persistence
|
|||||||
|
|
||||||
.. contents::
|
.. contents::
|
||||||
|
|
||||||
Corda offers developers the option to expose all or some part of a contract state to an *Object Relational Mapping*
|
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 RDBMS. The purpose of this is to assist *vault* development by effectively indexing
|
(ORM) tool to be persisted in a *Relational Database Management System* (RDBMS).
|
||||||
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.
|
|
||||||
|
|
||||||
The ORM mapping is specified using the `Java Persistence API <https://en.wikipedia.org/wiki/Java_Persistence_API>`_
|
The purpose of this, is to assist `vault <https://docs.corda.net/vault.html>`_
|
||||||
(JPA) as annotations and is converted to database table rows by the node automatically every time a state is recorded
|
development and allow for the persistence of state data to a custom database table. Persisted states held in the
|
||||||
in the node's local vault as part of a transaction.
|
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
|
The Object Relational Mapping is specified using `Java Persistence API <https://en.wikipedia.org/wiki/Java_Persistence_API>`_
|
||||||
candidate and the node will in the future support a range of database implementations via their JDBC drivers. Much
|
(JPA) annotations. This mapping is persisted to the database as a table row (a single, implicitly structured data item) by the node
|
||||||
of the node internal state is also persisted there. You can access the internal H2 database via JDBC, please see the
|
automatically every time a state is recorded in the node's local vault as part of a transaction.
|
||||||
info in ":doc:`node-administration`" for details.
|
|
||||||
|
.. 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
|
Schemas
|
||||||
-------
|
-------
|
||||||
Every ``ContractState`` can implement the ``QueryableState`` interface if it wishes to be inserted into the node's local
|
Every ``ContractState`` may implement the ``QueryableState`` interface if it wishes to be inserted into a custom table in the node's
|
||||||
database and accessible using SQL.
|
database and made accessible using SQL.
|
||||||
|
|
||||||
.. literalinclude:: ../../core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt
|
.. literalinclude:: ../../core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt
|
||||||
:language: kotlin
|
:language: kotlin
|
||||||
@ -34,12 +36,12 @@ database and accessible using SQL.
|
|||||||
:end-before: DOCEND QueryableState
|
:end-before: DOCEND QueryableState
|
||||||
|
|
||||||
The ``QueryableState`` interface requires the state to enumerate the different relational schemas it supports, for
|
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
|
instance in situations where the schema has evolved. Each relational schema is represented as a ``MappedSchema``
|
||||||
by the ``supportedSchemas()`` method. Once a schema is selected it must generate that representation when requested
|
object returned by the state's ``supportedSchemas`` method.
|
||||||
via the ``generateMappedObject()`` method which is then passed to the ORM.
|
|
||||||
|
|
||||||
Nodes have an internal ``SchemaService`` which decides what to persist and what not by selecting the ``MappedSchema``
|
Nodes have an internal ``SchemaService`` which decides what data to persist by selecting the ``MappedSchema`` to use.
|
||||||
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
|
.. literalinclude:: ../../node/src/main/kotlin/net/corda/node/services/api/SchemaService.kt
|
||||||
:language: kotlin
|
:language: kotlin
|
||||||
@ -51,13 +53,10 @@ to use.
|
|||||||
:start-after: DOCSTART MappedSchema
|
:start-after: DOCSTART MappedSchema
|
||||||
:end-before: DOCEND 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
|
With this framework, the relational view of ledger states can evolve in a controlled fashion in lock-step with internal systems or other
|
||||||
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.
|
||||||
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``.
|
|
||||||
|
|
||||||
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
|
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
|
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.
|
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.
|
preferred version of a schema.
|
||||||
|
|
||||||
The ``SchemaService`` is also responsible for the ``SchemaOptions`` that can be configured for a particular
|
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``.
|
other ``MappedSchema``.
|
||||||
|
|
||||||
.. note:: It is intended that there should be plugin support for the ``SchemaService`` to offer the version upgrading
|
.. note:: It is intended that there should be plugin support for the ``SchemaService`` to offer version upgrading, implementation
|
||||||
and additional schemas as part of Cordapps, and that the active schemas be configurable. However the present
|
of additional schemas, and enable active schemas as being configurable. The present implementation does not include these features
|
||||||
implementation offers none of this and simply results in all versions of all schemas supported by a
|
and simply results in all versions of all schemas supported by a ``QueryableState`` being persisted.
|
||||||
``QueryableState`` being persisted. This will change in due course. Similarly, it does not currently support
|
This will change in due course. Similarly, the service does not currently support
|
||||||
configuring ``SchemaOptions`` but will do so in the future.
|
configuring ``SchemaOptions`` but will do so in the future.
|
||||||
|
|
||||||
Custom schema registration
|
Custom schema registration
|
||||||
--------------------------
|
--------------------------
|
||||||
Custom contract schemas are automatically registered at startup time for CorDapps. The node bootstrap process will scan
|
Custom contract schemas are automatically registered at startup time for CorDapps. The node bootstrap process will scan for states that implement
|
||||||
for schemas (any class that extends the ``MappedSchema`` interface) in the `plugins` configuration directory in your CorDapp jar.
|
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:
|
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
|
Object relational mapping
|
||||||
-------------------------
|
-------------------------
|
||||||
The persisted representation of a ``QueryableState`` should be an instance of a ``PersistentState`` subclass,
|
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
|
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
|
associate a ``StateRef`` with a persisted representation of a ``ContractState`` and allows joining with the set of
|
||||||
unconsumed states in the vault.
|
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
|
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
|
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
|
entities can be included to model these properties where they are more complex, for example collections (:ref:`Persisting Hierarchical Data<persisting-hierarchical-data>`), so
|
||||||
does not have to be *flat*. The ``MappedSchema`` must provide a list of all of the JPA entity classes for that schema
|
the mapping does not have to be *flat*. The ``MappedSchema`` constructor accepts a list of all JPA entity classes for that schema in
|
||||||
in order to initialise the ORM layer.
|
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
|
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.
|
``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,
|
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).
|
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
|
Identity mapping
|
||||||
----------------
|
----------------
|
||||||
Schema entity attributes defined by identity types (``AbstractParty``, ``Party``, ``AnonymousParty``) are automatically
|
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
|
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.
|
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
|
.. 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. See Samples below.
|
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
|
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
|
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
|
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
|
.. 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
|
API: States
|
||||||
===========
|
===========
|
||||||
|
|
||||||
|
@ -22,108 +22,20 @@ A ``MockNetwork`` is created as follows:
|
|||||||
|
|
||||||
.. container:: codeset
|
.. 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 {
|
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/MockNetworkTestsTutorial.java
|
||||||
private lateinit var mockNet: MockNetwork
|
:language: java
|
||||||
|
:start-after: DOCSTART 1
|
||||||
|
:end-before: DOCEND 1
|
||||||
|
|
||||||
@Before
|
The ``MockNetwork`` requires at a minimum a list of CorDapps to be installed on each ``StartedMockNode``. The CorDapps are looked up on the
|
||||||
fun setup() {
|
classpath by package name, using ``TestCordapp.findCordapp``.
|
||||||
network = MockNetwork(listOf("my.cordapp.package", "my.other.cordapp.package"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
``MockNetworkParameters`` provides other properties for the network which can be tweaked. They default to sensible values if not specified.
|
||||||
.. 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()));
|
|
||||||
|
|
||||||
Adding nodes to the network
|
Adding nodes to the network
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
@ -132,73 +44,21 @@ Nodes are created on the ``MockNetwork`` using:
|
|||||||
|
|
||||||
.. container:: codeset
|
.. 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 {
|
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/MockNetworkTestsTutorial.java
|
||||||
private lateinit var mockNet: MockNetwork
|
:language: java
|
||||||
lateinit var nodeA: StartedMockNode
|
:start-after: DOCSTART 2
|
||||||
lateinit var nodeB: StartedMockNode
|
:end-before: DOCEND 2
|
||||||
|
|
||||||
@Before
|
Nodes added using ``createNode`` are provided a default set of node parameters. However, it is also possible to
|
||||||
fun setup() {
|
provide different parameters to each node using ``MockNodeParameters``. Of particular interest are ``configOverrides`` which allow you to
|
||||||
network = MockNetwork(listOf("my.cordapp.package", "my.other.cordapp.package"))
|
override some of the default node configuration options. Please refer to the ``MockNodeConfigOverrides`` class for details what can currently
|
||||||
nodeA = network.createPartyNode()
|
be overridden. Also, the ``additionalCordapps`` parameter allows you to add extra CorDapps to a specific node. This is useful when you wish
|
||||||
// We can optionally give the node a name.
|
for all nodes to load a common CorDapp but for a subset of nodes to load CorDapps specific to their role in the network.
|
||||||
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.
|
|
||||||
|
|
||||||
Running 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
|
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.
|
.. 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
|
.. container:: codeset
|
||||||
|
|
||||||
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt
|
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt
|
||||||
:language: kotlin
|
:language: kotlin
|
||||||
:start-after: DOCSTART 55
|
:start-after: DOCSTART 55
|
||||||
:end-before: DOCEND 55
|
:end-before: DOCEND 55
|
||||||
:dedent: 8
|
:dedent: 8
|
||||||
|
|
||||||
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java
|
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java
|
||||||
:language: java
|
:language: java
|
||||||
:start-after: DOCSTART 55
|
:start-after: DOCSTART 55
|
||||||
:end-before: DOCEND 55
|
:end-before: DOCEND 55
|
||||||
:dedent: 12
|
:dedent: 12
|
||||||
|
|
||||||
**Handling of update races:**
|
**Handling of update races:**
|
||||||
|
|
||||||
|
@ -64,7 +64,8 @@ filter criteria:
|
|||||||
- Use ``queryBy`` to obtain a current snapshot of data (for a given ``QueryCriteria``)
|
- 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``)
|
- 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
|
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
|
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
|
.. sourcecode:: groovy
|
||||||
|
|
||||||
ext.corda_release_version = '4.0'
|
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.kotlin_version = '1.2.71'
|
||||||
ext.quasar_version = '0.7.10'
|
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:
|
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.
|
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).
|
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:
|
As an example, let's take a very simple flow that finalises a transaction without the involvement of a counterpart flow:
|
||||||
|
|
||||||
.. container:: codeset
|
.. 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
|
:end-before: DOCEND SimpleFlowUsingOldApi
|
||||||
:dedent: 4
|
: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`` :
|
passed to ``FinalityFlow`` :
|
||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
@ -175,10 +178,17 @@ to record the finalised transaction:
|
|||||||
:end-before: DOCEND SimpleNewResponderFlow
|
:end-before: DOCEND SimpleNewResponderFlow
|
||||||
:dedent: 4
|
: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
|
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
|
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:
|
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
|
:end-before: DOCEND ExistingResponderFlow
|
||||||
:dedent: 12
|
:dedent: 12
|
||||||
|
|
||||||
The responder flow may be waiting for the finalised transaction to appear in the local node's vault using ``waitForLedgerCommit``.
|
You may already be using ``waitForLedgerCommit`` in your responder flow for the finalised transaction to appear in the local node's vault.
|
||||||
This is no longer necessary with ``ReceiveFinalityFlow`` and the call to ``waitForLedgerCommit`` can be removed.
|
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
|
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).
|
* 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
|
* The experimental confidential-identities is now a separate CorDapp and must now be loaded onto the node alongside any CorDapp that needs it.
|
||||||
whom to exchange the anonymous identity, a ``FlowSession`` to that party is required instead. The flow running on the other side must
|
This also means your gradle dependency for it should be ``cordapp`` and not ``cordaCompile``.
|
||||||
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``.
|
|
||||||
|
|
||||||
* Fixed a bug resulting in poor vault query performance and incorrect results when sorting.
|
* 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)
|
* 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.
|
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.
|
* 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.
|
* 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
|
* ``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
|
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.
|
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
|
* 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.
|
* 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.
|
* 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
|
* 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``
|
* 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
|
* "app", "rpc", "p2p" and "unknown" are no longer allowed as uploader values when importing attachments. These are used
|
||||||
internally in security sensitive code.
|
internally in security sensitive code.
|
||||||
@ -340,6 +311,9 @@ Version 4.0
|
|||||||
* Finance CorDapp is now build as a sealed and signed JAR file.
|
* 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.
|
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 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.
|
* 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
|
`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>`_.
|
`here <https://github.com/corda/spring-webserver>`_.
|
||||||
|
|
||||||
|
.. _clientrpc_connect_ref:
|
||||||
|
|
||||||
Connecting to a node via RPC
|
Connecting to a node via RPC
|
||||||
----------------------------
|
----------------------------
|
||||||
To use `CordaRPCClient`_, you must add ``net.corda:corda-rpc:$corda_release_version`` as a ``cordaCompile`` dependency
|
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
|
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:
|
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.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.jfx**: support for Java FX UI
|
||||||
* **net.corda.client.mock**: client mock utilities
|
* **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