mirror of
https://github.com/corda/corda.git
synced 2025-01-03 19:54:13 +00:00
Merge pull request #52 from corda/enterprise-merge-september-26
Enterprise merge september 26
This commit is contained in:
commit
e393fdd292
18
.github/PULL_REQUEST_TEMPLATE.md
vendored
18
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,15 +1,11 @@
|
|||||||
Thank you for choosing to contribute to Corda.
|
👮🏻👮🏻👮🏻 !!!! DESCRIBE YOUR CHANGES HERE !!!! DO NOT FORGET !!!! 👮🏻👮🏻👮🏻
|
||||||
|
|
||||||
Your PR must be approved by one or more reviewers and all tests must be passed on TeamCity (https://ci.corda.r3cev.com) in order to be merged.
|
|
||||||
|
|
||||||
Once you have submitted a PR you are responsible for keeping it up to date until the time it is merged.
|
# PR Checklist:
|
||||||
|
|
||||||
PR Checklist:
|
- [ ] Have you run the unit, integration and smoke tests as described here? https://docs.corda.net/head/testing.html
|
||||||
|
- [ ] If you added/changed public APIs, did you write/update the JavaDocs?
|
||||||
|
- [ ] If the changes are of interest to application developers, have you added them to the changelog, and potentially release notes?
|
||||||
|
- [ ] If you are contributing for the first time, please read the agreement in CONTRIBUTING.md now and add to this Pull Request that you agree to it.
|
||||||
|
|
||||||
1. Ensure any new code is tested as described in https://docs.corda.net/testing.html
|
Thanks for your code, it's appreciated! :)
|
||||||
2. Ensure you have done any relevant automated testing and manual testing
|
|
||||||
3. Add your changes to docs/source/changelog.rst
|
|
||||||
4. Update any documentation in docs/source relating to your changes and learn how to build them in https://docs.corda.net/building-the-docs.html
|
|
||||||
5. If you are contributing for the first time please read the agreement in CONTRIBUTING.md now and add to this Pull Request that you have read, and agreed to, the agreement.
|
|
||||||
|
|
||||||
Please remove this message when you have read it.
|
|
||||||
|
16
.idea/compiler.xml
generated
16
.idea/compiler.xml
generated
@ -12,6 +12,8 @@
|
|||||||
<module name="buildSrc_test" target="1.8" />
|
<module name="buildSrc_test" target="1.8" />
|
||||||
<module name="client_main" target="1.8" />
|
<module name="client_main" target="1.8" />
|
||||||
<module name="client_test" target="1.8" />
|
<module name="client_test" target="1.8" />
|
||||||
|
<module name="confidential-identities_main" target="1.8" />
|
||||||
|
<module name="confidential-identities_test" target="1.8" />
|
||||||
<module name="corda-project_main" target="1.8" />
|
<module name="corda-project_main" target="1.8" />
|
||||||
<module name="corda-project_test" target="1.8" />
|
<module name="corda-project_test" target="1.8" />
|
||||||
<module name="corda-webserver_integrationTest" target="1.8" />
|
<module name="corda-webserver_integrationTest" target="1.8" />
|
||||||
@ -19,6 +21,9 @@
|
|||||||
<module name="corda-webserver_test" target="1.8" />
|
<module name="corda-webserver_test" target="1.8" />
|
||||||
<module name="cordform-common_main" target="1.8" />
|
<module name="cordform-common_main" target="1.8" />
|
||||||
<module name="cordform-common_test" target="1.8" />
|
<module name="cordform-common_test" target="1.8" />
|
||||||
|
<module name="cordformation_main" target="1.8" />
|
||||||
|
<module name="cordformation_runnodes" target="1.8" />
|
||||||
|
<module name="cordformation_test" target="1.8" />
|
||||||
<module name="core_main" target="1.8" />
|
<module name="core_main" target="1.8" />
|
||||||
<module name="core_test" target="1.8" />
|
<module name="core_test" target="1.8" />
|
||||||
<module name="demobench_main" target="1.8" />
|
<module name="demobench_main" target="1.8" />
|
||||||
@ -72,8 +77,12 @@
|
|||||||
<module name="node_test" target="1.8" />
|
<module name="node_test" target="1.8" />
|
||||||
<module name="notary-demo_main" target="1.8" />
|
<module name="notary-demo_main" target="1.8" />
|
||||||
<module name="notary-demo_test" target="1.8" />
|
<module name="notary-demo_test" target="1.8" />
|
||||||
|
<module name="publish-utils_main" target="1.8" />
|
||||||
|
<module name="publish-utils_test" target="1.8" />
|
||||||
<module name="quasar-hook_main" target="1.8" />
|
<module name="quasar-hook_main" target="1.8" />
|
||||||
<module name="quasar-hook_test" target="1.8" />
|
<module name="quasar-hook_test" target="1.8" />
|
||||||
|
<module name="quasar-utils_main" target="1.8" />
|
||||||
|
<module name="quasar-utils_test" target="1.8" />
|
||||||
<module name="rpc_integrationTest" target="1.8" />
|
<module name="rpc_integrationTest" target="1.8" />
|
||||||
<module name="rpc_main" target="1.8" />
|
<module name="rpc_main" target="1.8" />
|
||||||
<module name="rpc_smokeTest" target="1.8" />
|
<module name="rpc_smokeTest" target="1.8" />
|
||||||
@ -93,6 +102,13 @@
|
|||||||
<module name="simm-valuation-demo_integrationTest" target="1.8" />
|
<module name="simm-valuation-demo_integrationTest" target="1.8" />
|
||||||
<module name="simm-valuation-demo_main" target="1.8" />
|
<module name="simm-valuation-demo_main" target="1.8" />
|
||||||
<module name="simm-valuation-demo_test" target="1.8" />
|
<module name="simm-valuation-demo_test" target="1.8" />
|
||||||
|
<module name="smoke-test-utils_main" target="1.8" />
|
||||||
|
<module name="smoke-test-utils_test" target="1.8" />
|
||||||
|
<module name="test-common_main" target="1.8" />
|
||||||
|
<module name="test-common_test" target="1.8" />
|
||||||
|
<module name="test-utils_integrationTest" target="1.8" />
|
||||||
|
<module name="test-utils_main" target="1.8" />
|
||||||
|
<module name="test-utils_test" target="1.8" />
|
||||||
<module name="testing-node-driver_integrationTest" target="1.8" />
|
<module name="testing-node-driver_integrationTest" target="1.8" />
|
||||||
<module name="testing-node-driver_main" target="1.8" />
|
<module name="testing-node-driver_main" target="1.8" />
|
||||||
<module name="testing-node-driver_test" target="1.8" />
|
<module name="testing-node-driver_test" target="1.8" />
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="explorer" type="TORNADOFX_RUNCONFIGURATION" factoryName="TornadoFX Configuration Factory" run-type="App" live-views="false" live-stylesheets="false" dump-stylesheets="false">
|
<configuration default="false" name="Explorer - GUI" type="TORNADOFX_RUNCONFIGURATION" factoryName="TornadoFX Configuration Factory" run-type="App" live-views="false" live-stylesheets="false" dump-stylesheets="false">
|
||||||
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
||||||
<option name="RUN_TYPE" value="App" />
|
<option name="RUN_TYPE" value="App" />
|
||||||
<option name="VIEW_CLASS_NAME" />
|
<option name="VIEW_CLASS_NAME" />
|
21
build.gradle
21
build.gradle
@ -36,7 +36,7 @@ buildscript {
|
|||||||
ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion")
|
ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion")
|
||||||
ext.fileupload_version = '1.3.2'
|
ext.fileupload_version = '1.3.2'
|
||||||
ext.junit_version = '4.12'
|
ext.junit_version = '4.12'
|
||||||
ext.mockito_version = '1.10.19'
|
ext.mockito_version = '2.10.0'
|
||||||
ext.jopt_simple_version = '5.0.2'
|
ext.jopt_simple_version = '5.0.2'
|
||||||
ext.jansi_version = '1.14'
|
ext.jansi_version = '1.14'
|
||||||
ext.hibernate_version = '5.2.6.Final'
|
ext.hibernate_version = '5.2.6.Final'
|
||||||
@ -146,10 +146,16 @@ allprojects {
|
|||||||
maven { url 'https://jitpack.io' }
|
maven { url 'https://jitpack.io' }
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations.compile {
|
configurations {
|
||||||
// We want to use SLF4J's version of these bindings: jcl-over-slf4j
|
compile {
|
||||||
// Remove any transitive dependency on Apache's version.
|
// We want to use SLF4J's version of these bindings: jcl-over-slf4j
|
||||||
exclude group: 'commons-logging', module: 'commons-logging'
|
// Remove any transitive dependency on Apache's version.
|
||||||
|
exclude group: 'commons-logging', module: 'commons-logging'
|
||||||
|
}
|
||||||
|
runtime {
|
||||||
|
// We never want isolated.jar on classPath, since we want to test jar being dynamically loaded as an attachment
|
||||||
|
exclude module: 'isolated'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,6 +190,7 @@ dependencies {
|
|||||||
cordaRuntime project(':client:mock')
|
cordaRuntime project(':client:mock')
|
||||||
cordaRuntime project(':client:rpc')
|
cordaRuntime project(':client:rpc')
|
||||||
cordaRuntime project(':core')
|
cordaRuntime project(':core')
|
||||||
|
cordaRuntime project(':confidential-identities')
|
||||||
cordaRuntime project(':finance')
|
cordaRuntime project(':finance')
|
||||||
cordaRuntime project(':webserver')
|
cordaRuntime project(':webserver')
|
||||||
testCompile project(':test-utils')
|
testCompile project(':test-utils')
|
||||||
@ -252,7 +259,7 @@ bintrayConfig {
|
|||||||
projectUrl = 'https://github.com/corda/corda'
|
projectUrl = 'https://github.com/corda/corda'
|
||||||
gpgSign = true
|
gpgSign = true
|
||||||
gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE')
|
gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE')
|
||||||
publications = ['corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver', 'corda-node-driver']
|
publications = ['corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver', 'corda-node-driver', 'corda-confidential-identities']
|
||||||
license {
|
license {
|
||||||
name = 'Apache-2.0'
|
name = 'Apache-2.0'
|
||||||
url = 'https://www.apache.org/licenses/LICENSE-2.0'
|
url = 'https://www.apache.org/licenses/LICENSE-2.0'
|
||||||
@ -287,7 +294,7 @@ artifactory {
|
|||||||
password = System.getenv('CORDA_ARTIFACTORY_PASSWORD')
|
password = System.getenv('CORDA_ARTIFACTORY_PASSWORD')
|
||||||
}
|
}
|
||||||
defaults {
|
defaults {
|
||||||
publications('corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'cordform-common', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver', 'corda-node-driver')
|
publications('corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'cordform-common', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver', 'corda-node-driver', 'corda-confidential-identities')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ import net.corda.core.crypto.*
|
|||||||
import net.corda.core.crypto.CompositeKey
|
import net.corda.core.crypto.CompositeKey
|
||||||
import net.corda.core.identity.AbstractParty
|
import net.corda.core.identity.AbstractParty
|
||||||
import net.corda.core.identity.AnonymousParty
|
import net.corda.core.identity.AnonymousParty
|
||||||
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.messaging.CordaRPCOps
|
import net.corda.core.messaging.CordaRPCOps
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
@ -30,7 +31,6 @@ import net.corda.core.utilities.OpaqueBytes
|
|||||||
import net.corda.core.utilities.parsePublicKeyBase58
|
import net.corda.core.utilities.parsePublicKeyBase58
|
||||||
import net.corda.core.utilities.toBase58String
|
import net.corda.core.utilities.toBase58String
|
||||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||||
import org.bouncycastle.asn1.x500.X500Name
|
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -46,25 +46,25 @@ object JacksonSupport {
|
|||||||
// If you change this API please update the docs in the docsite (json.rst)
|
// If you change this API please update the docs in the docsite (json.rst)
|
||||||
|
|
||||||
interface PartyObjectMapper {
|
interface PartyObjectMapper {
|
||||||
fun partyFromX500Name(name: X500Name): Party?
|
fun wellKnownPartyFromX500Name(name: CordaX500Name): Party?
|
||||||
fun partyFromKey(owningKey: PublicKey): Party?
|
fun partyFromKey(owningKey: PublicKey): Party?
|
||||||
fun partiesFromName(query: String): Set<Party>
|
fun partiesFromName(query: String): Set<Party>
|
||||||
}
|
}
|
||||||
|
|
||||||
class RpcObjectMapper(val rpc: CordaRPCOps, factory: JsonFactory, val fuzzyIdentityMatch: Boolean) : PartyObjectMapper, ObjectMapper(factory) {
|
class RpcObjectMapper(val rpc: CordaRPCOps, factory: JsonFactory, val fuzzyIdentityMatch: Boolean) : PartyObjectMapper, ObjectMapper(factory) {
|
||||||
override fun partyFromX500Name(name: X500Name): Party? = rpc.partyFromX500Name(name)
|
override fun wellKnownPartyFromX500Name(name: CordaX500Name): Party? = rpc.wellKnownPartyFromX500Name(name)
|
||||||
override fun partyFromKey(owningKey: PublicKey): Party? = rpc.partyFromKey(owningKey)
|
override fun partyFromKey(owningKey: PublicKey): Party? = rpc.partyFromKey(owningKey)
|
||||||
override fun partiesFromName(query: String) = rpc.partiesFromName(query, fuzzyIdentityMatch)
|
override fun partiesFromName(query: String) = rpc.partiesFromName(query, fuzzyIdentityMatch)
|
||||||
}
|
}
|
||||||
|
|
||||||
class IdentityObjectMapper(val identityService: IdentityService, factory: JsonFactory, val fuzzyIdentityMatch: Boolean) : PartyObjectMapper, ObjectMapper(factory) {
|
class IdentityObjectMapper(val identityService: IdentityService, factory: JsonFactory, val fuzzyIdentityMatch: Boolean) : PartyObjectMapper, ObjectMapper(factory) {
|
||||||
override fun partyFromX500Name(name: X500Name): Party? = identityService.partyFromX500Name(name)
|
override fun wellKnownPartyFromX500Name(name: CordaX500Name): Party? = identityService.wellKnownPartyFromX500Name(name)
|
||||||
override fun partyFromKey(owningKey: PublicKey): Party? = identityService.partyFromKey(owningKey)
|
override fun partyFromKey(owningKey: PublicKey): Party? = identityService.partyFromKey(owningKey)
|
||||||
override fun partiesFromName(query: String) = identityService.partiesFromName(query, fuzzyIdentityMatch)
|
override fun partiesFromName(query: String) = identityService.partiesFromName(query, fuzzyIdentityMatch)
|
||||||
}
|
}
|
||||||
|
|
||||||
class NoPartyObjectMapper(factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) {
|
class NoPartyObjectMapper(factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) {
|
||||||
override fun partyFromX500Name(name: X500Name): Party? = throw UnsupportedOperationException()
|
override fun wellKnownPartyFromX500Name(name: CordaX500Name): Party? = throw UnsupportedOperationException()
|
||||||
override fun partyFromKey(owningKey: PublicKey): Party? = throw UnsupportedOperationException()
|
override fun partyFromKey(owningKey: PublicKey): Party? = throw UnsupportedOperationException()
|
||||||
override fun partiesFromName(query: String) = throw UnsupportedOperationException()
|
override fun partiesFromName(query: String) = throw UnsupportedOperationException()
|
||||||
}
|
}
|
||||||
@ -105,8 +105,8 @@ object JacksonSupport {
|
|||||||
addSerializer(OpaqueBytes::class.java, OpaqueBytesSerializer)
|
addSerializer(OpaqueBytes::class.java, OpaqueBytesSerializer)
|
||||||
|
|
||||||
// For X.500 distinguished names
|
// For X.500 distinguished names
|
||||||
addDeserializer(X500Name::class.java, X500NameDeserializer)
|
addDeserializer(CordaX500Name::class.java, CordaX500NameDeserializer)
|
||||||
addSerializer(X500Name::class.java, X500NameSerializer)
|
addSerializer(CordaX500Name::class.java, CordaX500NameSerializer)
|
||||||
|
|
||||||
// Mixins for transaction types to prevent some properties from being serialized
|
// Mixins for transaction types to prevent some properties from being serialized
|
||||||
setMixInAnnotation(SignedTransaction::class.java, SignedTransactionMixin::class.java)
|
setMixInAnnotation(SignedTransaction::class.java, SignedTransactionMixin::class.java)
|
||||||
@ -191,8 +191,8 @@ object JacksonSupport {
|
|||||||
// Base58 keys never include an equals character, while X.500 names always will, so we use that to determine
|
// Base58 keys never include an equals character, while X.500 names always will, so we use that to determine
|
||||||
// how to parse the content
|
// how to parse the content
|
||||||
return if (parser.text.contains("=")) {
|
return if (parser.text.contains("=")) {
|
||||||
val principal = X500Name(parser.text)
|
val principal = CordaX500Name.parse(parser.text)
|
||||||
mapper.partyFromX500Name(principal) ?: throw JsonParseException(parser, "Could not find a Party with name $principal")
|
mapper.wellKnownPartyFromX500Name(principal) ?: throw JsonParseException(parser, "Could not find a Party with name $principal")
|
||||||
} else {
|
} else {
|
||||||
val nameMatches = mapper.partiesFromName(parser.text)
|
val nameMatches = mapper.partiesFromName(parser.text)
|
||||||
if (nameMatches.isEmpty()) {
|
if (nameMatches.isEmpty()) {
|
||||||
@ -211,22 +211,22 @@ object JacksonSupport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object X500NameSerializer : JsonSerializer<X500Name>() {
|
object CordaX500NameSerializer : JsonSerializer<CordaX500Name>() {
|
||||||
override fun serialize(obj: X500Name, generator: JsonGenerator, provider: SerializerProvider) {
|
override fun serialize(obj: CordaX500Name, generator: JsonGenerator, provider: SerializerProvider) {
|
||||||
generator.writeString(obj.toString())
|
generator.writeString(obj.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object X500NameDeserializer : JsonDeserializer<X500Name>() {
|
object CordaX500NameDeserializer : JsonDeserializer<CordaX500Name>() {
|
||||||
override fun deserialize(parser: JsonParser, context: DeserializationContext): X500Name {
|
override fun deserialize(parser: JsonParser, context: DeserializationContext): CordaX500Name {
|
||||||
if (parser.currentToken == JsonToken.FIELD_NAME) {
|
if (parser.currentToken == JsonToken.FIELD_NAME) {
|
||||||
parser.nextToken()
|
parser.nextToken()
|
||||||
}
|
}
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
X500Name(parser.text)
|
CordaX500Name.parse(parser.text)
|
||||||
} catch(ex: IllegalArgumentException) {
|
} catch(ex: IllegalArgumentException) {
|
||||||
throw JsonParseException(parser, "Invalid X.500 name ${parser.text}: ${ex.message}", ex)
|
throw JsonParseException(parser, "Invalid Corda X.500 name ${parser.text}: ${ex.message}", ex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,24 @@
|
|||||||
package net.corda.client.jackson
|
package net.corda.client.jackson
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.SerializationFeature
|
import com.fasterxml.jackson.databind.SerializationFeature
|
||||||
|
import com.nhaarman.mockito_kotlin.mock
|
||||||
|
import com.nhaarman.mockito_kotlin.whenever
|
||||||
import net.corda.core.contracts.Amount
|
import net.corda.core.contracts.Amount
|
||||||
import net.corda.finance.USD
|
import net.corda.core.cordapp.CordappProvider
|
||||||
import net.corda.core.crypto.Crypto
|
import net.corda.core.crypto.*
|
||||||
import net.corda.core.crypto.SignatureMetadata
|
import net.corda.core.node.ServiceHub
|
||||||
import net.corda.core.crypto.TransactionSignature
|
|
||||||
import net.corda.core.crypto.generateKeyPair
|
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
|
import net.corda.finance.USD
|
||||||
import net.corda.testing.ALICE_PUBKEY
|
import net.corda.testing.ALICE_PUBKEY
|
||||||
import net.corda.testing.DUMMY_NOTARY
|
import net.corda.testing.DUMMY_NOTARY
|
||||||
import net.corda.testing.MINI_CORP
|
import net.corda.testing.MINI_CORP
|
||||||
import net.corda.testing.TestDependencyInjectionBase
|
import net.corda.testing.TestDependencyInjectionBase
|
||||||
import net.corda.testing.contracts.DummyContract
|
import net.corda.testing.contracts.DummyContract
|
||||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||||
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.reflect.jvm.jvmName
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class JacksonSupportTest : TestDependencyInjectionBase() {
|
class JacksonSupportTest : TestDependencyInjectionBase() {
|
||||||
@ -23,6 +26,16 @@ class JacksonSupportTest : TestDependencyInjectionBase() {
|
|||||||
val mapper = JacksonSupport.createNonRpcMapper()
|
val mapper = JacksonSupport.createNonRpcMapper()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private lateinit var services: ServiceHub
|
||||||
|
private lateinit var cordappProvider: CordappProvider
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
services = mock()
|
||||||
|
cordappProvider = mock()
|
||||||
|
whenever(services.cordappProvider).thenReturn(cordappProvider)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun publicKeySerializingWorks() {
|
fun publicKeySerializingWorks() {
|
||||||
val publicKey = generateKeyPair().public
|
val publicKey = generateKeyPair().public
|
||||||
@ -57,8 +70,12 @@ class JacksonSupportTest : TestDependencyInjectionBase() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun writeTransaction() {
|
fun writeTransaction() {
|
||||||
|
val attachmentRef = SecureHash.randomSHA256()
|
||||||
|
whenever(cordappProvider.getContractAttachmentID(DummyContract.PROGRAM_ID))
|
||||||
|
.thenReturn(attachmentRef)
|
||||||
fun makeDummyTx(): SignedTransaction {
|
fun makeDummyTx(): SignedTransaction {
|
||||||
val wtx = DummyContract.generateInitial(1, DUMMY_NOTARY, MINI_CORP.ref(1)).toWireTransaction()
|
val wtx = DummyContract.generateInitial(1, DUMMY_NOTARY, MINI_CORP.ref(1))
|
||||||
|
.toWireTransaction(services)
|
||||||
val signatures = TransactionSignature(
|
val signatures = TransactionSignature(
|
||||||
ByteArray(1),
|
ByteArray(1),
|
||||||
ALICE_PUBKEY,
|
ALICE_PUBKEY,
|
||||||
|
@ -7,9 +7,6 @@ description 'Corda client JavaFX modules'
|
|||||||
|
|
||||||
//noinspection GroovyAssignabilityCheck
|
//noinspection GroovyAssignabilityCheck
|
||||||
configurations {
|
configurations {
|
||||||
// we don't want isolated.jar in classPath, since we want to test jar being dynamically loaded as an attachment
|
|
||||||
runtime.exclude module: 'isolated'
|
|
||||||
|
|
||||||
integrationTestCompile.extendsFrom testCompile
|
integrationTestCompile.extendsFrom testCompile
|
||||||
integrationTestRuntime.extendsFrom testRuntime
|
integrationTestRuntime.extendsFrom testRuntime
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,8 @@ import net.corda.core.crypto.isFulfilledBy
|
|||||||
import net.corda.core.crypto.keys
|
import net.corda.core.crypto.keys
|
||||||
import net.corda.core.flows.FlowInitiator
|
import net.corda.core.flows.FlowInitiator
|
||||||
import net.corda.core.flows.StateMachineRunId
|
import net.corda.core.flows.StateMachineRunId
|
||||||
|
import net.corda.core.identity.CordaX500Name
|
||||||
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.bufferUntilSubscribed
|
import net.corda.core.internal.bufferUntilSubscribed
|
||||||
import net.corda.core.messaging.CordaRPCOps
|
import net.corda.core.messaging.CordaRPCOps
|
||||||
import net.corda.core.messaging.StateMachineTransactionMapping
|
import net.corda.core.messaging.StateMachineTransactionMapping
|
||||||
@ -15,7 +17,6 @@ import net.corda.core.messaging.StateMachineUpdate
|
|||||||
import net.corda.core.messaging.startFlow
|
import net.corda.core.messaging.startFlow
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.node.services.NetworkMapCache
|
import net.corda.core.node.services.NetworkMapCache
|
||||||
import net.corda.core.node.services.ServiceInfo
|
|
||||||
import net.corda.core.node.services.Vault
|
import net.corda.core.node.services.Vault
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.utilities.OpaqueBytes
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
@ -25,21 +26,20 @@ import net.corda.finance.USD
|
|||||||
import net.corda.finance.flows.CashExitFlow
|
import net.corda.finance.flows.CashExitFlow
|
||||||
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.node.services.network.NetworkMapService
|
|
||||||
import net.corda.node.services.FlowPermissions.Companion.startFlowPermission
|
import net.corda.node.services.FlowPermissions.Companion.startFlowPermission
|
||||||
|
import net.corda.nodeapi.internal.ServiceInfo
|
||||||
import net.corda.node.services.transactions.SimpleNotaryService
|
import net.corda.node.services.transactions.SimpleNotaryService
|
||||||
import net.corda.nodeapi.User
|
import net.corda.nodeapi.User
|
||||||
import net.corda.testing.*
|
import net.corda.testing.*
|
||||||
import net.corda.testing.driver.driver
|
import net.corda.testing.driver.driver
|
||||||
import net.corda.testing.node.DriverBasedTest
|
import net.corda.testing.node.DriverBasedTest
|
||||||
import org.bouncycastle.asn1.x500.X500Name
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
class NodeMonitorModelTest : DriverBasedTest() {
|
class NodeMonitorModelTest : DriverBasedTest() {
|
||||||
lateinit var aliceNode: NodeInfo
|
lateinit var aliceNode: NodeInfo
|
||||||
lateinit var bobNode: NodeInfo
|
lateinit var bobNode: NodeInfo
|
||||||
lateinit var notaryNode: NodeInfo
|
lateinit var notaryParty: Party
|
||||||
|
|
||||||
lateinit var rpc: CordaRPCOps
|
lateinit var rpc: CordaRPCOps
|
||||||
lateinit var rpcBob: CordaRPCOps
|
lateinit var rpcBob: CordaRPCOps
|
||||||
@ -50,20 +50,18 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
|||||||
lateinit var transactions: Observable<SignedTransaction>
|
lateinit var transactions: Observable<SignedTransaction>
|
||||||
lateinit var vaultUpdates: Observable<Vault.Update<ContractState>>
|
lateinit var vaultUpdates: Observable<Vault.Update<ContractState>>
|
||||||
lateinit var networkMapUpdates: Observable<NetworkMapCache.MapChange>
|
lateinit var networkMapUpdates: Observable<NetworkMapCache.MapChange>
|
||||||
lateinit var newNode: (X500Name) -> NodeInfo
|
lateinit var newNode: (CordaX500Name) -> NodeInfo
|
||||||
|
|
||||||
override fun setup() = driver {
|
override fun setup() = driver(extraCordappPackagesToScan = listOf("net.corda.finance")) {
|
||||||
val cashUser = User("user1", "test", permissions = setOf(
|
val cashUser = User("user1", "test", permissions = setOf(
|
||||||
startFlowPermission<CashIssueFlow>(),
|
startFlowPermission<CashIssueFlow>(),
|
||||||
startFlowPermission<CashPaymentFlow>(),
|
startFlowPermission<CashPaymentFlow>(),
|
||||||
startFlowPermission<CashExitFlow>())
|
startFlowPermission<CashExitFlow>())
|
||||||
)
|
)
|
||||||
val aliceNodeFuture = startNode(providedName = ALICE.name, rpcUsers = listOf(cashUser))
|
val aliceNodeFuture = startNode(providedName = ALICE.name, rpcUsers = listOf(cashUser))
|
||||||
val notaryNodeFuture = startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
|
val notaryHandle = startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))).getOrThrow()
|
||||||
val aliceNodeHandle = aliceNodeFuture.getOrThrow()
|
val aliceNodeHandle = aliceNodeFuture.getOrThrow()
|
||||||
val notaryNodeHandle = notaryNodeFuture.getOrThrow()
|
|
||||||
aliceNode = aliceNodeHandle.nodeInfo
|
aliceNode = aliceNodeHandle.nodeInfo
|
||||||
notaryNode = notaryNodeHandle.nodeInfo
|
|
||||||
newNode = { nodeName -> startNode(providedName = nodeName).getOrThrow().nodeInfo }
|
newNode = { nodeName -> startNode(providedName = nodeName).getOrThrow().nodeInfo }
|
||||||
val monitor = NodeMonitorModel()
|
val monitor = NodeMonitorModel()
|
||||||
stateMachineTransactionMapping = monitor.stateMachineTransactionMapping.bufferUntilSubscribed()
|
stateMachineTransactionMapping = monitor.stateMachineTransactionMapping.bufferUntilSubscribed()
|
||||||
@ -75,6 +73,7 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
|||||||
|
|
||||||
monitor.register(aliceNodeHandle.configuration.rpcAddress!!, cashUser.username, cashUser.password, initialiseSerialization = false)
|
monitor.register(aliceNodeHandle.configuration.rpcAddress!!, cashUser.username, cashUser.password, initialiseSerialization = false)
|
||||||
rpc = monitor.proxyObservable.value!!
|
rpc = monitor.proxyObservable.value!!
|
||||||
|
notaryParty = notaryHandle.nodeInfo.legalIdentities[1]
|
||||||
|
|
||||||
val bobNodeHandle = startNode(providedName = BOB.name, rpcUsers = listOf(cashUser)).getOrThrow()
|
val bobNodeHandle = startNode(providedName = BOB.name, rpcUsers = listOf(cashUser)).getOrThrow()
|
||||||
bobNode = bobNodeHandle.nodeInfo
|
bobNode = bobNodeHandle.nodeInfo
|
||||||
@ -87,20 +86,20 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `network map update`() {
|
fun `network map update`() {
|
||||||
newNode(CHARLIE.name)
|
val charlieNode = newNode(CHARLIE.name)
|
||||||
networkMapUpdates.filter { !it.node.advertisedServices.any { it.info.type.isNotary() } }
|
val nonServiceIdentities = aliceNode.legalIdentitiesAndCerts + bobNode.legalIdentitiesAndCerts + charlieNode.legalIdentitiesAndCerts
|
||||||
.filter { !it.node.advertisedServices.any { it.info.type == NetworkMapService.type } }
|
networkMapUpdates.filter { it.node.legalIdentitiesAndCerts.any { it in nonServiceIdentities } }
|
||||||
.expectEvents(isStrict = false) {
|
.expectEvents(isStrict = false) {
|
||||||
sequence(
|
sequence(
|
||||||
// TODO : Add test for remove when driver DSL support individual node shutdown.
|
// TODO : Add test for remove when driver DSL support individual node shutdown.
|
||||||
expect { output: NetworkMapCache.MapChange ->
|
expect { output: NetworkMapCache.MapChange ->
|
||||||
require(output.node.legalIdentity.name == ALICE.name) { "Expecting : ${ALICE.name}, Actual : ${output.node.legalIdentity.name}" }
|
require(output.node.chooseIdentity().name == ALICE.name) { "Expecting : ${ALICE.name}, Actual : ${output.node.chooseIdentity().name}" }
|
||||||
},
|
},
|
||||||
expect { output: NetworkMapCache.MapChange ->
|
expect { output: NetworkMapCache.MapChange ->
|
||||||
require(output.node.legalIdentity.name == BOB.name) { "Expecting : ${BOB.name}, Actual : ${output.node.legalIdentity.name}" }
|
require(output.node.chooseIdentity().name == BOB.name) { "Expecting : ${BOB.name}, Actual : ${output.node.chooseIdentity().name}" }
|
||||||
},
|
},
|
||||||
expect { output: NetworkMapCache.MapChange ->
|
expect { output: NetworkMapCache.MapChange ->
|
||||||
require(output.node.legalIdentity.name == CHARLIE.name) { "Expecting : ${CHARLIE.name}, Actual : ${output.node.legalIdentity.name}" }
|
require(output.node.chooseIdentity().name == CHARLIE.name) { "Expecting : ${CHARLIE.name}, Actual : ${output.node.chooseIdentity().name}" }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -111,7 +110,7 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
|||||||
rpc.startFlow(::CashIssueFlow,
|
rpc.startFlow(::CashIssueFlow,
|
||||||
Amount(100, USD),
|
Amount(100, USD),
|
||||||
OpaqueBytes(ByteArray(1, { 1 })),
|
OpaqueBytes(ByteArray(1, { 1 })),
|
||||||
notaryNode.notaryIdentity
|
notaryParty
|
||||||
)
|
)
|
||||||
|
|
||||||
vaultUpdates.expectEvents(isStrict = false) {
|
vaultUpdates.expectEvents(isStrict = false) {
|
||||||
@ -132,9 +131,8 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `cash issue and move`() {
|
fun `cash issue and move`() {
|
||||||
val anonymous = false
|
val (_, issueIdentity) = rpc.startFlow(::CashIssueFlow, 100.DOLLARS, OpaqueBytes.of(1), notaryParty).returnValue.getOrThrow()
|
||||||
rpc.startFlow(::CashIssueFlow, 100.DOLLARS, OpaqueBytes.of(1), notaryNode.notaryIdentity).returnValue.getOrThrow()
|
rpc.startFlow(::CashPaymentFlow, 100.DOLLARS, bobNode.chooseIdentity()).returnValue.getOrThrow()
|
||||||
rpc.startFlow(::CashPaymentFlow, 100.DOLLARS, bobNode.legalIdentity, anonymous).returnValue.getOrThrow()
|
|
||||||
|
|
||||||
var issueSmId: StateMachineRunId? = null
|
var issueSmId: StateMachineRunId? = null
|
||||||
var moveSmId: StateMachineRunId? = null
|
var moveSmId: StateMachineRunId? = null
|
||||||
@ -152,7 +150,7 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
|||||||
require(remove.id == issueSmId)
|
require(remove.id == issueSmId)
|
||||||
},
|
},
|
||||||
// MOVE - N.B. There are other framework flows that happen in parallel for the remote resolve transactions flow
|
// MOVE - N.B. There are other framework flows that happen in parallel for the remote resolve transactions flow
|
||||||
expect(match = { it is StateMachineUpdate.Added && it.stateMachineInfo.flowLogicClassName == CashPaymentFlow::class.java.name }) { add: StateMachineUpdate.Added ->
|
expect(match = { it.stateMachineInfo.flowLogicClassName == CashPaymentFlow::class.java.name }) { add: StateMachineUpdate.Added ->
|
||||||
moveSmId = add.id
|
moveSmId = add.id
|
||||||
val initiator = add.stateMachineInfo.initiator
|
val initiator = add.stateMachineInfo.initiator
|
||||||
require(initiator is FlowInitiator.RPC && initiator.username == "user1")
|
require(initiator is FlowInitiator.RPC && initiator.username == "user1")
|
||||||
@ -167,7 +165,7 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
|||||||
// MOVE
|
// MOVE
|
||||||
expect { add: StateMachineUpdate.Added ->
|
expect { add: StateMachineUpdate.Added ->
|
||||||
val initiator = add.stateMachineInfo.initiator
|
val initiator = add.stateMachineInfo.initiator
|
||||||
require(initiator is FlowInitiator.Peer && initiator.party.name == aliceNode.legalIdentity.name)
|
require(initiator is FlowInitiator.Peer && aliceNode.isLegalIdentity(initiator.party))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -180,7 +178,7 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
|||||||
require(stx.tx.outputs.size == 1)
|
require(stx.tx.outputs.size == 1)
|
||||||
val signaturePubKeys = stx.sigs.map { it.by }.toSet()
|
val signaturePubKeys = stx.sigs.map { it.by }.toSet()
|
||||||
// Only Alice signed
|
// Only Alice signed
|
||||||
val aliceKey = aliceNode.legalIdentity.owningKey
|
val aliceKey = aliceNode.chooseIdentity().owningKey
|
||||||
require(signaturePubKeys.size <= aliceKey.keys.size)
|
require(signaturePubKeys.size <= aliceKey.keys.size)
|
||||||
require(aliceKey.isFulfilledBy(signaturePubKeys))
|
require(aliceKey.isFulfilledBy(signaturePubKeys))
|
||||||
issueTx = stx
|
issueTx = stx
|
||||||
@ -191,8 +189,8 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
|||||||
require(stx.tx.outputs.size == 1)
|
require(stx.tx.outputs.size == 1)
|
||||||
val signaturePubKeys = stx.sigs.map { it.by }.toSet()
|
val signaturePubKeys = stx.sigs.map { it.by }.toSet()
|
||||||
// Alice and Notary signed
|
// Alice and Notary signed
|
||||||
require(aliceNode.legalIdentity.owningKey.isFulfilledBy(signaturePubKeys))
|
require(issueIdentity!!.owningKey.isFulfilledBy(signaturePubKeys))
|
||||||
require(notaryNode.notaryIdentity.owningKey.isFulfilledBy(signaturePubKeys))
|
require(notaryParty.owningKey.isFulfilledBy(signaturePubKeys))
|
||||||
moveTx = stx
|
moveTx = stx
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -11,20 +11,16 @@ import rx.Observer
|
|||||||
import rx.subjects.Subject
|
import rx.subjects.Subject
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
import kotlin.reflect.KProperty
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This file defines a global [Models] store and delegates to inject event streams/sinks. Note that all streams here
|
* Global models store to allow decoupling of UI logic from stream initialisation and provide a central place to
|
||||||
* are global.
|
* inspect data flows. It also allows detecting of looping logic by constructing a stream dependency graph TODO do this.
|
||||||
*
|
|
||||||
* This allows decoupling of UI logic from stream initialisation and provides us with a central place to inspect data
|
|
||||||
* flows. It also allows detecting of looping logic by constructing a stream dependency graph TODO do this.
|
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* // Inject service -> client event stream
|
* // Inject service -> client event stream
|
||||||
* private val serviceToClient: EventStream<ServiceToClientEvent> by eventStream(WalletMonitorModel::serviceToClient)
|
* private val serviceToClient: EventStream<ServiceToClientEvent> by Models.eventStream(WalletMonitorModel::serviceToClient)
|
||||||
*
|
*
|
||||||
* Each Screen code should have a code layout like this:
|
* Each `Screen` code should have a code layout like this:
|
||||||
*
|
*
|
||||||
* class Screen {
|
* class Screen {
|
||||||
* val root = (..)
|
* val root = (..)
|
||||||
@ -41,12 +37,13 @@ import kotlin.reflect.KProperty
|
|||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* For example if I wanted to display a list of all USD cash states:
|
* For example if I wanted to display a list of all USD cash states:
|
||||||
|
*
|
||||||
* class USDCashStatesScreen {
|
* class USDCashStatesScreen {
|
||||||
* val root: Pane by fxml()
|
* val root: Pane by fxml()
|
||||||
*
|
*
|
||||||
* val usdCashStatesListView: ListView<Cash.State> by fxid("USDCashStatesListView")
|
* val usdCashStatesListView: ListView<Cash.State> by fxid("USDCashStatesListView")
|
||||||
*
|
*
|
||||||
* val cashStates: ObservableList<Cash.State> by observableList(ContractStateModel::cashStates)
|
* val cashStates: ObservableList<Cash.State> by Models.observableList(ContractStateModel::cashStates)
|
||||||
*
|
*
|
||||||
* val usdCashStates = cashStates.filter { it.(..).currency == USD }
|
* val usdCashStates = cashStates.filter { it.(..).currency == USD }
|
||||||
*
|
*
|
||||||
@ -66,37 +63,6 @@ import kotlin.reflect.KProperty
|
|||||||
* Another advantage of this separation is that once we start adding a lot of screens we can still track data dependencies
|
* Another advantage of this separation is that once we start adding a lot of screens we can still track data dependencies
|
||||||
* in a central place as opposed to ad-hoc wiring up the observables.
|
* in a central place as opposed to ad-hoc wiring up the observables.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
inline fun <reified M : Any, T> observable(noinline observableProperty: (M) -> Observable<T>) =
|
|
||||||
TrackedDelegate.ObservableDelegate(M::class, observableProperty)
|
|
||||||
|
|
||||||
inline fun <reified M : Any, T> observer(noinline observerProperty: (M) -> Observer<T>) =
|
|
||||||
TrackedDelegate.ObserverDelegate(M::class, observerProperty)
|
|
||||||
|
|
||||||
inline fun <reified M : Any, T> subject(noinline subjectProperty: (M) -> Subject<T, T>) =
|
|
||||||
TrackedDelegate.SubjectDelegate(M::class, subjectProperty)
|
|
||||||
|
|
||||||
inline fun <reified M : Any, T> eventStream(noinline streamProperty: (M) -> EventStream<T>) =
|
|
||||||
TrackedDelegate.EventStreamDelegate(M::class, streamProperty)
|
|
||||||
|
|
||||||
inline fun <reified M : Any, T> eventSink(noinline sinkProperty: (M) -> EventSink<T>) =
|
|
||||||
TrackedDelegate.EventSinkDelegate(M::class, sinkProperty)
|
|
||||||
|
|
||||||
inline fun <reified M : Any, T> observableValue(noinline observableValueProperty: (M) -> ObservableValue<T>) =
|
|
||||||
TrackedDelegate.ObservableValueDelegate(M::class, observableValueProperty)
|
|
||||||
|
|
||||||
inline fun <reified M : Any, T> writableValue(noinline writableValueProperty: (M) -> WritableValue<T>) =
|
|
||||||
TrackedDelegate.WritableValueDelegate(M::class, writableValueProperty)
|
|
||||||
|
|
||||||
inline fun <reified M : Any, T> objectProperty(noinline objectProperty: (M) -> ObjectProperty<T>) =
|
|
||||||
TrackedDelegate.ObjectPropertyDelegate(M::class, objectProperty)
|
|
||||||
|
|
||||||
inline fun <reified M : Any, T> observableList(noinline observableListProperty: (M) -> ObservableList<T>) =
|
|
||||||
TrackedDelegate.ObservableListDelegate(M::class, observableListProperty)
|
|
||||||
|
|
||||||
inline fun <reified M : Any, T> observableListReadOnly(noinline observableListProperty: (M) -> ObservableList<out T>) =
|
|
||||||
TrackedDelegate.ObservableListReadOnlyDelegate(M::class, observableListProperty)
|
|
||||||
|
|
||||||
object Models {
|
object Models {
|
||||||
private val modelStore = HashMap<KClass<*>, Any>()
|
private val modelStore = HashMap<KClass<*>, Any>()
|
||||||
|
|
||||||
@ -120,68 +86,3 @@ object Models {
|
|||||||
inline fun <reified M : Any> get(origin: KClass<*>): M = get(M::class, origin)
|
inline fun <reified M : Any> get(origin: KClass<*>): M = get(M::class, origin)
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class TrackedDelegate<M : Any>(val klass: KClass<M>) {
|
|
||||||
init {
|
|
||||||
Models.initModel(klass)
|
|
||||||
}
|
|
||||||
|
|
||||||
class ObservableDelegate<M : Any, T>(klass: KClass<M>, val observableProperty: (M) -> Observable<T>) : TrackedDelegate<M>(klass) {
|
|
||||||
operator fun getValue(thisRef: Any, property: KProperty<*>): Observable<T> {
|
|
||||||
return observableProperty(Models.get(klass, thisRef.javaClass.kotlin))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ObserverDelegate<M : Any, T>(klass: KClass<M>, val observerProperty: (M) -> Observer<T>) : TrackedDelegate<M>(klass) {
|
|
||||||
operator fun getValue(thisRef: Any, property: KProperty<*>): Observer<T> {
|
|
||||||
return observerProperty(Models.get(klass, thisRef.javaClass.kotlin))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SubjectDelegate<M : Any, T>(klass: KClass<M>, val subjectProperty: (M) -> Subject<T, T>) : TrackedDelegate<M>(klass) {
|
|
||||||
operator fun getValue(thisRef: Any, property: KProperty<*>): Subject<T, T> {
|
|
||||||
return subjectProperty(Models.get(klass, thisRef.javaClass.kotlin))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class EventStreamDelegate<M : Any, T>(klass: KClass<M>, val eventStreamProperty: (M) -> org.reactfx.EventStream<T>) : TrackedDelegate<M>(klass) {
|
|
||||||
operator fun getValue(thisRef: Any, property: KProperty<*>): org.reactfx.EventStream<T> {
|
|
||||||
return eventStreamProperty(Models.get(klass, thisRef.javaClass.kotlin))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class EventSinkDelegate<M : Any, T>(klass: KClass<M>, val eventSinkProperty: (M) -> org.reactfx.EventSink<T>) : TrackedDelegate<M>(klass) {
|
|
||||||
operator fun getValue(thisRef: Any, property: KProperty<*>): org.reactfx.EventSink<T> {
|
|
||||||
return eventSinkProperty(Models.get(klass, thisRef.javaClass.kotlin))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ObservableValueDelegate<M : Any, T>(klass: KClass<M>, val observableValueProperty: (M) -> ObservableValue<T>) : TrackedDelegate<M>(klass) {
|
|
||||||
operator fun getValue(thisRef: Any, property: KProperty<*>): ObservableValue<T> {
|
|
||||||
return observableValueProperty(Models.get(klass, thisRef.javaClass.kotlin))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WritableValueDelegate<M : Any, T>(klass: KClass<M>, val writableValueProperty: (M) -> WritableValue<T>) : TrackedDelegate<M>(klass) {
|
|
||||||
operator fun getValue(thisRef: Any, property: KProperty<*>): WritableValue<T> {
|
|
||||||
return writableValueProperty(Models.get(klass, thisRef.javaClass.kotlin))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ObservableListDelegate<M : Any, T>(klass: KClass<M>, val observableListProperty: (M) -> ObservableList<T>) : TrackedDelegate<M>(klass) {
|
|
||||||
operator fun getValue(thisRef: Any, property: KProperty<*>): ObservableList<T> {
|
|
||||||
return observableListProperty(Models.get(klass, thisRef.javaClass.kotlin))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ObservableListReadOnlyDelegate<M : Any, out T>(klass: KClass<M>, val observableListReadOnlyProperty: (M) -> ObservableList<out T>) : TrackedDelegate<M>(klass) {
|
|
||||||
operator fun getValue(thisRef: Any, property: KProperty<*>): ObservableList<out T> {
|
|
||||||
return observableListReadOnlyProperty(Models.get(klass, thisRef.javaClass.kotlin))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ObjectPropertyDelegate<M : Any, T>(klass: KClass<M>, val objectPropertyProperty: (M) -> ObjectProperty<T>) : TrackedDelegate<M>(klass) {
|
|
||||||
operator fun getValue(thisRef: Any, property: KProperty<*>): ObjectProperty<T> {
|
|
||||||
return objectPropertyProperty(Models.get(klass, thisRef.javaClass.kotlin))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
@file:JvmName("ModelsUtils")
|
||||||
|
package net.corda.client.jfx.model
|
||||||
|
|
||||||
|
import javafx.beans.property.ObjectProperty
|
||||||
|
import javafx.beans.value.ObservableValue
|
||||||
|
import javafx.beans.value.WritableValue
|
||||||
|
import javafx.collections.ObservableList
|
||||||
|
import org.reactfx.EventSink
|
||||||
|
import org.reactfx.EventStream
|
||||||
|
import rx.Observable
|
||||||
|
import rx.Observer
|
||||||
|
import rx.subjects.Subject
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
inline fun <reified M : Any, T> observable(noinline observableProperty: (M) -> Observable<T>) =
|
||||||
|
TrackedDelegate.ObservableDelegate(M::class, observableProperty)
|
||||||
|
|
||||||
|
inline fun <reified M : Any, T> observer(noinline observerProperty: (M) -> Observer<T>) =
|
||||||
|
TrackedDelegate.ObserverDelegate(M::class, observerProperty)
|
||||||
|
|
||||||
|
inline fun <reified M : Any, T> subject(noinline subjectProperty: (M) -> Subject<T, T>) =
|
||||||
|
TrackedDelegate.SubjectDelegate(M::class, subjectProperty)
|
||||||
|
|
||||||
|
inline fun <reified M : Any, T> eventStream(noinline streamProperty: (M) -> EventStream<T>) =
|
||||||
|
TrackedDelegate.EventStreamDelegate(M::class, streamProperty)
|
||||||
|
|
||||||
|
inline fun <reified M : Any, T> eventSink(noinline sinkProperty: (M) -> EventSink<T>) =
|
||||||
|
TrackedDelegate.EventSinkDelegate(M::class, sinkProperty)
|
||||||
|
|
||||||
|
inline fun <reified M : Any, T> observableValue(noinline observableValueProperty: (M) -> ObservableValue<T>) =
|
||||||
|
TrackedDelegate.ObservableValueDelegate(M::class, observableValueProperty)
|
||||||
|
|
||||||
|
inline fun <reified M : Any, T> writableValue(noinline writableValueProperty: (M) -> WritableValue<T>) =
|
||||||
|
TrackedDelegate.WritableValueDelegate(M::class, writableValueProperty)
|
||||||
|
|
||||||
|
inline fun <reified M : Any, T> objectProperty(noinline objectProperty: (M) -> ObjectProperty<T>) =
|
||||||
|
TrackedDelegate.ObjectPropertyDelegate(M::class, objectProperty)
|
||||||
|
|
||||||
|
inline fun <reified M : Any, T> observableList(noinline observableListProperty: (M) -> ObservableList<T>) =
|
||||||
|
TrackedDelegate.ObservableListDelegate(M::class, observableListProperty)
|
||||||
|
|
||||||
|
inline fun <reified M : Any, T> observableListReadOnly(noinline observableListProperty: (M) -> ObservableList<out T>) =
|
||||||
|
TrackedDelegate.ObservableListReadOnlyDelegate(M::class, observableListProperty)
|
@ -5,18 +5,20 @@ import com.google.common.cache.CacheLoader
|
|||||||
import javafx.beans.value.ObservableValue
|
import javafx.beans.value.ObservableValue
|
||||||
import javafx.collections.FXCollections
|
import javafx.collections.FXCollections
|
||||||
import javafx.collections.ObservableList
|
import javafx.collections.ObservableList
|
||||||
|
import net.corda.client.jfx.utils.filterNotNull
|
||||||
import net.corda.client.jfx.utils.fold
|
import net.corda.client.jfx.utils.fold
|
||||||
import net.corda.client.jfx.utils.map
|
import net.corda.client.jfx.utils.map
|
||||||
import net.corda.core.identity.AbstractParty
|
|
||||||
import net.corda.core.identity.AnonymousParty
|
import net.corda.core.identity.AnonymousParty
|
||||||
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.node.services.NetworkMapCache.MapChange
|
import net.corda.core.node.services.NetworkMapCache.MapChange
|
||||||
|
import net.corda.nodeapi.internal.ServiceType
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
|
||||||
class NetworkIdentityModel {
|
class NetworkIdentityModel {
|
||||||
private val networkIdentityObservable by observable(NodeMonitorModel::networkMap)
|
private val networkIdentityObservable by observable(NodeMonitorModel::networkMap)
|
||||||
|
|
||||||
val networkIdentities: ObservableList<NodeInfo> =
|
private val networkIdentities: ObservableList<NodeInfo> =
|
||||||
networkIdentityObservable.fold(FXCollections.observableArrayList()) { list, update ->
|
networkIdentityObservable.fold(FXCollections.observableArrayList()) { list, update ->
|
||||||
list.removeIf {
|
list.removeIf {
|
||||||
when (update) {
|
when (update) {
|
||||||
@ -31,19 +33,17 @@ class NetworkIdentityModel {
|
|||||||
private val rpcProxy by observableValue(NodeMonitorModel::proxyObservable)
|
private val rpcProxy by observableValue(NodeMonitorModel::proxyObservable)
|
||||||
|
|
||||||
private val identityCache = CacheBuilder.newBuilder()
|
private val identityCache = CacheBuilder.newBuilder()
|
||||||
.build<PublicKey, ObservableValue<NodeInfo?>>(CacheLoader.from {
|
.build<PublicKey, ObservableValue<NodeInfo?>>(CacheLoader.from { publicKey ->
|
||||||
publicKey ->
|
publicKey?.let { rpcProxy.map { it?.nodeInfoFromParty(AnonymousParty(publicKey)) } }
|
||||||
publicKey?.let { rpcProxy.map { it?.nodeIdentityFromParty(AnonymousParty(publicKey)) } }
|
|
||||||
})
|
})
|
||||||
|
|
||||||
val parties: ObservableList<NodeInfo> = networkIdentities.filtered { !it.isCordaService() }
|
val notaries: ObservableList<Party> = networkIdentities.map {
|
||||||
val notaries: ObservableList<NodeInfo> = networkIdentities.filtered { it.advertisedServices.any { it.info.type.isNotary() } }
|
it.legalIdentitiesAndCerts.find { it.name.commonName?.let { ServiceType.parse(it).isNotary() } ?: false }
|
||||||
val myIdentity = rpcProxy.map { it?.nodeIdentity() }
|
}.map { it?.party }.filterNotNull()
|
||||||
|
|
||||||
private fun NodeInfo.isCordaService(): Boolean {
|
val notaryNodes: ObservableList<NodeInfo> = notaries.map { rpcProxy.value?.nodeInfoFromParty(it) }.filterNotNull()
|
||||||
// TODO: better way to identify Corda service?
|
val parties: ObservableList<NodeInfo> = networkIdentities.filtered { it.legalIdentities.all { it !in notaries } }
|
||||||
return advertisedServices.any { it.info.type.isNetworkMap() || it.info.type.isNotary() }
|
val myIdentity = rpcProxy.map { it?.nodeInfo()?.legalIdentitiesAndCerts?.first()?.party }
|
||||||
}
|
|
||||||
|
|
||||||
fun partyFromPublicKey(publicKey: PublicKey): ObservableValue<NodeInfo?> = identityCache[publicKey]
|
fun partyFromPublicKey(publicKey: PublicKey): ObservableValue<NodeInfo?> = identityCache[publicKey]
|
||||||
}
|
}
|
||||||
|
@ -5,13 +5,17 @@ import net.corda.client.rpc.CordaRPCClient
|
|||||||
import net.corda.client.rpc.CordaRPCClientConfiguration
|
import net.corda.client.rpc.CordaRPCClientConfiguration
|
||||||
import net.corda.core.contracts.ContractState
|
import net.corda.core.contracts.ContractState
|
||||||
import net.corda.core.flows.StateMachineRunId
|
import net.corda.core.flows.StateMachineRunId
|
||||||
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.messaging.*
|
import net.corda.core.messaging.*
|
||||||
import net.corda.core.node.services.NetworkMapCache.MapChange
|
import net.corda.core.node.services.NetworkMapCache.MapChange
|
||||||
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.DEFAULT_PAGE_NUM
|
||||||
import net.corda.core.utilities.seconds
|
import net.corda.core.node.services.vault.MAX_PAGE_SIZE
|
||||||
|
import net.corda.core.node.services.vault.PageSpecification
|
||||||
|
import net.corda.core.node.services.vault.QueryCriteria
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
|
import net.corda.core.utilities.seconds
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.subjects.PublishSubject
|
import rx.subjects.PublishSubject
|
||||||
|
|
||||||
@ -45,6 +49,7 @@ class NodeMonitorModel {
|
|||||||
val networkMap: Observable<MapChange> = networkMapSubject
|
val networkMap: Observable<MapChange> = networkMapSubject
|
||||||
|
|
||||||
val proxyObservable = SimpleObjectProperty<CordaRPCOps?>()
|
val proxyObservable = SimpleObjectProperty<CordaRPCOps?>()
|
||||||
|
lateinit var notaryIdentities: List<Party>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register for updates to/from a given vault.
|
* Register for updates to/from a given vault.
|
||||||
@ -60,6 +65,7 @@ class NodeMonitorModel {
|
|||||||
)
|
)
|
||||||
val connection = client.start(username, password)
|
val connection = client.start(username, password)
|
||||||
val proxy = connection.proxy
|
val proxy = connection.proxy
|
||||||
|
notaryIdentities = proxy.notaryIdentities()
|
||||||
|
|
||||||
val (stateMachines, stateMachineUpdates) = proxy.stateMachinesFeed()
|
val (stateMachines, stateMachineUpdates) = proxy.stateMachinesFeed()
|
||||||
// Extract the flow tracking stream
|
// Extract the flow tracking stream
|
||||||
@ -83,8 +89,13 @@ class NodeMonitorModel {
|
|||||||
stateMachineUpdates.startWith(currentStateMachines).subscribe(stateMachineUpdatesSubject)
|
stateMachineUpdates.startWith(currentStateMachines).subscribe(stateMachineUpdatesSubject)
|
||||||
|
|
||||||
// Vault snapshot (force single page load with MAX_PAGE_SIZE) + updates
|
// Vault snapshot (force single page load with MAX_PAGE_SIZE) + updates
|
||||||
val (vaultSnapshot, vaultUpdates) = proxy.vaultTrackBy<ContractState>(QueryCriteria.VaultQueryCriteria(Vault.StateStatus.ALL),
|
val (_, vaultUpdates) = proxy.vaultTrackBy<ContractState>(QueryCriteria.VaultQueryCriteria(Vault.StateStatus.ALL),
|
||||||
PageSpecification(DEFAULT_PAGE_NUM, MAX_PAGE_SIZE))
|
PageSpecification(DEFAULT_PAGE_NUM, MAX_PAGE_SIZE))
|
||||||
|
|
||||||
|
val vaultSnapshot = proxy.vaultQueryBy<ContractState>(QueryCriteria.VaultQueryCriteria(Vault.StateStatus.UNCONSUMED),
|
||||||
|
PageSpecification(DEFAULT_PAGE_NUM, MAX_PAGE_SIZE))
|
||||||
|
// We have to fetch the snapshot separately since vault query API doesn't allow different criteria for snapshot and updates.
|
||||||
|
// TODO : This will create a small window of opportunity for inconsistent updates, might need to change the vault API to handle this case.
|
||||||
val initialVaultUpdate = Vault.Update(setOf(), vaultSnapshot.states.toSet())
|
val initialVaultUpdate = Vault.Update(setOf(), vaultSnapshot.states.toSet())
|
||||||
vaultUpdates.startWith(initialVaultUpdate).subscribe(vaultUpdatesSubject)
|
vaultUpdates.startWith(initialVaultUpdate).subscribe(vaultUpdatesSubject)
|
||||||
|
|
||||||
|
@ -0,0 +1,79 @@
|
|||||||
|
package net.corda.client.jfx.model
|
||||||
|
|
||||||
|
import javafx.beans.property.ObjectProperty
|
||||||
|
import javafx.beans.value.ObservableValue
|
||||||
|
import javafx.beans.value.WritableValue
|
||||||
|
import javafx.collections.ObservableList
|
||||||
|
import org.reactfx.EventSink
|
||||||
|
import org.reactfx.EventStream
|
||||||
|
import rx.Observable
|
||||||
|
import rx.Observer
|
||||||
|
import rx.subjects.Subject
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
|
sealed class TrackedDelegate<M : Any>(val klass: KClass<M>) {
|
||||||
|
init {
|
||||||
|
Models.initModel(klass)
|
||||||
|
}
|
||||||
|
|
||||||
|
class ObservableDelegate<M : Any, T>(klass: KClass<M>, val observableProperty: (M) -> Observable<T>) : TrackedDelegate<M>(klass) {
|
||||||
|
operator fun getValue(thisRef: Any, property: KProperty<*>): Observable<T> {
|
||||||
|
return observableProperty(Models.get(klass, thisRef.javaClass.kotlin))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ObserverDelegate<M : Any, T>(klass: KClass<M>, val observerProperty: (M) -> Observer<T>) : TrackedDelegate<M>(klass) {
|
||||||
|
operator fun getValue(thisRef: Any, property: KProperty<*>): Observer<T> {
|
||||||
|
return observerProperty(Models.get(klass, thisRef.javaClass.kotlin))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SubjectDelegate<M : Any, T>(klass: KClass<M>, val subjectProperty: (M) -> Subject<T, T>) : TrackedDelegate<M>(klass) {
|
||||||
|
operator fun getValue(thisRef: Any, property: KProperty<*>): Subject<T, T> {
|
||||||
|
return subjectProperty(Models.get(klass, thisRef.javaClass.kotlin))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EventStreamDelegate<M : Any, T>(klass: KClass<M>, val eventStreamProperty: (M) -> EventStream<T>) : TrackedDelegate<M>(klass) {
|
||||||
|
operator fun getValue(thisRef: Any, property: KProperty<*>): EventStream<T> {
|
||||||
|
return eventStreamProperty(Models.get(klass, thisRef.javaClass.kotlin))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EventSinkDelegate<M : Any, T>(klass: KClass<M>, val eventSinkProperty: (M) -> EventSink<T>) : TrackedDelegate<M>(klass) {
|
||||||
|
operator fun getValue(thisRef: Any, property: KProperty<*>): EventSink<T> {
|
||||||
|
return eventSinkProperty(Models.get(klass, thisRef.javaClass.kotlin))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ObservableValueDelegate<M : Any, T>(klass: KClass<M>, val observableValueProperty: (M) -> ObservableValue<T>) : TrackedDelegate<M>(klass) {
|
||||||
|
operator fun getValue(thisRef: Any, property: KProperty<*>): ObservableValue<T> {
|
||||||
|
return observableValueProperty(Models.get(klass, thisRef.javaClass.kotlin))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WritableValueDelegate<M : Any, T>(klass: KClass<M>, val writableValueProperty: (M) -> WritableValue<T>) : TrackedDelegate<M>(klass) {
|
||||||
|
operator fun getValue(thisRef: Any, property: KProperty<*>): WritableValue<T> {
|
||||||
|
return writableValueProperty(Models.get(klass, thisRef.javaClass.kotlin))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ObservableListDelegate<M : Any, T>(klass: KClass<M>, val observableListProperty: (M) -> ObservableList<T>) : TrackedDelegate<M>(klass) {
|
||||||
|
operator fun getValue(thisRef: Any, property: KProperty<*>): ObservableList<T> {
|
||||||
|
return observableListProperty(Models.get(klass, thisRef.javaClass.kotlin))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ObservableListReadOnlyDelegate<M : Any, out T>(klass: KClass<M>, val observableListReadOnlyProperty: (M) -> ObservableList<out T>) : TrackedDelegate<M>(klass) {
|
||||||
|
operator fun getValue(thisRef: Any, property: KProperty<*>): ObservableList<out T> {
|
||||||
|
return observableListReadOnlyProperty(Models.get(klass, thisRef.javaClass.kotlin))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ObjectPropertyDelegate<M : Any, T>(klass: KClass<M>, val objectPropertyProperty: (M) -> ObjectProperty<T>) : TrackedDelegate<M>(klass) {
|
||||||
|
operator fun getValue(thisRef: Any, property: KProperty<*>): ObjectProperty<T> {
|
||||||
|
return objectPropertyProperty(Models.get(klass, thisRef.javaClass.kotlin))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -54,7 +54,7 @@ data class PartiallyResolvedTransaction(
|
|||||||
class TransactionDataModel {
|
class TransactionDataModel {
|
||||||
private val transactions by observable(NodeMonitorModel::transactions)
|
private val transactions by observable(NodeMonitorModel::transactions)
|
||||||
private val collectedTransactions = transactions.recordInSequence()
|
private val collectedTransactions = transactions.recordInSequence()
|
||||||
private val transactionMap = collectedTransactions.associateBy(SignedTransaction::id)
|
private val transactionMap = transactions.recordAsAssociation(SignedTransaction::id)
|
||||||
|
|
||||||
val partiallyResolvedTransactions = collectedTransactions.map {
|
val partiallyResolvedTransactions = collectedTransactions.map {
|
||||||
PartiallyResolvedTransaction.fromSignedTransaction(it, transactionMap)
|
PartiallyResolvedTransaction.fromSignedTransaction(it, transactionMap)
|
||||||
|
@ -5,12 +5,6 @@ apply plugin: 'com.jfrog.artifactory'
|
|||||||
|
|
||||||
description 'Corda client mock modules'
|
description 'Corda client mock modules'
|
||||||
|
|
||||||
//noinspection GroovyAssignabilityCheck
|
|
||||||
configurations {
|
|
||||||
// we don't want isolated.jar in classPath, since we want to test jar being dynamically loaded as an attachment
|
|
||||||
runtime.exclude module: 'isolated'
|
|
||||||
}
|
|
||||||
|
|
||||||
// To find potential version conflicts, run "gradle htmlDependencyReport" and then look in
|
// To find potential version conflicts, run "gradle htmlDependencyReport" and then look in
|
||||||
// build/reports/project/dependencies/index.html for green highlighted parts of the tree.
|
// build/reports/project/dependencies/index.html for green highlighted parts of the tree.
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ class Generator<out A>(val generate: (SplittableRandom) -> Try<A>) {
|
|||||||
|
|
||||||
fun generateOrFail(random: SplittableRandom, numberOfTries: Int = 1): A {
|
fun generateOrFail(random: SplittableRandom, numberOfTries: Int = 1): A {
|
||||||
var error: Throwable? = null
|
var error: Throwable? = null
|
||||||
for (i in 0..numberOfTries - 1) {
|
for (i in 0 until numberOfTries) {
|
||||||
val result = generate(random)
|
val result = generate(random)
|
||||||
error = when (result) {
|
error = when (result) {
|
||||||
is Try.Success -> return result.value
|
is Try.Success -> return result.value
|
||||||
@ -115,13 +115,14 @@ class Generator<out A>(val generate: (SplittableRandom) -> Try<A>) {
|
|||||||
|
|
||||||
fun <A> frequency(vararg generators: Pair<Double, Generator<A>>) = frequency(generators.toList())
|
fun <A> frequency(vararg generators: Pair<Double, Generator<A>>) = frequency(generators.toList())
|
||||||
|
|
||||||
fun <A> sequence(generators: List<Generator<A>>) = Generator<List<A>> {
|
fun <A> sequence(generators: List<Generator<A>>) = Generator {
|
||||||
val result = mutableListOf<A>()
|
val result = mutableListOf<A>()
|
||||||
for (generator in generators) {
|
for (generator in generators) {
|
||||||
val element = generator.generate(it)
|
val element = generator.generate(it)
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
when (element) {
|
when (element) {
|
||||||
is Try.Success -> result.add(element.value)
|
is Try.Success -> result.add(element.value)
|
||||||
is Try.Failure -> return@Generator element
|
is Try.Failure -> return@Generator element as Try<List<A>>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Try.Success(result)
|
Try.Success(result)
|
||||||
@ -135,12 +136,12 @@ class Generator<out A>(val generate: (SplittableRandom) -> Try<A>) {
|
|||||||
|
|
||||||
fun intRange(range: IntRange) = intRange(range.first, range.last)
|
fun intRange(range: IntRange) = intRange(range.first, range.last)
|
||||||
fun intRange(from: Int, to: Int): Generator<Int> = Generator.success {
|
fun intRange(from: Int, to: Int): Generator<Int> = Generator.success {
|
||||||
(from + Math.abs(it.nextInt()) % (to - from + 1)).toInt()
|
(from + Math.abs(it.nextInt()) % (to - from + 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun longRange(range: LongRange) = longRange(range.first, range.last)
|
fun longRange(range: LongRange) = longRange(range.first, range.last)
|
||||||
fun longRange(from: Long, to: Long): Generator<Long> = Generator.success {
|
fun longRange(from: Long, to: Long): Generator<Long> = Generator.success {
|
||||||
(from + Math.abs(it.nextLong()) % (to - from + 1)).toLong()
|
(from + Math.abs(it.nextLong()) % (to - from + 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun double() = Generator.success { it.nextDouble() }
|
fun double() = Generator.success { it.nextDouble() }
|
||||||
@ -153,7 +154,7 @@ class Generator<out A>(val generate: (SplittableRandom) -> Try<A>) {
|
|||||||
if (Character.isValidCodePoint(codePoint)) {
|
if (Character.isValidCodePoint(codePoint)) {
|
||||||
return@Generator Try.Success(codePoint.toChar())
|
return@Generator Try.Success(codePoint.toChar())
|
||||||
} else {
|
} else {
|
||||||
Try.Failure(IllegalStateException("Could not generate valid codepoint"))
|
Try.Failure<Any>(IllegalStateException("Could not generate valid codepoint"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,7 +175,7 @@ class Generator<out A>(val generate: (SplittableRandom) -> Try<A>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun <A> replicatePoisson(meanSize: Double, generator: Generator<A>, atLeastOne: Boolean = false) = Generator<List<A>> {
|
fun <A> replicatePoisson(meanSize: Double, generator: Generator<A>, atLeastOne: Boolean = false) = Generator {
|
||||||
val chance = (meanSize - 1) / meanSize
|
val chance = (meanSize - 1) / meanSize
|
||||||
val result = mutableListOf<A>()
|
val result = mutableListOf<A>()
|
||||||
var finish = false
|
var finish = false
|
||||||
@ -190,7 +191,8 @@ class Generator<out A>(val generate: (SplittableRandom) -> Try<A>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (res is Try.Failure) {
|
if (res is Try.Failure) {
|
||||||
return@Generator res
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return@Generator res as Try<List<A>>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Try.Success(result)
|
Try.Success(result)
|
||||||
@ -200,11 +202,11 @@ class Generator<out A>(val generate: (SplittableRandom) -> Try<A>) {
|
|||||||
fun <A> pickN(number: Int, list: List<A>) = Generator<List<A>> {
|
fun <A> pickN(number: Int, list: List<A>) = Generator<List<A>> {
|
||||||
val mask = BitSet(list.size)
|
val mask = BitSet(list.size)
|
||||||
val size = Math.min(list.size, number)
|
val size = Math.min(list.size, number)
|
||||||
for (i in 0..size - 1) {
|
for (i in 0 until size) {
|
||||||
// mask[i] = 1 desugars into mask.set(i, 1), which sets a range instead of a bit
|
// mask[i] = 1 desugars into mask.set(i, 1), which sets a range instead of a bit
|
||||||
mask[i] = true
|
mask[i] = true
|
||||||
}
|
}
|
||||||
for (i in 0..list.size - 1) {
|
for (i in 0 until list.size) {
|
||||||
val bit = mask[i]
|
val bit = mask[i]
|
||||||
val swapIndex = i + it.nextInt(size - i)
|
val swapIndex = i + it.nextInt(size - i)
|
||||||
mask[i] = mask[swapIndex]
|
mask[i] = mask[swapIndex]
|
||||||
|
@ -7,9 +7,6 @@ description 'Corda client RPC modules'
|
|||||||
|
|
||||||
//noinspection GroovyAssignabilityCheck
|
//noinspection GroovyAssignabilityCheck
|
||||||
configurations {
|
configurations {
|
||||||
// we don't want isolated.jar in classPath, since we want to test jar being dynamically loaded as an attachment
|
|
||||||
runtime.exclude module: 'isolated'
|
|
||||||
|
|
||||||
integrationTestCompile.extendsFrom testCompile
|
integrationTestCompile.extendsFrom testCompile
|
||||||
integrationTestRuntime.extendsFrom testRuntime
|
integrationTestRuntime.extendsFrom testRuntime
|
||||||
|
|
||||||
|
@ -5,14 +5,17 @@ import net.corda.core.concurrent.CordaFuture;
|
|||||||
import net.corda.core.contracts.Amount;
|
import net.corda.core.contracts.Amount;
|
||||||
import net.corda.core.messaging.CordaRPCOps;
|
import net.corda.core.messaging.CordaRPCOps;
|
||||||
import net.corda.core.messaging.FlowHandle;
|
import net.corda.core.messaging.FlowHandle;
|
||||||
import net.corda.core.node.services.ServiceInfo;
|
|
||||||
import net.corda.core.utilities.OpaqueBytes;
|
import net.corda.core.utilities.OpaqueBytes;
|
||||||
import net.corda.finance.flows.AbstractCashFlow;
|
import net.corda.finance.flows.AbstractCashFlow;
|
||||||
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.finance.schemas.*;
|
||||||
import net.corda.node.internal.Node;
|
import net.corda.node.internal.Node;
|
||||||
|
import net.corda.node.internal.StartedNode;
|
||||||
import net.corda.node.services.transactions.ValidatingNotaryService;
|
import net.corda.node.services.transactions.ValidatingNotaryService;
|
||||||
|
import net.corda.nodeapi.internal.ServiceInfo;
|
||||||
import net.corda.nodeapi.User;
|
import net.corda.nodeapi.User;
|
||||||
|
import net.corda.testing.CoreTestUtils;
|
||||||
import net.corda.testing.node.NodeBasedTest;
|
import net.corda.testing.node.NodeBasedTest;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
@ -22,14 +25,14 @@ import java.io.IOException;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
import static java.util.Collections.emptyMap;
|
import static java.util.Collections.*;
|
||||||
import static java.util.Collections.singletonList;
|
|
||||||
import static java.util.Objects.requireNonNull;
|
import static java.util.Objects.requireNonNull;
|
||||||
import static kotlin.test.AssertionsKt.assertEquals;
|
import static kotlin.test.AssertionsKt.assertEquals;
|
||||||
import static net.corda.client.rpc.CordaRPCClientConfiguration.getDefault;
|
import static net.corda.client.rpc.CordaRPCClientConfiguration.getDefault;
|
||||||
import static net.corda.finance.CurrencyUtils.DOLLARS;
|
import static net.corda.finance.Currencies.DOLLARS;
|
||||||
import static net.corda.finance.contracts.GetBalances.getCashBalance;
|
import static net.corda.finance.contracts.GetBalances.getCashBalance;
|
||||||
import static net.corda.node.services.FlowPermissions.startFlowPermission;
|
import static net.corda.node.services.FlowPermissions.startFlowPermission;
|
||||||
|
import static net.corda.testing.CoreTestUtils.*;
|
||||||
import static net.corda.testing.TestConstants.getALICE;
|
import static net.corda.testing.TestConstants.getALICE;
|
||||||
|
|
||||||
public class CordaRPCJavaClientTest extends NodeBasedTest {
|
public class CordaRPCJavaClientTest extends NodeBasedTest {
|
||||||
@ -37,7 +40,7 @@ public class CordaRPCJavaClientTest extends NodeBasedTest {
|
|||||||
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);
|
||||||
|
|
||||||
private Node node;
|
private StartedNode<Node> node;
|
||||||
private CordaRPCClient client;
|
private CordaRPCClient client;
|
||||||
private RPCClient.RPCConnection<CordaRPCOps> connection = null;
|
private RPCClient.RPCConnection<CordaRPCOps> connection = null;
|
||||||
private CordaRPCOps rpcProxy;
|
private CordaRPCOps rpcProxy;
|
||||||
@ -49,15 +52,18 @@ public class CordaRPCJavaClientTest extends NodeBasedTest {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() throws ExecutionException, InterruptedException {
|
public void setUp() throws ExecutionException, InterruptedException {
|
||||||
|
setCordappPackages("net.corda.finance.contracts");
|
||||||
Set<ServiceInfo> services = new HashSet<>(singletonList(new ServiceInfo(ValidatingNotaryService.Companion.getType(), null)));
|
Set<ServiceInfo> services = new HashSet<>(singletonList(new ServiceInfo(ValidatingNotaryService.Companion.getType(), null)));
|
||||||
CordaFuture<Node> nodeFuture = startNode(getALICE().getName(), 1, services, singletonList(rpcUser), emptyMap());
|
CordaFuture<StartedNode<Node>> nodeFuture = startNode(getALICE().getName(), 1, services, singletonList(rpcUser), emptyMap());
|
||||||
node = nodeFuture.get();
|
node = nodeFuture.get();
|
||||||
client = new CordaRPCClient(requireNonNull(node.getConfiguration().getRpcAddress()), null, getDefault(), false);
|
node.getInternals().registerCustomSchemas(Collections.singleton(CashSchemaV1.INSTANCE));
|
||||||
|
client = new CordaRPCClient(requireNonNull(node.getInternals().getConfiguration().getRpcAddress()), getDefault(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
public void done() throws IOException {
|
public void done() throws IOException {
|
||||||
connection.close();
|
connection.close();
|
||||||
|
unsetCordappPackages();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -71,7 +77,7 @@ public class CordaRPCJavaClientTest extends NodeBasedTest {
|
|||||||
|
|
||||||
FlowHandle<AbstractCashFlow.Result> flowHandle = rpcProxy.startFlowDynamic(CashIssueFlow.class,
|
FlowHandle<AbstractCashFlow.Result> flowHandle = rpcProxy.startFlowDynamic(CashIssueFlow.class,
|
||||||
DOLLARS(123), OpaqueBytes.of("1".getBytes()),
|
DOLLARS(123), OpaqueBytes.of("1".getBytes()),
|
||||||
node.info.getLegalIdentity());
|
CoreTestUtils.chooseIdentity(node.getInfo()));
|
||||||
System.out.println("Started issuing cash, waiting on result");
|
System.out.println("Started issuing cash, waiting on result");
|
||||||
flowHandle.getReturnValue().get();
|
flowHandle.getReturnValue().get();
|
||||||
|
|
||||||
|
@ -2,8 +2,10 @@ package net.corda.client.rpc
|
|||||||
|
|
||||||
import net.corda.core.crypto.random63BitValue
|
import net.corda.core.crypto.random63BitValue
|
||||||
import net.corda.core.flows.FlowInitiator
|
import net.corda.core.flows.FlowInitiator
|
||||||
import net.corda.core.messaging.*
|
import net.corda.core.messaging.FlowProgressHandle
|
||||||
import net.corda.core.node.services.ServiceInfo
|
import net.corda.core.messaging.StateMachineUpdate
|
||||||
|
import net.corda.core.messaging.startFlow
|
||||||
|
import net.corda.core.messaging.startTrackedFlow
|
||||||
import net.corda.core.utilities.OpaqueBytes
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
import net.corda.finance.DOLLARS
|
import net.corda.finance.DOLLARS
|
||||||
@ -13,12 +15,18 @@ import net.corda.finance.contracts.getCashBalances
|
|||||||
import net.corda.finance.flows.CashException
|
import net.corda.finance.flows.CashException
|
||||||
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.finance.schemas.CashSchemaV1
|
||||||
import net.corda.node.internal.Node
|
import net.corda.node.internal.Node
|
||||||
|
import net.corda.node.internal.StartedNode
|
||||||
import net.corda.node.services.FlowPermissions.Companion.startFlowPermission
|
import net.corda.node.services.FlowPermissions.Companion.startFlowPermission
|
||||||
|
import net.corda.nodeapi.internal.ServiceInfo
|
||||||
import net.corda.node.services.transactions.ValidatingNotaryService
|
import net.corda.node.services.transactions.ValidatingNotaryService
|
||||||
import net.corda.nodeapi.User
|
import net.corda.nodeapi.User
|
||||||
import net.corda.testing.ALICE
|
import net.corda.testing.ALICE
|
||||||
|
import net.corda.testing.chooseIdentity
|
||||||
import net.corda.testing.node.NodeBasedTest
|
import net.corda.testing.node.NodeBasedTest
|
||||||
|
import net.corda.testing.setCordappPackages
|
||||||
|
import net.corda.testing.unsetCordappPackages
|
||||||
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
|
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
|
||||||
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
@ -33,7 +41,7 @@ class CordaRPCClientTest : NodeBasedTest() {
|
|||||||
startFlowPermission<CashIssueFlow>(),
|
startFlowPermission<CashIssueFlow>(),
|
||||||
startFlowPermission<CashPaymentFlow>()
|
startFlowPermission<CashPaymentFlow>()
|
||||||
))
|
))
|
||||||
private lateinit var node: Node
|
private lateinit var node: StartedNode<Node>
|
||||||
private lateinit var client: CordaRPCClient
|
private lateinit var client: CordaRPCClient
|
||||||
private var connection: CordaRPCConnection? = null
|
private var connection: CordaRPCConnection? = null
|
||||||
|
|
||||||
@ -43,13 +51,16 @@ class CordaRPCClientTest : NodeBasedTest() {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
|
setCordappPackages("net.corda.finance.contracts")
|
||||||
node = startNode(ALICE.name, rpcUsers = listOf(rpcUser), advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type))).getOrThrow()
|
node = startNode(ALICE.name, rpcUsers = listOf(rpcUser), advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type))).getOrThrow()
|
||||||
client = CordaRPCClient(node.configuration.rpcAddress!!, initialiseSerialization = false)
|
node.internals.registerCustomSchemas(setOf(CashSchemaV1))
|
||||||
|
client = CordaRPCClient(node.internals.configuration.rpcAddress!!, initialiseSerialization = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
fun done() {
|
fun done() {
|
||||||
connection?.close()
|
connection?.close()
|
||||||
|
unsetCordappPackages()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -78,7 +89,7 @@ class CordaRPCClientTest : NodeBasedTest() {
|
|||||||
println("Creating proxy")
|
println("Creating proxy")
|
||||||
println("Starting flow")
|
println("Starting flow")
|
||||||
val flowHandle = connection!!.proxy.startTrackedFlow(::CashIssueFlow,
|
val flowHandle = connection!!.proxy.startTrackedFlow(::CashIssueFlow,
|
||||||
20.DOLLARS, OpaqueBytes.of(0), node.info.legalIdentity
|
20.DOLLARS, OpaqueBytes.of(0), node.info.chooseIdentity()
|
||||||
)
|
)
|
||||||
println("Started flow, waiting on result")
|
println("Started flow, waiting on result")
|
||||||
flowHandle.progress.subscribe {
|
flowHandle.progress.subscribe {
|
||||||
@ -90,7 +101,7 @@ class CordaRPCClientTest : NodeBasedTest() {
|
|||||||
@Test
|
@Test
|
||||||
fun `sub-type of FlowException thrown by flow`() {
|
fun `sub-type of FlowException thrown by flow`() {
|
||||||
login(rpcUser.username, rpcUser.password)
|
login(rpcUser.username, rpcUser.password)
|
||||||
val handle = connection!!.proxy.startFlow(::CashPaymentFlow, 100.DOLLARS, node.info.legalIdentity)
|
val handle = connection!!.proxy.startFlow(::CashPaymentFlow, 100.DOLLARS, node.info.chooseIdentity())
|
||||||
assertThatExceptionOfType(CashException::class.java).isThrownBy {
|
assertThatExceptionOfType(CashException::class.java).isThrownBy {
|
||||||
handle.returnValue.getOrThrow()
|
handle.returnValue.getOrThrow()
|
||||||
}
|
}
|
||||||
@ -99,9 +110,8 @@ class CordaRPCClientTest : NodeBasedTest() {
|
|||||||
@Test
|
@Test
|
||||||
fun `check basic flow has no progress`() {
|
fun `check basic flow has no progress`() {
|
||||||
login(rpcUser.username, rpcUser.password)
|
login(rpcUser.username, rpcUser.password)
|
||||||
connection!!.proxy.startFlow(::CashPaymentFlow, 100.DOLLARS, node.info.legalIdentity).use {
|
connection!!.proxy.startFlow(::CashPaymentFlow, 100.DOLLARS, node.info.chooseIdentity()).use {
|
||||||
assertFalse(it is FlowProgressHandle<*>)
|
assertFalse(it is FlowProgressHandle<*>)
|
||||||
assertTrue(it is FlowHandle<*>)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,7 +123,7 @@ class CordaRPCClientTest : NodeBasedTest() {
|
|||||||
assertTrue(startCash.isEmpty(), "Should not start with any cash")
|
assertTrue(startCash.isEmpty(), "Should not start with any cash")
|
||||||
|
|
||||||
val flowHandle = proxy.startFlow(::CashIssueFlow,
|
val flowHandle = proxy.startFlow(::CashIssueFlow,
|
||||||
123.DOLLARS, OpaqueBytes.of(0), node.info.legalIdentity
|
123.DOLLARS, OpaqueBytes.of(0), node.info.chooseIdentity()
|
||||||
)
|
)
|
||||||
println("Started issuing cash, waiting on result")
|
println("Started issuing cash, waiting on result")
|
||||||
flowHandle.returnValue.get()
|
flowHandle.returnValue.get()
|
||||||
@ -138,7 +148,7 @@ class CordaRPCClientTest : NodeBasedTest() {
|
|||||||
countShellFlows++
|
countShellFlows++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val nodeIdentity = node.info.legalIdentity
|
val nodeIdentity = node.info.chooseIdentity()
|
||||||
node.services.startFlow(CashIssueFlow(2000.DOLLARS, OpaqueBytes.of(0), nodeIdentity), FlowInitiator.Shell).resultFuture.getOrThrow()
|
node.services.startFlow(CashIssueFlow(2000.DOLLARS, OpaqueBytes.of(0), nodeIdentity), FlowInitiator.Shell).resultFuture.getOrThrow()
|
||||||
proxy.startFlow(::CashIssueFlow,
|
proxy.startFlow(::CashIssueFlow,
|
||||||
123.DOLLARS,
|
123.DOLLARS,
|
||||||
|
@ -1,18 +1,13 @@
|
|||||||
package net.corda.client.rpc
|
package net.corda.client.rpc
|
||||||
|
|
||||||
|
import net.corda.client.rpc.internal.KryoClientSerializationScheme
|
||||||
import net.corda.client.rpc.internal.RPCClient
|
import net.corda.client.rpc.internal.RPCClient
|
||||||
import net.corda.client.rpc.internal.RPCClientConfiguration
|
import net.corda.client.rpc.internal.RPCClientConfiguration
|
||||||
import net.corda.client.rpc.serialization.KryoClientSerializationScheme
|
|
||||||
import net.corda.core.messaging.CordaRPCOps
|
import net.corda.core.messaging.CordaRPCOps
|
||||||
import net.corda.core.serialization.SerializationDefaults
|
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport
|
import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport
|
||||||
import net.corda.nodeapi.ConnectionDirection
|
import net.corda.nodeapi.ConnectionDirection
|
||||||
import net.corda.nodeapi.config.SSLConfiguration
|
|
||||||
import net.corda.nodeapi.internal.serialization.AMQPClientSerializationScheme
|
|
||||||
import net.corda.nodeapi.internal.serialization.KRYO_P2P_CONTEXT
|
|
||||||
import net.corda.nodeapi.internal.serialization.KRYO_RPC_CLIENT_CONTEXT
|
import net.corda.nodeapi.internal.serialization.KRYO_RPC_CLIENT_CONTEXT
|
||||||
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
|
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
|
|
||||||
/** @see RPCClient.RPCConnection */
|
/** @see RPCClient.RPCConnection */
|
||||||
@ -38,9 +33,9 @@ data class CordaRPCClientConfiguration(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @see RPCClient */
|
/** @see RPCClient */
|
||||||
|
//TODO Add SSL support
|
||||||
class CordaRPCClient(
|
class CordaRPCClient(
|
||||||
hostAndPort: NetworkHostAndPort,
|
hostAndPort: NetworkHostAndPort,
|
||||||
sslConfiguration: SSLConfiguration? = null,
|
|
||||||
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default,
|
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default,
|
||||||
initialiseSerialization: Boolean = true
|
initialiseSerialization: Boolean = true
|
||||||
) {
|
) {
|
||||||
@ -49,12 +44,12 @@ class CordaRPCClient(
|
|||||||
// others having registered first.
|
// others having registered first.
|
||||||
// TODO: allow clients to have serialization factory etc injected and align with RPC protocol version?
|
// TODO: allow clients to have serialization factory etc injected and align with RPC protocol version?
|
||||||
if (initialiseSerialization) {
|
if (initialiseSerialization) {
|
||||||
initialiseSerialization()
|
KryoClientSerializationScheme.initialiseSerialization()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val rpcClient = RPCClient<CordaRPCOps>(
|
private val rpcClient = RPCClient<CordaRPCOps>(
|
||||||
tcpTransport(ConnectionDirection.Outbound(), hostAndPort, sslConfiguration),
|
tcpTransport(ConnectionDirection.Outbound(), hostAndPort, config = null),
|
||||||
configuration.toRpcClientConfiguration(),
|
configuration.toRpcClientConfiguration(),
|
||||||
KRYO_RPC_CLIENT_CONTEXT
|
KRYO_RPC_CLIENT_CONTEXT
|
||||||
)
|
)
|
||||||
@ -66,21 +61,4 @@ class CordaRPCClient(
|
|||||||
inline fun <A> use(username: String, password: String, block: (CordaRPCConnection) -> A): A {
|
inline fun <A> use(username: String, password: String, block: (CordaRPCConnection) -> A): A {
|
||||||
return start(username, password).use(block)
|
return start(username, password).use(block)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun initialiseSerialization() {
|
|
||||||
try {
|
|
||||||
SerializationDefaults.SERIALIZATION_FACTORY = SerializationFactoryImpl().apply {
|
|
||||||
registerScheme(KryoClientSerializationScheme())
|
|
||||||
registerScheme(AMQPClientSerializationScheme())
|
|
||||||
}
|
|
||||||
SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT
|
|
||||||
SerializationDefaults.RPC_CLIENT_CONTEXT = KRYO_RPC_CLIENT_CONTEXT
|
|
||||||
} catch(e: IllegalStateException) {
|
|
||||||
// Check that it's registered as we expect
|
|
||||||
check(SerializationDefaults.SERIALIZATION_FACTORY is SerializationFactoryImpl) { "RPC client encountered conflicting configuration of serialization subsystem." }
|
|
||||||
check((SerializationDefaults.SERIALIZATION_FACTORY as SerializationFactoryImpl).alreadyRegisteredSchemes.any { it is KryoClientSerializationScheme }) { "RPC client encountered conflicting configuration of serialization subsystem." }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package net.corda.client.rpc
|
||||||
|
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown to indicate that the calling user does not have permission for something they have requested (for example
|
||||||
|
* calling a method).
|
||||||
|
*/
|
||||||
|
@CordaSerializable
|
||||||
|
class PermissionException(msg: String) : RuntimeException(msg)
|
@ -0,0 +1,11 @@
|
|||||||
|
package net.corda.client.rpc
|
||||||
|
|
||||||
|
import net.corda.core.CordaRuntimeException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown to indicate a fatal error in the RPC system itself, as opposed to an error generated by the invoked
|
||||||
|
* method.
|
||||||
|
*/
|
||||||
|
open class RPCException(message: String?, cause: Throwable?) : CordaRuntimeException(message, cause) {
|
||||||
|
constructor(msg: String) : this(msg, null)
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package net.corda.client.rpc
|
||||||
|
|
||||||
|
/** Records the protocol version in which this RPC was added. */
|
||||||
|
@Target(AnnotationTarget.FUNCTION)
|
||||||
|
@MustBeDocumented
|
||||||
|
annotation class RPCSinceVersion(val version: Int)
|
@ -0,0 +1,41 @@
|
|||||||
|
package net.corda.client.rpc.internal
|
||||||
|
|
||||||
|
import com.esotericsoftware.kryo.pool.KryoPool
|
||||||
|
import net.corda.core.serialization.SerializationContext
|
||||||
|
import net.corda.core.serialization.SerializationDefaults
|
||||||
|
import net.corda.core.utilities.ByteSequence
|
||||||
|
import net.corda.nodeapi.internal.serialization.*
|
||||||
|
|
||||||
|
class KryoClientSerializationScheme : AbstractKryoSerializationScheme() {
|
||||||
|
override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean {
|
||||||
|
return byteSequence == KryoHeaderV0_1 && (target == SerializationContext.UseCase.RPCClient || target == SerializationContext.UseCase.P2P)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun rpcClientKryoPool(context: SerializationContext): KryoPool {
|
||||||
|
return KryoPool.Builder {
|
||||||
|
DefaultKryoCustomizer.customize(RPCKryo(RpcClientObservableSerializer, context)).apply {
|
||||||
|
classLoader = context.deserializationClassLoader
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're on the client and don't have access to server classes.
|
||||||
|
override fun rpcServerKryoPool(context: SerializationContext): KryoPool = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun initialiseSerialization() {
|
||||||
|
try {
|
||||||
|
SerializationDefaults.SERIALIZATION_FACTORY = SerializationFactoryImpl().apply {
|
||||||
|
registerScheme(KryoClientSerializationScheme())
|
||||||
|
registerScheme(AMQPClientSerializationScheme())
|
||||||
|
}
|
||||||
|
SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT
|
||||||
|
SerializationDefaults.RPC_CLIENT_CONTEXT = KRYO_RPC_CLIENT_CONTEXT
|
||||||
|
} catch(e: IllegalStateException) {
|
||||||
|
// Check that it's registered as we expect
|
||||||
|
check(SerializationDefaults.SERIALIZATION_FACTORY is SerializationFactoryImpl) { "RPC client encountered conflicting configuration of serialization subsystem." }
|
||||||
|
check((SerializationDefaults.SERIALIZATION_FACTORY as SerializationFactoryImpl).alreadyRegisteredSchemes.any { it is KryoClientSerializationScheme }) { "RPC client encountered conflicting configuration of serialization subsystem." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
package net.corda.client.rpc.internal
|
package net.corda.client.rpc.internal
|
||||||
|
|
||||||
|
import net.corda.client.rpc.RPCException
|
||||||
import net.corda.core.crypto.random63BitValue
|
import net.corda.core.crypto.random63BitValue
|
||||||
import net.corda.core.internal.logElapsedTime
|
import net.corda.core.internal.logElapsedTime
|
||||||
import net.corda.core.messaging.RPCOps
|
import net.corda.core.messaging.RPCOps
|
||||||
@ -12,7 +13,6 @@ import net.corda.core.utilities.seconds
|
|||||||
import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport
|
import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport
|
||||||
import net.corda.nodeapi.ConnectionDirection
|
import net.corda.nodeapi.ConnectionDirection
|
||||||
import net.corda.nodeapi.RPCApi
|
import net.corda.nodeapi.RPCApi
|
||||||
import net.corda.nodeapi.RPCException
|
|
||||||
import net.corda.nodeapi.config.SSLConfiguration
|
import net.corda.nodeapi.config.SSLConfiguration
|
||||||
import org.apache.activemq.artemis.api.core.SimpleString
|
import org.apache.activemq.artemis.api.core.SimpleString
|
||||||
import org.apache.activemq.artemis.api.core.TransportConfiguration
|
import org.apache.activemq.artemis.api.core.TransportConfiguration
|
||||||
|
@ -10,6 +10,8 @@ import com.google.common.cache.RemovalCause
|
|||||||
import com.google.common.cache.RemovalListener
|
import com.google.common.cache.RemovalListener
|
||||||
import com.google.common.util.concurrent.SettableFuture
|
import com.google.common.util.concurrent.SettableFuture
|
||||||
import com.google.common.util.concurrent.ThreadFactoryBuilder
|
import com.google.common.util.concurrent.ThreadFactoryBuilder
|
||||||
|
import net.corda.client.rpc.RPCException
|
||||||
|
import net.corda.client.rpc.RPCSinceVersion
|
||||||
import net.corda.core.crypto.random63BitValue
|
import net.corda.core.crypto.random63BitValue
|
||||||
import net.corda.core.internal.LazyPool
|
import net.corda.core.internal.LazyPool
|
||||||
import net.corda.core.internal.LazyStickyPool
|
import net.corda.core.internal.LazyStickyPool
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
package net.corda.client.rpc.serialization
|
|
||||||
|
|
||||||
import com.esotericsoftware.kryo.pool.KryoPool
|
|
||||||
import net.corda.client.rpc.internal.RpcClientObservableSerializer
|
|
||||||
import net.corda.core.serialization.SerializationContext
|
|
||||||
import net.corda.core.utilities.ByteSequence
|
|
||||||
import net.corda.nodeapi.RPCKryo
|
|
||||||
import net.corda.nodeapi.internal.serialization.AbstractKryoSerializationScheme
|
|
||||||
import net.corda.nodeapi.internal.serialization.DefaultKryoCustomizer
|
|
||||||
import net.corda.nodeapi.internal.serialization.KryoHeaderV0_1
|
|
||||||
|
|
||||||
class KryoClientSerializationScheme : AbstractKryoSerializationScheme() {
|
|
||||||
override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean {
|
|
||||||
return byteSequence == KryoHeaderV0_1 && (target == SerializationContext.UseCase.RPCClient || target == SerializationContext.UseCase.P2P)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun rpcClientKryoPool(context: SerializationContext): KryoPool {
|
|
||||||
return KryoPool.Builder {
|
|
||||||
DefaultKryoCustomizer.customize(RPCKryo(RpcClientObservableSerializer, context)).apply { classLoader = context.deserializationClassLoader }
|
|
||||||
}.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
// We're on the client and don't have access to server classes.
|
|
||||||
override fun rpcServerKryoPool(context: SerializationContext): KryoPool {
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,11 +2,12 @@ package net.corda.java.rpc;
|
|||||||
|
|
||||||
import net.corda.client.rpc.CordaRPCConnection;
|
import net.corda.client.rpc.CordaRPCConnection;
|
||||||
import net.corda.core.contracts.Amount;
|
import net.corda.core.contracts.Amount;
|
||||||
|
import net.corda.core.identity.CordaX500Name;
|
||||||
|
import net.corda.core.identity.Party;
|
||||||
import net.corda.core.messaging.CordaRPCOps;
|
import net.corda.core.messaging.CordaRPCOps;
|
||||||
import net.corda.core.messaging.FlowHandle;
|
import net.corda.core.messaging.FlowHandle;
|
||||||
import net.corda.core.node.NodeInfo;
|
import net.corda.core.node.NodeInfo;
|
||||||
import net.corda.core.utilities.OpaqueBytes;
|
import net.corda.core.utilities.OpaqueBytes;
|
||||||
import net.corda.core.utilities.X500NameUtils;
|
|
||||||
import net.corda.finance.flows.AbstractCashFlow;
|
import net.corda.finance.flows.AbstractCashFlow;
|
||||||
import net.corda.finance.flows.CashIssueFlow;
|
import net.corda.finance.flows.CashIssueFlow;
|
||||||
import net.corda.nodeapi.User;
|
import net.corda.nodeapi.User;
|
||||||
@ -41,9 +42,10 @@ public class StandaloneCordaRPCJavaClientTest {
|
|||||||
private CordaRPCOps rpcProxy;
|
private CordaRPCOps rpcProxy;
|
||||||
private CordaRPCConnection connection;
|
private CordaRPCConnection connection;
|
||||||
private NodeInfo notaryNode;
|
private NodeInfo notaryNode;
|
||||||
|
private Party notaryNodeIdentity;
|
||||||
|
|
||||||
private NodeConfig notaryConfig = new NodeConfig(
|
private NodeConfig notaryConfig = new NodeConfig(
|
||||||
X500NameUtils.getX500Name("Notary Service", "Zurich", "CH"),
|
new CordaX500Name("Notary Service", "Zurich", "CH"),
|
||||||
port.getAndIncrement(),
|
port.getAndIncrement(),
|
||||||
port.getAndIncrement(),
|
port.getAndIncrement(),
|
||||||
port.getAndIncrement(),
|
port.getAndIncrement(),
|
||||||
@ -60,6 +62,7 @@ public class StandaloneCordaRPCJavaClientTest {
|
|||||||
connection = notary.connect();
|
connection = notary.connect();
|
||||||
rpcProxy = connection.getProxy();
|
rpcProxy = connection.getProxy();
|
||||||
notaryNode = fetchNotaryIdentity();
|
notaryNode = fetchNotaryIdentity();
|
||||||
|
notaryNodeIdentity = rpcProxy.nodeInfo().getLegalIdentities().get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
@ -106,7 +109,7 @@ public class StandaloneCordaRPCJavaClientTest {
|
|||||||
|
|
||||||
FlowHandle<AbstractCashFlow.Result> flowHandle = rpcProxy.startFlowDynamic(CashIssueFlow.class,
|
FlowHandle<AbstractCashFlow.Result> flowHandle = rpcProxy.startFlowDynamic(CashIssueFlow.class,
|
||||||
dollars123, OpaqueBytes.of("1".getBytes()),
|
dollars123, OpaqueBytes.of("1".getBytes()),
|
||||||
notaryNode.getLegalIdentity());
|
notaryNodeIdentity);
|
||||||
System.out.println("Started issuing cash, waiting on result");
|
System.out.println("Started issuing cash, waiting on result");
|
||||||
flowHandle.getReturnValue().get();
|
flowHandle.getReturnValue().get();
|
||||||
|
|
||||||
|
@ -4,12 +4,17 @@ import com.google.common.hash.Hashing
|
|||||||
import com.google.common.hash.HashingInputStream
|
import com.google.common.hash.HashingInputStream
|
||||||
import net.corda.client.rpc.CordaRPCConnection
|
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.Party
|
||||||
import net.corda.core.internal.*
|
import net.corda.core.internal.*
|
||||||
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.getOrThrow
|
||||||
|
import net.corda.core.utilities.loggerFor
|
||||||
|
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
|
||||||
@ -52,9 +57,10 @@ class StandaloneCordaRPClientTest {
|
|||||||
private lateinit var rpcProxy: CordaRPCOps
|
private lateinit var rpcProxy: CordaRPCOps
|
||||||
private lateinit var connection: CordaRPCConnection
|
private lateinit var connection: CordaRPCConnection
|
||||||
private lateinit var notaryNode: NodeInfo
|
private lateinit var notaryNode: NodeInfo
|
||||||
|
private lateinit var notaryNodeIdentity: Party
|
||||||
|
|
||||||
private val notaryConfig = NodeConfig(
|
private val notaryConfig = NodeConfig(
|
||||||
legalName = getX500Name(O = "Notary Service", OU = "corda", L = "Zurich", C = "CH"),
|
legalName = CordaX500Name(organisation = "Notary Service", locality = "Zurich", country = "CH"),
|
||||||
p2pPort = port.andIncrement,
|
p2pPort = port.andIncrement,
|
||||||
rpcPort = port.andIncrement,
|
rpcPort = port.andIncrement,
|
||||||
webPort = port.andIncrement,
|
webPort = port.andIncrement,
|
||||||
@ -70,6 +76,7 @@ class StandaloneCordaRPClientTest {
|
|||||||
connection = notary.connect()
|
connection = notary.connect()
|
||||||
rpcProxy = connection.proxy
|
rpcProxy = connection.proxy
|
||||||
notaryNode = fetchNotaryIdentity()
|
notaryNode = fetchNotaryIdentity()
|
||||||
|
notaryNodeIdentity = rpcProxy.nodeInfo().legalIdentitiesAndCerts.first().party
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
@ -106,7 +113,7 @@ class StandaloneCordaRPClientTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test starting flow`() {
|
fun `test starting flow`() {
|
||||||
rpcProxy.startFlow(::CashIssueFlow, 127.POUNDS, OpaqueBytes.of(0), notaryNode.notaryIdentity)
|
rpcProxy.startFlow(::CashIssueFlow, 127.POUNDS, OpaqueBytes.of(0), notaryNodeIdentity)
|
||||||
.returnValue.getOrThrow(timeout)
|
.returnValue.getOrThrow(timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,7 +121,7 @@ class StandaloneCordaRPClientTest {
|
|||||||
fun `test starting tracked flow`() {
|
fun `test starting tracked flow`() {
|
||||||
var trackCount = 0
|
var trackCount = 0
|
||||||
val handle = rpcProxy.startTrackedFlow(
|
val handle = rpcProxy.startTrackedFlow(
|
||||||
::CashIssueFlow, 429.DOLLARS, OpaqueBytes.of(0), notaryNode.notaryIdentity
|
::CashIssueFlow, 429.DOLLARS, OpaqueBytes.of(0), notaryNodeIdentity
|
||||||
)
|
)
|
||||||
val updateLatch = CountDownLatch(1)
|
val updateLatch = CountDownLatch(1)
|
||||||
handle.progress.subscribe { msg ->
|
handle.progress.subscribe { msg ->
|
||||||
@ -129,7 +136,7 @@ class StandaloneCordaRPClientTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test network map`() {
|
fun `test network map`() {
|
||||||
assertEquals(notaryConfig.legalName, notaryNode.legalIdentity.name)
|
assertEquals(notaryConfig.legalName, notaryNodeIdentity.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -148,7 +155,7 @@ class StandaloneCordaRPClientTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Now issue some cash
|
// Now issue some cash
|
||||||
rpcProxy.startFlow(::CashIssueFlow, 513.SWISS_FRANCS, OpaqueBytes.of(0), notaryNode.notaryIdentity)
|
rpcProxy.startFlow(::CashIssueFlow, 513.SWISS_FRANCS, OpaqueBytes.of(0), notaryNodeIdentity)
|
||||||
.returnValue.getOrThrow(timeout)
|
.returnValue.getOrThrow(timeout)
|
||||||
updateLatch.await()
|
updateLatch.await()
|
||||||
assertEquals(1, updateCount.get())
|
assertEquals(1, updateCount.get())
|
||||||
@ -166,7 +173,7 @@ class StandaloneCordaRPClientTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Now issue some cash
|
// Now issue some cash
|
||||||
rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryNode.notaryIdentity)
|
rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryNodeIdentity)
|
||||||
.returnValue.getOrThrow(timeout)
|
.returnValue.getOrThrow(timeout)
|
||||||
updateLatch.await()
|
updateLatch.await()
|
||||||
|
|
||||||
@ -180,7 +187,7 @@ class StandaloneCordaRPClientTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `test vault query by`() {
|
fun `test vault query by`() {
|
||||||
// Now issue some cash
|
// Now issue some cash
|
||||||
rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryNode.notaryIdentity)
|
rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryNodeIdentity)
|
||||||
.returnValue.getOrThrow(timeout)
|
.returnValue.getOrThrow(timeout)
|
||||||
|
|
||||||
val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL)
|
val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL)
|
||||||
@ -191,7 +198,7 @@ class StandaloneCordaRPClientTest {
|
|||||||
assertEquals(1, queryResults.totalStatesAvailable)
|
assertEquals(1, queryResults.totalStatesAvailable)
|
||||||
assertEquals(queryResults.states.first().state.data.amount.quantity, 629.POUNDS.quantity)
|
assertEquals(queryResults.states.first().state.data.amount.quantity, 629.POUNDS.quantity)
|
||||||
|
|
||||||
rpcProxy.startFlow(::CashPaymentFlow, 100.POUNDS, notaryNode.legalIdentity).returnValue.getOrThrow()
|
rpcProxy.startFlow(::CashPaymentFlow, 100.POUNDS, notaryNodeIdentity).returnValue.getOrThrow()
|
||||||
|
|
||||||
val moreResults = rpcProxy.vaultQueryBy<Cash.State>(criteria, paging, sorting)
|
val moreResults = rpcProxy.vaultQueryBy<Cash.State>(criteria, paging, sorting)
|
||||||
assertEquals(3, moreResults.totalStatesAvailable) // 629 - 100 + 100
|
assertEquals(3, moreResults.totalStatesAvailable) // 629 - 100 + 100
|
||||||
@ -209,7 +216,7 @@ class StandaloneCordaRPClientTest {
|
|||||||
println(startCash)
|
println(startCash)
|
||||||
assertTrue(startCash.isEmpty(), "Should not start with any cash")
|
assertTrue(startCash.isEmpty(), "Should not start with any cash")
|
||||||
|
|
||||||
val flowHandle = rpcProxy.startFlow(::CashIssueFlow, 629.DOLLARS, OpaqueBytes.of(0), notaryNode.legalIdentity)
|
val flowHandle = rpcProxy.startFlow(::CashIssueFlow, 629.DOLLARS, OpaqueBytes.of(0), notaryNodeIdentity)
|
||||||
println("Started issuing cash, waiting on result")
|
println("Started issuing cash, waiting on result")
|
||||||
flowHandle.returnValue.get()
|
flowHandle.returnValue.get()
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ import net.corda.core.internal.concurrent.thenMatch
|
|||||||
import net.corda.core.messaging.RPCOps
|
import net.corda.core.messaging.RPCOps
|
||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
import net.corda.node.services.messaging.getRpcContext
|
import net.corda.node.services.messaging.getRpcContext
|
||||||
import net.corda.nodeapi.RPCSinceVersion
|
|
||||||
import net.corda.testing.RPCDriverExposedDSLInterface
|
import net.corda.testing.RPCDriverExposedDSLInterface
|
||||||
import net.corda.testing.rpcDriver
|
import net.corda.testing.rpcDriver
|
||||||
import net.corda.testing.rpcTestUser
|
import net.corda.testing.rpcTestUser
|
||||||
|
@ -3,7 +3,6 @@ package net.corda.client.rpc
|
|||||||
import net.corda.core.messaging.RPCOps
|
import net.corda.core.messaging.RPCOps
|
||||||
import net.corda.node.services.messaging.getRpcContext
|
import net.corda.node.services.messaging.getRpcContext
|
||||||
import net.corda.node.services.messaging.requirePermission
|
import net.corda.node.services.messaging.requirePermission
|
||||||
import net.corda.nodeapi.PermissionException
|
|
||||||
import net.corda.nodeapi.User
|
import net.corda.nodeapi.User
|
||||||
import net.corda.testing.RPCDriverExposedDSLInterface
|
import net.corda.testing.RPCDriverExposedDSLInterface
|
||||||
import net.corda.testing.rpcDriver
|
import net.corda.testing.rpcDriver
|
||||||
|
@ -14,11 +14,11 @@ class RepeatingBytesInputStream(val bytesToRepeat: ByteArray, val numberOfBytes:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
override fun read(byteArray: ByteArray, offset: Int, length: Int): Int {
|
override fun read(byteArray: ByteArray, offset: Int, length: Int): Int {
|
||||||
val until = Math.min(Math.min(offset + length, byteArray.size), offset + bytesLeft)
|
val lastIdx = Math.min(Math.min(offset + length, byteArray.size), offset + bytesLeft)
|
||||||
for (i in offset .. until - 1) {
|
for (i in offset until lastIdx) {
|
||||||
byteArray[i] = bytesToRepeat[(numberOfBytes - bytesLeft + i - offset) % bytesToRepeat.size]
|
byteArray[i] = bytesToRepeat[(numberOfBytes - bytesLeft + i - offset) % bytesToRepeat.size]
|
||||||
}
|
}
|
||||||
val bytesRead = until - offset
|
val bytesRead = lastIdx - offset
|
||||||
bytesLeft -= bytesRead
|
bytesLeft -= bytesRead
|
||||||
return if (bytesRead == 0 && bytesLeft == 0) -1 else bytesRead
|
return if (bytesRead == 0 && bytesLeft == 0) -1 else bytesRead
|
||||||
}
|
}
|
||||||
|
42
confidential-identities/build.gradle
Normal file
42
confidential-identities/build.gradle
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
// Experimental Confidential Identities support for 1.0
|
||||||
|
// This contains the prototype SwapIdentitiesFlow and SwapIdentitiesHandler, which can be used
|
||||||
|
// for exchanging confidential identities as part of a flow, until a permanent solution is prepared.
|
||||||
|
// Expect this module to be removed and merged into core in a later release.
|
||||||
|
apply plugin: 'kotlin'
|
||||||
|
apply plugin: CanonicalizerPlugin
|
||||||
|
apply plugin: 'net.corda.plugins.publish-utils'
|
||||||
|
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||||
|
apply plugin: 'net.corda.plugins.cordformation'
|
||||||
|
apply plugin: 'com.jfrog.artifactory'
|
||||||
|
|
||||||
|
description 'Corda Experimental Confidential Identities'
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// Note the :confidential-identities module is a CorDapp in its own right
|
||||||
|
// and CorDapps using :confidential-identities features should use 'cordapp' not 'compile' linkage.
|
||||||
|
cordaCompile project(':core')
|
||||||
|
|
||||||
|
// Quasar, for suspendable fibres.
|
||||||
|
compileOnly "co.paralleluniverse:quasar-core:$quasar_version:jdk8"
|
||||||
|
|
||||||
|
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||||
|
testCompile "junit:junit:$junit_version"
|
||||||
|
|
||||||
|
// Guava: Google test library (collections test suite)
|
||||||
|
testCompile "com.google.guava:guava-testlib:$guava_version"
|
||||||
|
|
||||||
|
// Bring in the MockNode infrastructure for writing protocol unit tests.
|
||||||
|
testCompile project(":node")
|
||||||
|
testCompile project(":node-driver")
|
||||||
|
|
||||||
|
// AssertJ: for fluent assertions for testing
|
||||||
|
testCompile "org.assertj:assertj-core:$assertj_version"
|
||||||
|
}
|
||||||
|
|
||||||
|
jar {
|
||||||
|
baseName 'corda-confidential-identities'
|
||||||
|
}
|
||||||
|
|
||||||
|
publish {
|
||||||
|
name jar.baseName
|
||||||
|
}
|
@ -1,9 +1,10 @@
|
|||||||
package net.corda.core.flows
|
package net.corda.confidential
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.contracts.ContractState
|
import net.corda.core.contracts.ContractState
|
||||||
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.flows.FlowSession
|
||||||
import net.corda.core.identity.AbstractParty
|
import net.corda.core.identity.AbstractParty
|
||||||
import net.corda.core.identity.Party
|
|
||||||
import net.corda.core.identity.PartyAndCertificate
|
import net.corda.core.identity.PartyAndCertificate
|
||||||
import net.corda.core.transactions.WireTransaction
|
import net.corda.core.transactions.WireTransaction
|
||||||
import net.corda.core.utilities.ProgressTracker
|
import net.corda.core.utilities.ProgressTracker
|
||||||
@ -21,10 +22,10 @@ object IdentitySyncFlow {
|
|||||||
* @return a mapping of well known identities to the confidential identities used in the transaction.
|
* @return a mapping of well known identities to the confidential identities used in the transaction.
|
||||||
*/
|
*/
|
||||||
// TODO: Can this be triggered automatically from [SendTransactionFlow]
|
// TODO: Can this be triggered automatically from [SendTransactionFlow]
|
||||||
class Send(val otherSides: Set<Party>,
|
class Send(val otherSideSessions: Set<FlowSession>,
|
||||||
val tx: WireTransaction,
|
val tx: WireTransaction,
|
||||||
override val progressTracker: ProgressTracker) : FlowLogic<Unit>() {
|
override val progressTracker: ProgressTracker) : FlowLogic<Unit>() {
|
||||||
constructor(otherSide: Party, tx: WireTransaction) : this(setOf(otherSide), tx, tracker())
|
constructor(otherSide: FlowSession, tx: WireTransaction) : this(setOf(otherSide), tx, tracker())
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
object SYNCING_IDENTITIES : ProgressTracker.Step("Syncing identities")
|
object SYNCING_IDENTITIES : ProgressTracker.Step("Syncing identities")
|
||||||
@ -39,14 +40,14 @@ object IdentitySyncFlow {
|
|||||||
val identities: Set<AbstractParty> = states.flatMap { it.participants }.toSet()
|
val identities: Set<AbstractParty> = states.flatMap { it.participants }.toSet()
|
||||||
// Filter participants down to the set of those not in the network map (are not well known)
|
// Filter participants down to the set of those not in the network map (are not well known)
|
||||||
val confidentialIdentities = identities
|
val confidentialIdentities = identities
|
||||||
.filter { serviceHub.networkMapCache.getNodeByLegalIdentityKey(it.owningKey) == null }
|
.filter { serviceHub.networkMapCache.getNodesByLegalIdentityKey(it.owningKey).isEmpty() }
|
||||||
.toList()
|
.toList()
|
||||||
val identityCertificates: Map<AbstractParty, PartyAndCertificate?> = identities
|
val identityCertificates: Map<AbstractParty, PartyAndCertificate?> = identities
|
||||||
.map { Pair(it, serviceHub.identityService.certificateFromKey(it.owningKey)) }.toMap()
|
.map { Pair(it, serviceHub.identityService.certificateFromKey(it.owningKey)) }.toMap()
|
||||||
|
|
||||||
otherSides.forEach { otherSide ->
|
otherSideSessions.forEach { otherSideSession ->
|
||||||
val requestedIdentities: List<AbstractParty> = sendAndReceive<List<AbstractParty>>(otherSide, confidentialIdentities).unwrap { req ->
|
val requestedIdentities: List<AbstractParty> = otherSideSession.sendAndReceive<List<AbstractParty>>(confidentialIdentities).unwrap { req ->
|
||||||
require(req.all { it in identityCertificates.keys }) { "${otherSide} requested a confidential identity not part of transaction: ${tx.id}" }
|
require(req.all { it in identityCertificates.keys }) { "${otherSideSession.counterparty} requested a confidential identity not part of transaction: ${tx.id}" }
|
||||||
req
|
req
|
||||||
}
|
}
|
||||||
val sendIdentities: List<PartyAndCertificate?> = requestedIdentities.map {
|
val sendIdentities: List<PartyAndCertificate?> = requestedIdentities.map {
|
||||||
@ -56,7 +57,7 @@ object IdentitySyncFlow {
|
|||||||
else
|
else
|
||||||
throw IllegalStateException("Counterparty requested a confidential identity for which we do not have the certificate path: ${tx.id}")
|
throw IllegalStateException("Counterparty requested a confidential identity for which we do not have the certificate path: ${tx.id}")
|
||||||
}
|
}
|
||||||
send(otherSide, sendIdentities)
|
otherSideSession.send(sendIdentities)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,7 +67,7 @@ object IdentitySyncFlow {
|
|||||||
* Handle an offer to provide proof of identity (in the form of certificate paths) for confidential identities which
|
* Handle an offer to provide proof of identity (in the form of certificate paths) for confidential identities which
|
||||||
* we do not yet know about.
|
* we do not yet know about.
|
||||||
*/
|
*/
|
||||||
class Receive(val otherSide: Party) : FlowLogic<Unit>() {
|
class Receive(val otherSideSession: FlowSession) : FlowLogic<Unit>() {
|
||||||
companion object {
|
companion object {
|
||||||
object RECEIVING_IDENTITIES : ProgressTracker.Step("Receiving confidential identities")
|
object RECEIVING_IDENTITIES : ProgressTracker.Step("Receiving confidential identities")
|
||||||
object RECEIVING_CERTIFICATES : ProgressTracker.Step("Receiving certificates for unknown identities")
|
object RECEIVING_CERTIFICATES : ProgressTracker.Step("Receiving certificates for unknown identities")
|
||||||
@ -77,10 +78,10 @@ object IdentitySyncFlow {
|
|||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call(): Unit {
|
override fun call(): Unit {
|
||||||
progressTracker.currentStep = RECEIVING_IDENTITIES
|
progressTracker.currentStep = RECEIVING_IDENTITIES
|
||||||
val allIdentities = receive<List<AbstractParty>>(otherSide).unwrap { it }
|
val allIdentities = otherSideSession.receive<List<AbstractParty>>().unwrap { it }
|
||||||
val unknownIdentities = allIdentities.filter { serviceHub.identityService.partyFromAnonymous(it) == null }
|
val unknownIdentities = allIdentities.filter { serviceHub.identityService.wellKnownPartyFromAnonymous(it) == null }
|
||||||
progressTracker.currentStep = RECEIVING_CERTIFICATES
|
progressTracker.currentStep = RECEIVING_CERTIFICATES
|
||||||
val missingIdentities = sendAndReceive<List<PartyAndCertificate>>(otherSide, unknownIdentities)
|
val missingIdentities = otherSideSession.sendAndReceive<List<PartyAndCertificate>>(unknownIdentities)
|
||||||
|
|
||||||
// Batch verify the identities we've received, so we know they're all correct before we start storing them in
|
// Batch verify the identities we've received, so we know they're all correct before we start storing them in
|
||||||
// the identity service
|
// the identity service
|
@ -1,23 +1,27 @@
|
|||||||
package net.corda.core.flows
|
package net.corda.confidential
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.flows.InitiatingFlow
|
||||||
|
import net.corda.core.flows.StartableByRPC
|
||||||
import net.corda.core.identity.AnonymousParty
|
import net.corda.core.identity.AnonymousParty
|
||||||
import net.corda.core.identity.PartyAndCertificate
|
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.identity.PartyAndCertificate
|
||||||
import net.corda.core.node.services.IdentityService
|
import net.corda.core.node.services.IdentityService
|
||||||
import net.corda.core.utilities.ProgressTracker
|
import net.corda.core.utilities.ProgressTracker
|
||||||
import net.corda.core.utilities.unwrap
|
import net.corda.core.utilities.unwrap
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Very basic flow which exchanges transaction key and certificate paths between two parties in a transaction.
|
* Very basic flow which generates new confidential identities for parties in a transaction and exchanges the transaction
|
||||||
* This is intended for use as a subflow of another flow.
|
* key and certificate paths between the parties. This is intended for use as a subflow of another flow which builds a
|
||||||
|
* transaction.
|
||||||
*/
|
*/
|
||||||
@StartableByRPC
|
@StartableByRPC
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
class TransactionKeyFlow(val otherSide: Party,
|
class SwapIdentitiesFlow(private val otherParty: Party,
|
||||||
val revocationEnabled: Boolean,
|
private val revocationEnabled: Boolean,
|
||||||
override val progressTracker: ProgressTracker) : FlowLogic<LinkedHashMap<Party, AnonymousParty>>() {
|
override val progressTracker: ProgressTracker) : FlowLogic<LinkedHashMap<Party, AnonymousParty>>() {
|
||||||
constructor(otherSide: Party) : this(otherSide, false, tracker())
|
constructor(otherParty: Party) : this(otherParty, false, tracker())
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
object AWAITING_KEY : ProgressTracker.Step("Awaiting key")
|
object AWAITING_KEY : ProgressTracker.Step("Awaiting key")
|
||||||
@ -35,18 +39,19 @@ class TransactionKeyFlow(val otherSide: Party,
|
|||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call(): LinkedHashMap<Party, AnonymousParty> {
|
override fun call(): LinkedHashMap<Party, AnonymousParty> {
|
||||||
progressTracker.currentStep = AWAITING_KEY
|
progressTracker.currentStep = AWAITING_KEY
|
||||||
val legalIdentityAnonymous = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, revocationEnabled)
|
val legalIdentityAnonymous = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, revocationEnabled)
|
||||||
|
|
||||||
// Special case that if we're both parties, a single identity is generated
|
// Special case that if we're both parties, a single identity is generated
|
||||||
val identities = LinkedHashMap<Party, AnonymousParty>()
|
val identities = LinkedHashMap<Party, AnonymousParty>()
|
||||||
if (otherSide == serviceHub.myInfo.legalIdentity) {
|
if (serviceHub.myInfo.isLegalIdentity(otherParty)) {
|
||||||
identities.put(otherSide, legalIdentityAnonymous.party.anonymise())
|
identities.put(otherParty, legalIdentityAnonymous.party.anonymise())
|
||||||
} else {
|
} else {
|
||||||
val anonymousOtherSide = sendAndReceive<PartyAndCertificate>(otherSide, legalIdentityAnonymous).unwrap { confidentialIdentity ->
|
val otherSession = initiateFlow(otherParty)
|
||||||
validateAndRegisterIdentity(serviceHub.identityService, otherSide, confidentialIdentity)
|
val anonymousOtherSide = otherSession.sendAndReceive<PartyAndCertificate>(legalIdentityAnonymous).unwrap { confidentialIdentity ->
|
||||||
|
validateAndRegisterIdentity(serviceHub.identityService, otherSession.counterparty, confidentialIdentity)
|
||||||
}
|
}
|
||||||
identities.put(serviceHub.myInfo.legalIdentity, legalIdentityAnonymous.party.anonymise())
|
identities.put(ourIdentity, legalIdentityAnonymous.party.anonymise())
|
||||||
identities.put(otherSide, anonymousOtherSide.party.anonymise())
|
identities.put(otherSession.counterparty, anonymousOtherSide.party.anonymise())
|
||||||
}
|
}
|
||||||
return identities
|
return identities
|
||||||
}
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
package net.corda.confidential
|
||||||
|
|
||||||
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.flows.FlowSession
|
||||||
|
import net.corda.core.identity.PartyAndCertificate
|
||||||
|
import net.corda.core.utilities.ProgressTracker
|
||||||
|
import net.corda.core.utilities.unwrap
|
||||||
|
|
||||||
|
class SwapIdentitiesHandler(val otherSideSession: FlowSession, val revocationEnabled: Boolean) : FlowLogic<Unit>() {
|
||||||
|
constructor(otherSideSession: FlowSession) : this(otherSideSession, false)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
object SENDING_KEY : ProgressTracker.Step("Sending key")
|
||||||
|
}
|
||||||
|
|
||||||
|
override val progressTracker: ProgressTracker = ProgressTracker(SENDING_KEY)
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
val revocationEnabled = false
|
||||||
|
progressTracker.currentStep = SENDING_KEY
|
||||||
|
val legalIdentityAnonymous = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, revocationEnabled)
|
||||||
|
otherSideSession.sendAndReceive<PartyAndCertificate>(legalIdentityAnonymous).unwrap { confidentialIdentity ->
|
||||||
|
SwapIdentitiesFlow.validateAndRegisterIdentity(serviceHub.identityService, otherSideSession.counterparty, confidentialIdentity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,10 @@
|
|||||||
package net.corda.core.flows
|
package net.corda.confidential
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.flows.FlowSession
|
||||||
|
import net.corda.core.flows.InitiatedBy
|
||||||
|
import net.corda.core.flows.InitiatingFlow
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.transactions.WireTransaction
|
import net.corda.core.transactions.WireTransaction
|
||||||
import net.corda.core.utilities.OpaqueBytes
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
@ -9,9 +13,7 @@ import net.corda.core.utilities.unwrap
|
|||||||
import net.corda.finance.DOLLARS
|
import net.corda.finance.DOLLARS
|
||||||
import net.corda.finance.contracts.asset.Cash
|
import net.corda.finance.contracts.asset.Cash
|
||||||
import net.corda.finance.flows.CashIssueAndPaymentFlow
|
import net.corda.finance.flows.CashIssueAndPaymentFlow
|
||||||
import net.corda.testing.ALICE
|
import net.corda.testing.*
|
||||||
import net.corda.testing.BOB
|
|
||||||
import net.corda.testing.DUMMY_NOTARY
|
|
||||||
import net.corda.testing.node.MockNetwork
|
import net.corda.testing.node.MockNetwork
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
@ -24,6 +26,7 @@ class IdentitySyncFlowTests {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun before() {
|
fun before() {
|
||||||
|
setCordappPackages("net.corda.finance.contracts.asset")
|
||||||
// We run this in parallel threads to help catch any race conditions that may exist.
|
// We run this in parallel threads to help catch any race conditions that may exist.
|
||||||
mockNet = MockNetwork(networkSendManuallyPumped = false, threadPerNode = true)
|
mockNet = MockNetwork(networkSendManuallyPumped = false, threadPerNode = true)
|
||||||
}
|
}
|
||||||
@ -31,6 +34,7 @@ class IdentitySyncFlowTests {
|
|||||||
@After
|
@After
|
||||||
fun cleanUp() {
|
fun cleanUp() {
|
||||||
mockNet.stopNodes()
|
mockNet.stopNodes()
|
||||||
|
unsetCordappPackages()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -39,25 +43,26 @@ class IdentitySyncFlowTests {
|
|||||||
val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name)
|
val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name)
|
||||||
val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name)
|
val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name)
|
||||||
val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name)
|
val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name)
|
||||||
val alice: Party = aliceNode.services.myInfo.legalIdentity
|
val alice: Party = aliceNode.services.myInfo.chooseIdentity()
|
||||||
val bob: Party = bobNode.services.myInfo.legalIdentity
|
val bob: Party = bobNode.services.myInfo.chooseIdentity()
|
||||||
bobNode.registerInitiatedFlow(Receive::class.java)
|
bobNode.internals.registerInitiatedFlow(Receive::class.java)
|
||||||
|
|
||||||
// Alice issues then pays some cash to a new confidential identity that Bob doesn't know about
|
// Alice issues then pays some cash to a new confidential identity that Bob doesn't know about
|
||||||
val anonymous = true
|
val anonymous = true
|
||||||
val ref = OpaqueBytes.of(0x01)
|
val ref = OpaqueBytes.of(0x01)
|
||||||
val issueFlow = aliceNode.services.startFlow(CashIssueAndPaymentFlow(1000.DOLLARS, ref, alice, anonymous, notaryNode.services.myInfo.notaryIdentity))
|
val notary = aliceNode.services.getDefaultNotary()
|
||||||
|
val issueFlow = aliceNode.services.startFlow(CashIssueAndPaymentFlow(1000.DOLLARS, ref, alice, anonymous, notary))
|
||||||
val issueTx = issueFlow.resultFuture.getOrThrow().stx
|
val issueTx = issueFlow.resultFuture.getOrThrow().stx
|
||||||
val confidentialIdentity = issueTx.tx.outputs.map { it.data }.filterIsInstance<Cash.State>().single().owner
|
val confidentialIdentity = issueTx.tx.outputs.map { it.data }.filterIsInstance<Cash.State>().single().owner
|
||||||
assertNull(bobNode.database.transaction { bobNode.services.identityService.partyFromAnonymous(confidentialIdentity) })
|
assertNull(bobNode.database.transaction { bobNode.services.identityService.wellKnownPartyFromAnonymous(confidentialIdentity) })
|
||||||
|
|
||||||
// Run the flow to sync up the identities
|
// Run the flow to sync up the identities
|
||||||
aliceNode.services.startFlow(Initiator(bob, issueTx.tx)).resultFuture.getOrThrow()
|
aliceNode.services.startFlow(Initiator(bob, issueTx.tx)).resultFuture.getOrThrow()
|
||||||
val expected = aliceNode.database.transaction {
|
val expected = aliceNode.database.transaction {
|
||||||
aliceNode.services.identityService.partyFromAnonymous(confidentialIdentity)
|
aliceNode.services.identityService.wellKnownPartyFromAnonymous(confidentialIdentity)
|
||||||
}
|
}
|
||||||
val actual = bobNode.database.transaction {
|
val actual = bobNode.database.transaction {
|
||||||
bobNode.services.identityService.partyFromAnonymous(confidentialIdentity)
|
bobNode.services.identityService.wellKnownPartyFromAnonymous(confidentialIdentity)
|
||||||
}
|
}
|
||||||
assertEquals(expected, actual)
|
assertEquals(expected, actual)
|
||||||
}
|
}
|
||||||
@ -69,19 +74,20 @@ class IdentitySyncFlowTests {
|
|||||||
class Initiator(val otherSide: Party, val tx: WireTransaction): FlowLogic<Boolean>() {
|
class Initiator(val otherSide: Party, val tx: WireTransaction): FlowLogic<Boolean>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call(): Boolean {
|
override fun call(): Boolean {
|
||||||
subFlow(IdentitySyncFlow.Send(otherSide, tx))
|
val session = initiateFlow(otherSide)
|
||||||
|
subFlow(IdentitySyncFlow.Send(session, tx))
|
||||||
// Wait for the counterparty to indicate they're done
|
// Wait for the counterparty to indicate they're done
|
||||||
return receive<Boolean>(otherSide).unwrap { it }
|
return session.receive<Boolean>().unwrap { it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@InitiatedBy(IdentitySyncFlowTests.Initiator::class)
|
@InitiatedBy(IdentitySyncFlowTests.Initiator::class)
|
||||||
class Receive(val otherSide: Party): FlowLogic<Unit>() {
|
class Receive(val otherSideSession: FlowSession): FlowLogic<Unit>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call() {
|
override fun call() {
|
||||||
subFlow(IdentitySyncFlow.Receive(otherSide))
|
subFlow(IdentitySyncFlow.Receive(otherSideSession))
|
||||||
// Notify the initiator that we've finished syncing
|
// Notify the initiator that we've finished syncing
|
||||||
send(otherSide, true)
|
otherSideSession.send(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package net.corda.core.flows
|
package net.corda.confidential
|
||||||
|
|
||||||
import net.corda.core.identity.AbstractParty
|
import net.corda.core.identity.AbstractParty
|
||||||
import net.corda.core.identity.AnonymousParty
|
import net.corda.core.identity.AnonymousParty
|
||||||
@ -7,6 +7,7 @@ import net.corda.core.utilities.getOrThrow
|
|||||||
import net.corda.testing.ALICE
|
import net.corda.testing.ALICE
|
||||||
import net.corda.testing.BOB
|
import net.corda.testing.BOB
|
||||||
import net.corda.testing.DUMMY_NOTARY
|
import net.corda.testing.DUMMY_NOTARY
|
||||||
|
import net.corda.testing.chooseIdentity
|
||||||
import net.corda.testing.node.MockNetwork
|
import net.corda.testing.node.MockNetwork
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
@ -14,7 +15,7 @@ import kotlin.test.assertFalse
|
|||||||
import kotlin.test.assertNotEquals
|
import kotlin.test.assertNotEquals
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
class TransactionKeyFlowTests {
|
class SwapIdentitiesFlowTests {
|
||||||
@Test
|
@Test
|
||||||
fun `issue key`() {
|
fun `issue key`() {
|
||||||
// We run this in parallel threads to help catch any race conditions that may exist.
|
// We run this in parallel threads to help catch any race conditions that may exist.
|
||||||
@ -24,11 +25,12 @@ class TransactionKeyFlowTests {
|
|||||||
val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name)
|
val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name)
|
||||||
val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name)
|
val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name)
|
||||||
val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name)
|
val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name)
|
||||||
val alice: Party = aliceNode.services.myInfo.legalIdentity
|
val alice: Party = aliceNode.services.myInfo.chooseIdentity()
|
||||||
val bob: Party = bobNode.services.myInfo.legalIdentity
|
val bob: Party = bobNode.services.myInfo.chooseIdentity()
|
||||||
|
mockNet.registerIdentities()
|
||||||
|
|
||||||
// Run the flows
|
// Run the flows
|
||||||
val requesterFlow = aliceNode.services.startFlow(TransactionKeyFlow(bob))
|
val requesterFlow = aliceNode.services.startFlow(SwapIdentitiesFlow(bob))
|
||||||
|
|
||||||
// Get the results
|
// Get the results
|
||||||
val actual: Map<Party, AnonymousParty> = requesterFlow.resultFuture.getOrThrow().toMap()
|
val actual: Map<Party, AnonymousParty> = requesterFlow.resultFuture.getOrThrow().toMap()
|
||||||
@ -40,8 +42,8 @@ class TransactionKeyFlowTests {
|
|||||||
assertNotEquals<AbstractParty>(bob, bobAnonymousIdentity)
|
assertNotEquals<AbstractParty>(bob, bobAnonymousIdentity)
|
||||||
|
|
||||||
// Verify that the anonymous identities look sane
|
// Verify that the anonymous identities look sane
|
||||||
assertEquals(alice.name, aliceNode.database.transaction { aliceNode.services.identityService.partyFromAnonymous(aliceAnonymousIdentity)!!.name })
|
assertEquals(alice.name, aliceNode.database.transaction { aliceNode.services.identityService.wellKnownPartyFromAnonymous(aliceAnonymousIdentity)!!.name })
|
||||||
assertEquals(bob.name, bobNode.database.transaction { bobNode.services.identityService.partyFromAnonymous(bobAnonymousIdentity)!!.name })
|
assertEquals(bob.name, bobNode.database.transaction { bobNode.services.identityService.wellKnownPartyFromAnonymous(bobAnonymousIdentity)!!.name })
|
||||||
|
|
||||||
// Verify that the nodes have the right anonymous identities
|
// Verify that the nodes have the right anonymous identities
|
||||||
assertTrue { aliceAnonymousIdentity.owningKey in aliceNode.services.keyManagementService.keys }
|
assertTrue { aliceAnonymousIdentity.owningKey in aliceNode.services.keyManagementService.keys }
|
@ -1,5 +1,5 @@
|
|||||||
gradlePluginsVersion=0.16.2
|
gradlePluginsVersion=1.0.0
|
||||||
kotlinVersion=1.1.4
|
kotlinVersion=1.1.50
|
||||||
guavaVersion=21.0
|
guavaVersion=21.0
|
||||||
bouncycastleVersion=1.57
|
bouncycastleVersion=1.57
|
||||||
typesafeConfigVersion=1.3.1
|
typesafeConfigVersion=1.3.1
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
package net.corda.core.contracts
|
||||||
|
|
||||||
|
import net.corda.core.crypto.SecureHash
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
|
||||||
|
/** Constrain which contract-code-containing attachment can be used with a [ContractState]. */
|
||||||
|
@CordaSerializable
|
||||||
|
interface AttachmentConstraint {
|
||||||
|
/** Returns whether the given contract attachment can be used with the [ContractState] associated with this constraint object. */
|
||||||
|
fun isSatisfiedBy(attachment: Attachment): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An [AttachmentConstraint] where [isSatisfiedBy] always returns true. */
|
||||||
|
object AlwaysAcceptAttachmentConstraint : AttachmentConstraint {
|
||||||
|
override fun isSatisfiedBy(attachment: Attachment) = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An [AttachmentConstraint] that verifies by hash */
|
||||||
|
data class HashAttachmentConstraint(val attachmentId: SecureHash) : AttachmentConstraint {
|
||||||
|
override fun isSatisfiedBy(attachment: Attachment) = attachment.id == attachmentId
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This [AttachmentConstraint] is a convenience class that will be automatically resolved to a [HashAttachmentConstraint].
|
||||||
|
* The resolution occurs in [TransactionBuilder.toWireTransaction] and uses the [TransactionState.contract] value
|
||||||
|
* to find a corresponding loaded [Cordapp] that contains such a contract, and then uses that [Cordapp] as the
|
||||||
|
* [Attachment].
|
||||||
|
*
|
||||||
|
* If, for any reason, this class is not automatically resolved the default implementation is to fail, because the
|
||||||
|
* intent of this class is that it should be replaced by a correct [HashAttachmentConstraint] and verify against an
|
||||||
|
* actual [Attachment].
|
||||||
|
*/
|
||||||
|
object AutomaticHashConstraint : AttachmentConstraint {
|
||||||
|
override fun isSatisfiedBy(attachment: Attachment): Boolean {
|
||||||
|
throw UnsupportedOperationException("Contracts cannot be satisfied by an AutomaticHashConstraint placeholder")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package net.corda.core.contracts
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An enum, for which each property corresponds to a transaction component group. The position in the enum class
|
||||||
|
* declaration (ordinal) is used for component-leaf ordering when computing the Merkle tree.
|
||||||
|
*/
|
||||||
|
enum class ComponentGroupEnum {
|
||||||
|
INPUTS_GROUP, // ordinal = 0.
|
||||||
|
OUTPUTS_GROUP, // ordinal = 1.
|
||||||
|
COMMANDS_GROUP, // ordinal = 2.
|
||||||
|
ATTACHMENTS_GROUP, // ordinal = 3.
|
||||||
|
NOTARY_GROUP, // ordinal = 4.
|
||||||
|
TIMEWINDOW_GROUP // ordinal = 5.
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package net.corda.core.contracts
|
||||||
|
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap an attachment in this if it is to be used as an executable contract attachment
|
||||||
|
*
|
||||||
|
* @property attachment The attachment representing the contract JAR
|
||||||
|
* @property contract The contract name contained within the JAR
|
||||||
|
*/
|
||||||
|
@CordaSerializable
|
||||||
|
class ContractAttachment(val attachment: Attachment, val contract: ContractClassName) : Attachment by attachment
|
@ -0,0 +1,29 @@
|
|||||||
|
package net.corda.core.contracts
|
||||||
|
|
||||||
|
import net.corda.core.identity.AbstractParty
|
||||||
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
|
||||||
|
// DOCSTART 1
|
||||||
|
/**
|
||||||
|
* A contract state (or just "state") contains opaque data used by a contract program. It can be thought of as a disk
|
||||||
|
* file that the program can use to persist data across transactions. States are immutable: once created they are never
|
||||||
|
* updated, instead, any changes must generate a new successor state. States can be updated (consumed) only once: the
|
||||||
|
* notary is responsible for ensuring there is no "double spending" by only signing a transaction if the input states
|
||||||
|
* are all free.
|
||||||
|
*/
|
||||||
|
@CordaSerializable
|
||||||
|
interface ContractState {
|
||||||
|
/**
|
||||||
|
* A _participant_ is any party that is able to consume this state in a valid transaction.
|
||||||
|
*
|
||||||
|
* The list of participants is required for certain types of transactions. For example, when changing the notary
|
||||||
|
* for this state, every participant has to be involved and approve the transaction
|
||||||
|
* so that they receive the updated state, and don't end up in a situation where they can no longer use a state
|
||||||
|
* they possess, since someone consumed that state during the notary change process.
|
||||||
|
*
|
||||||
|
* The participants list should normally be derived from the contents of the state.
|
||||||
|
*/
|
||||||
|
val participants: List<AbstractParty>
|
||||||
|
}
|
||||||
|
// DOCEND 1
|
@ -4,6 +4,7 @@ package net.corda.core.contracts
|
|||||||
|
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.secureRandomBytes
|
import net.corda.core.crypto.secureRandomBytes
|
||||||
|
import net.corda.core.crypto.toStringShort
|
||||||
import net.corda.core.flows.FlowLogicRef
|
import net.corda.core.flows.FlowLogicRef
|
||||||
import net.corda.core.flows.FlowLogicRefFactory
|
import net.corda.core.flows.FlowLogicRefFactory
|
||||||
import net.corda.core.identity.AbstractParty
|
import net.corda.core.identity.AbstractParty
|
||||||
@ -15,111 +16,13 @@ import net.corda.core.utilities.OpaqueBytes
|
|||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
|
// DOCSTART 1
|
||||||
/** Implemented by anything that can be named by a secure hash value (e.g. transactions, attachments). */
|
/** Implemented by anything that can be named by a secure hash value (e.g. transactions, attachments). */
|
||||||
interface NamedByHash {
|
interface NamedByHash {
|
||||||
val id: SecureHash
|
val id: SecureHash
|
||||||
}
|
}
|
||||||
|
|
||||||
// DOCSTART 1
|
|
||||||
/**
|
|
||||||
* A contract state (or just "state") contains opaque data used by a contract program. It can be thought of as a disk
|
|
||||||
* file that the program can use to persist data across transactions. States are immutable: once created they are never
|
|
||||||
* updated, instead, any changes must generate a new successor state. States can be updated (consumed) only once: the
|
|
||||||
* notary is responsible for ensuring there is no "double spending" by only signing a transaction if the input states
|
|
||||||
* are all free.
|
|
||||||
*/
|
|
||||||
@CordaSerializable
|
|
||||||
interface ContractState {
|
|
||||||
/**
|
|
||||||
* An instance of the contract class that will verify this state.
|
|
||||||
*
|
|
||||||
* # Discussion
|
|
||||||
*
|
|
||||||
* This field is not the final design, it's just a piece of temporary scaffolding. Once the contract sandbox is
|
|
||||||
* further along, this field will become a description of which attachments are acceptable for defining the
|
|
||||||
* contract.
|
|
||||||
*
|
|
||||||
* Recall that an attachment is a zip file that can be referenced from any transaction. The contents of the
|
|
||||||
* attachments are merged together and cannot define any overlapping files, thus for any given transaction there
|
|
||||||
* is a miniature file system in which each file can be precisely mapped to the defining attachment.
|
|
||||||
*
|
|
||||||
* Attachments may contain many things (data files, legal documents, etc) but mostly they contain JVM bytecode.
|
|
||||||
* The class files inside define not only [Contract] implementations but also the classes that define the states.
|
|
||||||
* Within the rest of a transaction, user-providable components are referenced by name only.
|
|
||||||
*
|
|
||||||
* This means that a smart contract in Corda does two things:
|
|
||||||
*
|
|
||||||
* 1. Define the data structures that compose the ledger (the states)
|
|
||||||
* 2. Define the rules for updating those structures
|
|
||||||
*
|
|
||||||
* The first is merely a utility role ... in theory contract code could manually parse byte streams by hand.
|
|
||||||
* The second is vital to the integrity of the ledger. So this field needs to be able to express constraints like:
|
|
||||||
*
|
|
||||||
* - Only attachment 733c350f396a727655be1363c06635ba355036bd54a5ed6e594fd0b5d05f42f6 may be used with this state.
|
|
||||||
* - Any attachment signed by public key 2d1ce0e330c52b8055258d776c40 may be used with this state.
|
|
||||||
* - Attachments (1, 2, 3) may all be used with this state.
|
|
||||||
*
|
|
||||||
* and so on. In this way it becomes possible for the business logic governing a state to be evolved, if the
|
|
||||||
* constraints are flexible enough.
|
|
||||||
*
|
|
||||||
* Because contract classes often also define utilities that generate relevant transactions, and because attachments
|
|
||||||
* cannot know their own hashes, we will have to provide various utilities to assist with obtaining the right
|
|
||||||
* code constraints from within the contract code itself.
|
|
||||||
*
|
|
||||||
* TODO: Implement the above description. See COR-226
|
|
||||||
*/
|
|
||||||
val contract: Contract
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A _participant_ is any party that is able to consume this state in a valid transaction.
|
|
||||||
*
|
|
||||||
* The list of participants is required for certain types of transactions. For example, when changing the notary
|
|
||||||
* for this state, every participant has to be involved and approve the transaction
|
|
||||||
* so that they receive the updated state, and don't end up in a situation where they can no longer use a state
|
|
||||||
* they possess, since someone consumed that state during the notary change process.
|
|
||||||
*
|
|
||||||
* The participants list should normally be derived from the contents of the state.
|
|
||||||
*/
|
|
||||||
val participants: List<AbstractParty>
|
|
||||||
}
|
|
||||||
// DOCEND 1
|
// DOCEND 1
|
||||||
|
|
||||||
// DOCSTART 4
|
|
||||||
/**
|
|
||||||
* A wrapper for [ContractState] containing additional platform-level state information.
|
|
||||||
* This is the definitive state that is stored on the ledger and used in transaction outputs.
|
|
||||||
*/
|
|
||||||
@CordaSerializable
|
|
||||||
data class TransactionState<out T : ContractState> @JvmOverloads constructor(
|
|
||||||
/** The custom contract state */
|
|
||||||
val data: T,
|
|
||||||
/** Identity of the notary that ensures the state is not used as an input to a transaction more than once */
|
|
||||||
val notary: Party,
|
|
||||||
/**
|
|
||||||
* All contract states may be _encumbered_ by up to one other state.
|
|
||||||
*
|
|
||||||
* The encumbrance state, if present, forces additional controls over the encumbered state, since the platform checks
|
|
||||||
* that the encumbrance state is present as an input in the same transaction that consumes the encumbered state, and
|
|
||||||
* the contract code and rules of the encumbrance state will also be verified during the execution of the transaction.
|
|
||||||
* For example, a cash contract state could be encumbered with a time-lock contract state; the cash state is then only
|
|
||||||
* processable in a transaction that verifies that the time specified in the encumbrance time-lock has passed.
|
|
||||||
*
|
|
||||||
* The encumbered state refers to another by index, and the referred encumbrance state
|
|
||||||
* is an output state in a particular position on the same transaction that created the encumbered state. An alternative
|
|
||||||
* implementation would be encumbering by reference to a [StateRef], which would allow the specification of encumbrance
|
|
||||||
* by a state created in a prior transaction.
|
|
||||||
*
|
|
||||||
* Note that an encumbered state that is being consumed must have its encumbrance consumed in the same transaction,
|
|
||||||
* otherwise the transaction is not valid.
|
|
||||||
*/
|
|
||||||
val encumbrance: Int? = null)
|
|
||||||
// DOCEND 4
|
|
||||||
|
|
||||||
/** Wraps the [ContractState] in a [TransactionState] object */
|
|
||||||
infix fun <T : ContractState> T.`with notary`(newNotary: Party) = withNotary(newNotary)
|
|
||||||
|
|
||||||
infix fun <T : ContractState> T.withNotary(newNotary: Party) = TransactionState(this, newNotary)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [Issued] data class holds the details of an on ledger digital asset.
|
* The [Issued] data class holds the details of an on ledger digital asset.
|
||||||
* In particular it gives the public credentials of the entity that created these digital tokens
|
* In particular it gives the public credentials of the entity that created these digital tokens
|
||||||
@ -250,7 +153,7 @@ data class StateAndRef<out T : ContractState>(val state: TransactionState<T>, va
|
|||||||
|
|
||||||
/** Filters a list of [StateAndRef] objects according to the type of the states */
|
/** Filters a list of [StateAndRef] objects according to the type of the states */
|
||||||
inline fun <reified T : ContractState> Iterable<StateAndRef<ContractState>>.filterStatesOfType(): List<StateAndRef<T>> {
|
inline fun <reified T : ContractState> Iterable<StateAndRef<ContractState>>.filterStatesOfType(): List<StateAndRef<T>> {
|
||||||
return mapNotNull { if (it.state.data is T) StateAndRef(TransactionState(it.state.data, it.state.notary), it.ref) else null }
|
return mapNotNull { if (it.state.data is T) StateAndRef(TransactionState(it.state.data, it.state.contract, it.state.notary), it.ref) else null }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -283,7 +186,7 @@ data class Command<T : CommandData>(val value: T, val signers: List<PublicKey>)
|
|||||||
constructor(data: T, key: PublicKey) : this(data, listOf(key))
|
constructor(data: T, key: PublicKey) : this(data, listOf(key))
|
||||||
|
|
||||||
private fun commandDataToString() = value.toString().let { if (it.contains("@")) it.replace('$', '.').split("@")[0] else it }
|
private fun commandDataToString() = value.toString().let { if (it.contains("@")) it.replace('$', '.').split("@")[0] else it }
|
||||||
override fun toString() = "${commandDataToString()} with pubkeys ${signers.joinToString()}"
|
override fun toString() = "${commandDataToString()} with pubkeys ${signers.map { it.toStringShort() }.joinToString()}"
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A common move command for contract states which can change owner. */
|
/** A common move command for contract states which can change owner. */
|
||||||
@ -297,7 +200,7 @@ interface MoveCommand : CommandData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Indicates that this transaction replaces the inputs contract state to another contract state */
|
/** Indicates that this transaction replaces the inputs contract state to another contract state */
|
||||||
data class UpgradeCommand(val upgradedContractClass: Class<out UpgradedContract<*, *>>) : CommandData
|
data class UpgradeCommand(val upgradedContractClass: ContractClassName) : CommandData
|
||||||
|
|
||||||
// DOCSTART 6
|
// DOCSTART 6
|
||||||
/** A [Command] where the signing parties have been looked up if they have a well known/recognised institutional key. */
|
/** A [Command] where the signing parties have been looked up if they have a well known/recognised institutional key. */
|
||||||
@ -345,7 +248,7 @@ annotation class LegalProseReference(val uri: String)
|
|||||||
* @param NewState the upgraded contract state.
|
* @param NewState the upgraded contract state.
|
||||||
*/
|
*/
|
||||||
interface UpgradedContract<in OldState : ContractState, out NewState : ContractState> : Contract {
|
interface UpgradedContract<in OldState : ContractState, out NewState : ContractState> : Contract {
|
||||||
val legacyContract: Class<out Contract>
|
val legacyContract: ContractClassName
|
||||||
/**
|
/**
|
||||||
* Upgrade contract's state object to a new state object.
|
* Upgrade contract's state object to a new state object.
|
||||||
*
|
*
|
||||||
@ -371,6 +274,14 @@ class PrivacySalt(bytes: ByteArray) : OpaqueBytes(bytes) {
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
require(bytes.size == 32) { "Privacy salt should be 32 bytes." }
|
require(bytes.size == 32) { "Privacy salt should be 32 bytes." }
|
||||||
require(!bytes.all { it == 0.toByte() }) { "Privacy salt should not be all zeros." }
|
require(bytes.any { it != 0.toByte() }) { "Privacy salt should not be all zeros." }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A convenience class for passing around a state and it's contract
|
||||||
|
*
|
||||||
|
* @property state A state
|
||||||
|
* @property contract The contract that should verify the state
|
||||||
|
*/
|
||||||
|
data class StateAndContract(val state: ContractState, val contract: ContractClassName)
|
||||||
|
@ -1,61 +0,0 @@
|
|||||||
package net.corda.core.contracts
|
|
||||||
|
|
||||||
import net.corda.core.crypto.SecureHash
|
|
||||||
import net.corda.core.node.services.TransactionStorage
|
|
||||||
import net.corda.core.transactions.SignedTransaction
|
|
||||||
import net.corda.core.transactions.WireTransaction
|
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.Callable
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a map of transaction id to [SignedTransaction], performs a breadth first search of the dependency graph from
|
|
||||||
* the starting point down in order to find transactions that match the given query criteria.
|
|
||||||
*
|
|
||||||
* Currently, only one kind of query is supported: find any transaction that contains a command of the given type.
|
|
||||||
*
|
|
||||||
* In future, this should support restricting the search by time, and other types of useful query.
|
|
||||||
*
|
|
||||||
* @param transactions map of transaction id to [SignedTransaction].
|
|
||||||
* @param startPoints transactions to use as starting points for the search.
|
|
||||||
*/
|
|
||||||
class TransactionGraphSearch(val transactions: TransactionStorage,
|
|
||||||
val startPoints: List<WireTransaction>) : Callable<List<WireTransaction>> {
|
|
||||||
class Query(
|
|
||||||
val withCommandOfType: Class<out CommandData>? = null,
|
|
||||||
val followInputsOfType: Class<out ContractState>? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
var query: Query = Query()
|
|
||||||
|
|
||||||
override fun call(): List<WireTransaction> {
|
|
||||||
val q = query
|
|
||||||
|
|
||||||
val alreadyVisited = HashSet<SecureHash>()
|
|
||||||
val next = ArrayList<WireTransaction>(startPoints)
|
|
||||||
|
|
||||||
val results = ArrayList<WireTransaction>()
|
|
||||||
|
|
||||||
while (next.isNotEmpty()) {
|
|
||||||
val tx = next.removeAt(next.lastIndex)
|
|
||||||
|
|
||||||
if (q.matches(tx))
|
|
||||||
results += tx
|
|
||||||
|
|
||||||
val inputsLeadingToUnvisitedTx: Iterable<StateRef> = tx.inputs.filter { it.txhash !in alreadyVisited }
|
|
||||||
val unvisitedInputTxs: Map<SecureHash, SignedTransaction> = inputsLeadingToUnvisitedTx.map { it.txhash }.toHashSet().map { transactions.getTransaction(it) }.filterNotNull().associateBy { it.id }
|
|
||||||
val unvisitedInputTxsWithInputIndex: Iterable<Pair<SignedTransaction, Int>> = inputsLeadingToUnvisitedTx.filter { it.txhash in unvisitedInputTxs.keys }.map { Pair(unvisitedInputTxs[it.txhash]!!, it.index) }
|
|
||||||
next += (unvisitedInputTxsWithInputIndex.filter { q.followInputsOfType == null || it.first.tx.outputs[it.second].data.javaClass == q.followInputsOfType }
|
|
||||||
.map { it.first }.filter { alreadyVisited.add(it.id) }.map { it.tx })
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Query.matches(tx: WireTransaction): Boolean {
|
|
||||||
if (withCommandOfType != null) {
|
|
||||||
if (tx.commands.any { it.value.javaClass.isAssignableFrom(withCommandOfType) })
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,53 @@
|
|||||||
|
package net.corda.core.contracts
|
||||||
|
|
||||||
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
|
||||||
|
// DOCSTART 1
|
||||||
|
typealias ContractClassName = String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper for [ContractState] containing additional platform-level state information and contract information.
|
||||||
|
* This is the definitive state that is stored on the ledger and used in transaction outputs.
|
||||||
|
*/
|
||||||
|
@CordaSerializable
|
||||||
|
data class TransactionState<out T : ContractState> @JvmOverloads constructor(
|
||||||
|
/** The custom contract state */
|
||||||
|
val data: T,
|
||||||
|
/**
|
||||||
|
* The contract class name that will verify this state that will be created via reflection.
|
||||||
|
* The attachment containing this class will be automatically added to the transaction at transaction creation
|
||||||
|
* time.
|
||||||
|
*
|
||||||
|
* Currently these are loaded from the classpath of the node which includes the cordapp directory - at some
|
||||||
|
* point these will also be loaded and run from the attachment store directly, allowing contracts to be
|
||||||
|
* sent across, and run, from the network from within a sandbox environment.
|
||||||
|
*
|
||||||
|
* TODO: Implement the contract sandbox loading of the contract attachments
|
||||||
|
* */
|
||||||
|
val contract: ContractClassName,
|
||||||
|
/** Identity of the notary that ensures the state is not used as an input to a transaction more than once */
|
||||||
|
val notary: Party,
|
||||||
|
/**
|
||||||
|
* All contract states may be _encumbered_ by up to one other state.
|
||||||
|
*
|
||||||
|
* The encumbrance state, if present, forces additional controls over the encumbered state, since the platform checks
|
||||||
|
* that the encumbrance state is present as an input in the same transaction that consumes the encumbered state, and
|
||||||
|
* the contract code and rules of the encumbrance state will also be verified during the execution of the transaction.
|
||||||
|
* For example, a cash contract state could be encumbered with a time-lock contract state; the cash state is then only
|
||||||
|
* processable in a transaction that verifies that the time specified in the encumbrance time-lock has passed.
|
||||||
|
*
|
||||||
|
* The encumbered state refers to another by index, and the referred encumbrance state
|
||||||
|
* is an output state in a particular position on the same transaction that created the encumbered state. An alternative
|
||||||
|
* implementation would be encumbering by reference to a [StateRef], which would allow the specification of encumbrance
|
||||||
|
* by a state created in a prior transaction.
|
||||||
|
*
|
||||||
|
* Note that an encumbered state that is being consumed must have its encumbrance consumed in the same transaction,
|
||||||
|
* otherwise the transaction is not valid.
|
||||||
|
*/
|
||||||
|
val encumbrance: Int? = null,
|
||||||
|
/**
|
||||||
|
* A validator for the contract attachments on the transaction.
|
||||||
|
*/
|
||||||
|
val constraint: AttachmentConstraint = AutomaticHashConstraint)
|
||||||
|
// DOCEND 1
|
@ -16,6 +16,15 @@ sealed class TransactionVerificationException(val txId: SecureHash, message: Str
|
|||||||
class ContractRejection(txId: SecureHash, contract: Contract, cause: Throwable)
|
class ContractRejection(txId: SecureHash, contract: Contract, cause: Throwable)
|
||||||
: TransactionVerificationException(txId, "Contract verification failed: ${cause.message}, contract: $contract", cause)
|
: TransactionVerificationException(txId, "Contract verification failed: ${cause.message}, contract: $contract", cause)
|
||||||
|
|
||||||
|
class ContractConstraintRejection(txId: SecureHash, contractClass: String)
|
||||||
|
: TransactionVerificationException(txId, "Contract constraints failed for $contractClass", null)
|
||||||
|
|
||||||
|
class MissingAttachmentRejection(txId: SecureHash, contractClass: String)
|
||||||
|
: TransactionVerificationException(txId, "Contract constraints failed, could not find attachment for: $contractClass", null)
|
||||||
|
|
||||||
|
class ContractCreationError(txId: SecureHash, contractClass: String, cause: Throwable)
|
||||||
|
: TransactionVerificationException(txId, "Contract verification failed: ${cause.message}, could not create contract class: $contractClass", cause)
|
||||||
|
|
||||||
class MoreThanOneNotary(txId: SecureHash)
|
class MoreThanOneNotary(txId: SecureHash)
|
||||||
: TransactionVerificationException(txId, "More than one notary", null)
|
: TransactionVerificationException(txId, "More than one notary", null)
|
||||||
|
|
||||||
|
33
core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt
Normal file
33
core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package net.corda.core.cordapp
|
||||||
|
|
||||||
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.node.CordaPluginRegistry
|
||||||
|
import net.corda.core.schemas.MappedSchema
|
||||||
|
import net.corda.core.serialization.SerializeAsToken
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a cordapp by registering the JAR that contains it and all important classes for Corda.
|
||||||
|
* Instances of this class are generated automatically at startup of a node and can get retrieved from
|
||||||
|
* [CordappProvider.getAppContext] from the [CordappContext] it returns.
|
||||||
|
*
|
||||||
|
* This will only need to be constructed manually for certain kinds of tests.
|
||||||
|
*
|
||||||
|
* @property contractClassNames List of contracts
|
||||||
|
* @property initiatedFlows List of initiatable flow classes
|
||||||
|
* @property rpcFlows List of RPC initiable flows classes
|
||||||
|
* @property servies List of RPC services
|
||||||
|
* @property plugins List of Corda plugin registries
|
||||||
|
* @property customSchemas List of custom schemas
|
||||||
|
* @property jarPath The path to the JAR for this CorDapp
|
||||||
|
*/
|
||||||
|
interface Cordapp {
|
||||||
|
val contractClassNames: List<String>
|
||||||
|
val initiatedFlows: List<Class<out FlowLogic<*>>>
|
||||||
|
val rpcFlows: List<Class<out FlowLogic<*>>>
|
||||||
|
val services: List<Class<out SerializeAsToken>>
|
||||||
|
val plugins: List<CordaPluginRegistry>
|
||||||
|
val customSchemas: Set<MappedSchema>
|
||||||
|
val jarPath: URL
|
||||||
|
val cordappClasses: List<String>
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package net.corda.core.cordapp
|
||||||
|
|
||||||
|
import net.corda.core.crypto.SecureHash
|
||||||
|
|
||||||
|
// TODO: Add per app config
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An app context provides information about where an app was loaded from, access to its classloader,
|
||||||
|
* and (in the included [Cordapp] object) lists of annotated classes discovered via scanning the JAR.
|
||||||
|
*
|
||||||
|
* A CordappContext is obtained from [CordappProvider.getAppContext] which resides on a [ServiceHub]. This will be
|
||||||
|
* used primarily from within flows.
|
||||||
|
*
|
||||||
|
* @property cordapp The cordapp this context is about
|
||||||
|
* @property attachmentId For CorDapps containing [Contract] or [UpgradedContract] implementations this will be populated
|
||||||
|
* with the attachment containing those class files
|
||||||
|
* @property classLoader the classloader used to load this cordapp's classes
|
||||||
|
*/
|
||||||
|
class CordappContext(val cordapp: Cordapp, val attachmentId: SecureHash?, val classLoader: ClassLoader)
|
@ -0,0 +1,28 @@
|
|||||||
|
package net.corda.core.cordapp
|
||||||
|
|
||||||
|
import net.corda.core.contracts.ContractClassName
|
||||||
|
import net.corda.core.node.services.AttachmentId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides access to what the node knows about loaded applications.
|
||||||
|
*/
|
||||||
|
interface CordappProvider {
|
||||||
|
/**
|
||||||
|
* Exposes the current CorDapp context which will contain information and configuration of the CorDapp that
|
||||||
|
* is currently running.
|
||||||
|
*
|
||||||
|
* The calling application is found via stack walking and finding the first class on the stack that matches any class
|
||||||
|
* contained within the automatically resolved [Cordapp]s loaded by the [CordappLoader]
|
||||||
|
*
|
||||||
|
* @throws IllegalStateException When called from a non-app context
|
||||||
|
*/
|
||||||
|
fun getAppContext(): CordappContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve an attachment ID for a given contract name
|
||||||
|
*
|
||||||
|
* @param contractClassName The contract to find the attachment for
|
||||||
|
* @return An attachment ID if it exists
|
||||||
|
*/
|
||||||
|
fun getContractAttachmentID(contractClassName: ContractClassName): AttachmentId?
|
||||||
|
}
|
@ -30,7 +30,7 @@ import java.util.*
|
|||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
class CompositeKey private constructor(val threshold: Int, children: List<NodeAndWeight>) : PublicKey {
|
class CompositeKey private constructor(val threshold: Int, children: List<NodeAndWeight>) : PublicKey {
|
||||||
companion object {
|
companion object {
|
||||||
val KEY_ALGORITHM = "COMPOSITE"
|
const val KEY_ALGORITHM = "COMPOSITE"
|
||||||
/**
|
/**
|
||||||
* Build a composite key from a DER encoded form.
|
* Build a composite key from a DER encoded form.
|
||||||
*/
|
*/
|
||||||
@ -88,7 +88,9 @@ class CompositeKey private constructor(val threshold: Int, children: List<NodeAn
|
|||||||
if (node is CompositeKey) {
|
if (node is CompositeKey) {
|
||||||
val curVisitedMap = IdentityHashMap<CompositeKey, Boolean>()
|
val curVisitedMap = IdentityHashMap<CompositeKey, Boolean>()
|
||||||
curVisitedMap.putAll(visitedMap)
|
curVisitedMap.putAll(visitedMap)
|
||||||
require(!curVisitedMap.contains(node)) { "Cycle detected for CompositeKey: $node" }
|
// We can't print the node details, because doing so involves serializing the node, which we can't
|
||||||
|
// do because of the cyclic graph.
|
||||||
|
require(!curVisitedMap.contains(node)) { "Cycle detected for CompositeKey" }
|
||||||
curVisitedMap.put(node, true)
|
curVisitedMap.put(node, true)
|
||||||
node.cycleDetection(curVisitedMap)
|
node.cycleDetection(curVisitedMap)
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import java.security.spec.AlgorithmParameterSpec
|
|||||||
class CompositeSignature : Signature(SIGNATURE_ALGORITHM) {
|
class CompositeSignature : Signature(SIGNATURE_ALGORITHM) {
|
||||||
companion object {
|
companion object {
|
||||||
const val SIGNATURE_ALGORITHM = "COMPOSITESIG"
|
const val SIGNATURE_ALGORITHM = "COMPOSITESIG"
|
||||||
|
@JvmStatic
|
||||||
fun getService(provider: Provider) = Provider.Service(provider, "Signature", SIGNATURE_ALGORITHM, CompositeSignature::class.java.name, emptyList(), emptyMap())
|
fun getService(provider: Provider) = Provider.Service(provider, "Signature", SIGNATURE_ALGORITHM, CompositeSignature::class.java.name, emptyList(), emptyMap())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
package net.corda.core.crypto
|
package net.corda.core.crypto
|
||||||
|
|
||||||
|
import net.corda.core.internal.X509EdDSAEngine
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.i2p.crypto.eddsa.EdDSAEngine
|
import net.i2p.crypto.eddsa.*
|
||||||
import net.i2p.crypto.eddsa.EdDSAPrivateKey
|
|
||||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
|
||||||
import net.i2p.crypto.eddsa.EdDSASecurityProvider
|
|
||||||
import net.i2p.crypto.eddsa.math.GroupElement
|
import net.i2p.crypto.eddsa.math.GroupElement
|
||||||
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec
|
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec
|
||||||
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable
|
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable
|
||||||
@ -22,7 +20,6 @@ import org.bouncycastle.asn1.sec.SECObjectIdentifiers
|
|||||||
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
|
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
|
||||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
||||||
import org.bouncycastle.asn1.x9.X9ObjectIdentifiers
|
import org.bouncycastle.asn1.x9.X9ObjectIdentifiers
|
||||||
import org.bouncycastle.cert.X509CertificateHolder
|
|
||||||
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey
|
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey
|
||||||
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey
|
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey
|
||||||
import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPrivateKey
|
import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPrivateKey
|
||||||
@ -42,6 +39,8 @@ import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PublicKey
|
|||||||
import org.bouncycastle.pqc.jcajce.spec.SPHINCS256KeyGenParameterSpec
|
import org.bouncycastle.pqc.jcajce.spec.SPHINCS256KeyGenParameterSpec
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.security.*
|
import java.security.*
|
||||||
|
import java.security.KeyFactory
|
||||||
|
import java.security.KeyPairGenerator
|
||||||
import java.security.spec.InvalidKeySpecException
|
import java.security.spec.InvalidKeySpecException
|
||||||
import java.security.spec.PKCS8EncodedKeySpec
|
import java.security.spec.PKCS8EncodedKeySpec
|
||||||
import java.security.spec.X509EncodedKeySpec
|
import java.security.spec.X509EncodedKeySpec
|
||||||
@ -201,6 +200,8 @@ object Crypto {
|
|||||||
|
|
||||||
private fun getBouncyCastleProvider() = BouncyCastleProvider().apply {
|
private fun getBouncyCastleProvider() = BouncyCastleProvider().apply {
|
||||||
putAll(EdDSASecurityProvider())
|
putAll(EdDSASecurityProvider())
|
||||||
|
// Override the normal EdDSA engine with one which can handle X509 keys.
|
||||||
|
put("Signature.${EdDSAEngine.SIGNATURE_ALGORITHM}", X509EdDSAEngine::class.qualifiedName)
|
||||||
addKeyInfoConverter(EDDSA_ED25519_SHA512.signatureOID.algorithm, KeyInfoConverter(EDDSA_ED25519_SHA512))
|
addKeyInfoConverter(EDDSA_ED25519_SHA512.signatureOID.algorithm, KeyInfoConverter(EDDSA_ED25519_SHA512))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -920,8 +921,7 @@ object Crypto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a public key to a supported implementation. This method is usually required to retrieve a key from an
|
* Convert a public key to a supported implementation.
|
||||||
* [X509CertificateHolder].
|
|
||||||
*
|
*
|
||||||
* @param key a public key.
|
* @param key a public key.
|
||||||
* @return a supported implementation of the input public key.
|
* @return a supported implementation of the input public key.
|
||||||
|
@ -2,11 +2,15 @@
|
|||||||
|
|
||||||
package net.corda.core.crypto
|
package net.corda.core.crypto
|
||||||
|
|
||||||
|
import net.corda.core.contracts.PrivacySalt
|
||||||
|
import net.corda.core.serialization.SerializationDefaults
|
||||||
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.core.utilities.OpaqueBytes
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
import net.corda.core.utilities.toBase58
|
import net.corda.core.utilities.toBase58
|
||||||
import net.corda.core.utilities.toSHA256Bytes
|
import net.corda.core.utilities.toSHA256Bytes
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import net.corda.core.utilities.SgxSupport
|
import net.corda.core.utilities.SgxSupport
|
||||||
|
import java.nio.ByteBuffer
|
||||||
import java.security.*
|
import java.security.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -209,3 +213,27 @@ fun random63BitValue(): Long {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the hash of each serialised component so as to be used as Merkle tree leaf. The resultant output (leaf) is
|
||||||
|
* calculated using the SHA256d algorithm, thus SHA256(SHA256(nonce || serializedComponent)), where nonce is computed
|
||||||
|
* from [computeNonce].
|
||||||
|
*/
|
||||||
|
fun componentHash(opaqueBytes: OpaqueBytes, privacySalt: PrivacySalt, componentGroupIndex: Int, internalIndex: Int): SecureHash =
|
||||||
|
componentHash(computeNonce(privacySalt, componentGroupIndex, internalIndex), opaqueBytes)
|
||||||
|
|
||||||
|
/** Return the SHA256(SHA256(nonce || serializedComponent)). */
|
||||||
|
fun componentHash(nonce: SecureHash, opaqueBytes: OpaqueBytes): SecureHash = SecureHash.sha256Twice(nonce.bytes + opaqueBytes.bytes)
|
||||||
|
|
||||||
|
/** Serialise the object and return the hash of the serialized bytes. */
|
||||||
|
fun <T : Any> serializedHash(x: T): SecureHash = x.serialize(context = SerializationDefaults.P2P_CONTEXT.withoutReferences()).bytes.sha256()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to compute a nonce based on privacySalt, component group index and component internal index.
|
||||||
|
* SHA256d (double SHA256) is used to prevent length extension attacks.
|
||||||
|
* @param privacySalt a [PrivacySalt].
|
||||||
|
* @param groupIndex the fixed index (ordinal) of this component group.
|
||||||
|
* @param internalIndex the internal index of this object in its corresponding components list.
|
||||||
|
* @return SHA256(SHA256(privacySalt || groupIndex || internalIndex))
|
||||||
|
*/
|
||||||
|
fun computeNonce(privacySalt: PrivacySalt, groupIndex: Int, internalIndex: Int) = SecureHash.sha256Twice(privacySalt.bytes + ByteBuffer.allocate(8).putInt(groupIndex).putInt(internalIndex).array())
|
||||||
|
@ -121,6 +121,28 @@ class PartialMerkleTree(val root: PartialTree) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursive calculation of root of this partial tree.
|
||||||
|
* Modifies usedHashes to later check for inclusion with hashes provided.
|
||||||
|
* @param node the partial Merkle tree for which we want to calculate the Merkle root.
|
||||||
|
* @param usedHashes a mutable list that at the end of this recursive algorithm, it will consist of the included leaves (hashes of the visible components).
|
||||||
|
* @return the root [SecureHash] of this partial Merkle tree.
|
||||||
|
*/
|
||||||
|
fun rootAndUsedHashes(node: PartialTree, usedHashes: MutableList<SecureHash>): SecureHash {
|
||||||
|
return when (node) {
|
||||||
|
is PartialTree.IncludedLeaf -> {
|
||||||
|
usedHashes.add(node.hash)
|
||||||
|
node.hash
|
||||||
|
}
|
||||||
|
is PartialTree.Leaf -> node.hash
|
||||||
|
is PartialTree.Node -> {
|
||||||
|
val leftHash = rootAndUsedHashes(node.left, usedHashes)
|
||||||
|
val rightHash = rootAndUsedHashes(node.right, usedHashes)
|
||||||
|
return leftHash.hashConcat(rightHash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -129,29 +151,10 @@ class PartialMerkleTree(val root: PartialTree) {
|
|||||||
*/
|
*/
|
||||||
fun verify(merkleRootHash: SecureHash, hashesToCheck: List<SecureHash>): Boolean {
|
fun verify(merkleRootHash: SecureHash, hashesToCheck: List<SecureHash>): Boolean {
|
||||||
val usedHashes = ArrayList<SecureHash>()
|
val usedHashes = ArrayList<SecureHash>()
|
||||||
val verifyRoot = verify(root, usedHashes)
|
val verifyRoot = rootAndUsedHashes(root, usedHashes)
|
||||||
// It means that we obtained more/fewer hashes than needed or different sets of hashes.
|
// It means that we obtained more/fewer hashes than needed or different sets of hashes.
|
||||||
if (hashesToCheck.groupBy { it } != usedHashes.groupBy { it })
|
if (hashesToCheck.groupBy { it } != usedHashes.groupBy { it })
|
||||||
return false
|
return false
|
||||||
return (verifyRoot == merkleRootHash)
|
return (verifyRoot == merkleRootHash)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursive calculation of root of this partial tree.
|
|
||||||
* Modifies usedHashes to later check for inclusion with hashes provided.
|
|
||||||
*/
|
|
||||||
private fun verify(node: PartialTree, usedHashes: MutableList<SecureHash>): SecureHash {
|
|
||||||
return when (node) {
|
|
||||||
is PartialTree.IncludedLeaf -> {
|
|
||||||
usedHashes.add(node.hash)
|
|
||||||
node.hash
|
|
||||||
}
|
|
||||||
is PartialTree.Leaf -> node.hash
|
|
||||||
is PartialTree.Node -> {
|
|
||||||
val leftHash = verify(node.left, usedHashes)
|
|
||||||
val rightHash = verify(node.right, usedHashes)
|
|
||||||
return leftHash.hashConcat(rightHash)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,7 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
|
|||||||
|
|
||||||
@JvmStatic fun randomSHA256() = sha256(newSecureRandom().generateSeed(32))
|
@JvmStatic fun randomSHA256() = sha256(newSecureRandom().generateSeed(32))
|
||||||
val zeroHash = SecureHash.SHA256(ByteArray(32, { 0.toByte() }))
|
val zeroHash = SecureHash.SHA256(ByteArray(32, { 0.toByte() }))
|
||||||
|
val allOnesHash = SecureHash.SHA256(ByteArray(32, { 255.toByte() }))
|
||||||
}
|
}
|
||||||
|
|
||||||
// In future, maybe SHA3, truncated hashes etc.
|
// In future, maybe SHA3, truncated hashes etc.
|
||||||
|
@ -6,13 +6,14 @@ import net.corda.core.contracts.StateAndRef
|
|||||||
import net.corda.core.contracts.StateRef
|
import net.corda.core.contracts.StateRef
|
||||||
import net.corda.core.crypto.TransactionSignature
|
import net.corda.core.crypto.TransactionSignature
|
||||||
import net.corda.core.crypto.isFulfilledBy
|
import net.corda.core.crypto.isFulfilledBy
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.AbstractParty
|
||||||
|
import net.corda.core.identity.excludeHostNode
|
||||||
|
import net.corda.core.identity.groupAbstractPartyByWellKnownParty
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
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.UntrustworthyData
|
import net.corda.core.utilities.UntrustworthyData
|
||||||
import net.corda.core.utilities.unwrap
|
import net.corda.core.utilities.unwrap
|
||||||
import java.security.PublicKey
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract flow to be used for replacing one state with another, for example when changing the notary of a state.
|
* Abstract flow to be used for replacing one state with another, for example when changing the notary of a state.
|
||||||
@ -33,10 +34,8 @@ abstract class AbstractStateReplacementFlow {
|
|||||||
* The assembled transaction for upgrading a contract.
|
* The assembled transaction for upgrading a contract.
|
||||||
*
|
*
|
||||||
* @param stx signed transaction to do the upgrade.
|
* @param stx signed transaction to do the upgrade.
|
||||||
* @param participants the parties involved in the upgrade transaction.
|
|
||||||
* @param myKey key
|
|
||||||
*/
|
*/
|
||||||
data class UpgradeTx(val stx: SignedTransaction, val participants: Iterable<PublicKey>, val myKey: PublicKey)
|
data class UpgradeTx(val stx: SignedTransaction)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [Instigator] assembles the transaction for state replacement and sends out change proposals to all participants
|
* The [Instigator] assembles the transaction for state replacement and sends out change proposals to all participants
|
||||||
@ -62,15 +61,11 @@ abstract class AbstractStateReplacementFlow {
|
|||||||
@Suspendable
|
@Suspendable
|
||||||
@Throws(StateReplacementException::class)
|
@Throws(StateReplacementException::class)
|
||||||
override fun call(): StateAndRef<T> {
|
override fun call(): StateAndRef<T> {
|
||||||
val (stx, participantKeys, myKey) = assembleTx()
|
val (stx) = assembleTx()
|
||||||
|
val participantSessions = getParticipantSessions()
|
||||||
progressTracker.currentStep = SIGNING
|
progressTracker.currentStep = SIGNING
|
||||||
|
|
||||||
val signatures = if (participantKeys.singleOrNull() == myKey) {
|
val signatures = collectSignatures(participantSessions, stx)
|
||||||
getNotarySignatures(stx)
|
|
||||||
} else {
|
|
||||||
collectSignatures(participantKeys - myKey, stx)
|
|
||||||
}
|
|
||||||
|
|
||||||
val finalTx = stx + signatures
|
val finalTx = stx + signatures
|
||||||
serviceHub.recordTransactions(finalTx)
|
serviceHub.recordTransactions(finalTx)
|
||||||
@ -89,35 +84,38 @@ abstract class AbstractStateReplacementFlow {
|
|||||||
/**
|
/**
|
||||||
* Build the upgrade transaction.
|
* Build the upgrade transaction.
|
||||||
*
|
*
|
||||||
* @return a triple of the transaction, the public keys of all participants, and the participating public key of
|
* @return the transaction
|
||||||
* this node.
|
|
||||||
*/
|
*/
|
||||||
abstract protected fun assembleTx(): UpgradeTx
|
abstract protected fun assembleTx(): UpgradeTx
|
||||||
|
|
||||||
@Suspendable
|
/**
|
||||||
private fun collectSignatures(participants: Iterable<PublicKey>, stx: SignedTransaction): List<TransactionSignature> {
|
* Initiate sessions with parties we want signatures from.
|
||||||
val parties = participants.map {
|
*/
|
||||||
val participantNode = serviceHub.networkMapCache.getNodeByLegalIdentityKey(it) ?:
|
open fun getParticipantSessions(): List<Pair<FlowSession, List<AbstractParty>>> {
|
||||||
throw IllegalStateException("Participant $it to state $originalState not found on the network")
|
return excludeHostNode(serviceHub, groupAbstractPartyByWellKnownParty(serviceHub, originalState.state.data.participants)).map { initiateFlow(it.key) to it.value }
|
||||||
participantNode.legalIdentity
|
}
|
||||||
}
|
|
||||||
|
|
||||||
val participantSignatures = parties.map { getParticipantSignature(it, stx) }
|
@Suspendable
|
||||||
|
private fun collectSignatures(sessions: List<Pair<FlowSession, List<AbstractParty>>>, stx: SignedTransaction): List<TransactionSignature> {
|
||||||
|
val participantSignatures = sessions.map { getParticipantSignature(it.first, it.second, stx) }
|
||||||
|
|
||||||
val allPartySignedTx = stx + participantSignatures
|
val allPartySignedTx = stx + participantSignatures
|
||||||
|
|
||||||
val allSignatures = participantSignatures + getNotarySignatures(allPartySignedTx)
|
val allSignatures = participantSignatures + getNotarySignatures(allPartySignedTx)
|
||||||
parties.forEach { send(it, allSignatures) }
|
sessions.forEach { it.first.send(allSignatures) }
|
||||||
|
|
||||||
return allSignatures
|
return allSignatures
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
private fun getParticipantSignature(party: Party, stx: SignedTransaction): TransactionSignature {
|
private fun getParticipantSignature(session: FlowSession, party: List<AbstractParty>, stx: SignedTransaction): TransactionSignature {
|
||||||
|
require(party.size == 1) {
|
||||||
|
"We do not currently support multiple signatures from the same party ${session.counterparty}: $party"
|
||||||
|
}
|
||||||
val proposal = Proposal(originalState.ref, modification)
|
val proposal = Proposal(originalState.ref, modification)
|
||||||
subFlow(SendTransactionFlow(party, stx))
|
subFlow(SendTransactionFlow(session, stx))
|
||||||
return sendAndReceive<TransactionSignature>(party, proposal).unwrap {
|
return session.sendAndReceive<TransactionSignature>(proposal).unwrap {
|
||||||
check(party.owningKey.isFulfilledBy(it.by)) { "Not signed by the required participant" }
|
check(party.single().owningKey.isFulfilledBy(it.by)) { "Not signed by the required participant" }
|
||||||
it.verify(stx.id)
|
it.verify(stx.id)
|
||||||
it
|
it
|
||||||
}
|
}
|
||||||
@ -136,9 +134,9 @@ abstract class AbstractStateReplacementFlow {
|
|||||||
|
|
||||||
// Type parameter should ideally be Unit but that prevents Java code from subclassing it (https://youtrack.jetbrains.com/issue/KT-15964).
|
// Type parameter should ideally be Unit but that prevents Java code from subclassing it (https://youtrack.jetbrains.com/issue/KT-15964).
|
||||||
// We use Void? instead of Unit? as that's what you'd use in Java.
|
// We use Void? instead of Unit? as that's what you'd use in Java.
|
||||||
abstract class Acceptor<in T>(val otherSide: Party,
|
abstract class Acceptor<in T>(val initiatingSession: FlowSession,
|
||||||
override val progressTracker: ProgressTracker = Acceptor.tracker()) : FlowLogic<Void?>() {
|
override val progressTracker: ProgressTracker = Acceptor.tracker()) : FlowLogic<Void?>() {
|
||||||
constructor(otherSide: Party) : this(otherSide, Acceptor.tracker())
|
constructor(initiatingSession: FlowSession) : this(initiatingSession, Acceptor.tracker())
|
||||||
companion object {
|
companion object {
|
||||||
object VERIFYING : ProgressTracker.Step("Verifying state replacement proposal")
|
object VERIFYING : ProgressTracker.Step("Verifying state replacement proposal")
|
||||||
object APPROVING : ProgressTracker.Step("State replacement approved")
|
object APPROVING : ProgressTracker.Step("State replacement approved")
|
||||||
@ -151,9 +149,9 @@ abstract class AbstractStateReplacementFlow {
|
|||||||
override fun call(): Void? {
|
override fun call(): Void? {
|
||||||
progressTracker.currentStep = VERIFYING
|
progressTracker.currentStep = VERIFYING
|
||||||
// We expect stx to have insufficient signatures here
|
// We expect stx to have insufficient signatures here
|
||||||
val stx = subFlow(ReceiveTransactionFlow(otherSide, checkSufficientSignatures = false))
|
val stx = subFlow(ReceiveTransactionFlow(initiatingSession, checkSufficientSignatures = false))
|
||||||
checkMySignatureRequired(stx)
|
checkMySignatureRequired(stx)
|
||||||
val maybeProposal: UntrustworthyData<Proposal<T>> = receive(otherSide)
|
val maybeProposal: UntrustworthyData<Proposal<T>> = initiatingSession.receive()
|
||||||
maybeProposal.unwrap {
|
maybeProposal.unwrap {
|
||||||
verifyProposal(stx, it)
|
verifyProposal(stx, it)
|
||||||
}
|
}
|
||||||
@ -166,7 +164,7 @@ abstract class AbstractStateReplacementFlow {
|
|||||||
progressTracker.currentStep = APPROVING
|
progressTracker.currentStep = APPROVING
|
||||||
|
|
||||||
val mySignature = sign(stx)
|
val mySignature = sign(stx)
|
||||||
val swapSignatures = sendAndReceive<List<TransactionSignature>>(otherSide, mySignature)
|
val swapSignatures = initiatingSession.sendAndReceive<List<TransactionSignature>>(mySignature)
|
||||||
|
|
||||||
// TODO: This step should not be necessary, as signatures are re-checked in verifyRequiredSignatures.
|
// TODO: This step should not be necessary, as signatures are re-checked in verifyRequiredSignatures.
|
||||||
val allSignatures = swapSignatures.unwrap { signatures ->
|
val allSignatures = swapSignatures.unwrap { signatures ->
|
||||||
@ -193,7 +191,8 @@ abstract class AbstractStateReplacementFlow {
|
|||||||
|
|
||||||
private fun checkMySignatureRequired(stx: SignedTransaction) {
|
private fun checkMySignatureRequired(stx: SignedTransaction) {
|
||||||
// TODO: use keys from the keyManagementService instead
|
// TODO: use keys from the keyManagementService instead
|
||||||
val myKey = serviceHub.myInfo.legalIdentity.owningKey
|
// TODO Check the set of multiple identities?
|
||||||
|
val myKey = ourIdentity.owningKey
|
||||||
|
|
||||||
val requiredKeys = if (stx.isNotaryChangeTransaction()) {
|
val requiredKeys = if (stx.isNotaryChangeTransaction()) {
|
||||||
stx.resolveNotaryChangeTransaction(serviceHub).requiredSigningKeys
|
stx.resolveNotaryChangeTransaction(serviceHub).requiredSigningKeys
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
package net.corda.core.flows
|
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
|
||||||
import net.corda.core.identity.Party
|
|
||||||
import net.corda.core.transactions.SignedTransaction
|
|
||||||
import net.corda.core.utilities.NonEmptySet
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notify the specified parties about a transaction. The remote peers will download this transaction and its
|
|
||||||
* dependency graph, verifying them all. The flow returns when all peers have acknowledged the transactions
|
|
||||||
* as valid. Normally you wouldn't use this directly, it would be called via [FinalityFlow].
|
|
||||||
*
|
|
||||||
* @param notarisedTransaction transaction which has been notarised (if needed) and is ready to notify nodes about.
|
|
||||||
* @param participants a list of participants involved in the transaction.
|
|
||||||
* @return a list of participants who were successfully notified of the transaction.
|
|
||||||
*/
|
|
||||||
@InitiatingFlow
|
|
||||||
class BroadcastTransactionFlow(val notarisedTransaction: SignedTransaction,
|
|
||||||
val participants: NonEmptySet<Party>) : FlowLogic<Unit>() {
|
|
||||||
@Suspendable
|
|
||||||
override fun call() {
|
|
||||||
// TODO: Messaging layer should handle this broadcast for us
|
|
||||||
participants.filter { it != serviceHub.myInfo.legalIdentity }.forEach { participant ->
|
|
||||||
// SendTransactionFlow allows otherParty to access our data to resolve the transaction.
|
|
||||||
subFlow(SendTransactionFlow(participant, notarisedTransaction))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,9 +3,8 @@ package net.corda.core.flows
|
|||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.crypto.TransactionSignature
|
import net.corda.core.crypto.TransactionSignature
|
||||||
import net.corda.core.crypto.isFulfilledBy
|
import net.corda.core.crypto.isFulfilledBy
|
||||||
import net.corda.core.utilities.toBase58String
|
|
||||||
import net.corda.core.identity.AnonymousParty
|
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.identity.groupPublicKeysByWellKnownParty
|
||||||
import net.corda.core.node.ServiceHub
|
import net.corda.core.node.ServiceHub
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.transactions.WireTransaction
|
import net.corda.core.transactions.WireTransaction
|
||||||
@ -57,27 +56,31 @@ import java.security.PublicKey
|
|||||||
* val stx = subFlow(CollectSignaturesFlow(ptx))
|
* val stx = subFlow(CollectSignaturesFlow(ptx))
|
||||||
*
|
*
|
||||||
* @param partiallySignedTx Transaction to collect the remaining signatures for
|
* @param partiallySignedTx Transaction to collect the remaining signatures for
|
||||||
|
* @param sessionsToCollectFrom A session for every party we need to collect a signature from. Must be an exact match.
|
||||||
* @param myOptionalKeys set of keys in the transaction which are owned by this node. This includes keys used on commands, not
|
* @param myOptionalKeys set of keys in the transaction which are owned by this node. This includes keys used on commands, not
|
||||||
* just in the states. If null, the default well known identity of the node is used.
|
* just in the states. If null, the default well known identity of the node is used.
|
||||||
*/
|
*/
|
||||||
// TODO: AbstractStateReplacementFlow needs updating to use this flow.
|
// TODO: AbstractStateReplacementFlow needs updating to use this flow.
|
||||||
class CollectSignaturesFlow @JvmOverloads constructor (val partiallySignedTx: SignedTransaction,
|
class CollectSignaturesFlow @JvmOverloads constructor (val partiallySignedTx: SignedTransaction,
|
||||||
|
val sessionsToCollectFrom: Collection<FlowSession>,
|
||||||
val myOptionalKeys: Iterable<PublicKey>?,
|
val myOptionalKeys: Iterable<PublicKey>?,
|
||||||
override val progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : FlowLogic<SignedTransaction>() {
|
override val progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : FlowLogic<SignedTransaction>() {
|
||||||
@JvmOverloads constructor(partiallySignedTx: SignedTransaction, progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : this(partiallySignedTx, null, progressTracker)
|
@JvmOverloads constructor(partiallySignedTx: SignedTransaction, sessionsToCollectFrom: Collection<FlowSession>, progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : this(partiallySignedTx, sessionsToCollectFrom, null, progressTracker)
|
||||||
companion object {
|
companion object {
|
||||||
object COLLECTING : ProgressTracker.Step("Collecting signatures from counter-parties.")
|
object COLLECTING : ProgressTracker.Step("Collecting signatures from counter-parties.")
|
||||||
object VERIFYING : ProgressTracker.Step("Verifying collected signatures.")
|
object VERIFYING : ProgressTracker.Step("Verifying collected signatures.")
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
fun tracker() = ProgressTracker(COLLECTING, VERIFYING)
|
fun tracker() = ProgressTracker(COLLECTING, VERIFYING)
|
||||||
|
|
||||||
// TODO: Make the progress tracker adapt to the number of counter-parties to collect from.
|
// TODO: Make the progress tracker adapt to the number of counter-parties to collect from.
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable override fun call(): SignedTransaction {
|
@Suspendable override fun call(): SignedTransaction {
|
||||||
// Check the signatures which have already been provided and that the transaction is valid.
|
// Check the signatures which have already been provided and that the transaction is valid.
|
||||||
// Usually just the Initiator and possibly an oracle would have signed at this point.
|
// Usually just the Initiator and possibly an oracle would have signed at this point.
|
||||||
val myKeys: Iterable<PublicKey> = myOptionalKeys ?: listOf(serviceHub.myInfo.legalIdentity.owningKey)
|
val myKeys: Iterable<PublicKey> = myOptionalKeys ?: listOf(ourIdentity.owningKey)
|
||||||
val signed = partiallySignedTx.sigs.map { it.by }
|
val signed = partiallySignedTx.sigs.map { it.by }
|
||||||
val notSigned = partiallySignedTx.tx.requiredSigningKeys - signed
|
val notSigned = partiallySignedTx.tx.requiredSigningKeys - signed
|
||||||
|
|
||||||
@ -100,8 +103,15 @@ class CollectSignaturesFlow @JvmOverloads constructor (val partiallySignedTx: Si
|
|||||||
// If the unsigned counter-parties list is empty then we don't need to collect any more signatures here.
|
// If the unsigned counter-parties list is empty then we don't need to collect any more signatures here.
|
||||||
if (unsigned.isEmpty()) return partiallySignedTx
|
if (unsigned.isEmpty()) return partiallySignedTx
|
||||||
|
|
||||||
|
val partyToKeysMap = groupPublicKeysByWellKnownParty(serviceHub, unsigned)
|
||||||
|
// Check that we have a session for all parties. No more, no less.
|
||||||
|
require(sessionsToCollectFrom.map { it.counterparty }.toSet() == partyToKeysMap.keys) {
|
||||||
|
"The Initiator of CollectSignaturesFlow must pass in exactly the sessions required to sign the transaction."
|
||||||
|
}
|
||||||
// Collect signatures from all counter-parties and append them to the partially signed transaction.
|
// Collect signatures from all counter-parties and append them to the partially signed transaction.
|
||||||
val counterpartySignatures = keysToParties(unsigned).map { collectSignature(it.first, it.second) }
|
val counterpartySignatures = sessionsToCollectFrom.flatMap { session ->
|
||||||
|
subFlow(CollectSignatureFlow(partiallySignedTx, session, partyToKeysMap[session.counterparty]!!))
|
||||||
|
}
|
||||||
val stx = partiallySignedTx + counterpartySignatures
|
val stx = partiallySignedTx + counterpartySignatures
|
||||||
|
|
||||||
// Verify all but the notary's signature if the transaction requires a notary, otherwise verify all signatures.
|
// Verify all but the notary's signature if the transaction requires a notary, otherwise verify all signatures.
|
||||||
@ -110,40 +120,38 @@ class CollectSignaturesFlow @JvmOverloads constructor (val partiallySignedTx: Si
|
|||||||
|
|
||||||
return stx
|
return stx
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
// DOCSTART 1
|
||||||
* Lookup the [Party] object for each [PublicKey] using the [ServiceHub.networkMapCache].
|
/**
|
||||||
*
|
* Get and check the required signature.
|
||||||
* @return a pair of the well known identity to contact for a signature, and the public key that party should sign
|
*
|
||||||
* with (this may belong to a confidential identity).
|
* @param partiallySignedTx the transaction to sign.
|
||||||
*/
|
* @param session the [FlowSession] to connect to to get the signature.
|
||||||
@Suspendable private fun keysToParties(keys: Collection<PublicKey>): List<Pair<Party, PublicKey>> = keys.map {
|
* @param signingKeys the list of keys the party should use to sign the transaction.
|
||||||
val party = serviceHub.identityService.partyFromAnonymous(AnonymousParty(it))
|
*/
|
||||||
?: throw IllegalStateException("Party ${it.toBase58String()} not found on the network.")
|
@Suspendable
|
||||||
Pair(party, it)
|
class CollectSignatureFlow(val partiallySignedTx: SignedTransaction, val session: FlowSession, val signingKeys: List<PublicKey>) : FlowLogic<List<TransactionSignature>>() {
|
||||||
}
|
constructor(partiallySignedTx: SignedTransaction, session: FlowSession, vararg signingKeys: PublicKey) :
|
||||||
|
this(partiallySignedTx, session, listOf(*signingKeys))
|
||||||
// DOCSTART 1
|
@Suspendable
|
||||||
/**
|
override fun call(): List<TransactionSignature> {
|
||||||
* Get and check the required signature.
|
|
||||||
*
|
|
||||||
* @param counterparty the party to request a signature from.
|
|
||||||
* @param signingKey the key the party should use to sign the transaction.
|
|
||||||
*/
|
|
||||||
@Suspendable private fun collectSignature(counterparty: Party, signingKey: PublicKey): TransactionSignature {
|
|
||||||
// SendTransactionFlow allows counterparty to access our data to resolve the transaction.
|
// SendTransactionFlow allows counterparty to access our data to resolve the transaction.
|
||||||
subFlow(SendTransactionFlow(counterparty, partiallySignedTx))
|
subFlow(SendTransactionFlow(session, partiallySignedTx))
|
||||||
// Send the key we expect the counterparty to sign with - this is important where they may have several
|
// Send the key we expect the counterparty to sign with - this is important where they may have several
|
||||||
// keys to sign with, as it makes it faster for them to identify the key to sign with, and more straight forward
|
// keys to sign with, as it makes it faster for them to identify the key to sign with, and more straight forward
|
||||||
// for us to check we have the expected signature returned.
|
// for us to check we have the expected signature returned.
|
||||||
send(counterparty, signingKey)
|
session.send(signingKeys)
|
||||||
return receive<TransactionSignature>(counterparty).unwrap {
|
return session.receive<List<TransactionSignature>>().unwrap { signatures ->
|
||||||
require(signingKey.isFulfilledBy(it.by)) { "Not signed by the required signing key." }
|
require(signatures.size == signingKeys.size) { "Need signature for each signing key" }
|
||||||
it
|
signatures.forEachIndexed { index, signature ->
|
||||||
|
require(signingKeys[index].isFulfilledBy(signature.by)) { "Not signed by the required signing key." }
|
||||||
|
}
|
||||||
|
signatures
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// DOCEND 1
|
|
||||||
}
|
}
|
||||||
|
// DOCEND 1
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [SignTransactionFlow] should be called in response to the [CollectSignaturesFlow]. It automates the signing of
|
* The [SignTransactionFlow] should be called in response to the [CollectSignaturesFlow]. It automates the signing of
|
||||||
@ -159,15 +167,15 @@ class CollectSignaturesFlow @JvmOverloads constructor (val partiallySignedTx: Si
|
|||||||
* - Subclass [SignTransactionFlow] - this can be done inside an existing flow (as shown below)
|
* - Subclass [SignTransactionFlow] - this can be done inside an existing flow (as shown below)
|
||||||
* - Override the [checkTransaction] method to add some custom verification logic
|
* - Override the [checkTransaction] method to add some custom verification logic
|
||||||
* - Call the flow via [FlowLogic.subFlow]
|
* - Call the flow via [FlowLogic.subFlow]
|
||||||
* - The flow returns the fully signed transaction once it has been committed to the ledger
|
* - The flow returns the transaction signed with the additional signature.
|
||||||
*
|
*
|
||||||
* Example - checking and signing a transaction involving a [net.corda.core.contracts.DummyContract], see
|
* Example - checking and signing a transaction involving a [net.corda.core.contracts.DummyContract], see
|
||||||
* CollectSignaturesFlowTests.kt for further examples:
|
* CollectSignaturesFlowTests.kt for further examples:
|
||||||
*
|
*
|
||||||
* class Responder(val otherParty: Party): FlowLogic<SignedTransaction>() {
|
* class Responder(val otherPartySession: FlowSession): FlowLogic<SignedTransaction>() {
|
||||||
* @Suspendable override fun call(): SignedTransaction {
|
* @Suspendable override fun call(): SignedTransaction {
|
||||||
* // [SignTransactionFlow] sub-classed as a singleton object.
|
* // [SignTransactionFlow] sub-classed as a singleton object.
|
||||||
* val flow = object : SignTransactionFlow(otherParty) {
|
* val flow = object : SignTransactionFlow(otherPartySession) {
|
||||||
* @Suspendable override fun checkTransaction(stx: SignedTransaction) = requireThat {
|
* @Suspendable override fun checkTransaction(stx: SignedTransaction) = requireThat {
|
||||||
* val tx = stx.tx
|
* val tx = stx.tx
|
||||||
* val magicNumberState = tx.outputs.single().data as DummyContract.MultiOwnerState
|
* val magicNumberState = tx.outputs.single().data as DummyContract.MultiOwnerState
|
||||||
@ -182,9 +190,9 @@ class CollectSignaturesFlow @JvmOverloads constructor (val partiallySignedTx: Si
|
|||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* @param otherParty The counter-party which is providing you a transaction to sign.
|
* @param otherSideSession The session which is providing you a transaction to sign.
|
||||||
*/
|
*/
|
||||||
abstract class SignTransactionFlow(val otherParty: Party,
|
abstract class SignTransactionFlow(val otherSideSession: FlowSession,
|
||||||
override val progressTracker: ProgressTracker = SignTransactionFlow.tracker()) : FlowLogic<SignedTransaction>() {
|
override val progressTracker: ProgressTracker = SignTransactionFlow.tracker()) : FlowLogic<SignedTransaction>() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -192,23 +200,24 @@ abstract class SignTransactionFlow(val otherParty: Party,
|
|||||||
object VERIFYING : ProgressTracker.Step("Verifying transaction proposal.")
|
object VERIFYING : ProgressTracker.Step("Verifying transaction proposal.")
|
||||||
object SIGNING : ProgressTracker.Step("Signing transaction proposal.")
|
object SIGNING : ProgressTracker.Step("Signing transaction proposal.")
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
fun tracker() = ProgressTracker(RECEIVING, VERIFYING, SIGNING)
|
fun tracker() = ProgressTracker(RECEIVING, VERIFYING, SIGNING)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable override fun call(): SignedTransaction {
|
@Suspendable override fun call(): SignedTransaction {
|
||||||
progressTracker.currentStep = RECEIVING
|
progressTracker.currentStep = RECEIVING
|
||||||
// Receive transaction and resolve dependencies, check sufficient signatures is disabled as we don't have all signatures.
|
// Receive transaction and resolve dependencies, check sufficient signatures is disabled as we don't have all signatures.
|
||||||
val stx = subFlow(ReceiveTransactionFlow(otherParty, checkSufficientSignatures = false))
|
val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false))
|
||||||
// Receive the signing key that the party requesting the signature expects us to sign with. Having this provided
|
// Receive the signing key that the party requesting the signature expects us to sign with. Having this provided
|
||||||
// means we only have to check we own that one key, rather than matching all keys in the transaction against all
|
// means we only have to check we own that one key, rather than matching all keys in the transaction against all
|
||||||
// keys we own.
|
// keys we own.
|
||||||
val signingKey = receive<PublicKey>(otherParty).unwrap {
|
val signingKeys = otherSideSession.receive<List<PublicKey>>().unwrap { keys ->
|
||||||
// TODO: We should have a faster way of verifying we own a single key
|
// TODO: We should have a faster way of verifying we own a single key
|
||||||
serviceHub.keyManagementService.filterMyKeys(listOf(it)).single()
|
serviceHub.keyManagementService.filterMyKeys(keys)
|
||||||
}
|
}
|
||||||
progressTracker.currentStep = VERIFYING
|
progressTracker.currentStep = VERIFYING
|
||||||
// Check that the Responder actually needs to sign.
|
// Check that the Responder actually needs to sign.
|
||||||
checkMySignatureRequired(stx, signingKey)
|
checkMySignaturesRequired(stx, signingKeys)
|
||||||
// Check the signatures which have already been provided. Usually the Initiators and possibly an Oracle's.
|
// Check the signatures which have already been provided. Usually the Initiators and possibly an Oracle's.
|
||||||
checkSignatures(stx)
|
checkSignatures(stx)
|
||||||
stx.tx.toLedgerTransaction(serviceHub).verify()
|
stx.tx.toLedgerTransaction(serviceHub).verify()
|
||||||
@ -223,18 +232,19 @@ abstract class SignTransactionFlow(val otherParty: Party,
|
|||||||
}
|
}
|
||||||
// Sign and send back our signature to the Initiator.
|
// Sign and send back our signature to the Initiator.
|
||||||
progressTracker.currentStep = SIGNING
|
progressTracker.currentStep = SIGNING
|
||||||
val mySignature = serviceHub.createSignature(stx, signingKey)
|
val mySignatures = signingKeys.map { key ->
|
||||||
send(otherParty, mySignature)
|
serviceHub.createSignature(stx, key)
|
||||||
|
}
|
||||||
|
otherSideSession.send(mySignatures)
|
||||||
|
|
||||||
// Return the fully signed transaction once it has been committed.
|
// Return the additionally signed transaction.
|
||||||
return waitForLedgerCommit(stx.id)
|
return stx + mySignatures
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable private fun checkSignatures(stx: SignedTransaction) {
|
@Suspendable private fun checkSignatures(stx: SignedTransaction) {
|
||||||
val signingIdentities = stx.sigs.map(TransactionSignature::by).mapNotNull(serviceHub.identityService::partyFromKey)
|
val signingWellKnownIdentities = groupPublicKeysByWellKnownParty(serviceHub, stx.sigs.map(TransactionSignature::by))
|
||||||
val signingWellKnownIdentities = signingIdentities.mapNotNull(serviceHub.identityService::partyFromAnonymous)
|
require(otherSideSession.counterparty in signingWellKnownIdentities) {
|
||||||
require(otherParty in signingWellKnownIdentities) {
|
"The Initiator of CollectSignaturesFlow must have signed the transaction. Found ${signingWellKnownIdentities}, expected ${otherSideSession}"
|
||||||
"The Initiator of CollectSignaturesFlow must have signed the transaction. Found ${signingWellKnownIdentities}, expected ${otherParty}"
|
|
||||||
}
|
}
|
||||||
val signed = stx.sigs.map { it.by }
|
val signed = stx.sigs.map { it.by }
|
||||||
val allSigners = stx.tx.requiredSigningKeys
|
val allSigners = stx.tx.requiredSigningKeys
|
||||||
@ -266,9 +276,9 @@ abstract class SignTransactionFlow(val otherParty: Party,
|
|||||||
@Throws(FlowException::class)
|
@Throws(FlowException::class)
|
||||||
abstract protected fun checkTransaction(stx: SignedTransaction)
|
abstract protected fun checkTransaction(stx: SignedTransaction)
|
||||||
|
|
||||||
@Suspendable private fun checkMySignatureRequired(stx: SignedTransaction, signingKey: PublicKey) {
|
@Suspendable private fun checkMySignaturesRequired(stx: SignedTransaction, signingKeys: Iterable<PublicKey>) {
|
||||||
require(signingKey in stx.tx.requiredSigningKeys) {
|
require(signingKeys.all { it in stx.tx.requiredSigningKeys }) {
|
||||||
"Party is not a participant for any of the input states of transaction ${stx.id}"
|
"A signature was requested for a key that isn't part of the required signing keys for transaction ${stx.id}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,8 @@ package net.corda.core.flows
|
|||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.internal.ContractUpgradeUtils
|
||||||
import net.corda.core.transactions.LedgerTransaction
|
import net.corda.core.transactions.LedgerTransaction
|
||||||
import net.corda.core.transactions.SignedTransaction
|
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
|
||||||
@ -18,11 +17,37 @@ import java.security.PublicKey
|
|||||||
*/
|
*/
|
||||||
object ContractUpgradeFlow {
|
object ContractUpgradeFlow {
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun verify(tx: LedgerTransaction) {
|
||||||
|
// Contract Upgrade transaction should have 1 input, 1 output and 1 command.
|
||||||
|
verify(tx.inputs.single().state,
|
||||||
|
tx.outputs.single(),
|
||||||
|
tx.commandsOfType<UpgradeCommand>().single())
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun verify(input: TransactionState<ContractState>, output: TransactionState<ContractState>, commandData: Command<UpgradeCommand>) {
|
||||||
|
val command = commandData.value
|
||||||
|
val participantKeys: Set<PublicKey> = input.data.participants.map { it.owningKey }.toSet()
|
||||||
|
val keysThatSigned: Set<PublicKey> = commandData.signers.toSet()
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val upgradedContract = javaClass.classLoader.loadClass(command.upgradedContractClass).newInstance() as UpgradedContract<ContractState, *>
|
||||||
|
requireThat {
|
||||||
|
"The signing keys include all participant keys" using keysThatSigned.containsAll(participantKeys)
|
||||||
|
"Inputs state reference the legacy contract" using (input.contract == upgradedContract.legacyContract)
|
||||||
|
"Outputs state reference the upgraded contract" using (output.contract == command.upgradedContractClass)
|
||||||
|
"Output state must be an upgraded version of the input state" using (output.data == upgradedContract.upgrade(input.data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authorise a contract state upgrade.
|
* Authorise a contract state upgrade.
|
||||||
* This will store the upgrade authorisation in persistent store, and will be queried by [ContractUpgradeFlow.Acceptor] during contract upgrade process.
|
*
|
||||||
* Invoking this flow indicates the node is willing to upgrade the [StateAndRef] using the [UpgradedContract] class.
|
* This will store the upgrade authorisation in persistent store, and will be queried by [ContractUpgradeFlow.Acceptor]
|
||||||
* This method will NOT initiate the upgrade process. To start the upgrade process, see [Initiator].
|
* during contract upgrade process. Invoking this flow indicates the node is willing to upgrade the [StateAndRef] using
|
||||||
|
* the [UpgradedContract] class.
|
||||||
|
*
|
||||||
|
* This flow will NOT initiate the upgrade process. To start the upgrade process, see [Initiate].
|
||||||
*/
|
*/
|
||||||
@StartableByRPC
|
@StartableByRPC
|
||||||
class Authorise(
|
class Authorise(
|
||||||
@ -32,7 +57,7 @@ object ContractUpgradeFlow {
|
|||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call(): Void? {
|
override fun call(): Void? {
|
||||||
val upgrade = upgradedContractClass.newInstance()
|
val upgrade = upgradedContractClass.newInstance()
|
||||||
if (upgrade.legacyContract != stateAndRef.state.data.contract.javaClass) {
|
if (upgrade.legacyContract != stateAndRef.state.contract) {
|
||||||
throw FlowException("The contract state cannot be upgraded using provided UpgradedContract.")
|
throw FlowException("The contract state cannot be upgraded using provided UpgradedContract.")
|
||||||
}
|
}
|
||||||
serviceHub.contractUpgradeService.storeAuthorisedContractUpgrade(stateAndRef.ref, upgradedContractClass)
|
serviceHub.contractUpgradeService.storeAuthorisedContractUpgrade(stateAndRef.ref, upgradedContractClass)
|
||||||
@ -46,9 +71,7 @@ object ContractUpgradeFlow {
|
|||||||
* This will remove the upgrade authorisation from persistent store (and prevent any further upgrade)
|
* This will remove the upgrade authorisation from persistent store (and prevent any further upgrade)
|
||||||
*/
|
*/
|
||||||
@StartableByRPC
|
@StartableByRPC
|
||||||
class Deauthorise(
|
class Deauthorise(val stateRef: StateRef) : FlowLogic<Void?>() {
|
||||||
val stateRef: StateRef
|
|
||||||
) : FlowLogic< Void?>() {
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call(): Void? {
|
override fun call(): Void? {
|
||||||
serviceHub.contractUpgradeService.removeAuthorisedContractUpgrade(stateRef)
|
serviceHub.contractUpgradeService.removeAuthorisedContractUpgrade(stateRef)
|
||||||
@ -56,9 +79,12 @@ object ContractUpgradeFlow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This flow begins the contract upgrade process.
|
||||||
|
*/
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
@StartableByRPC
|
@StartableByRPC
|
||||||
class Initiator<OldState : ContractState, out NewState : ContractState>(
|
class Initiate<OldState : ContractState, out NewState : ContractState>(
|
||||||
originalState: StateAndRef<OldState>,
|
originalState: StateAndRef<OldState>,
|
||||||
newContractClass: Class<out UpgradedContract<OldState, NewState>>
|
newContractClass: Class<out UpgradedContract<OldState, NewState>>
|
||||||
) : AbstractStateReplacementFlow.Instigator<OldState, NewState, Class<out UpgradedContract<OldState, NewState>>>(originalState, newContractClass) {
|
) : AbstractStateReplacementFlow.Instigator<OldState, NewState, Class<out UpgradedContract<OldState, NewState>>>(originalState, newContractClass) {
|
||||||
@ -73,8 +99,8 @@ object ContractUpgradeFlow {
|
|||||||
return TransactionBuilder(stateRef.state.notary)
|
return TransactionBuilder(stateRef.state.notary)
|
||||||
.withItems(
|
.withItems(
|
||||||
stateRef,
|
stateRef,
|
||||||
contractUpgrade.upgrade(stateRef.state.data),
|
StateAndContract(contractUpgrade.upgrade(stateRef.state.data), upgradedContractClass.name),
|
||||||
Command(UpgradeCommand(upgradedContractClass), stateRef.state.data.participants.map { it.owningKey }),
|
Command(UpgradeCommand(upgradedContractClass.name), stateRef.state.data.participants.map { it.owningKey }),
|
||||||
privacySalt
|
privacySalt
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -82,65 +108,12 @@ object ContractUpgradeFlow {
|
|||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx {
|
override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx {
|
||||||
val baseTx = assembleBareTx(originalState, modification, PrivacySalt())
|
val baseTx = ContractUpgradeUtils.assembleBareTx(originalState, modification, PrivacySalt())
|
||||||
val participantKeys = originalState.state.data.participants.map { it.owningKey }.toSet()
|
val participantKeys = originalState.state.data.participants.map { it.owningKey }.toSet()
|
||||||
// TODO: We need a much faster way of finding our key in the transaction
|
// TODO: We need a much faster way of finding our key in the transaction
|
||||||
val myKey = serviceHub.keyManagementService.filterMyKeys(participantKeys).single()
|
val myKey = serviceHub.keyManagementService.filterMyKeys(participantKeys).single()
|
||||||
val stx = serviceHub.signInitialTransaction(baseTx, myKey)
|
val stx = serviceHub.signInitialTransaction(baseTx, myKey)
|
||||||
return AbstractStateReplacementFlow.UpgradeTx(stx, participantKeys, myKey)
|
return AbstractStateReplacementFlow.UpgradeTx(stx)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@StartableByRPC
|
|
||||||
@InitiatedBy(ContractUpgradeFlow.Initiator::class)
|
|
||||||
class Acceptor(otherSide: Party) : AbstractStateReplacementFlow.Acceptor<Class<out UpgradedContract<ContractState, *>>>(otherSide) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
@JvmStatic
|
|
||||||
fun verify(tx: LedgerTransaction) {
|
|
||||||
// Contract Upgrade transaction should have 1 input, 1 output and 1 command.
|
|
||||||
verify(tx.inputStates.single(),
|
|
||||||
tx.outputStates.single(),
|
|
||||||
tx.commandsOfType<UpgradeCommand>().single())
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun verify(input: ContractState, output: ContractState, commandData: Command<UpgradeCommand>) {
|
|
||||||
val command = commandData.value
|
|
||||||
val participantKeys: Set<PublicKey> = input.participants.map { it.owningKey }.toSet()
|
|
||||||
val keysThatSigned: Set<PublicKey> = commandData.signers.toSet()
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val upgradedContract = command.upgradedContractClass.newInstance() as UpgradedContract<ContractState, *>
|
|
||||||
requireThat {
|
|
||||||
"The signing keys include all participant keys" using keysThatSigned.containsAll(participantKeys)
|
|
||||||
"Inputs state reference the legacy contract" using (input.contract.javaClass == upgradedContract.legacyContract)
|
|
||||||
"Outputs state reference the upgraded contract" using (output.contract.javaClass == command.upgradedContractClass)
|
|
||||||
"Output state must be an upgraded version of the input state" using (output == upgradedContract.upgrade(input))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suspendable
|
|
||||||
@Throws(StateReplacementException::class)
|
|
||||||
override fun verifyProposal(stx: SignedTransaction, proposal: AbstractStateReplacementFlow.Proposal<Class<out UpgradedContract<ContractState, *>>>) {
|
|
||||||
// Retrieve signed transaction from our side, we will apply the upgrade logic to the transaction on our side, and
|
|
||||||
// verify outputs matches the proposed upgrade.
|
|
||||||
val ourSTX = serviceHub.validatedTransactions.getTransaction(proposal.stateRef.txhash)
|
|
||||||
requireNotNull(ourSTX) { "We don't have a copy of the referenced state" }
|
|
||||||
val oldStateAndRef = ourSTX!!.tx.outRef<ContractState>(proposal.stateRef.index)
|
|
||||||
val authorisedUpgrade = serviceHub.contractUpgradeService.getAuthorisedContractUpgrade(oldStateAndRef.ref) ?:
|
|
||||||
throw IllegalStateException("Contract state upgrade is unauthorised. State hash : ${oldStateAndRef.ref}")
|
|
||||||
val proposedTx = stx.tx
|
|
||||||
val expectedTx = ContractUpgradeFlow.Initiator.assembleBareTx(oldStateAndRef, proposal.modification, proposedTx.privacySalt).toWireTransaction()
|
|
||||||
requireThat {
|
|
||||||
"The instigator is one of the participants" using (otherSide in oldStateAndRef.state.data.participants)
|
|
||||||
"The proposed upgrade ${proposal.modification.javaClass} is a trusted upgrade path" using (proposal.modification.name == authorisedUpgrade)
|
|
||||||
"The proposed tx matches the expected tx for this upgrade" using (proposedTx == expectedTx)
|
|
||||||
}
|
|
||||||
ContractUpgradeFlow.Acceptor.verify(
|
|
||||||
oldStateAndRef.state.data,
|
|
||||||
expectedTx.outRef<ContractState>(0).state.data,
|
|
||||||
expectedTx.toLedgerTransaction(serviceHub).commandsOfType<UpgradeCommand>().single())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,46 +1,36 @@
|
|||||||
package net.corda.core.flows
|
package net.corda.core.flows
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.contracts.ContractState
|
|
||||||
import net.corda.core.contracts.StateRef
|
|
||||||
import net.corda.core.contracts.TransactionState
|
|
||||||
import net.corda.core.crypto.isFulfilledBy
|
import net.corda.core.crypto.isFulfilledBy
|
||||||
import net.corda.core.identity.AbstractParty
|
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.ResolveTransactionsFlow
|
import net.corda.core.identity.groupAbstractPartyByWellKnownParty
|
||||||
import net.corda.core.node.ServiceHub
|
|
||||||
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.toNonEmptySet
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies the given transactions, then sends them to the named notary. If the notary agrees that the transactions
|
* Verifies the given transaction, then sends it to the named notary. If the notary agrees that the transaction
|
||||||
* are acceptable then they are from that point onwards committed to the ledger, and will be written through to the
|
* is acceptable then it is from that point onwards committed to the ledger, and will be written through to the
|
||||||
* vault. Additionally they will be distributed to the parties reflected in the participants list of the states.
|
* vault. Additionally it will be distributed to the parties reflected in the participants list of the states.
|
||||||
*
|
*
|
||||||
* The transactions will be topologically sorted before commitment to ensure that dependencies are committed before
|
* The transaction is expected to have already been resolved: if its dependencies are not available in local
|
||||||
* dependers, so you don't need to do this yourself.
|
* storage, verification will fail. It must have signatures from all necessary parties other than the notary.
|
||||||
*
|
*
|
||||||
* The transactions are expected to have already been resolved: if their dependencies are not available in local
|
* If specified, the extra recipients are sent the given transaction. The base set of parties to inform are calculated
|
||||||
* storage or within the given set, verification will fail. They must have signatures from all necessary parties
|
* from the contract-given set of participants.
|
||||||
* other than the notary.
|
|
||||||
*
|
*
|
||||||
* If specified, the extra recipients are sent all the given transactions. The base set of parties to inform of each
|
* The flow returns the same transaction but with the additional signatures from the notary.
|
||||||
* transaction are calculated on a per transaction basis from the contract-given set of participants.
|
|
||||||
*
|
*
|
||||||
* The flow returns the same transactions, in the same order, with the additional signatures.
|
* @param transaction What to commit.
|
||||||
*
|
|
||||||
* @param transactions What to commit.
|
|
||||||
* @param extraRecipients A list of additional participants to inform of the transaction.
|
* @param extraRecipients A list of additional participants to inform of the transaction.
|
||||||
*/
|
*/
|
||||||
open class FinalityFlow(val transactions: Iterable<SignedTransaction>,
|
@InitiatingFlow
|
||||||
val extraRecipients: Set<Party>,
|
class FinalityFlow(val transaction: SignedTransaction,
|
||||||
override val progressTracker: ProgressTracker) : FlowLogic<List<SignedTransaction>>() {
|
private val extraRecipients: Set<Party>,
|
||||||
val extraParticipants: Set<Participant> = extraRecipients.map { it -> Participant(it, it) }.toSet()
|
override val progressTracker: ProgressTracker) : FlowLogic<SignedTransaction>() {
|
||||||
constructor(transaction: SignedTransaction, extraParticipants: Set<Party>) : this(listOf(transaction), extraParticipants, tracker())
|
constructor(transaction: SignedTransaction, extraParticipants: Set<Party>) : this(transaction, extraParticipants, tracker())
|
||||||
constructor(transaction: SignedTransaction) : this(listOf(transaction), emptySet(), tracker())
|
constructor(transaction: SignedTransaction) : this(transaction, emptySet(), tracker())
|
||||||
constructor(transaction: SignedTransaction, progressTracker: ProgressTracker) : this(listOf(transaction), emptySet(), progressTracker)
|
constructor(transaction: SignedTransaction, progressTracker: ProgressTracker) : this(transaction, emptySet(), progressTracker)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
object NOTARISING : ProgressTracker.Step("Requesting signature by notary service") {
|
object NOTARISING : ProgressTracker.Step("Requesting signature by notary service") {
|
||||||
@ -49,56 +39,41 @@ open class FinalityFlow(val transactions: Iterable<SignedTransaction>,
|
|||||||
|
|
||||||
object BROADCASTING : ProgressTracker.Step("Broadcasting transaction to participants")
|
object BROADCASTING : ProgressTracker.Step("Broadcasting transaction to participants")
|
||||||
|
|
||||||
// TODO: Make all tracker() methods @JvmStatic
|
@JvmStatic
|
||||||
fun tracker() = ProgressTracker(NOTARISING, BROADCASTING)
|
fun tracker() = ProgressTracker(NOTARISING, BROADCASTING)
|
||||||
}
|
}
|
||||||
|
|
||||||
open protected val me
|
|
||||||
get() = serviceHub.myInfo.legalIdentity
|
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
@Throws(NotaryException::class)
|
@Throws(NotaryException::class)
|
||||||
override fun call(): List<SignedTransaction> {
|
override fun call(): SignedTransaction {
|
||||||
// Note: this method is carefully broken up to minimize the amount of data reachable from the stack at
|
// Note: this method is carefully broken up to minimize the amount of data reachable from the stack at
|
||||||
// the point where subFlow is invoked, as that minimizes the checkpointing work to be done.
|
// the point where subFlow is invoked, as that minimizes the checkpointing work to be done.
|
||||||
//
|
//
|
||||||
// Lookup the resolved transactions and use them to map each signed transaction to the list of participants.
|
// Lookup the resolved transactions and use them to map each signed transaction to the list of participants.
|
||||||
// Then send to the notary if needed, record locally and distribute.
|
// Then send to the notary if needed, record locally and distribute.
|
||||||
|
val parties = getPartiesToSend(verifyTx())
|
||||||
progressTracker.currentStep = NOTARISING
|
progressTracker.currentStep = NOTARISING
|
||||||
val notarisedTxns: List<Pair<SignedTransaction, Set<Participant>>> = resolveDependenciesOf(transactions)
|
val notarised = notariseAndRecord()
|
||||||
.map { (stx, ltx) -> Pair(notariseAndRecord(stx), lookupParties(ltx)) }
|
|
||||||
|
|
||||||
// Each transaction has its own set of recipients, but extra recipients get them all.
|
// Each transaction has its own set of recipients, but extra recipients get them all.
|
||||||
progressTracker.currentStep = BROADCASTING
|
progressTracker.currentStep = BROADCASTING
|
||||||
for ((stx, parties) in notarisedTxns) {
|
for (party in parties) {
|
||||||
broadcastTransaction(stx, (parties + extraParticipants).filter { it.wellKnown != me })
|
if (!serviceHub.myInfo.isLegalIdentity(party)) {
|
||||||
|
val session = initiateFlow(party)
|
||||||
|
subFlow(SendTransactionFlow(session, notarised))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return notarisedTxns.map { it.first }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
return notarised
|
||||||
* Broadcast a transaction to the participants. By default calls [BroadcastTransactionFlow], however can be
|
|
||||||
* overridden for more complex transaction delivery protocols (for example where not all parties know each other).
|
|
||||||
* This implementation will filter out any participants for whom there is no well known identity.
|
|
||||||
*
|
|
||||||
* @param participants the participants to send the transaction to. This is expected to include extra participants
|
|
||||||
* and exclude the local node.
|
|
||||||
*/
|
|
||||||
@Suspendable
|
|
||||||
open protected fun broadcastTransaction(stx: SignedTransaction, participants: Iterable<Participant>) {
|
|
||||||
val wellKnownParticipants = participants.map { it.wellKnown }.filterNotNull()
|
|
||||||
if (wellKnownParticipants.isNotEmpty()) {
|
|
||||||
subFlow(BroadcastTransactionFlow(stx, wellKnownParticipants.toNonEmptySet()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
private fun notariseAndRecord(stx: SignedTransaction): SignedTransaction {
|
private fun notariseAndRecord(): SignedTransaction {
|
||||||
val notarised = if (needsNotarySignature(stx)) {
|
val notarised = if (needsNotarySignature(transaction)) {
|
||||||
val notarySignatures = subFlow(NotaryFlow.Client(stx))
|
val notarySignatures = subFlow(NotaryFlow.Client(transaction))
|
||||||
stx + notarySignatures
|
transaction + notarySignatures
|
||||||
} else {
|
} else {
|
||||||
stx
|
transaction
|
||||||
}
|
}
|
||||||
serviceHub.recordTransactions(notarised)
|
serviceHub.recordTransactions(notarised)
|
||||||
return notarised
|
return notarised
|
||||||
@ -108,7 +83,6 @@ open class FinalityFlow(val transactions: Iterable<SignedTransaction>,
|
|||||||
val wtx = stx.tx
|
val wtx = stx.tx
|
||||||
val needsNotarisation = wtx.inputs.isNotEmpty() || wtx.timeWindow != null
|
val needsNotarisation = wtx.inputs.isNotEmpty() || wtx.timeWindow != null
|
||||||
return needsNotarisation && hasNoNotarySignature(stx)
|
return needsNotarisation && hasNoNotarySignature(stx)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hasNoNotarySignature(stx: SignedTransaction): Boolean {
|
private fun hasNoNotarySignature(stx: SignedTransaction): Boolean {
|
||||||
@ -117,55 +91,17 @@ open class FinalityFlow(val transactions: Iterable<SignedTransaction>,
|
|||||||
return !(notaryKey?.isFulfilledBy(signers) ?: false)
|
return !(notaryKey?.isFulfilledBy(signers) ?: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun getPartiesToSend(ltx: LedgerTransaction): Set<Party> {
|
||||||
* Resolve the parties involved in a transaction.
|
val participants = ltx.outputStates.flatMap { it.participants } + ltx.inputStates.flatMap { it.participants }
|
||||||
*
|
return groupAbstractPartyByWellKnownParty(serviceHub, participants).keys + extraRecipients
|
||||||
* @return the set of participants and their resolved well known identities (where known).
|
|
||||||
*/
|
|
||||||
open protected fun lookupParties(ltx: LedgerTransaction): Set<Participant> {
|
|
||||||
// Calculate who is meant to see the results based on the participants involved.
|
|
||||||
return extractParticipants(ltx)
|
|
||||||
.map(this::partyFromAnonymous)
|
|
||||||
.toSet()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun verifyTx(): LedgerTransaction {
|
||||||
* Helper function to extract all participants from a ledger transaction. Intended to help implement [lookupParties]
|
val notary = transaction.tx.notary
|
||||||
* overriding functions.
|
// The notary signature(s) are allowed to be missing but no others.
|
||||||
*/
|
if (notary != null) transaction.verifySignaturesExcept(notary.owningKey) else transaction.verifyRequiredSignatures()
|
||||||
protected fun extractParticipants(ltx: LedgerTransaction): List<AbstractParty> {
|
val ltx = transaction.toLedgerTransaction(serviceHub, false)
|
||||||
return ltx.outputStates.flatMap { it.participants } + ltx.inputStates.flatMap { it.participants }
|
ltx.verify()
|
||||||
|
return ltx
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function which wraps [IdentityService.partyFromAnonymous] so it can be called as a lambda function.
|
|
||||||
*/
|
|
||||||
protected fun partyFromAnonymous(anon: AbstractParty): Participant {
|
|
||||||
return Participant(anon, serviceHub.identityService.partyFromAnonymous(anon))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resolveDependenciesOf(signedTransactions: Iterable<SignedTransaction>): List<Pair<SignedTransaction, LedgerTransaction>> {
|
|
||||||
// Make sure the dependencies come before the dependers.
|
|
||||||
val sorted = ResolveTransactionsFlow.topologicalSort(signedTransactions.toList())
|
|
||||||
// Build a ServiceHub that consults the argument list as well as what's in local tx storage so uncommitted
|
|
||||||
// transactions can depend on each other.
|
|
||||||
val augmentedLookup = object : ServiceHub by serviceHub {
|
|
||||||
val hashToTx = sorted.associateBy { it.id }
|
|
||||||
override fun loadState(stateRef: StateRef): TransactionState<*> {
|
|
||||||
val provided: TransactionState<ContractState>? = hashToTx[stateRef.txhash]?.let { it.tx.outputs[stateRef.index] }
|
|
||||||
return provided ?: super.loadState(stateRef)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Load and verify each transaction.
|
|
||||||
return sorted.map { stx ->
|
|
||||||
val notary = stx.tx.notary
|
|
||||||
// The notary signature(s) are allowed to be missing but no others.
|
|
||||||
if (notary != null) stx.verifySignaturesExcept(notary.owningKey) else stx.verifyRequiredSignatures()
|
|
||||||
val ltx = stx.toLedgerTransaction(augmentedLookup, false)
|
|
||||||
ltx.verify()
|
|
||||||
stx to ltx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Participant(val participant: AbstractParty, val wellKnown: Party?)
|
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import net.corda.core.CordaRuntimeException
|
|||||||
/**
|
/**
|
||||||
* Exception which can be thrown by a [FlowLogic] at any point in its logic to unexpectedly bring it to a permanent end.
|
* Exception which can be thrown by a [FlowLogic] at any point in its logic to unexpectedly bring it to a permanent end.
|
||||||
* The exception will propagate to all counterparty flows and will be thrown on their end the next time they wait on a
|
* The exception will propagate to all counterparty flows and will be thrown on their end the next time they wait on a
|
||||||
* [FlowLogic.receive] or [FlowLogic.sendAndReceive]. Any flow which no longer needs to do a receive, or has already ended,
|
* [FlowSession.receive] or [FlowSession.sendAndReceive]. Any flow which no longer needs to do a receive, or has already ended,
|
||||||
* will not receive the exception (if this is required then have them wait for a confirmation message).
|
* will not receive the exception (if this is required then have them wait for a confirmation message).
|
||||||
*
|
*
|
||||||
* [FlowException] (or a subclass) can be a valid expected response from a flow, particularly ones which act as a service.
|
* [FlowException] (or a subclass) can be a valid expected response from a flow, particularly ones which act as a service.
|
||||||
|
@ -3,9 +3,11 @@ package net.corda.core.flows
|
|||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.identity.PartyAndCertificate
|
||||||
import net.corda.core.internal.FlowStateMachine
|
import net.corda.core.internal.FlowStateMachine
|
||||||
import net.corda.core.internal.abbreviate
|
import net.corda.core.internal.abbreviate
|
||||||
import net.corda.core.messaging.DataFeed
|
import net.corda.core.messaging.DataFeed
|
||||||
|
import net.corda.core.node.NodeInfo
|
||||||
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.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
@ -53,15 +55,39 @@ abstract class FlowLogic<out T> {
|
|||||||
*/
|
*/
|
||||||
val serviceHub: ServiceHub get() = stateMachine.serviceHub
|
val serviceHub: ServiceHub get() = stateMachine.serviceHub
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
fun initiateFlow(party: Party): FlowSession = stateMachine.initiateFlow(party, flowUsedForSessions)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a [FlowContext] object describing the flow [otherParty] is using. With [FlowContext.flowVersion] it
|
* Specifies the identity, with certificate, to use for this flow. This will be one of the multiple identities that
|
||||||
|
* belong to this node.
|
||||||
|
* @see NodeInfo.legalIdentitiesAndCerts
|
||||||
|
*
|
||||||
|
* Note: The current implementation returns the single identity of the node. This will change once multiple identities
|
||||||
|
* is implemented.
|
||||||
|
*/
|
||||||
|
val ourIdentityAndCert: PartyAndCertificate get() = stateMachine.ourIdentityAndCert
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the identity to use for this flow. This will be one of the multiple identities that belong to this node.
|
||||||
|
* This is the same as calling `ourIdentityAndCert.party`.
|
||||||
|
* @see NodeInfo.legalIdentities
|
||||||
|
*
|
||||||
|
* Note: The current implementation returns the single identity of the node. This will change once multiple identities
|
||||||
|
* is implemented.
|
||||||
|
*/
|
||||||
|
val ourIdentity: Party get() = ourIdentityAndCert.party
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a [FlowInfo] object describing the flow [otherParty] is using. With [FlowInfo.flowVersion] it
|
||||||
* provides the necessary information needed for the evolution of flows and enabling backwards compatibility.
|
* provides the necessary information needed for the evolution of flows and enabling backwards compatibility.
|
||||||
*
|
*
|
||||||
* This method can be called before any send or receive has been done with [otherParty]. In such a case this will force
|
* This method can be called before any send or receive has been done with [otherParty]. In such a case this will force
|
||||||
* them to start their flow.
|
* them to start their flow.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated("Use FlowSession.getFlowInfo()", level = DeprecationLevel.WARNING)
|
||||||
@Suspendable
|
@Suspendable
|
||||||
fun getFlowContext(otherParty: Party): FlowContext = stateMachine.getFlowContext(otherParty, flowUsedForSessions)
|
fun getFlowInfo(otherParty: Party): FlowInfo = stateMachine.getFlowInfo(otherParty, flowUsedForSessions)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serializes and queues the given [payload] object for sending to the [otherParty]. Suspends until a response
|
* Serializes and queues the given [payload] object for sending to the [otherParty]. Suspends until a response
|
||||||
@ -76,6 +102,7 @@ abstract class FlowLogic<out T> {
|
|||||||
*
|
*
|
||||||
* @returns an [UntrustworthyData] wrapper around the received object.
|
* @returns an [UntrustworthyData] wrapper around the received object.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated("Use FlowSession.sendAndReceive()", level = DeprecationLevel.WARNING)
|
||||||
inline fun <reified R : Any> sendAndReceive(otherParty: Party, payload: Any): UntrustworthyData<R> {
|
inline fun <reified R : Any> sendAndReceive(otherParty: Party, payload: Any): UntrustworthyData<R> {
|
||||||
return sendAndReceive(R::class.java, otherParty, payload)
|
return sendAndReceive(R::class.java, otherParty, payload)
|
||||||
}
|
}
|
||||||
@ -91,6 +118,7 @@ abstract class FlowLogic<out T> {
|
|||||||
*
|
*
|
||||||
* @returns an [UntrustworthyData] wrapper around the received object.
|
* @returns an [UntrustworthyData] wrapper around the received object.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated("Use FlowSession.sendAndReceive()", level = DeprecationLevel.WARNING)
|
||||||
@Suspendable
|
@Suspendable
|
||||||
open fun <R : Any> sendAndReceive(receiveType: Class<R>, otherParty: Party, payload: Any): UntrustworthyData<R> {
|
open fun <R : Any> sendAndReceive(receiveType: Class<R>, otherParty: Party, payload: Any): UntrustworthyData<R> {
|
||||||
return stateMachine.sendAndReceive(receiveType, otherParty, payload, flowUsedForSessions)
|
return stateMachine.sendAndReceive(receiveType, otherParty, payload, flowUsedForSessions)
|
||||||
@ -105,8 +133,19 @@ abstract class FlowLogic<out T> {
|
|||||||
* oracle services. If one or more nodes in the service cluster go down mid-session, the message will be redelivered
|
* oracle services. If one or more nodes in the service cluster go down mid-session, the message will be redelivered
|
||||||
* to a different one, so there is no need to wait until the initial node comes back up to obtain a response.
|
* to a different one, so there is no need to wait until the initial node comes back up to obtain a response.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated("Use FlowSession.sendAndReceiveWithRetry()", level = DeprecationLevel.WARNING)
|
||||||
internal inline fun <reified R : Any> sendAndReceiveWithRetry(otherParty: Party, payload: Any): UntrustworthyData<R> {
|
internal inline fun <reified R : Any> sendAndReceiveWithRetry(otherParty: Party, payload: Any): UntrustworthyData<R> {
|
||||||
return stateMachine.sendAndReceive(R::class.java, otherParty, payload, flowUsedForSessions, true)
|
return stateMachine.sendAndReceive(R::class.java, otherParty, payload, flowUsedForSessions, retrySend = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
internal fun <R : Any> FlowSession.sendAndReceiveWithRetry(receiveType: Class<R>, payload: Any): UntrustworthyData<R> {
|
||||||
|
return stateMachine.sendAndReceive(receiveType, counterparty, payload, flowUsedForSessions, retrySend = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
internal inline fun <reified R : Any> FlowSession.sendAndReceiveWithRetry(payload: Any): UntrustworthyData<R> {
|
||||||
|
return stateMachine.sendAndReceive(R::class.java, counterparty, payload, flowUsedForSessions, retrySend = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -116,6 +155,7 @@ abstract class FlowLogic<out T> {
|
|||||||
* verified for consistency and that all expectations are satisfied, as a malicious peer may send you subtly
|
* verified for consistency and that all expectations are satisfied, as a malicious peer may send you subtly
|
||||||
* corrupted data in order to exploit your code.
|
* corrupted data in order to exploit your code.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated("Use FlowSession.receive()", level = DeprecationLevel.WARNING)
|
||||||
inline fun <reified R : Any> receive(otherParty: Party): UntrustworthyData<R> = receive(R::class.java, otherParty)
|
inline fun <reified R : Any> receive(otherParty: Party): UntrustworthyData<R> = receive(R::class.java, otherParty)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -127,6 +167,7 @@ abstract class FlowLogic<out T> {
|
|||||||
*
|
*
|
||||||
* @returns an [UntrustworthyData] wrapper around the received object.
|
* @returns an [UntrustworthyData] wrapper around the received object.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated("Use FlowSession.receive()", level = DeprecationLevel.WARNING)
|
||||||
@Suspendable
|
@Suspendable
|
||||||
open fun <R : Any> receive(receiveType: Class<R>, otherParty: Party): UntrustworthyData<R> {
|
open fun <R : Any> receive(receiveType: Class<R>, otherParty: Party): UntrustworthyData<R> {
|
||||||
return stateMachine.receive(receiveType, otherParty, flowUsedForSessions)
|
return stateMachine.receive(receiveType, otherParty, flowUsedForSessions)
|
||||||
@ -139,6 +180,7 @@ abstract class FlowLogic<out T> {
|
|||||||
* is offline then message delivery will be retried until it comes back or until the message is older than the
|
* is offline then message delivery will be retried until it comes back or until the message is older than the
|
||||||
* network's event horizon time.
|
* network's event horizon time.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated("Use FlowSession.send()", level = DeprecationLevel.WARNING)
|
||||||
@Suspendable
|
@Suspendable
|
||||||
open fun send(otherParty: Party, payload: Any) = stateMachine.send(otherParty, payload, flowUsedForSessions)
|
open fun send(otherParty: Party, payload: Any) = stateMachine.send(otherParty, payload, flowUsedForSessions)
|
||||||
|
|
||||||
@ -294,7 +336,7 @@ abstract class FlowLogic<out T> {
|
|||||||
* Version and name of the CorDapp hosting the other side of the flow.
|
* Version and name of the CorDapp hosting the other side of the flow.
|
||||||
*/
|
*/
|
||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
data class FlowContext(
|
data class FlowInfo(
|
||||||
/**
|
/**
|
||||||
* The integer flow version the other side is using.
|
* The integer flow version the other side is using.
|
||||||
* @see InitiatingFlow
|
* @see InitiatingFlow
|
||||||
|
109
core/src/main/kotlin/net/corda/core/flows/FlowSession.kt
Normal file
109
core/src/main/kotlin/net/corda/core/flows/FlowSession.kt
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
package net.corda.core.flows
|
||||||
|
|
||||||
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.utilities.UntrustworthyData
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To port existing flows:
|
||||||
|
*
|
||||||
|
* Look for [Deprecated] usages of send/receive/sendAndReceive/getFlowInfo.
|
||||||
|
*
|
||||||
|
* If it's an InitiatingFlow:
|
||||||
|
*
|
||||||
|
* Look for the send/receive that kicks off the counter flow. Insert a
|
||||||
|
*
|
||||||
|
* val session = initiateFlow(party)
|
||||||
|
*
|
||||||
|
* and use this session afterwards for send/receives.
|
||||||
|
* For example:
|
||||||
|
* send(party, something)
|
||||||
|
* will become
|
||||||
|
* session.send(something)
|
||||||
|
*
|
||||||
|
* If it's an InitiatedBy flow:
|
||||||
|
*
|
||||||
|
* Change the constructor to take an otherSideSession: FlowSession instead of a counterparty: Party
|
||||||
|
* Then look for usages of the deprecated functions and change them to use the FlowSession
|
||||||
|
* For example:
|
||||||
|
* send(counterparty, something)
|
||||||
|
* will become
|
||||||
|
* otherSideSession.send(something)
|
||||||
|
*/
|
||||||
|
abstract class FlowSession {
|
||||||
|
abstract val counterparty: Party
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a [FlowInfo] object describing the flow [counterparty] is using. With [FlowInfo.flowVersion] it
|
||||||
|
* provides the necessary information needed for the evolution of flows and enabling backwards compatibility.
|
||||||
|
*
|
||||||
|
* This method can be called before any send or receive has been done with [counterparty]. In such a case this will force
|
||||||
|
* them to start their flow.
|
||||||
|
*/
|
||||||
|
@Suspendable
|
||||||
|
abstract fun getCounterpartyFlowInfo(): FlowInfo
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes and queues the given [payload] object for sending to the [counterparty]. Suspends until a response
|
||||||
|
* is received, which must be of the given [R] type.
|
||||||
|
*
|
||||||
|
* Remember that when receiving data from other parties the data should not be trusted until it's been thoroughly
|
||||||
|
* verified for consistency and that all expectations are satisfied, as a malicious peer may send you subtly
|
||||||
|
* corrupted data in order to exploit your code.
|
||||||
|
*
|
||||||
|
* Note that this function is not just a simple send+receive pair: it is more efficient and more correct to
|
||||||
|
* use this when you expect to do a message swap than do use [send] and then [receive] in turn.
|
||||||
|
*
|
||||||
|
* @returns an [UntrustworthyData] wrapper around the received object.
|
||||||
|
*/
|
||||||
|
@Suspendable
|
||||||
|
inline fun <reified R : Any> sendAndReceive(payload: Any): UntrustworthyData<R> {
|
||||||
|
return sendAndReceive(R::class.java, payload)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Serializes and queues the given [payload] object for sending to the [counterparty]. Suspends until a response
|
||||||
|
* is received, which must be of the given [receiveType]. Remember that when receiving data from other parties the data
|
||||||
|
* should not be trusted until it's been thoroughly verified for consistency and that all expectations are
|
||||||
|
* satisfied, as a malicious peer may send you subtly corrupted data in order to exploit your code.
|
||||||
|
*
|
||||||
|
* Note that this function is not just a simple send+receive pair: it is more efficient and more correct to
|
||||||
|
* use this when you expect to do a message swap than do use [send] and then [receive] in turn.
|
||||||
|
*
|
||||||
|
* @returns an [UntrustworthyData] wrapper around the received object.
|
||||||
|
*/
|
||||||
|
@Suspendable
|
||||||
|
abstract fun <R : Any> sendAndReceive(receiveType: Class<R>, payload: Any): UntrustworthyData<R>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Suspends until [counterparty] sends us a message of type [R].
|
||||||
|
*
|
||||||
|
* Remember that when receiving data from other parties the data should not be trusted until it's been thoroughly
|
||||||
|
* verified for consistency and that all expectations are satisfied, as a malicious peer may send you subtly
|
||||||
|
* corrupted data in order to exploit your code.
|
||||||
|
*/
|
||||||
|
@Suspendable
|
||||||
|
inline fun <reified R : Any> receive(): UntrustworthyData<R> {
|
||||||
|
return receive(R::class.java)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Suspends until [counterparty] sends us a message of type [receiveType].
|
||||||
|
*
|
||||||
|
* Remember that when receiving data from other parties the data should not be trusted until it's been thoroughly
|
||||||
|
* verified for consistency and that all expectations are satisfied, as a malicious peer may send you subtly
|
||||||
|
* corrupted data in order to exploit your code.
|
||||||
|
*
|
||||||
|
* @returns an [UntrustworthyData] wrapper around the received object.
|
||||||
|
*/
|
||||||
|
@Suspendable
|
||||||
|
abstract fun <R : Any> receive(receiveType: Class<R>): UntrustworthyData<R>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queues the given [payload] for sending to the [counterparty] and continues without suspending.
|
||||||
|
*
|
||||||
|
* Note that the other party may receive the message at some arbitrary later point or not at all: if [counterparty]
|
||||||
|
* is offline then message delivery will be retried until it comes back or until the message is older than the
|
||||||
|
* network's event horizon time.
|
||||||
|
*/
|
||||||
|
@Suspendable
|
||||||
|
abstract fun send(payload: Any)
|
||||||
|
}
|
@ -1,20 +0,0 @@
|
|||||||
package net.corda.core.flows
|
|
||||||
|
|
||||||
import net.corda.core.identity.Party
|
|
||||||
import net.corda.core.transactions.LedgerTransaction
|
|
||||||
import net.corda.core.transactions.SignedTransaction
|
|
||||||
import net.corda.core.utilities.ProgressTracker
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alternative finality flow which only does not attempt to take participants from the transaction, but instead all
|
|
||||||
* participating parties must be provided manually.
|
|
||||||
*
|
|
||||||
* @param transactions What to commit.
|
|
||||||
* @param extraRecipients A list of additional participants to inform of the transaction.
|
|
||||||
*/
|
|
||||||
class ManualFinalityFlow(transactions: Iterable<SignedTransaction>,
|
|
||||||
recipients: Set<Party>,
|
|
||||||
progressTracker: ProgressTracker) : FinalityFlow(transactions, recipients, progressTracker) {
|
|
||||||
constructor(transaction: SignedTransaction, extraParticipants: Set<Party>) : this(listOf(transaction), extraParticipants, tracker())
|
|
||||||
override fun lookupParties(ltx: LedgerTransaction): Set<Participant> = emptySet()
|
|
||||||
}
|
|
@ -43,7 +43,7 @@ class NotaryChangeFlow<out T : ContractState>(
|
|||||||
val mySignature = serviceHub.keyManagementService.sign(signableData, myKey)
|
val mySignature = serviceHub.keyManagementService.sign(signableData, myKey)
|
||||||
val stx = SignedTransaction(tx, listOf(mySignature))
|
val stx = SignedTransaction(tx, listOf(mySignature))
|
||||||
|
|
||||||
return AbstractStateReplacementFlow.UpgradeTx(stx, participantKeys, myKey)
|
return AbstractStateReplacementFlow.UpgradeTx(stx)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Resolves the encumbrance state chain for the given [state] */
|
/** Resolves the encumbrance state chain for the given [state] */
|
||||||
|
@ -13,13 +13,15 @@ import net.corda.core.node.services.NotaryService
|
|||||||
import net.corda.core.node.services.TrustedAuthorityNotaryService
|
import net.corda.core.node.services.TrustedAuthorityNotaryService
|
||||||
import net.corda.core.node.services.UniquenessProvider
|
import net.corda.core.node.services.UniquenessProvider
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
import net.corda.core.transactions.FilteredTransaction
|
||||||
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.UntrustworthyData
|
||||||
import net.corda.core.utilities.unwrap
|
import net.corda.core.utilities.unwrap
|
||||||
import java.security.SignatureException
|
import java.security.SignatureException
|
||||||
import java.util.function.Predicate
|
import java.util.function.Predicate
|
||||||
|
|
||||||
object NotaryFlow {
|
class NotaryFlow {
|
||||||
/**
|
/**
|
||||||
* A flow to be used by a party for obtaining signature(s) from a [NotaryService] ascertaining the transaction
|
* A flow to be used by a party for obtaining signature(s) from a [NotaryService] ascertaining the transaction
|
||||||
* time-window is correct and none of its inputs have been used in another completed transaction.
|
* time-window is correct and none of its inputs have been used in another completed transaction.
|
||||||
@ -42,14 +44,12 @@ object NotaryFlow {
|
|||||||
fun tracker() = ProgressTracker(REQUESTING, VALIDATING)
|
fun tracker() = ProgressTracker(REQUESTING, VALIDATING)
|
||||||
}
|
}
|
||||||
|
|
||||||
lateinit var notaryParty: Party
|
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
@Throws(NotaryException::class)
|
@Throws(NotaryException::class)
|
||||||
override fun call(): List<TransactionSignature> {
|
override fun call(): List<TransactionSignature> {
|
||||||
progressTracker.currentStep = REQUESTING
|
progressTracker.currentStep = REQUESTING
|
||||||
|
|
||||||
notaryParty = stx.notary ?: throw IllegalStateException("Transaction does not specify a Notary")
|
val notaryParty = stx.notary ?: throw IllegalStateException("Transaction does not specify a Notary")
|
||||||
check(stx.inputs.all { stateRef -> serviceHub.loadState(stateRef).notary == notaryParty }) {
|
check(stx.inputs.all { stateRef -> serviceHub.loadState(stateRef).notary == notaryParty }) {
|
||||||
"Input states must have the same Notary"
|
"Input states must have the same Notary"
|
||||||
}
|
}
|
||||||
@ -65,16 +65,17 @@ object NotaryFlow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val response = try {
|
val response = try {
|
||||||
|
val session = initiateFlow(notaryParty)
|
||||||
if (serviceHub.networkMapCache.isValidatingNotary(notaryParty)) {
|
if (serviceHub.networkMapCache.isValidatingNotary(notaryParty)) {
|
||||||
subFlow(SendTransactionWithRetry(notaryParty, stx))
|
subFlow(SendTransactionWithRetry(session, stx))
|
||||||
receive<List<TransactionSignature>>(notaryParty)
|
session.receive<List<TransactionSignature>>()
|
||||||
} else {
|
} else {
|
||||||
val tx: Any = if (stx.isNotaryChangeTransaction()) {
|
val tx: Any = if (stx.isNotaryChangeTransaction()) {
|
||||||
stx.notaryChangeTx
|
stx.notaryChangeTx
|
||||||
} else {
|
} else {
|
||||||
stx.buildFilteredTransaction(Predicate { it is StateRef || it is TimeWindow })
|
stx.buildFilteredTransaction(Predicate { it is StateRef || it is TimeWindow || it == notaryParty })
|
||||||
}
|
}
|
||||||
sendAndReceiveWithRetry(notaryParty, tx)
|
session.sendAndReceiveWithRetry(tx)
|
||||||
}
|
}
|
||||||
} catch (e: NotaryException) {
|
} catch (e: NotaryException) {
|
||||||
if (e.error is NotaryError.Conflict) {
|
if (e.error is NotaryError.Conflict) {
|
||||||
@ -84,17 +85,28 @@ object NotaryFlow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return response.unwrap { signatures ->
|
return response.unwrap { signatures ->
|
||||||
signatures.forEach { validateSignature(it, stx.id) }
|
signatures.forEach { validateSignature(it, stx.id, notaryParty) }
|
||||||
signatures
|
signatures
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun validateSignature(sig: TransactionSignature, txId: SecureHash) {
|
private fun validateSignature(sig: TransactionSignature, txId: SecureHash, notaryParty: Party) {
|
||||||
check(sig.by in notaryParty.owningKey.keys) { "Invalid signer for the notary result" }
|
check(sig.by in notaryParty.owningKey.keys) { "Invalid signer for the notary result" }
|
||||||
sig.verify(txId)
|
sig.verify(txId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The [SendTransactionWithRetry] flow is equivalent to [SendTransactionFlow] but using [sendAndReceiveWithRetry]
|
||||||
|
* instead of [sendAndReceive], [SendTransactionWithRetry] is intended to be use by the notary client only.
|
||||||
|
*/
|
||||||
|
private class SendTransactionWithRetry(otherSideSession: FlowSession, stx: SignedTransaction) : SendTransactionFlow(otherSideSession, stx) {
|
||||||
|
@Suspendable
|
||||||
|
override fun sendPayloadAndReceiveDataRequest(otherSideSession: FlowSession, payload: Any): UntrustworthyData<FetchDataFlow.Request> {
|
||||||
|
return otherSideSession.sendAndReceiveWithRetry(payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A flow run by a notary service that handles notarisation requests.
|
* A flow run by a notary service that handles notarisation requests.
|
||||||
*
|
*
|
||||||
@ -104,13 +116,14 @@ object NotaryFlow {
|
|||||||
* Additional transaction validation logic can be added when implementing [receiveAndVerifyTx].
|
* Additional transaction validation logic can be added when implementing [receiveAndVerifyTx].
|
||||||
*/
|
*/
|
||||||
// See AbstractStateReplacementFlow.Acceptor for why it's Void?
|
// See AbstractStateReplacementFlow.Acceptor for why it's Void?
|
||||||
abstract class Service(val otherSide: Party, val service: TrustedAuthorityNotaryService) : FlowLogic<Void?>() {
|
abstract class Service(val otherSideSession: FlowSession, val service: TrustedAuthorityNotaryService) : FlowLogic<Void?>() {
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call(): Void? {
|
override fun call(): Void? {
|
||||||
val (id, inputs, timeWindow) = receiveAndVerifyTx()
|
val (id, inputs, timeWindow, notary) = receiveAndVerifyTx()
|
||||||
|
checkNotary(notary)
|
||||||
service.validateTimeWindow(timeWindow)
|
service.validateTimeWindow(timeWindow)
|
||||||
service.commitInputStates(inputs, id, otherSide)
|
service.commitInputStates(inputs, id, otherSideSession.counterparty)
|
||||||
signAndSendResponse(id)
|
signAndSendResponse(id)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -122,10 +135,17 @@ object NotaryFlow {
|
|||||||
@Suspendable
|
@Suspendable
|
||||||
abstract fun receiveAndVerifyTx(): TransactionParts
|
abstract fun receiveAndVerifyTx(): TransactionParts
|
||||||
|
|
||||||
|
// Check if transaction is intended to be signed by this notary.
|
||||||
|
@Suspendable
|
||||||
|
protected fun checkNotary(notary: Party?) {
|
||||||
|
if (notary !in serviceHub.myInfo.legalIdentities)
|
||||||
|
throw NotaryException(NotaryError.WrongNotary)
|
||||||
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
private fun signAndSendResponse(txId: SecureHash) {
|
private fun signAndSendResponse(txId: SecureHash) {
|
||||||
val signature = service.sign(txId)
|
val signature = service.sign(txId)
|
||||||
send(otherSide, listOf(signature))
|
otherSideSession.send(listOf(signature))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -134,7 +154,7 @@ object NotaryFlow {
|
|||||||
* The minimum amount of information needed to notarise a transaction. Note that this does not include
|
* The minimum amount of information needed to notarise a transaction. Note that this does not include
|
||||||
* any sensitive transaction details.
|
* any sensitive transaction details.
|
||||||
*/
|
*/
|
||||||
data class TransactionParts(val id: SecureHash, val inputs: List<StateRef>, val timestamp: TimeWindow?)
|
data class TransactionParts(val id: SecureHash, val inputs: List<StateRef>, val timestamp: TimeWindow?, val notary: Party?)
|
||||||
|
|
||||||
class NotaryException(val error: NotaryError) : FlowException("Error response from Notary - $error")
|
class NotaryException(val error: NotaryError) : FlowException("Error response from Notary - $error")
|
||||||
|
|
||||||
@ -150,13 +170,6 @@ sealed class NotaryError {
|
|||||||
data class TransactionInvalid(val cause: Throwable) : NotaryError() {
|
data class TransactionInvalid(val cause: Throwable) : NotaryError() {
|
||||||
override fun toString() = cause.toString()
|
override fun toString() = cause.toString()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
object WrongNotary: NotaryError()
|
||||||
* The [SendTransactionWithRetry] flow is equivalent to [SendTransactionFlow] but using [sendAndReceiveWithRetry]
|
|
||||||
* instead of [sendAndReceive], [SendTransactionWithRetry] is intended to be use by the notary client only.
|
|
||||||
*/
|
|
||||||
private class SendTransactionWithRetry(otherSide: Party, stx: SignedTransaction) : SendTransactionFlow(otherSide, stx) {
|
|
||||||
@Suspendable
|
|
||||||
override fun sendPayloadAndReceiveDataRequest(otherSide: Party, payload: Any) = sendAndReceiveWithRetry<FetchDataFlow.Request>(otherSide, payload)
|
|
||||||
}
|
}
|
@ -2,7 +2,6 @@ package net.corda.core.flows
|
|||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.identity.Party
|
|
||||||
import net.corda.core.internal.ResolveTransactionsFlow
|
import net.corda.core.internal.ResolveTransactionsFlow
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.utilities.unwrap
|
import net.corda.core.utilities.unwrap
|
||||||
@ -11,18 +10,26 @@ import java.security.SignatureException
|
|||||||
/**
|
/**
|
||||||
* The [ReceiveTransactionFlow] should be called in response to the [SendTransactionFlow].
|
* The [ReceiveTransactionFlow] should be called in response to the [SendTransactionFlow].
|
||||||
*
|
*
|
||||||
* This flow is a combination of [receive], resolve and [SignedTransaction.verify]. This flow will receive the [SignedTransaction]
|
* This flow is a combination of [FlowSession.receive], resolve and [SignedTransaction.verify]. This flow will receive the
|
||||||
* and perform the resolution back-and-forth required to check the dependencies and download any missing attachments.
|
* [SignedTransaction] and perform the resolution back-and-forth required to check the dependencies and download any missing
|
||||||
* The flow will return the [SignedTransaction] after it is resolved and then verified using [SignedTransaction.verify].
|
* attachments. The flow will return the [SignedTransaction] after it is resolved and then verified using [SignedTransaction.verify].
|
||||||
|
*
|
||||||
|
* @param otherSideSession session to the other side which is calling [SendTransactionFlow].
|
||||||
|
* @param checkSufficientSignatures if true checks all required signatures are present. See [SignedTransaction.verify].
|
||||||
*/
|
*/
|
||||||
class ReceiveTransactionFlow
|
class ReceiveTransactionFlow(private val otherSideSession: FlowSession,
|
||||||
@JvmOverloads
|
private val checkSufficientSignatures: Boolean) : FlowLogic<SignedTransaction>() {
|
||||||
constructor(private val otherParty: Party, private val checkSufficientSignatures: Boolean = true) : FlowLogic<SignedTransaction>() {
|
/** Receives a [SignedTransaction] from [otherSideSession], verifies it and then records it in the vault. */
|
||||||
|
constructor(otherSideSession: FlowSession) : this(otherSideSession, true)
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
@Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class)
|
@Throws(SignatureException::class,
|
||||||
|
AttachmentResolutionException::class,
|
||||||
|
TransactionResolutionException::class,
|
||||||
|
TransactionVerificationException::class)
|
||||||
override fun call(): SignedTransaction {
|
override fun call(): SignedTransaction {
|
||||||
return receive<SignedTransaction>(otherParty).unwrap {
|
return otherSideSession.receive<SignedTransaction>().unwrap {
|
||||||
subFlow(ResolveTransactionsFlow(it, otherParty))
|
subFlow(ResolveTransactionsFlow(it, otherSideSession))
|
||||||
it.verify(serviceHub, checkSufficientSignatures)
|
it.verify(serviceHub, checkSufficientSignatures)
|
||||||
it
|
it
|
||||||
}
|
}
|
||||||
@ -32,16 +39,16 @@ constructor(private val otherParty: Party, private val checkSufficientSignatures
|
|||||||
/**
|
/**
|
||||||
* The [ReceiveStateAndRefFlow] should be called in response to the [SendStateAndRefFlow].
|
* The [ReceiveStateAndRefFlow] should be called in response to the [SendStateAndRefFlow].
|
||||||
*
|
*
|
||||||
* This flow is a combination of [receive] and resolve. This flow will receive a list of [StateAndRef]
|
* This flow is a combination of [FlowSession.receive] and resolve. This flow will receive a list of [StateAndRef]
|
||||||
* and perform the resolution back-and-forth required to check the dependencies.
|
* and perform the resolution back-and-forth required to check the dependencies.
|
||||||
* The flow will return the list of [StateAndRef] after it is resolved.
|
* The flow will return the list of [StateAndRef] after it is resolved.
|
||||||
*/
|
*/
|
||||||
// @JvmSuppressWildcards is used to suppress wildcards in return type when calling `subFlow(new ReceiveStateAndRef<T>(otherParty))` in java.
|
// @JvmSuppressWildcards is used to suppress wildcards in return type when calling `subFlow(new ReceiveStateAndRef<T>(otherParty))` in java.
|
||||||
class ReceiveStateAndRefFlow<out T : ContractState>(private val otherParty: Party) : FlowLogic<@JvmSuppressWildcards List<StateAndRef<T>>>() {
|
class ReceiveStateAndRefFlow<out T : ContractState>(private val otherSideSession: FlowSession) : FlowLogic<@JvmSuppressWildcards List<StateAndRef<T>>>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call(): List<StateAndRef<T>> {
|
override fun call(): List<StateAndRef<T>> {
|
||||||
return receive<List<StateAndRef<T>>>(otherParty).unwrap {
|
return otherSideSession.receive<List<StateAndRef<T>>>().unwrap {
|
||||||
subFlow(ResolveTransactionsFlow(it.map { it.ref.txhash }.toSet(), otherParty))
|
subFlow(ResolveTransactionsFlow(it.map { it.ref.txhash }.toSet(), otherSideSession))
|
||||||
it
|
it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ package net.corda.core.flows
|
|||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.contracts.StateAndRef
|
import net.corda.core.contracts.StateAndRef
|
||||||
import net.corda.core.identity.Party
|
|
||||||
import net.corda.core.internal.FetchDataFlow
|
import net.corda.core.internal.FetchDataFlow
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.utilities.unwrap
|
import net.corda.core.utilities.unwrap
|
||||||
@ -14,9 +13,9 @@ import net.corda.core.utilities.unwrap
|
|||||||
* to check the dependencies and download any missing attachments.
|
* to check the dependencies and download any missing attachments.
|
||||||
*
|
*
|
||||||
* @param otherSide the target party.
|
* @param otherSide the target party.
|
||||||
* @param stx the [SignedTransaction] being sent to the [otherSide].
|
* @param stx the [SignedTransaction] being sent to the [otherSideSession].
|
||||||
*/
|
*/
|
||||||
open class SendTransactionFlow(otherSide: Party, stx: SignedTransaction) : DataVendingFlow(otherSide, stx)
|
open class SendTransactionFlow(otherSide: FlowSession, stx: SignedTransaction) : DataVendingFlow(otherSide, stx)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [SendStateAndRefFlow] should be used to send a list of input [StateAndRef] to another peer that wishes to verify
|
* The [SendStateAndRefFlow] should be used to send a list of input [StateAndRef] to another peer that wishes to verify
|
||||||
@ -24,14 +23,14 @@ open class SendTransactionFlow(otherSide: Party, stx: SignedTransaction) : DataV
|
|||||||
* at the right point in the conversation to receive the input state and ref and perform the resolution back-and-forth
|
* at the right point in the conversation to receive the input state and ref and perform the resolution back-and-forth
|
||||||
* required to check the dependencies.
|
* required to check the dependencies.
|
||||||
*
|
*
|
||||||
* @param otherSide the target party.
|
* @param otherSideSession the target session.
|
||||||
* @param stateAndRefs the list of [StateAndRef] being sent to the [otherSide].
|
* @param stateAndRefs the list of [StateAndRef] being sent to the [otherSideSession].
|
||||||
*/
|
*/
|
||||||
open class SendStateAndRefFlow(otherSide: Party, stateAndRefs: List<StateAndRef<*>>) : DataVendingFlow(otherSide, stateAndRefs)
|
open class SendStateAndRefFlow(otherSideSession: FlowSession, stateAndRefs: List<StateAndRef<*>>) : DataVendingFlow(otherSideSession, stateAndRefs)
|
||||||
|
|
||||||
sealed class DataVendingFlow(val otherSide: Party, val payload: Any) : FlowLogic<Void?>() {
|
sealed class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any) : FlowLogic<Void?>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
protected open fun sendPayloadAndReceiveDataRequest(otherSide: Party, payload: Any) = sendAndReceive<FetchDataFlow.Request>(otherSide, payload)
|
protected open fun sendPayloadAndReceiveDataRequest(otherSideSession: FlowSession, payload: Any) = otherSideSession.sendAndReceive<FetchDataFlow.Request>(payload)
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
protected open fun verifyDataRequest(dataRequest: FetchDataFlow.Request.Data) {
|
protected open fun verifyDataRequest(dataRequest: FetchDataFlow.Request.Data) {
|
||||||
@ -42,11 +41,11 @@ sealed class DataVendingFlow(val otherSide: Party, val payload: Any) : FlowLogic
|
|||||||
override fun call(): Void? {
|
override fun call(): Void? {
|
||||||
// The first payload will be the transaction data, subsequent payload will be the transaction/attachment data.
|
// The first payload will be the transaction data, subsequent payload will be the transaction/attachment data.
|
||||||
var payload = payload
|
var payload = payload
|
||||||
// This loop will receive [FetchDataFlow.Request] continuously until the `otherSide` has all the data they need
|
// This loop will receive [FetchDataFlow.Request] continuously until the `otherSideSession` has all the data they need
|
||||||
// to resolve the transaction, a [FetchDataFlow.EndRequest] will be sent from the `otherSide` to indicate end of
|
// to resolve the transaction, a [FetchDataFlow.EndRequest] will be sent from the `otherSideSession` to indicate end of
|
||||||
// data request.
|
// data request.
|
||||||
while (true) {
|
while (true) {
|
||||||
val dataRequest = sendPayloadAndReceiveDataRequest(otherSide, payload).unwrap { request ->
|
val dataRequest = sendPayloadAndReceiveDataRequest(otherSideSession, payload).unwrap { request ->
|
||||||
when (request) {
|
when (request) {
|
||||||
is FetchDataFlow.Request.Data -> {
|
is FetchDataFlow.Request.Data -> {
|
||||||
// Security TODO: Check for abnormally large or malformed data requests
|
// Security TODO: Check for abnormally large or malformed data requests
|
||||||
|
@ -3,7 +3,6 @@ package net.corda.core.identity
|
|||||||
import net.corda.core.contracts.PartyAndReference
|
import net.corda.core.contracts.PartyAndReference
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.utilities.OpaqueBytes
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
import org.bouncycastle.asn1.x500.X500Name
|
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -15,7 +14,7 @@ abstract class AbstractParty(val owningKey: PublicKey) {
|
|||||||
/** Anonymised parties do not include any detail apart from owning key, so equality is dependent solely on the key */
|
/** Anonymised parties do not include any detail apart from owning key, so equality is dependent solely on the key */
|
||||||
override fun equals(other: Any?): Boolean = other === this || other is AbstractParty && other.owningKey == owningKey
|
override fun equals(other: Any?): Boolean = other === this || other is AbstractParty && other.owningKey == owningKey
|
||||||
override fun hashCode(): Int = owningKey.hashCode()
|
override fun hashCode(): Int = owningKey.hashCode()
|
||||||
abstract fun nameOrNull(): X500Name?
|
abstract fun nameOrNull(): CordaX500Name?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a reference to something being stored or issued by a party e.g. in a vault or (more likely) on their normal
|
* Build a reference to something being stored or issued by a party e.g. in a vault or (more likely) on their normal
|
||||||
|
@ -3,7 +3,6 @@ package net.corda.core.identity
|
|||||||
import net.corda.core.contracts.PartyAndReference
|
import net.corda.core.contracts.PartyAndReference
|
||||||
import net.corda.core.crypto.toStringShort
|
import net.corda.core.crypto.toStringShort
|
||||||
import net.corda.core.utilities.OpaqueBytes
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
import org.bouncycastle.asn1.x500.X500Name
|
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -11,7 +10,7 @@ import java.security.PublicKey
|
|||||||
* information such as name. It is intended to represent a party on the distributed ledger.
|
* information such as name. It is intended to represent a party on the distributed ledger.
|
||||||
*/
|
*/
|
||||||
class AnonymousParty(owningKey: PublicKey) : AbstractParty(owningKey) {
|
class AnonymousParty(owningKey: PublicKey) : AbstractParty(owningKey) {
|
||||||
override fun nameOrNull(): X500Name? = null
|
override fun nameOrNull(): CordaX500Name? = null
|
||||||
override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes)
|
override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes)
|
||||||
override fun toString() = "Anonymous(${owningKey.toStringShort()})"
|
override fun toString() = "Anonymous(${owningKey.toStringShort()})"
|
||||||
}
|
}
|
132
core/src/main/kotlin/net/corda/core/identity/CordaX500Name.kt
Normal file
132
core/src/main/kotlin/net/corda/core/identity/CordaX500Name.kt
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
package net.corda.core.identity
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableSet
|
||||||
|
import net.corda.core.internal.LegalNameValidator
|
||||||
|
import net.corda.core.internal.x500Name
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
import org.bouncycastle.asn1.ASN1Encodable
|
||||||
|
import org.bouncycastle.asn1.ASN1ObjectIdentifier
|
||||||
|
import org.bouncycastle.asn1.x500.AttributeTypeAndValue
|
||||||
|
import org.bouncycastle.asn1.x500.X500Name
|
||||||
|
import org.bouncycastle.asn1.x500.style.BCStyle
|
||||||
|
import java.util.Locale
|
||||||
|
import javax.security.auth.x500.X500Principal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* X.500 distinguished name data type customised to how Corda uses names. This restricts the attributes to those Corda
|
||||||
|
* supports, and requires that organisation, locality and country attributes are specified. See also RFC 4519 for
|
||||||
|
* the underlying attribute type definitions
|
||||||
|
*
|
||||||
|
* @property commonName optional name by the which the entity is usually known. Used only for services (for
|
||||||
|
* organisations, the [organisation] property is the name). Corresponds to the "CN" attribute type.
|
||||||
|
* @property organisationUnit optional name of a unit within the [organisation]. Corresponds to the "OU" attribute type.
|
||||||
|
* @property organisation name of the organisation. Corresponds to the "O" attribute type.
|
||||||
|
* @property locality locality of the organisation, typically nearest major city. For distributed services this would be
|
||||||
|
* where one of the organisations is based. Corresponds to the "L" attribute type.
|
||||||
|
* @property state the full name of the state or province the organisation is based in. Corresponds to the "ST"
|
||||||
|
* attribute type.
|
||||||
|
* @property country country the organisation is in, as an ISO 3166-1 2-letter country code. Corresponds to the "C"
|
||||||
|
* attribute type.
|
||||||
|
*/
|
||||||
|
@CordaSerializable
|
||||||
|
data class CordaX500Name(val commonName: String?,
|
||||||
|
val organisationUnit: String?,
|
||||||
|
val organisation: String,
|
||||||
|
val locality: String,
|
||||||
|
val state: String?,
|
||||||
|
val country: String) {
|
||||||
|
constructor(commonName: String, organisation: String, locality: String, country: String) : this(commonName = commonName, organisationUnit = null, organisation = organisation, locality = locality, state = null, country = country)
|
||||||
|
/**
|
||||||
|
* @param organisation name of the organisation.
|
||||||
|
* @param locality locality of the organisation, typically nearest major city.
|
||||||
|
* @param country country the organisation is in, as an ISO 3166-1 2-letter country code.
|
||||||
|
*/
|
||||||
|
constructor(organisation: String, locality: String, country: String) : this(null, null, organisation, locality, null, country)
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Legal name checks.
|
||||||
|
LegalNameValidator.validateLegalName(organisation)
|
||||||
|
|
||||||
|
// Attribute data width checks.
|
||||||
|
require(country.length == LENGTH_COUNTRY) { "Invalid country '$country' Country code must be $LENGTH_COUNTRY letters ISO code " }
|
||||||
|
require(country.toUpperCase() == country) { "Country code should be in upper case." }
|
||||||
|
require(country in countryCodes) { "Invalid country code $country" }
|
||||||
|
|
||||||
|
require(organisation.length < MAX_LENGTH_ORGANISATION) {
|
||||||
|
"Organisation attribute (O) must contain less then $MAX_LENGTH_ORGANISATION characters."
|
||||||
|
}
|
||||||
|
require(locality.length < MAX_LENGTH_LOCALITY) { "Locality attribute (L) must contain less then $MAX_LENGTH_LOCALITY characters." }
|
||||||
|
|
||||||
|
state?.let { require(it.length < MAX_LENGTH_STATE) { "State attribute (ST) must contain less then $MAX_LENGTH_STATE characters." } }
|
||||||
|
organisationUnit?.let {
|
||||||
|
require(it.length < MAX_LENGTH_ORGANISATION_UNIT) {
|
||||||
|
"Organisation Unit attribute (OU) must contain less then $MAX_LENGTH_ORGANISATION_UNIT characters."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
commonName?.let {
|
||||||
|
require(it.length < MAX_LENGTH_COMMON_NAME) {
|
||||||
|
"Common Name attribute (CN) must contain less then $MAX_LENGTH_COMMON_NAME characters."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val LENGTH_COUNTRY = 2
|
||||||
|
const val MAX_LENGTH_ORGANISATION = 128
|
||||||
|
const val MAX_LENGTH_LOCALITY = 64
|
||||||
|
const val MAX_LENGTH_STATE = 64
|
||||||
|
const val MAX_LENGTH_ORGANISATION_UNIT = 64
|
||||||
|
const val MAX_LENGTH_COMMON_NAME = 64
|
||||||
|
private val supportedAttributes = setOf(BCStyle.O, BCStyle.C, BCStyle.L, BCStyle.CN, BCStyle.ST, BCStyle.OU)
|
||||||
|
private val countryCodes: Set<String> = ImmutableSet.copyOf(Locale.getISOCountries())
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun build(principal: X500Principal) : CordaX500Name {
|
||||||
|
val x500Name = X500Name.getInstance(principal.encoded)
|
||||||
|
val attrsMap: Map<ASN1ObjectIdentifier, ASN1Encodable> = x500Name.rdNs
|
||||||
|
.flatMap { it.typesAndValues.asList() }
|
||||||
|
.groupBy(AttributeTypeAndValue::getType, AttributeTypeAndValue::getValue)
|
||||||
|
.mapValues {
|
||||||
|
require(it.value.size == 1) { "Duplicate attribute ${it.key}" }
|
||||||
|
it.value[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supported attribute checks.
|
||||||
|
(attrsMap.keys - supportedAttributes).let { unsupported ->
|
||||||
|
require(unsupported.isEmpty()) {
|
||||||
|
"The following attribute${if (unsupported.size > 1) "s are" else " is"} not supported in Corda: " +
|
||||||
|
unsupported.map { BCStyle.INSTANCE.oidToDisplayName(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val CN = attrsMap[BCStyle.CN]?.toString()
|
||||||
|
val OU = attrsMap[BCStyle.OU]?.toString()
|
||||||
|
val O = attrsMap[BCStyle.O]?.toString() ?: throw IllegalArgumentException("Corda X.500 names must include an O attribute")
|
||||||
|
val L = attrsMap[BCStyle.L]?.toString() ?: throw IllegalArgumentException("Corda X.500 names must include an L attribute")
|
||||||
|
val ST = attrsMap[BCStyle.ST]?.toString()
|
||||||
|
val C = attrsMap[BCStyle.C]?.toString() ?: throw IllegalArgumentException("Corda X.500 names must include an C attribute")
|
||||||
|
return CordaX500Name(CN, OU, O, L, ST, C)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun parse(name: String) : CordaX500Name = build(X500Principal(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
private var x500Cache: X500Name? = null
|
||||||
|
|
||||||
|
val x500Principal: X500Principal
|
||||||
|
get() {
|
||||||
|
if (x500Cache == null) {
|
||||||
|
x500Cache = this.x500Name
|
||||||
|
}
|
||||||
|
return X500Principal(x500Cache!!.encoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
if (x500Cache == null) {
|
||||||
|
x500Cache = this.x500Name
|
||||||
|
}
|
||||||
|
return x500Cache.toString()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
@file:JvmName("IdentityUtils")
|
||||||
|
|
||||||
|
package net.corda.core.identity
|
||||||
|
|
||||||
|
import net.corda.core.internal.toMultiMap
|
||||||
|
import net.corda.core.node.ServiceHub
|
||||||
|
import net.corda.core.transactions.SignedTransaction
|
||||||
|
import java.security.PublicKey
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Group each [PublicKey] by the well known party using the [ServiceHub.identityService], in preparation for
|
||||||
|
* creating [FlowSession]s, for example.
|
||||||
|
*
|
||||||
|
* @param publicKeys the [PublicKey]s to group.
|
||||||
|
* @param ignoreUnrecognisedParties if this is false, throw an exception if some of the [PublicKey]s cannot be mapped
|
||||||
|
* to a [Party].
|
||||||
|
* @return a map of well known [Party] to associated [PublicKey]s.
|
||||||
|
*/
|
||||||
|
@Throws(IllegalArgumentException::class)
|
||||||
|
fun groupPublicKeysByWellKnownParty(serviceHub: ServiceHub, publicKeys: Collection<PublicKey>, ignoreUnrecognisedParties: Boolean): Map<Party, List<PublicKey>> =
|
||||||
|
groupAbstractPartyByWellKnownParty(serviceHub, publicKeys.map { AnonymousParty(it) }, ignoreUnrecognisedParties).mapValues { it.value.map { it.owningKey } }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Group each [PublicKey] by the well known party using the [ServiceHub.identityService], in preparation for
|
||||||
|
* creating [FlowSession]s, for example. Throw an exception if some of the [PublicKey]s cannot be mapped
|
||||||
|
* to a [Party].
|
||||||
|
*
|
||||||
|
* @param publicKeys the [PublicKey]s to group.
|
||||||
|
* @return a map of well known [Party] to associated [PublicKey]s.
|
||||||
|
*/
|
||||||
|
// Cannot use @JvmOverloads in interface
|
||||||
|
@Throws(IllegalArgumentException::class)
|
||||||
|
fun groupPublicKeysByWellKnownParty(serviceHub: ServiceHub, publicKeys: Collection<PublicKey>): Map<Party, List<PublicKey>> = groupPublicKeysByWellKnownParty(serviceHub, publicKeys, false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Group each [AbstractParty] by the well known party using the [ServiceHub.identityService], in preparation for
|
||||||
|
* creating [FlowSession]s, for example.
|
||||||
|
*
|
||||||
|
* @param parties the [AbstractParty]s to group.
|
||||||
|
* @param ignoreUnrecognisedParties if this is false, throw an exception if some of the [AbstractParty]s cannot be mapped
|
||||||
|
* to a [Party].
|
||||||
|
* @return a map of well known [Party] to associated [AbstractParty]s.
|
||||||
|
*/
|
||||||
|
@Throws(IllegalArgumentException::class)
|
||||||
|
fun groupAbstractPartyByWellKnownParty(serviceHub: ServiceHub, parties: Collection<AbstractParty>, ignoreUnrecognisedParties: Boolean): Map<Party, List<AbstractParty>> {
|
||||||
|
val partyToPublicKey: Iterable<Pair<Party, AbstractParty>> = parties.mapNotNull {
|
||||||
|
(serviceHub.identityService.wellKnownPartyFromAnonymous(it) ?: if (ignoreUnrecognisedParties) return@mapNotNull null else throw IllegalArgumentException("Could not find Party for $it")) to it
|
||||||
|
}
|
||||||
|
return partyToPublicKey.toMultiMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Group each [AbstractParty] by the well known party using the [ServiceHub.identityService], in preparation for
|
||||||
|
* creating [FlowSession]s, for example. Throw an exception if some of the [AbstractParty]s cannot be mapped
|
||||||
|
* to a [Party].
|
||||||
|
*
|
||||||
|
* @param parties the [AbstractParty]s to group.
|
||||||
|
* @return a map of well known [Party] to associated [AbstractParty]s.
|
||||||
|
*/
|
||||||
|
// Cannot use @JvmOverloads in interface
|
||||||
|
@Throws(IllegalArgumentException::class)
|
||||||
|
fun groupAbstractPartyByWellKnownParty(serviceHub: ServiceHub, parties: Collection<AbstractParty>): Map<Party, List<AbstractParty>> {
|
||||||
|
return groupAbstractPartyByWellKnownParty(serviceHub, parties, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove this node from a map of well known [Party]s.
|
||||||
|
*
|
||||||
|
* @return a new copy of the map, with he well known [Party] for this node removed.
|
||||||
|
*/
|
||||||
|
fun <T> excludeHostNode(serviceHub: ServiceHub, map: Map<Party, T>): Map<Party, T> = map.filterKeys { !serviceHub.myInfo.isLegalIdentity(it) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the [Party] associated with the notary of a [SignedTransaction] from the a map of [Party]s. It is a no-op
|
||||||
|
* if the notary is null.
|
||||||
|
*
|
||||||
|
* @return a new copy of the map, with the well known [Party] for the notary removed.
|
||||||
|
*/
|
||||||
|
fun <T> excludeNotary(map: Map<Party, T>, stx: SignedTransaction): Map<Party, T> = map.filterKeys { it != stx.notary }
|
@ -1,12 +1,11 @@
|
|||||||
package net.corda.core.identity
|
package net.corda.core.identity
|
||||||
|
|
||||||
import net.corda.core.contracts.PartyAndReference
|
import net.corda.core.contracts.PartyAndReference
|
||||||
import net.corda.core.crypto.Crypto
|
|
||||||
import net.corda.core.crypto.CompositeKey
|
import net.corda.core.crypto.CompositeKey
|
||||||
|
import net.corda.core.crypto.Crypto
|
||||||
import net.corda.core.utilities.OpaqueBytes
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
import org.bouncycastle.asn1.x500.X500Name
|
|
||||||
import org.bouncycastle.cert.X509CertificateHolder
|
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [Party] class represents an entity on the network, which is typically identified by a legal [name] and public key
|
* The [Party] class represents an entity on the network, which is typically identified by a legal [name] and public key
|
||||||
@ -27,9 +26,10 @@ import java.security.PublicKey
|
|||||||
*
|
*
|
||||||
* @see CompositeKey
|
* @see CompositeKey
|
||||||
*/
|
*/
|
||||||
class Party(val name: X500Name, owningKey: PublicKey) : AbstractParty(owningKey) {
|
class Party(val name: CordaX500Name, owningKey: PublicKey) : AbstractParty(owningKey) {
|
||||||
constructor(certificate: X509CertificateHolder) : this(certificate.subject, Crypto.toSupportedPublicKey(certificate.subjectPublicKeyInfo))
|
constructor(certificate: X509Certificate)
|
||||||
override fun nameOrNull(): X500Name = name
|
: this(CordaX500Name.build(certificate.subjectX500Principal), Crypto.toSupportedPublicKey(certificate.publicKey))
|
||||||
|
override fun nameOrNull(): CordaX500Name = name
|
||||||
fun anonymise(): AnonymousParty = AnonymousParty(owningKey)
|
fun anonymise(): AnonymousParty = AnonymousParty(owningKey)
|
||||||
override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes)
|
override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes)
|
||||||
override fun toString() = name.toString()
|
override fun toString() = name.toString()
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
package net.corda.core.identity
|
package net.corda.core.identity
|
||||||
|
|
||||||
import net.corda.core.internal.toX509CertHolder
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import org.bouncycastle.asn1.x500.X500Name
|
|
||||||
import org.bouncycastle.cert.X509CertificateHolder
|
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.security.cert.*
|
import java.security.cert.*
|
||||||
|
|
||||||
@ -11,23 +9,23 @@ import java.security.cert.*
|
|||||||
* [PartyAndCertificate] instances is based on the party only, as certificate and path are data associated with the party,
|
* [PartyAndCertificate] instances is based on the party only, as certificate and path are data associated with the party,
|
||||||
* not part of the identifier themselves.
|
* not part of the identifier themselves.
|
||||||
*/
|
*/
|
||||||
//TODO Is VerifiableIdentity a better name?
|
@CordaSerializable
|
||||||
class PartyAndCertificate(val certPath: CertPath) {
|
class PartyAndCertificate(val certPath: CertPath) {
|
||||||
@Transient val certificate: X509CertificateHolder
|
@Transient val certificate: X509Certificate
|
||||||
init {
|
init {
|
||||||
require(certPath.type == "X.509") { "Only X.509 certificates supported" }
|
require(certPath.type == "X.509") { "Only X.509 certificates supported" }
|
||||||
val certs = certPath.certificates
|
val certs = certPath.certificates
|
||||||
require(certs.size >= 2) { "Certificate path must at least include subject and issuing certificates" }
|
require(certs.size >= 2) { "Certificate path must at least include subject and issuing certificates" }
|
||||||
certificate = certs[0].toX509CertHolder()
|
certificate = certs[0] as X509Certificate
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transient val party: Party = Party(certificate)
|
@Transient val party: Party = Party(certificate)
|
||||||
|
|
||||||
val owningKey: PublicKey get() = party.owningKey
|
val owningKey: PublicKey get() = party.owningKey
|
||||||
val name: X500Name get() = party.name
|
val name: CordaX500Name get() = party.name
|
||||||
|
|
||||||
operator fun component1(): Party = party
|
operator fun component1(): Party = party
|
||||||
operator fun component2(): X509CertificateHolder = certificate
|
operator fun component2(): X509Certificate = certificate
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean = other === this || other is PartyAndCertificate && other.party == party
|
override fun equals(other: Any?): Boolean = other === this || other is PartyAndCertificate && other.party == party
|
||||||
override fun hashCode(): Int = party.hashCode()
|
override fun hashCode(): Int = party.hashCode()
|
||||||
|
@ -10,6 +10,7 @@ import java.io.IOException
|
|||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.security.CodeSigner
|
import java.security.CodeSigner
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
import java.util.jar.JarInputStream
|
import java.util.jar.JarInputStream
|
||||||
|
|
||||||
abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment {
|
abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment {
|
||||||
@ -23,7 +24,6 @@ abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment {
|
|||||||
|
|
||||||
/** @see <https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File> */
|
/** @see <https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File> */
|
||||||
private val unsignableEntryName = "META-INF/(?:.*[.](?:SF|DSA|RSA)|SIG-.*)".toRegex()
|
private val unsignableEntryName = "META-INF/(?:.*[.](?:SF|DSA|RSA)|SIG-.*)".toRegex()
|
||||||
private val shredder = ByteArray(1024)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected val attachmentData: ByteArray by lazy(dataLoader)
|
protected val attachmentData: ByteArray by lazy(dataLoader)
|
||||||
@ -32,6 +32,7 @@ abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment {
|
|||||||
// Can't start with empty set if we're doing intersections. Logically the null means "all possible signers":
|
// Can't start with empty set if we're doing intersections. Logically the null means "all possible signers":
|
||||||
var attachmentSigners: MutableSet<CodeSigner>? = null
|
var attachmentSigners: MutableSet<CodeSigner>? = null
|
||||||
openAsJAR().use { jar ->
|
openAsJAR().use { jar ->
|
||||||
|
val shredder = ByteArray(1024)
|
||||||
while (true) {
|
while (true) {
|
||||||
val entry = jar.nextJarEntry ?: break
|
val entry = jar.nextJarEntry ?: break
|
||||||
if (entry.isDirectory || unsignableEntryName.matches(entry.name)) continue
|
if (entry.isDirectory || unsignableEntryName.matches(entry.name)) continue
|
||||||
@ -44,7 +45,7 @@ abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
(attachmentSigners ?: emptySet<CodeSigner>()).map {
|
(attachmentSigners ?: emptySet<CodeSigner>()).map {
|
||||||
Party(it.signerCertPath.certificates[0].toX509CertHolder())
|
Party(it.signerCertPath.certificates[0] as X509Certificate)
|
||||||
}.sortedBy { it.name.toString() } // Determinism.
|
}.sortedBy { it.name.toString() } // Determinism.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
package net.corda.core.internal
|
||||||
|
|
||||||
|
import net.corda.core.contracts.*
|
||||||
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
|
|
||||||
|
object ContractUpgradeUtils {
|
||||||
|
fun <OldState : ContractState, NewState : ContractState> assembleBareTx(
|
||||||
|
stateRef: StateAndRef<OldState>,
|
||||||
|
upgradedContractClass: Class<out UpgradedContract<OldState, NewState>>,
|
||||||
|
privacySalt: PrivacySalt
|
||||||
|
): TransactionBuilder {
|
||||||
|
val contractUpgrade = upgradedContractClass.newInstance()
|
||||||
|
return TransactionBuilder(stateRef.state.notary)
|
||||||
|
.withItems(
|
||||||
|
stateRef,
|
||||||
|
StateAndContract(contractUpgrade.upgrade(stateRef.state.data), upgradedContractClass.name),
|
||||||
|
Command(UpgradeCommand(upgradedContractClass.name), stateRef.state.data.participants.map { it.owningKey }),
|
||||||
|
privacySalt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -7,7 +7,7 @@ import net.corda.core.crypto.SecureHash
|
|||||||
import net.corda.core.crypto.sha256
|
import net.corda.core.crypto.sha256
|
||||||
import net.corda.core.flows.FlowException
|
import net.corda.core.flows.FlowException
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.flows.FlowSession
|
||||||
import net.corda.core.internal.FetchDataFlow.DownloadedVsRequestedDataMismatch
|
import net.corda.core.internal.FetchDataFlow.DownloadedVsRequestedDataMismatch
|
||||||
import net.corda.core.internal.FetchDataFlow.HashNotFound
|
import net.corda.core.internal.FetchDataFlow.HashNotFound
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
@ -38,7 +38,7 @@ import java.util.*
|
|||||||
*/
|
*/
|
||||||
sealed class FetchDataFlow<T : NamedByHash, in W : Any>(
|
sealed class FetchDataFlow<T : NamedByHash, in W : Any>(
|
||||||
protected val requests: Set<SecureHash>,
|
protected val requests: Set<SecureHash>,
|
||||||
protected val otherSide: Party,
|
protected val otherSideSession: FlowSession,
|
||||||
protected val dataType: DataType) : FlowLogic<FetchDataFlow.Result<T>>() {
|
protected val dataType: DataType) : FlowLogic<FetchDataFlow.Result<T>>() {
|
||||||
|
|
||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
@ -72,7 +72,7 @@ sealed class FetchDataFlow<T : NamedByHash, in W : Any>(
|
|||||||
return if (toFetch.isEmpty()) {
|
return if (toFetch.isEmpty()) {
|
||||||
Result(fromDisk, emptyList())
|
Result(fromDisk, emptyList())
|
||||||
} else {
|
} else {
|
||||||
logger.info("Requesting ${toFetch.size} dependency(s) for verification from ${otherSide.name}")
|
logger.info("Requesting ${toFetch.size} dependency(s) for verification from ${otherSideSession.counterparty.name}")
|
||||||
|
|
||||||
// TODO: Support "large message" response streaming so response sizes are not limited by RAM.
|
// TODO: Support "large message" response streaming so response sizes are not limited by RAM.
|
||||||
// We can then switch to requesting items in large batches to minimise the latency penalty.
|
// We can then switch to requesting items in large batches to minimise the latency penalty.
|
||||||
@ -85,11 +85,11 @@ sealed class FetchDataFlow<T : NamedByHash, in W : Any>(
|
|||||||
for (hash in toFetch) {
|
for (hash in toFetch) {
|
||||||
// We skip the validation here (with unwrap { it }) because we will do it below in validateFetchResponse.
|
// We skip the validation here (with unwrap { it }) because we will do it below in validateFetchResponse.
|
||||||
// The only thing checked is the object type. It is a protocol violation to send results out of order.
|
// The only thing checked is the object type. It is a protocol violation to send results out of order.
|
||||||
maybeItems += sendAndReceive<List<W>>(otherSide, Request.Data(NonEmptySet.of(hash), dataType)).unwrap { it }
|
maybeItems += otherSideSession.sendAndReceive<List<W>>(Request.Data(NonEmptySet.of(hash), dataType)).unwrap { it }
|
||||||
}
|
}
|
||||||
// Check for a buggy/malicious peer answering with something that we didn't ask for.
|
// Check for a buggy/malicious peer answering with something that we didn't ask for.
|
||||||
val downloaded = validateFetchResponse(UntrustworthyData(maybeItems), toFetch)
|
val downloaded = validateFetchResponse(UntrustworthyData(maybeItems), toFetch)
|
||||||
logger.info("Fetched ${downloaded.size} elements from ${otherSide.name}")
|
logger.info("Fetched ${downloaded.size} elements from ${otherSideSession.counterparty.name}")
|
||||||
maybeWriteToDisk(downloaded)
|
maybeWriteToDisk(downloaded)
|
||||||
Result(fromDisk, downloaded)
|
Result(fromDisk, downloaded)
|
||||||
}
|
}
|
||||||
@ -140,7 +140,7 @@ sealed class FetchDataFlow<T : NamedByHash, in W : Any>(
|
|||||||
* attachments are saved to local storage automatically.
|
* attachments are saved to local storage automatically.
|
||||||
*/
|
*/
|
||||||
class FetchAttachmentsFlow(requests: Set<SecureHash>,
|
class FetchAttachmentsFlow(requests: Set<SecureHash>,
|
||||||
otherSide: Party) : FetchDataFlow<Attachment, ByteArray>(requests, otherSide, DataType.ATTACHMENT) {
|
otherSide: FlowSession) : FetchDataFlow<Attachment, ByteArray>(requests, otherSide, DataType.ATTACHMENT) {
|
||||||
|
|
||||||
override fun load(txid: SecureHash): Attachment? = serviceHub.attachments.openAttachment(txid)
|
override fun load(txid: SecureHash): Attachment? = serviceHub.attachments.openAttachment(txid)
|
||||||
|
|
||||||
@ -171,7 +171,7 @@ class FetchAttachmentsFlow(requests: Set<SecureHash>,
|
|||||||
* results in a [FetchDataFlow.HashNotFound] exception. Note that returned transactions are not inserted into
|
* results in a [FetchDataFlow.HashNotFound] exception. Note that returned transactions are not inserted into
|
||||||
* the database, because it's up to the caller to actually verify the transactions are valid.
|
* the database, because it's up to the caller to actually verify the transactions are valid.
|
||||||
*/
|
*/
|
||||||
class FetchTransactionsFlow(requests: Set<SecureHash>, otherSide: Party) :
|
class FetchTransactionsFlow(requests: Set<SecureHash>, otherSide: FlowSession) :
|
||||||
FetchDataFlow<SignedTransaction, SignedTransaction>(requests, otherSide, DataType.TRANSACTION) {
|
FetchDataFlow<SignedTransaction, SignedTransaction>(requests, otherSide, DataType.TRANSACTION) {
|
||||||
|
|
||||||
override fun load(txid: SecureHash): SignedTransaction? = serviceHub.validatedTransactions.getTransaction(txid)
|
override fun load(txid: SecureHash): SignedTransaction? = serviceHub.validatedTransactions.getTransaction(txid)
|
||||||
|
@ -5,6 +5,7 @@ import net.corda.core.concurrent.CordaFuture
|
|||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.flows.*
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.identity.PartyAndCertificate
|
||||||
import net.corda.core.node.ServiceHub
|
import net.corda.core.node.ServiceHub
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.utilities.UntrustworthyData
|
import net.corda.core.utilities.UntrustworthyData
|
||||||
@ -13,7 +14,10 @@ import org.slf4j.Logger
|
|||||||
/** This is an internal interface that is implemented by code in the node module. You should look at [FlowLogic]. */
|
/** This is an internal interface that is implemented by code in the node module. You should look at [FlowLogic]. */
|
||||||
interface FlowStateMachine<R> {
|
interface FlowStateMachine<R> {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
fun getFlowContext(otherParty: Party, sessionFlow: FlowLogic<*>): FlowContext
|
fun getFlowInfo(otherParty: Party, sessionFlow: FlowLogic<*>): FlowInfo
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
fun initiateFlow(otherParty: Party, sessionFlow: FlowLogic<*>): FlowSession
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
fun <T : Any> sendAndReceive(receiveType: Class<T>,
|
fun <T : Any> sendAndReceive(receiveType: Class<T>,
|
||||||
@ -46,4 +50,5 @@ interface FlowStateMachine<R> {
|
|||||||
val id: StateMachineRunId
|
val id: StateMachineRunId
|
||||||
val resultFuture: CordaFuture<R>
|
val resultFuture: CordaFuture<R>
|
||||||
val flowInitiator: FlowInitiator
|
val flowInitiator: FlowInitiator
|
||||||
|
val ourIdentityAndCert: PartyAndCertificate
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package net.corda.core.internal
|
|||||||
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 org.bouncycastle.cert.X509CertificateHolder
|
import org.bouncycastle.cert.X509CertificateHolder
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Observer
|
import rx.Observer
|
||||||
@ -15,6 +16,8 @@ import java.nio.charset.Charset
|
|||||||
import java.nio.charset.StandardCharsets.UTF_8
|
import java.nio.charset.StandardCharsets.UTF_8
|
||||||
import java.nio.file.*
|
import java.nio.file.*
|
||||||
import java.nio.file.attribute.FileAttribute
|
import java.nio.file.attribute.FileAttribute
|
||||||
|
import java.security.cert.Certificate
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.temporal.Temporal
|
import java.time.temporal.Temporal
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -26,6 +29,7 @@ import java.util.zip.Deflater
|
|||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipOutputStream
|
import java.util.zip.ZipOutputStream
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
import kotlin.reflect.full.createInstance
|
||||||
|
|
||||||
val Throwable.rootCause: Throwable get() = cause?.rootCause ?: this
|
val Throwable.rootCause: Throwable get() = cause?.rootCause ?: this
|
||||||
fun Throwable.getStackTraceAsString() = StringWriter().also { printStackTrace(PrintWriter(it)) }.toString()
|
fun Throwable.getStackTraceAsString() = StringWriter().also { printStackTrace(PrintWriter(it)) }.toString()
|
||||||
@ -166,8 +170,8 @@ fun <T> logElapsedTime(label: String, logger: Logger? = null, body: () -> T): T
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun java.security.cert.Certificate.toX509CertHolder() = X509CertificateHolder(encoded)
|
fun Certificate.toX509CertHolder() = X509CertificateHolder(encoded)
|
||||||
fun javax.security.cert.Certificate.toX509CertHolder() = X509CertificateHolder(encoded)
|
val X509CertificateHolder.cert: X509Certificate get() = JcaX509CertificateConverter().getCertificate(this)
|
||||||
|
|
||||||
/** Convert a [ByteArrayOutputStream] to [InputStreamAndHash]. */
|
/** Convert a [ByteArrayOutputStream] to [InputStreamAndHash]. */
|
||||||
fun ByteArrayOutputStream.toInputStreamAndHash(): InputStreamAndHash {
|
fun ByteArrayOutputStream.toInputStreamAndHash(): InputStreamAndHash {
|
||||||
@ -239,6 +243,11 @@ fun <T> Any.declaredField(name: String): DeclaredField<T> = DeclaredField(javaCl
|
|||||||
*/
|
*/
|
||||||
fun <T> Any.declaredField(clazz: KClass<*>, name: String): DeclaredField<T> = DeclaredField(clazz.java, name, this)
|
fun <T> Any.declaredField(clazz: KClass<*>, name: String): DeclaredField<T> = DeclaredField(clazz.java, name, this)
|
||||||
|
|
||||||
|
/** creates a new instance if not a Kotlin object */
|
||||||
|
fun <T: Any> KClass<T>.objectOrNewInstance(): T {
|
||||||
|
return this.objectInstance ?: this.createInstance()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple wrapper around a [Field] object providing type safe read and write access using [value], ignoring the field's
|
* A simple wrapper around a [Field] object providing type safe read and write access using [value], ignoring the field's
|
||||||
* visibility.
|
* visibility.
|
||||||
@ -260,3 +269,8 @@ class DeclaredField<T>(clazz: Class<*>, name: String, private val receiver: Any?
|
|||||||
@Retention(AnnotationRetention.SOURCE)
|
@Retention(AnnotationRetention.SOURCE)
|
||||||
@MustBeDocumented
|
@MustBeDocumented
|
||||||
annotation class VisibleForTesting
|
annotation class VisibleForTesting
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun <T, U : T> uncheckedCast(obj: T) = obj as U
|
||||||
|
|
||||||
|
fun <K, V> Iterable<Pair<K, V>>.toMultiMap(): Map<K, List<V>> = this.groupBy({ it.first }) { it.second }
|
@ -0,0 +1,117 @@
|
|||||||
|
package net.corda.core.internal
|
||||||
|
|
||||||
|
import java.lang.Character.UnicodeScript.*
|
||||||
|
import java.text.Normalizer
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
import javax.security.auth.x500.X500Principal
|
||||||
|
|
||||||
|
object LegalNameValidator {
|
||||||
|
/**
|
||||||
|
* The validation function will validate the input string using the following rules:
|
||||||
|
*
|
||||||
|
* - No blacklisted words like "node", "server".
|
||||||
|
* - Restrict names to Latin scripts for now to avoid right-to-left issues, debugging issues when we can't pronounce
|
||||||
|
* names over the phone, and character confusability attacks.
|
||||||
|
* - Must consist of at least three letters and should start with a capital letter.
|
||||||
|
* - No commas or equals signs.
|
||||||
|
* - No dollars or quote marks, we might need to relax the quote mark constraint in future to handle Irish company names.
|
||||||
|
*
|
||||||
|
* @throws IllegalArgumentException if the name does not meet the required rules. The message indicates why not.
|
||||||
|
*/
|
||||||
|
fun validateLegalName(normalizedLegalName: String) {
|
||||||
|
Rule.legalNameRules.forEach { it.validate(normalizedLegalName) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The normalize function will trim the input string, replace any multiple spaces with a single space,
|
||||||
|
* and normalize the string according to NFKC normalization form.
|
||||||
|
*/
|
||||||
|
fun normaliseLegalName(legalName: String): String {
|
||||||
|
val trimmedLegalName = legalName.trim().replace(WHITESPACE, " ")
|
||||||
|
return Normalizer.normalize(trimmedLegalName, Normalizer.Form.NFKC)
|
||||||
|
}
|
||||||
|
|
||||||
|
val WHITESPACE = "\\s++".toRegex()
|
||||||
|
|
||||||
|
sealed class Rule<in T> {
|
||||||
|
companion object {
|
||||||
|
val legalNameRules: List<Rule<String>> = listOf(
|
||||||
|
UnicodeNormalizationRule(),
|
||||||
|
CharacterRule(',', '=', '$', '"', '\'', '\\'),
|
||||||
|
WordRule("node", "server"),
|
||||||
|
LengthRule(maxLength = 255),
|
||||||
|
// TODO: Implement confusable character detection if we add more scripts.
|
||||||
|
UnicodeRangeRule(LATIN, COMMON, INHERITED),
|
||||||
|
CapitalLetterRule(),
|
||||||
|
X500NameRule(),
|
||||||
|
MustHaveAtLeastTwoLettersRule()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun validate(legalName: T)
|
||||||
|
|
||||||
|
private class UnicodeNormalizationRule : Rule<String>() {
|
||||||
|
override fun validate(legalName: String) {
|
||||||
|
require(legalName == normaliseLegalName(legalName)) { "Legal name must be normalized. Please use 'normaliseLegalName' to normalize the legal name before validation." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class UnicodeRangeRule(vararg supportScripts: Character.UnicodeScript) : Rule<String>() {
|
||||||
|
private val pattern = supportScripts.map { "\\p{Is$it}" }.joinToString(separator = "", prefix = "[", postfix = "]*").let { Pattern.compile(it) }
|
||||||
|
|
||||||
|
override fun validate(legalName: String) {
|
||||||
|
require(pattern.matcher(legalName).matches()) {
|
||||||
|
val illegalChars = legalName.replace(pattern.toRegex(), "").toSet()
|
||||||
|
if (illegalChars.size > 1) {
|
||||||
|
"Forbidden characters $illegalChars in \"$legalName\"."
|
||||||
|
} else {
|
||||||
|
"Forbidden character $illegalChars in \"$legalName\"."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CharacterRule(vararg val bannedChars: Char) : Rule<String>() {
|
||||||
|
override fun validate(legalName: String) {
|
||||||
|
bannedChars.forEach {
|
||||||
|
require(!legalName.contains(it, true)) { "Character not allowed in legal names: $it" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class WordRule(vararg val bannedWords: String) : Rule<String>() {
|
||||||
|
override fun validate(legalName: String) {
|
||||||
|
bannedWords.forEach {
|
||||||
|
require(!legalName.contains(it, ignoreCase = true)) { "Word not allowed in legal names: $it" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LengthRule(val maxLength: Int) : Rule<String>() {
|
||||||
|
override fun validate(legalName: String) {
|
||||||
|
require(legalName.length <= maxLength) { "Legal name longer then $maxLength characters." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CapitalLetterRule : Rule<String>() {
|
||||||
|
override fun validate(legalName: String) {
|
||||||
|
val capitalizedLegalName = legalName.capitalize()
|
||||||
|
require(legalName == capitalizedLegalName) { "Legal name should be capitalized. i.e. '$capitalizedLegalName'" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class X500NameRule : Rule<String>() {
|
||||||
|
override fun validate(legalName: String) {
|
||||||
|
// This will throw IllegalArgumentException if the name does not comply with X500 name format.
|
||||||
|
X500Principal("CN=$legalName")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MustHaveAtLeastTwoLettersRule : Rule<String>() {
|
||||||
|
override fun validate(legalName: String) {
|
||||||
|
// Try to exclude names like "/", "£", "X" etc.
|
||||||
|
require(legalName.count { it.isLetter() } >= 2) { "Illegal input legal name '$legalName'. Legal name must have at least two letters" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,7 @@ package net.corda.core.internal
|
|||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.flows.FlowSession
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.utilities.exactAdd
|
import net.corda.core.utilities.exactAdd
|
||||||
@ -17,14 +17,14 @@ import java.util.*
|
|||||||
* @return a list of verified [SignedTransaction] objects, in a depth-first order.
|
* @return a list of verified [SignedTransaction] objects, in a depth-first order.
|
||||||
*/
|
*/
|
||||||
class ResolveTransactionsFlow(private val txHashes: Set<SecureHash>,
|
class ResolveTransactionsFlow(private val txHashes: Set<SecureHash>,
|
||||||
private val otherSide: Party) : FlowLogic<List<SignedTransaction>>() {
|
private val otherSide: FlowSession) : FlowLogic<List<SignedTransaction>>() {
|
||||||
/**
|
/**
|
||||||
* Resolves and validates the dependencies of the specified [signedTransaction]. Fetches the attachments, but does
|
* Resolves and validates the dependencies of the specified [signedTransaction]. Fetches the attachments, but does
|
||||||
* *not* validate or store the [signedTransaction] itself.
|
* *not* validate or store the [signedTransaction] itself.
|
||||||
*
|
*
|
||||||
* @return a list of verified [SignedTransaction] objects, in a depth-first order.
|
* @return a list of verified [SignedTransaction] objects, in a depth-first order.
|
||||||
*/
|
*/
|
||||||
constructor(signedTransaction: SignedTransaction, otherSide: Party) : this(dependencyIDs(signedTransaction), otherSide) {
|
constructor(signedTransaction: SignedTransaction, otherSide: FlowSession) : this(dependencyIDs(signedTransaction), otherSide) {
|
||||||
this.signedTransaction = signedTransaction
|
this.signedTransaction = signedTransaction
|
||||||
}
|
}
|
||||||
companion object {
|
companion object {
|
||||||
@ -82,7 +82,7 @@ class ResolveTransactionsFlow(private val txHashes: Set<SecureHash>,
|
|||||||
// Start fetching data.
|
// Start fetching data.
|
||||||
val newTxns = downloadDependencies(txHashes)
|
val newTxns = downloadDependencies(txHashes)
|
||||||
fetchMissingAttachments(signedTransaction?.let { newTxns + it } ?: newTxns)
|
fetchMissingAttachments(signedTransaction?.let { newTxns + it } ?: newTxns)
|
||||||
send(otherSide, FetchDataFlow.Request.End)
|
otherSide.send(FetchDataFlow.Request.End)
|
||||||
// Finish fetching data.
|
// Finish fetching data.
|
||||||
|
|
||||||
val result = topologicalSort(newTxns)
|
val result = topologicalSort(newTxns)
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
@file:JvmName("X500NameUtils")
|
||||||
|
|
||||||
|
package net.corda.core.internal
|
||||||
|
|
||||||
|
import net.corda.core.identity.CordaX500Name
|
||||||
|
import org.bouncycastle.asn1.ASN1ObjectIdentifier
|
||||||
|
import org.bouncycastle.asn1.x500.X500Name
|
||||||
|
import org.bouncycastle.asn1.x500.X500NameBuilder
|
||||||
|
import org.bouncycastle.asn1.x500.style.BCStyle
|
||||||
|
|
||||||
|
val X500Name.commonName: String? get() = getRDNValueString(BCStyle.CN)
|
||||||
|
val X500Name.state: String? get() = getRDNValueString(BCStyle.ST)
|
||||||
|
val X500Name.organisation: String get() = getRDNValueString(BCStyle.O) ?: throw IllegalArgumentException("Malformed X500 name, organisation attribute (O) cannot be empty.")
|
||||||
|
val X500Name.locality: String get() = getRDNValueString(BCStyle.L) ?: throw IllegalArgumentException("Malformed X500 name, locality attribute (L) cannot be empty.")
|
||||||
|
val X500Name.country: String get() = getRDNValueString(BCStyle.C) ?: throw IllegalArgumentException("Malformed X500 name, country attribute (C) cannot be empty.")
|
||||||
|
|
||||||
|
private fun X500Name.getRDNValueString(identifier: ASN1ObjectIdentifier): String? = getRDNs(identifier).firstOrNull()?.first?.value?.toString()
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the underlying X.500 name from this Corda-safe X.500 name. These are guaranteed to have a consistent
|
||||||
|
* ordering, such that their `toString()` function returns the same value every time for the same [CordaX500Name].
|
||||||
|
*/
|
||||||
|
val CordaX500Name.x500Name: X500Name
|
||||||
|
get() {
|
||||||
|
return X500NameBuilder(BCStyle.INSTANCE).apply {
|
||||||
|
addRDN(BCStyle.C, country)
|
||||||
|
state?.let { addRDN(BCStyle.ST, it) }
|
||||||
|
addRDN(BCStyle.L, locality)
|
||||||
|
addRDN(BCStyle.O, organisation)
|
||||||
|
organisationUnit?.let { addRDN(BCStyle.OU, it) }
|
||||||
|
commonName?.let { addRDN(BCStyle.CN, it) }
|
||||||
|
}.build()
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
package net.corda.core.internal
|
||||||
|
|
||||||
|
import net.i2p.crypto.eddsa.EdDSAEngine
|
||||||
|
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||||
|
import java.security.*
|
||||||
|
import java.security.spec.AlgorithmParameterSpec
|
||||||
|
import java.security.spec.X509EncodedKeySpec
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper around [EdDSAEngine] which can intelligently rewrite X509Keys to a [EdDSAPublicKey]. This is a temporary
|
||||||
|
* solution until this is integrated upstream and/or a custom certificate factory implemented to force the correct
|
||||||
|
* key type. Only intercepts public keys passed into [engineInitVerify], as there is no equivalent issue with private
|
||||||
|
* keys.
|
||||||
|
*/
|
||||||
|
class X509EdDSAEngine : Signature {
|
||||||
|
private val engine: EdDSAEngine
|
||||||
|
|
||||||
|
constructor() : super(EdDSAEngine.SIGNATURE_ALGORITHM) {
|
||||||
|
engine = EdDSAEngine()
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(digest: MessageDigest) : super(EdDSAEngine.SIGNATURE_ALGORITHM) {
|
||||||
|
engine = EdDSAEngine(digest)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun engineInitSign(privateKey: PrivateKey) = engine.initSign(privateKey)
|
||||||
|
override fun engineInitSign(privateKey: PrivateKey, random: SecureRandom) = engine.initSign(privateKey, random)
|
||||||
|
|
||||||
|
override fun engineInitVerify(publicKey: PublicKey) {
|
||||||
|
val parsedKey = if (publicKey is sun.security.x509.X509Key) {
|
||||||
|
EdDSAPublicKey(X509EncodedKeySpec(publicKey.encoded))
|
||||||
|
} else {
|
||||||
|
publicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
engine.initVerify(parsedKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun engineSign(): ByteArray = engine.sign()
|
||||||
|
override fun engineVerify(sigBytes: ByteArray): Boolean = engine.verify(sigBytes)
|
||||||
|
|
||||||
|
override fun engineUpdate(b: Byte) = engine.update(b)
|
||||||
|
override fun engineUpdate(b: ByteArray, off: Int, len: Int) = engine.update(b, off, len)
|
||||||
|
|
||||||
|
override fun engineGetParameters(): AlgorithmParameters = engine.parameters
|
||||||
|
override fun engineSetParameter(params: AlgorithmParameterSpec) = engine.setParameter(params)
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
override fun engineGetParameter(param: String): Any = engine.getParameter(param)
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
override fun engineSetParameter(param: String, value: Any?) = engine.setParameter(param, value)
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package net.corda.core.internal.cordapp
|
||||||
|
|
||||||
|
import net.corda.core.cordapp.Cordapp
|
||||||
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.node.CordaPluginRegistry
|
||||||
|
import net.corda.core.schemas.MappedSchema
|
||||||
|
import net.corda.core.serialization.SerializeAsToken
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
data class CordappImpl(
|
||||||
|
override val contractClassNames: List<String>,
|
||||||
|
override val initiatedFlows: List<Class<out FlowLogic<*>>>,
|
||||||
|
override val rpcFlows: List<Class<out FlowLogic<*>>>,
|
||||||
|
override val services: List<Class<out SerializeAsToken>>,
|
||||||
|
override val plugins: List<CordaPluginRegistry>,
|
||||||
|
override val customSchemas: Set<MappedSchema>,
|
||||||
|
override val jarPath: URL) : Cordapp {
|
||||||
|
/**
|
||||||
|
* An exhaustive list of all classes relevant to the node within this CorDapp
|
||||||
|
*
|
||||||
|
* TODO: Also add [SchedulableFlow] as a Cordapp class
|
||||||
|
*/
|
||||||
|
override val cordappClasses = ((rpcFlows + initiatedFlows + services + plugins.map { javaClass }).map { it.name } + contractClassNames)
|
||||||
|
}
|
@ -2,13 +2,12 @@ package net.corda.core.messaging
|
|||||||
|
|
||||||
import net.corda.core.concurrent.CordaFuture
|
import net.corda.core.concurrent.CordaFuture
|
||||||
import net.corda.core.contracts.ContractState
|
import net.corda.core.contracts.ContractState
|
||||||
import net.corda.core.contracts.StateAndRef
|
|
||||||
import net.corda.core.contracts.UpgradedContract
|
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.flows.FlowInitiator
|
import net.corda.core.flows.FlowInitiator
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
import net.corda.core.flows.StateMachineRunId
|
import net.corda.core.flows.StateMachineRunId
|
||||||
import net.corda.core.identity.AbstractParty
|
import net.corda.core.identity.AbstractParty
|
||||||
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.node.services.NetworkMapCache
|
import net.corda.core.node.services.NetworkMapCache
|
||||||
@ -21,7 +20,6 @@ import net.corda.core.node.services.vault.Sort
|
|||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.utilities.Try
|
import net.corda.core.utilities.Try
|
||||||
import org.bouncycastle.asn1.x500.X500Name
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
@ -58,15 +56,14 @@ interface CordaRPCOps : RPCOps {
|
|||||||
* Returns the RPC protocol version, which is the same the node's Platform Version. Exists since version 1 so guaranteed
|
* Returns the RPC protocol version, which is the same the node's Platform Version. Exists since version 1 so guaranteed
|
||||||
* to be present.
|
* to be present.
|
||||||
*/
|
*/
|
||||||
override val protocolVersion: Int get() = nodeIdentity().platformVersion
|
override val protocolVersion: Int get() = nodeInfo().platformVersion
|
||||||
|
|
||||||
/**
|
/** Returns a list of currently in-progress state machine infos. */
|
||||||
* Returns a list of currently in-progress state machine infos.
|
|
||||||
*/
|
|
||||||
fun stateMachinesSnapshot(): List<StateMachineInfo>
|
fun stateMachinesSnapshot(): List<StateMachineInfo>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a data feed of currently in-progress state machine infos and an observable of future state machine adds/removes.
|
* Returns a data feed of currently in-progress state machine infos and an observable of
|
||||||
|
* future state machine adds/removes.
|
||||||
*/
|
*/
|
||||||
@RPCReturnsObservables
|
@RPCReturnsObservables
|
||||||
fun stateMachinesFeed(): DataFeed<List<StateMachineInfo>, StateMachineUpdate>
|
fun stateMachinesFeed(): DataFeed<List<StateMachineInfo>, StateMachineUpdate>
|
||||||
@ -97,24 +94,24 @@ interface CordaRPCOps : RPCOps {
|
|||||||
fun <T : ContractState> vaultQueryBy(criteria: QueryCriteria,
|
fun <T : ContractState> vaultQueryBy(criteria: QueryCriteria,
|
||||||
paging: PageSpecification,
|
paging: PageSpecification,
|
||||||
sorting: Sort,
|
sorting: Sort,
|
||||||
contractType: Class<out T>): Vault.Page<T>
|
contractStateType: Class<out T>): Vault.Page<T>
|
||||||
// DOCEND VaultQueryByAPI
|
// DOCEND VaultQueryByAPI
|
||||||
|
|
||||||
// Note: cannot apply @JvmOverloads to interfaces nor interface implementations
|
// Note: cannot apply @JvmOverloads to interfaces nor interface implementations
|
||||||
// Java Helpers
|
// Java Helpers
|
||||||
|
|
||||||
// DOCSTART VaultQueryAPIHelpers
|
// DOCSTART VaultQueryAPIHelpers
|
||||||
fun <T : ContractState> vaultQuery(contractType: Class<out T>): Vault.Page<T> {
|
fun <T : ContractState> vaultQuery(contractStateType: Class<out T>): Vault.Page<T> {
|
||||||
return vaultQueryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractType)
|
return vaultQueryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractStateType)
|
||||||
}
|
}
|
||||||
fun <T : ContractState> vaultQueryByCriteria(criteria: QueryCriteria, contractType: Class<out T>): Vault.Page<T> {
|
fun <T : ContractState> vaultQueryByCriteria(criteria: QueryCriteria, contractStateType: Class<out T>): Vault.Page<T> {
|
||||||
return vaultQueryBy(criteria, PageSpecification(), Sort(emptySet()), contractType)
|
return vaultQueryBy(criteria, PageSpecification(), Sort(emptySet()), contractStateType)
|
||||||
}
|
}
|
||||||
fun <T : ContractState> vaultQueryByWithPagingSpec(contractType: Class<out T>, criteria: QueryCriteria, paging: PageSpecification): Vault.Page<T> {
|
fun <T : ContractState> vaultQueryByWithPagingSpec(contractStateType: Class<out T>, criteria: QueryCriteria, paging: PageSpecification): Vault.Page<T> {
|
||||||
return vaultQueryBy(criteria, paging, Sort(emptySet()), contractType)
|
return vaultQueryBy(criteria, paging, Sort(emptySet()), contractStateType)
|
||||||
}
|
}
|
||||||
fun <T : ContractState> vaultQueryByWithSorting(contractType: Class<out T>, criteria: QueryCriteria, sorting: Sort): Vault.Page<T> {
|
fun <T : ContractState> vaultQueryByWithSorting(contractStateType: Class<out T>, criteria: QueryCriteria, sorting: Sort): Vault.Page<T> {
|
||||||
return vaultQueryBy(criteria, PageSpecification(), sorting, contractType)
|
return vaultQueryBy(criteria, PageSpecification(), sorting, contractStateType)
|
||||||
}
|
}
|
||||||
// DOCEND VaultQueryAPIHelpers
|
// DOCEND VaultQueryAPIHelpers
|
||||||
|
|
||||||
@ -135,24 +132,24 @@ interface CordaRPCOps : RPCOps {
|
|||||||
fun <T : ContractState> vaultTrackBy(criteria: QueryCriteria,
|
fun <T : ContractState> vaultTrackBy(criteria: QueryCriteria,
|
||||||
paging: PageSpecification,
|
paging: PageSpecification,
|
||||||
sorting: Sort,
|
sorting: Sort,
|
||||||
contractType: Class<out T>): DataFeed<Vault.Page<T>, Vault.Update<T>>
|
contractStateType: Class<out T>): DataFeed<Vault.Page<T>, Vault.Update<T>>
|
||||||
// DOCEND VaultTrackByAPI
|
// DOCEND VaultTrackByAPI
|
||||||
|
|
||||||
// Note: cannot apply @JvmOverloads to interfaces nor interface implementations
|
// Note: cannot apply @JvmOverloads to interfaces nor interface implementations
|
||||||
// Java Helpers
|
// Java Helpers
|
||||||
|
|
||||||
// DOCSTART VaultTrackAPIHelpers
|
// DOCSTART VaultTrackAPIHelpers
|
||||||
fun <T : ContractState> vaultTrack(contractType: Class<out T>): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
fun <T : ContractState> vaultTrack(contractStateType: Class<out T>): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
||||||
return vaultTrackBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractType)
|
return vaultTrackBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractStateType)
|
||||||
}
|
}
|
||||||
fun <T : ContractState> vaultTrackByCriteria(contractType: Class<out T>, criteria: QueryCriteria): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
fun <T : ContractState> vaultTrackByCriteria(contractStateType: Class<out T>, criteria: QueryCriteria): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
||||||
return vaultTrackBy(criteria, PageSpecification(), Sort(emptySet()), contractType)
|
return vaultTrackBy(criteria, PageSpecification(), Sort(emptySet()), contractStateType)
|
||||||
}
|
}
|
||||||
fun <T : ContractState> vaultTrackByWithPagingSpec(contractType: Class<out T>, criteria: QueryCriteria, paging: PageSpecification): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
fun <T : ContractState> vaultTrackByWithPagingSpec(contractStateType: Class<out T>, criteria: QueryCriteria, paging: PageSpecification): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
||||||
return vaultTrackBy(criteria, paging, Sort(emptySet()), contractType)
|
return vaultTrackBy(criteria, paging, Sort(emptySet()), contractStateType)
|
||||||
}
|
}
|
||||||
fun <T : ContractState> vaultTrackByWithSorting(contractType: Class<out T>, criteria: QueryCriteria, sorting: Sort): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
fun <T : ContractState> vaultTrackByWithSorting(contractStateType: Class<out T>, criteria: QueryCriteria, sorting: Sort): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
||||||
return vaultTrackBy(criteria, PageSpecification(), sorting, contractType)
|
return vaultTrackBy(criteria, PageSpecification(), sorting, contractStateType)
|
||||||
}
|
}
|
||||||
// DOCEND VaultTrackAPIHelpers
|
// DOCEND VaultTrackAPIHelpers
|
||||||
|
|
||||||
@ -173,9 +170,7 @@ interface CordaRPCOps : RPCOps {
|
|||||||
@RPCReturnsObservables
|
@RPCReturnsObservables
|
||||||
fun internalVerifiedTransactionsFeed(): DataFeed<List<SignedTransaction>, SignedTransaction>
|
fun internalVerifiedTransactionsFeed(): DataFeed<List<SignedTransaction>, SignedTransaction>
|
||||||
|
|
||||||
/**
|
/** Returns a snapshot list of existing state machine id - recorded transaction hash mappings. */
|
||||||
* Returns a snapshot list of existing state machine id - recorded transaction hash mappings.
|
|
||||||
*/
|
|
||||||
fun stateMachineRecordedTransactionMappingSnapshot(): List<StateMachineTransactionMapping>
|
fun stateMachineRecordedTransactionMappingSnapshot(): List<StateMachineTransactionMapping>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -185,19 +180,19 @@ interface CordaRPCOps : RPCOps {
|
|||||||
@RPCReturnsObservables
|
@RPCReturnsObservables
|
||||||
fun stateMachineRecordedTransactionMappingFeed(): DataFeed<List<StateMachineTransactionMapping>, StateMachineTransactionMapping>
|
fun stateMachineRecordedTransactionMappingFeed(): DataFeed<List<StateMachineTransactionMapping>, StateMachineTransactionMapping>
|
||||||
|
|
||||||
/**
|
/** Returns all parties currently visible on the network with their advertised services. */
|
||||||
* Returns all parties currently visible on the network with their advertised services.
|
|
||||||
*/
|
|
||||||
fun networkMapSnapshot(): List<NodeInfo>
|
fun networkMapSnapshot(): List<NodeInfo>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all parties currently visible on the network with their advertised services and an observable of future updates to the network.
|
* Returns all parties currently visible on the network with their advertised services and an observable of
|
||||||
|
* future updates to the network.
|
||||||
*/
|
*/
|
||||||
@RPCReturnsObservables
|
@RPCReturnsObservables
|
||||||
fun networkMapFeed(): DataFeed<List<NodeInfo>, NetworkMapCache.MapChange>
|
fun networkMapFeed(): DataFeed<List<NodeInfo>, NetworkMapCache.MapChange>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the given flow with the given arguments. [logicType] must be annotated with [net.corda.core.flows.StartableByRPC].
|
* Start the given flow with the given arguments. [logicType] must be annotated
|
||||||
|
* with [net.corda.core.flows.StartableByRPC].
|
||||||
*/
|
*/
|
||||||
@RPCReturnsObservables
|
@RPCReturnsObservables
|
||||||
fun <T> startFlowDynamic(logicType: Class<out FlowLogic<T>>, vararg args: Any?): FlowHandle<T>
|
fun <T> startFlowDynamic(logicType: Class<out FlowLogic<T>>, vararg args: Any?): FlowHandle<T>
|
||||||
@ -209,39 +204,32 @@ interface CordaRPCOps : RPCOps {
|
|||||||
@RPCReturnsObservables
|
@RPCReturnsObservables
|
||||||
fun <T> startTrackedFlowDynamic(logicType: Class<out FlowLogic<T>>, vararg args: Any?): FlowProgressHandle<T>
|
fun <T> startTrackedFlowDynamic(logicType: Class<out FlowLogic<T>>, vararg args: Any?): FlowProgressHandle<T>
|
||||||
|
|
||||||
/**
|
/** Returns Node's NodeInfo, assuming this will not change while the node is running. */
|
||||||
* Returns Node's identity, assuming this will not change while the node is running.
|
fun nodeInfo(): NodeInfo
|
||||||
*/
|
|
||||||
fun nodeIdentity(): NodeInfo
|
|
||||||
|
|
||||||
/*
|
/**
|
||||||
* Add note(s) to an existing Vault transaction
|
* Returns network's notary identities, assuming this will not change while the node is running.
|
||||||
|
*
|
||||||
|
* Note that the identities are sorted based on legal name, and the ordering might change once new notaries are introduced.
|
||||||
*/
|
*/
|
||||||
|
fun notaryIdentities(): List<Party>
|
||||||
|
|
||||||
|
/** Add note(s) to an existing Vault transaction. */
|
||||||
fun addVaultTransactionNote(txnId: SecureHash, txnNote: String)
|
fun addVaultTransactionNote(txnId: SecureHash, txnNote: String)
|
||||||
|
|
||||||
/*
|
/** Retrieve existing note(s) for a given Vault transaction. */
|
||||||
* Retrieve existing note(s) for a given Vault transaction
|
|
||||||
*/
|
|
||||||
fun getVaultTransactionNotes(txnId: SecureHash): Iterable<String>
|
fun getVaultTransactionNotes(txnId: SecureHash): Iterable<String>
|
||||||
|
|
||||||
/**
|
/** Checks whether an attachment with the given hash is stored on the node. */
|
||||||
* Checks whether an attachment with the given hash is stored on the node.
|
|
||||||
*/
|
|
||||||
fun attachmentExists(id: SecureHash): Boolean
|
fun attachmentExists(id: SecureHash): Boolean
|
||||||
|
|
||||||
/**
|
/** Download an attachment JAR by ID. */
|
||||||
* Download an attachment JAR by ID
|
|
||||||
*/
|
|
||||||
fun openAttachment(id: SecureHash): InputStream
|
fun openAttachment(id: SecureHash): InputStream
|
||||||
|
|
||||||
/**
|
/** Uploads a jar to the node, returns it's hash. */
|
||||||
* Uploads a jar to the node, returns it's hash.
|
|
||||||
*/
|
|
||||||
fun uploadAttachment(jar: InputStream): SecureHash
|
fun uploadAttachment(jar: InputStream): SecureHash
|
||||||
|
|
||||||
/**
|
/** Returns the node's current time. */
|
||||||
* Returns the node's current time.
|
|
||||||
*/
|
|
||||||
fun currentNodeTime(): Instant
|
fun currentNodeTime(): Instant
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -261,16 +249,12 @@ interface CordaRPCOps : RPCOps {
|
|||||||
* @param party identity to determine well known identity for.
|
* @param party identity to determine well known identity for.
|
||||||
* @return well known identity, if found.
|
* @return well known identity, if found.
|
||||||
*/
|
*/
|
||||||
fun partyFromAnonymous(party: AbstractParty): Party?
|
fun wellKnownPartyFromAnonymous(party: AbstractParty): Party?
|
||||||
/**
|
/** Returns the [Party] corresponding to the given key, if found. */
|
||||||
* Returns the [Party] corresponding to the given key, if found.
|
|
||||||
*/
|
|
||||||
fun partyFromKey(key: PublicKey): Party?
|
fun partyFromKey(key: PublicKey): Party?
|
||||||
|
|
||||||
/**
|
/** Returns the [Party] with the X.500 principal as it's [Party.name]. */
|
||||||
* Returns the [Party] with the X.500 principal as it's [Party.name]
|
fun wellKnownPartyFromX500Name(x500Name: CordaX500Name): Party?
|
||||||
*/
|
|
||||||
fun partyFromX500Name(x500Name: X500Name): Party?
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a list of candidate matches for a given string, with optional fuzzy(ish) matching. Fuzzy matching may
|
* Returns a list of candidate matches for a given string, with optional fuzzy(ish) matching. Fuzzy matching may
|
||||||
@ -286,15 +270,13 @@ interface CordaRPCOps : RPCOps {
|
|||||||
fun registeredFlows(): List<String>
|
fun registeredFlows(): List<String>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a node's identity from the network map cache, where known.
|
* Returns a node's info from the network map cache, where known.
|
||||||
*
|
*
|
||||||
* @return the node info if available.
|
* @return the node info if available.
|
||||||
*/
|
*/
|
||||||
fun nodeIdentityFromParty(party: AbstractParty): NodeInfo?
|
fun nodeInfoFromParty(party: AbstractParty): NodeInfo?
|
||||||
|
|
||||||
/**
|
/** Clear all network map data from local node cache. */
|
||||||
* Clear all network map data from local node cache.
|
|
||||||
*/
|
|
||||||
fun clearNetworkMapCache()
|
fun clearNetworkMapCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,15 +292,9 @@ inline fun <reified T : ContractState> CordaRPCOps.vaultTrackBy(criteria: QueryC
|
|||||||
return vaultTrackBy(criteria, paging, sorting, T::class.java)
|
return vaultTrackBy(criteria, paging, sorting, T::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Note that the passed in constructor function is only used for unification of other type parameters and reification of
|
||||||
* These allow type safe invocations of flows from Kotlin, e.g.:
|
// the Class instance of the flow. This could be changed to use the constructor function directly.
|
||||||
*
|
|
||||||
* val rpc: CordaRPCOps = (..)
|
|
||||||
* rpc.startFlow(::ResolveTransactionsFlow, setOf<SecureHash>(), aliceIdentity)
|
|
||||||
*
|
|
||||||
* Note that the passed in constructor function is only used for unification of other type parameters and reification of
|
|
||||||
* the Class instance of the flow. This could be changed to use the constructor function directly.
|
|
||||||
*/
|
|
||||||
inline fun <T, reified R : FlowLogic<T>> CordaRPCOps.startFlow(
|
inline fun <T, reified R : FlowLogic<T>> CordaRPCOps.startFlow(
|
||||||
@Suppress("UNUSED_PARAMETER")
|
@Suppress("UNUSED_PARAMETER")
|
||||||
flowConstructor: () -> R
|
flowConstructor: () -> R
|
||||||
@ -330,6 +306,12 @@ inline fun <T , A, reified R : FlowLogic<T>> CordaRPCOps.startFlow(
|
|||||||
arg0: A
|
arg0: A
|
||||||
): FlowHandle<T> = startFlowDynamic(R::class.java, arg0)
|
): FlowHandle<T> = startFlowDynamic(R::class.java, arg0)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extension function for type safe invocation of flows from Kotlin, for example:
|
||||||
|
*
|
||||||
|
* val rpc: CordaRPCOps = (..)
|
||||||
|
* rpc.startFlow(::ResolveTransactionsFlow, setOf<SecureHash>(), aliceIdentity)
|
||||||
|
*/
|
||||||
inline fun <T, A, B, reified R : FlowLogic<T>> CordaRPCOps.startFlow(
|
inline fun <T, A, B, reified R : FlowLogic<T>> CordaRPCOps.startFlow(
|
||||||
@Suppress("UNUSED_PARAMETER")
|
@Suppress("UNUSED_PARAMETER")
|
||||||
flowConstructor: (A, B) -> R,
|
flowConstructor: (A, B) -> R,
|
||||||
@ -376,7 +358,7 @@ inline fun <T, A, B, C, D, E, F, reified R : FlowLogic<T>> CordaRPCOps.startFlow
|
|||||||
): FlowHandle<T> = startFlowDynamic(R::class.java, arg0, arg1, arg2, arg3, arg4, arg5)
|
): FlowHandle<T> = startFlowDynamic(R::class.java, arg0, arg1, arg2, arg3, arg4, arg5)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Same again, except this time with progress-tracking enabled.
|
* Extension function for type safe invocation of flows from Kotlin, with progress tracking enabled.
|
||||||
*/
|
*/
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
inline fun <T, reified R : FlowLogic<T>> CordaRPCOps.startTrackedFlow(
|
inline fun <T, reified R : FlowLogic<T>> CordaRPCOps.startTrackedFlow(
|
||||||
|
@ -2,7 +2,6 @@ package net.corda.core.messaging
|
|||||||
|
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
|
||||||
|
|
||||||
/** The interface for a group of message recipients (which may contain only one recipient) */
|
/** The interface for a group of message recipients (which may contain only one recipient) */
|
||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
interface MessageRecipients
|
interface MessageRecipients
|
||||||
|
@ -22,13 +22,4 @@ abstract class CordaPluginRegistry {
|
|||||||
* @return true if you register types, otherwise you will be filtered out of the list of plugins considered in future.
|
* @return true if you register types, otherwise you will be filtered out of the list of plugins considered in future.
|
||||||
*/
|
*/
|
||||||
open fun customizeSerialization(custom: SerializationCustomization): Boolean = false
|
open fun customizeSerialization(custom: SerializationCustomization): Boolean = false
|
||||||
|
|
||||||
/**
|
|
||||||
* Optionally, custom schemas to be used for contract state persistence and vault custom querying
|
|
||||||
*
|
|
||||||
* For example, if you implement the [QueryableState] interface on a new [ContractState]
|
|
||||||
* it needs to be registered here if you wish to perform custom queries on schema entity attributes using the
|
|
||||||
* [VaultQueryService] API
|
|
||||||
*/
|
|
||||||
open val requiredSchemas: Set<MappedSchema> get() = emptySet()
|
|
||||||
}
|
}
|
@ -2,49 +2,29 @@ package net.corda.core.node
|
|||||||
|
|
||||||
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.node.services.ServiceInfo
|
|
||||||
import net.corda.core.node.services.ServiceType
|
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
import net.corda.core.utilities.locality
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Information for an advertised service including the service specific identity information.
|
|
||||||
* The identity can be used in flows and is distinct from the Node's legalIdentity
|
|
||||||
*/
|
|
||||||
@CordaSerializable
|
|
||||||
data class ServiceEntry(val info: ServiceInfo, val identity: PartyAndCertificate)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Info about a network node that acts on behalf of some form of contract party.
|
* Info about a network node that acts on behalf of some form of contract party.
|
||||||
|
* @param legalIdentitiesAndCerts is a non-empty list, where the first identity is assumed to be the default identity of the node.
|
||||||
*/
|
*/
|
||||||
// TODO We currently don't support multi-IP/multi-identity nodes, we only left slots in the data structures.
|
// TODO We currently don't support multi-IP/multi-identity nodes, we only left slots in the data structures.
|
||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
data class NodeInfo(val addresses: List<NetworkHostAndPort>,
|
data class NodeInfo(val addresses: List<NetworkHostAndPort>,
|
||||||
// TODO After removing of services these two fields will be merged together and made NonEmptySet.
|
val legalIdentitiesAndCerts: List<PartyAndCertificate>,
|
||||||
val legalIdentityAndCert: PartyAndCertificate,
|
|
||||||
val legalIdentitiesAndCerts: Set<PartyAndCertificate>,
|
|
||||||
val platformVersion: Int,
|
val platformVersion: Int,
|
||||||
val advertisedServices: List<ServiceEntry> = emptyList(),
|
|
||||||
val serial: Long
|
val serial: Long
|
||||||
) {
|
) {
|
||||||
init {
|
init {
|
||||||
require(advertisedServices.none { it.identity == legalIdentityAndCert }) {
|
require(legalIdentitiesAndCerts.isNotEmpty()) { "Node should have at least one legal identity" }
|
||||||
"Service identities must be different from node legal identity"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val legalIdentity: Party get() = legalIdentityAndCert.party
|
@Transient private var _legalIdentities: List<Party>? = null
|
||||||
val notaryIdentity: Party get() = advertisedServices.single { it.info.type.isNotary() }.identity.party
|
val legalIdentities: List<Party> get() {
|
||||||
fun serviceIdentities(type: ServiceType): List<Party> {
|
return _legalIdentities ?: legalIdentitiesAndCerts.map { it.party }.also { _legalIdentities = it }
|
||||||
return advertisedServices.mapNotNull { if (it.info.type.isSubTypeOf(type)) it.identity.party else null }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Returns true if [party] is one of the identities of this node, else false. */
|
||||||
* Uses node's owner X500 name to infer the node's location. Used in Explorer in map view.
|
fun isLegalIdentity(party: Party): Boolean = party in legalIdentities
|
||||||
*/
|
|
||||||
fun getWorldMapLocation(): WorldMapLocation? {
|
|
||||||
val nodeOwnerLocation = legalIdentity.name.locality
|
|
||||||
return nodeOwnerLocation.let { CityDatabase[it] }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
package net.corda.core.node
|
|
||||||
|
|
||||||
import net.corda.core.flows.FlowLogic
|
|
||||||
import net.corda.core.identity.Party
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A service hub to be used by the [CordaPluginRegistry]
|
|
||||||
*/
|
|
||||||
interface PluginServiceHub : ServiceHub {
|
|
||||||
@Deprecated("This is no longer used. Instead annotate the flows produced by your factory with @InitiatedBy and have " +
|
|
||||||
"them point to the initiating flow class.", level = DeprecationLevel.ERROR)
|
|
||||||
fun registerFlowInitiator(initiatingFlowClass: Class<out FlowLogic<*>>, serviceFlowFactory: (Party) -> FlowLogic<*>) = Unit
|
|
||||||
}
|
|
@ -1,10 +1,15 @@
|
|||||||
package net.corda.core.node
|
package net.corda.core.node
|
||||||
|
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
|
import net.corda.core.cordapp.CordappProvider
|
||||||
import net.corda.core.crypto.Crypto
|
import net.corda.core.crypto.Crypto
|
||||||
import net.corda.core.crypto.SignableData
|
import net.corda.core.crypto.SignableData
|
||||||
import net.corda.core.crypto.SignatureMetadata
|
import net.corda.core.crypto.SignatureMetadata
|
||||||
import net.corda.core.crypto.TransactionSignature
|
import net.corda.core.crypto.TransactionSignature
|
||||||
|
import net.corda.core.identity.AbstractParty
|
||||||
|
import net.corda.core.identity.AnonymousParty
|
||||||
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.internal.toMultiMap
|
||||||
import net.corda.core.node.services.*
|
import net.corda.core.node.services.*
|
||||||
import net.corda.core.serialization.SerializeAsToken
|
import net.corda.core.serialization.SerializeAsToken
|
||||||
import net.corda.core.transactions.FilteredTransaction
|
import net.corda.core.transactions.FilteredTransaction
|
||||||
@ -26,6 +31,9 @@ interface ServicesForResolution {
|
|||||||
/** Provides access to storage of arbitrary JAR files (which may contain only data, no code). */
|
/** Provides access to storage of arbitrary JAR files (which may contain only data, no code). */
|
||||||
val attachments: AttachmentStorage
|
val attachments: AttachmentStorage
|
||||||
|
|
||||||
|
/** Provides access to anything relating to cordapps including contract attachment resolution and app context */
|
||||||
|
val cordappProvider: CordappProvider
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a [StateRef] loads the referenced transaction and looks up the specified output [ContractState].
|
* Given a [StateRef] loads the referenced transaction and looks up the specified output [ContractState].
|
||||||
*
|
*
|
||||||
@ -123,38 +131,17 @@ interface ServiceHub : ServicesForResolution {
|
|||||||
fun <T : ContractState> toStateAndRef(stateRef: StateRef): StateAndRef<T> {
|
fun <T : ContractState> toStateAndRef(stateRef: StateRef): StateAndRef<T> {
|
||||||
val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash)
|
val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash)
|
||||||
return if (stx.isNotaryChangeTransaction()) {
|
return if (stx.isNotaryChangeTransaction()) {
|
||||||
stx.resolveNotaryChangeTransaction(this).outRef<T>(stateRef.index)
|
stx.resolveNotaryChangeTransaction(this).outRef(stateRef.index)
|
||||||
} else {
|
} else {
|
||||||
stx.tx.outRef<T>(stateRef.index)
|
stx.tx.outRef(stateRef.index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private val legalIdentityKey: PublicKey get() = this.myInfo.legalIdentitiesAndCerts.first().owningKey
|
||||||
* Helper property to shorten code for fetching the the [PublicKey] portion of the
|
|
||||||
* Node's primary signing identity.
|
|
||||||
* Typical use is during signing in flows and for unit test signing.
|
|
||||||
* When this [PublicKey] is passed into the signing methods below, or on the KeyManagementService
|
|
||||||
* the matching [java.security.PrivateKey] will be looked up internally and used to sign.
|
|
||||||
* If the key is actually a CompositeKey, the first leaf key hosted on this node
|
|
||||||
* will be used to create the signature.
|
|
||||||
*/
|
|
||||||
val legalIdentityKey: PublicKey get() = this.myInfo.legalIdentity.owningKey
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper property to shorten code for fetching the the [PublicKey] portion of the
|
|
||||||
* Node's Notary signing identity. It is required that the Node hosts a notary service,
|
|
||||||
* otherwise an [IllegalArgumentException] will be thrown.
|
|
||||||
* Typical use is during signing in flows and for unit test signing.
|
|
||||||
* When this [PublicKey] is passed into the signing methods below, or on the KeyManagementService
|
|
||||||
* the matching [java.security.PrivateKey] will be looked up internally and used to sign.
|
|
||||||
* If the key is actually a [net.corda.core.crypto.CompositeKey], the first leaf key hosted on this node
|
|
||||||
* will be used to create the signature.
|
|
||||||
*/
|
|
||||||
val notaryIdentityKey: PublicKey get() = this.myInfo.notaryIdentity.owningKey
|
|
||||||
|
|
||||||
// Helper method to construct an initial partially signed transaction from a [TransactionBuilder].
|
// Helper method to construct an initial partially signed transaction from a [TransactionBuilder].
|
||||||
private fun signInitialTransaction(builder: TransactionBuilder, publicKey: PublicKey, signatureMetadata: SignatureMetadata): SignedTransaction {
|
private fun signInitialTransaction(builder: TransactionBuilder, publicKey: PublicKey, signatureMetadata: SignatureMetadata): SignedTransaction {
|
||||||
return builder.toSignedTransaction(keyManagementService, publicKey, signatureMetadata)
|
return builder.toSignedTransaction(keyManagementService, publicKey, signatureMetadata, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -6,6 +6,8 @@ import java.io.IOException
|
|||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.nio.file.FileAlreadyExistsException
|
import java.nio.file.FileAlreadyExistsException
|
||||||
|
|
||||||
|
typealias AttachmentId = SecureHash
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An attachment store records potentially large binary objects, identified by their hash.
|
* An attachment store records potentially large binary objects, identified by their hash.
|
||||||
*/
|
*/
|
||||||
@ -14,7 +16,7 @@ interface AttachmentStorage {
|
|||||||
* Returns a handle to a locally stored attachment, or null if it's not known. The handle can be used to open
|
* Returns a handle to a locally stored attachment, or null if it's not known. The handle can be used to open
|
||||||
* a stream for the data, which will be a zip/jar file.
|
* a stream for the data, which will be a zip/jar file.
|
||||||
*/
|
*/
|
||||||
fun openAttachment(id: SecureHash): Attachment?
|
fun openAttachment(id: AttachmentId): Attachment?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts the given attachment into the store, does *not* close the input stream. This can be an intensive
|
* Inserts the given attachment into the store, does *not* close the input stream. This can be an intensive
|
||||||
@ -28,6 +30,6 @@ interface AttachmentStorage {
|
|||||||
* @throws IOException if something went wrong.
|
* @throws IOException if something went wrong.
|
||||||
*/
|
*/
|
||||||
@Throws(FileAlreadyExistsException::class, IOException::class)
|
@Throws(FileAlreadyExistsException::class, IOException::class)
|
||||||
fun importAttachment(jar: InputStream): SecureHash
|
fun importAttachment(jar: InputStream): AttachmentId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
package net.corda.core.node.services
|
package net.corda.core.node.services
|
||||||
|
|
||||||
|
import net.corda.core.node.ServiceHub
|
||||||
|
import net.corda.core.serialization.SerializeAsToken
|
||||||
|
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||||
import kotlin.annotation.AnnotationTarget.CLASS
|
import kotlin.annotation.AnnotationTarget.CLASS
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Annotate any class that needs to be a long-lived service within the node, such as an oracle, with this annotation.
|
* Annotate any class that needs to be a long-lived service within the node, such as an oracle, with this annotation.
|
||||||
* Such a class needs to have a constructor with a single parameter of type [net.corda.core.node.PluginServiceHub]. This
|
* Such a class needs to have a constructor with a single parameter of type [ServiceHub]. This constructor will be invoked
|
||||||
* construtor will be invoked during node start to initialise the service. The service hub provided can be used to get
|
* during node start to initialise the service. The service hub provided can be used to get information about the node
|
||||||
* information about the node that may be necessary for the service. Corda services are created as singletons within
|
* that may be necessary for the service. Corda services are created as singletons within the node and are available
|
||||||
* the node and are available to flows via [net.corda.core.node.ServiceHub.cordaService].
|
* to flows via [ServiceHub.cordaService].
|
||||||
*
|
*
|
||||||
* The service class has to implement [net.corda.core.serialization.SerializeAsToken] to ensure correct usage within flows.
|
* The service class has to implement [SerializeAsToken] to ensure correct usage within flows. (If possible extend
|
||||||
* (If possible extend [net.corda.core.serialization.SingletonSerializeAsToken] instead as it removes the boilerplate.)
|
* [SingletonSerializeAsToken] instead as it removes the boilerplate.)
|
||||||
*
|
|
||||||
* The annotated class should expose its [ServiceType] via a public static field named `type`, so that the service is
|
|
||||||
* only loaded in nodes that declare the type in their advertisedServices.
|
|
||||||
*/
|
*/
|
||||||
// TODO Handle the singleton serialisation of Corda services automatically, removing the need to implement SerializeAsToken
|
// TODO Handle the singleton serialisation of Corda services automatically, removing the need to implement SerializeAsToken
|
||||||
// TODO Perhaps this should be an interface or abstract class due to the need for it to implement SerializeAsToken and
|
// TODO Perhaps this should be an interface or abstract class due to the need for it to implement SerializeAsToken and
|
||||||
|
@ -1,12 +1,7 @@
|
|||||||
package net.corda.core.node.services
|
package net.corda.core.node.services
|
||||||
|
|
||||||
import net.corda.core.contracts.PartyAndReference
|
import net.corda.core.contracts.PartyAndReference
|
||||||
import net.corda.core.identity.AbstractParty
|
import net.corda.core.identity.*
|
||||||
import net.corda.core.identity.AnonymousParty
|
|
||||||
import net.corda.core.identity.Party
|
|
||||||
import net.corda.core.identity.PartyAndCertificate
|
|
||||||
import org.bouncycastle.asn1.x500.X500Name
|
|
||||||
import org.bouncycastle.cert.X509CertificateHolder
|
|
||||||
import java.security.InvalidAlgorithmParameterException
|
import java.security.InvalidAlgorithmParameterException
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.security.cert.*
|
import java.security.cert.*
|
||||||
@ -18,20 +13,9 @@ import java.security.cert.*
|
|||||||
*/
|
*/
|
||||||
interface IdentityService {
|
interface IdentityService {
|
||||||
val trustRoot: X509Certificate
|
val trustRoot: X509Certificate
|
||||||
val trustRootHolder: X509CertificateHolder
|
|
||||||
val trustAnchor: TrustAnchor
|
val trustAnchor: TrustAnchor
|
||||||
val caCertStore: CertStore
|
val caCertStore: CertStore
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify and then store an identity.
|
|
||||||
*
|
|
||||||
* @param party a party representing a legal entity and the certificate path linking them to the network trust root.
|
|
||||||
* @throws IllegalArgumentException if the certificate path is invalid.
|
|
||||||
*/
|
|
||||||
@Throws(CertificateExpiredException::class, CertificateNotYetValidException::class, InvalidAlgorithmParameterException::class)
|
|
||||||
@Deprecated("Use verifyAndRegisterIdentity() instead, which is the same function with a better name")
|
|
||||||
fun registerIdentity(party: PartyAndCertificate)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify and then store an identity.
|
* Verify and then store an identity.
|
||||||
*
|
*
|
||||||
@ -58,46 +42,48 @@ interface IdentityService {
|
|||||||
fun getAllIdentities(): Iterable<PartyAndCertificate>
|
fun getAllIdentities(): Iterable<PartyAndCertificate>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the certificate and path for a well known identity's owning key.
|
* Get the certificate and path for a known identity's owning key.
|
||||||
*
|
*
|
||||||
* @return the party and certificate, or null if unknown.
|
* @return the party and certificate, or null if unknown.
|
||||||
*/
|
*/
|
||||||
fun certificateFromKey(owningKey: PublicKey): PartyAndCertificate?
|
fun certificateFromKey(owningKey: PublicKey): PartyAndCertificate?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the certificate and path for a well known identity.
|
* Converts an owning [PublicKey] to the X500Name extended [Party] object if the [Party] has been
|
||||||
*
|
* previously registered with the [IdentityService] either as a well known network map identity,
|
||||||
* @return the party and certificate.
|
* or as a part of flows creating and exchanging the identity.
|
||||||
* @throws IllegalArgumentException if the certificate and path are unknown. This should never happen for a well
|
* @param key The owning [PublicKey] of the [Party].
|
||||||
* known identity.
|
* @return Returns a [Party] with a matching owningKey if known, else returns null.
|
||||||
*/
|
*/
|
||||||
fun certificateFromParty(party: Party): PartyAndCertificate
|
|
||||||
|
|
||||||
// There is no method for removing identities, as once we are made aware of a Party we want to keep track of them
|
|
||||||
// indefinitely. It may be that in the long term we need to drop or archive very old Party information for space,
|
|
||||||
// but for now this is not supported.
|
|
||||||
|
|
||||||
fun partyFromKey(key: PublicKey): Party?
|
fun partyFromKey(key: PublicKey): Party?
|
||||||
|
|
||||||
fun partyFromX500Name(principal: X500Name): Party?
|
/**
|
||||||
|
* Resolves a party name to the well known identity [Party] instance for this name.
|
||||||
|
* @param name The [CordaX500Name] to search for.
|
||||||
|
* @return If known the canonical [Party] with that name, else null.
|
||||||
|
*/
|
||||||
|
fun wellKnownPartyFromX500Name(name: CordaX500Name): Party?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the well known identity from an abstract party. This is intended to resolve the well known identity
|
* Returns the well known identity from an [AbstractParty]. This is intended to resolve the well known identity,
|
||||||
* from a confidential identity, however it transparently handles returning the well known identity back if
|
* as visible in the [NetworkMapCache] from a confidential identity.
|
||||||
|
* It transparently handles returning the well known identity back if
|
||||||
* a well known identity is passed in.
|
* a well known identity is passed in.
|
||||||
*
|
*
|
||||||
* @param party identity to determine well known identity for.
|
* @param party identity to determine well known identity for.
|
||||||
* @return well known identity, if found.
|
* @return well known identity, if found.
|
||||||
*/
|
*/
|
||||||
fun partyFromAnonymous(party: AbstractParty): Party?
|
fun wellKnownPartyFromAnonymous(party: AbstractParty): Party?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the well known identity of a party. If the party passed in is already a well known identity
|
* Returns the well known identity from a PartyAndReference. This is intended to resolve the well known identity,
|
||||||
* (i.e. a [Party]) this returns it as-is.
|
* as visible in the [NetworkMapCache] from a confidential identity.
|
||||||
|
* It transparently handles returning the well known identity back if
|
||||||
|
* a well known identity is passed in.
|
||||||
*
|
*
|
||||||
* @return the well known identity, or null if unknown.
|
* @return the well known identity, or null if unknown.
|
||||||
*/
|
*/
|
||||||
fun partyFromAnonymous(partyRef: PartyAndReference) = partyFromAnonymous(partyRef.party)
|
fun wellKnownPartyFromAnonymous(partyRef: PartyAndReference) = wellKnownPartyFromAnonymous(partyRef.party)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the well known identity of a party. Throws an exception if the party cannot be identified.
|
* Resolve the well known identity of a party. Throws an exception if the party cannot be identified.
|
||||||
@ -106,7 +92,7 @@ interface IdentityService {
|
|||||||
* @return the well known identity.
|
* @return the well known identity.
|
||||||
* @throws IllegalArgumentException
|
* @throws IllegalArgumentException
|
||||||
*/
|
*/
|
||||||
fun requirePartyFromAnonymous(party: AbstractParty): Party
|
fun requireWellKnownPartyFromAnonymous(party: AbstractParty): Party
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a list of candidate matches for a given string, with optional fuzzy(ish) matching. Fuzzy matching may
|
* Returns a list of candidate matches for a given string, with optional fuzzy(ish) matching. Fuzzy matching may
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
package net.corda.core.node.services
|
package net.corda.core.node.services
|
||||||
|
|
||||||
import net.corda.core.concurrent.CordaFuture
|
import net.corda.core.concurrent.CordaFuture
|
||||||
import net.corda.core.contracts.Contract
|
|
||||||
import net.corda.core.identity.AbstractParty
|
import net.corda.core.identity.AbstractParty
|
||||||
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.randomOrNull
|
|
||||||
import net.corda.core.messaging.DataFeed
|
import net.corda.core.messaging.DataFeed
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
import org.bouncycastle.asn1.x500.X500Name
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
|
||||||
@ -30,19 +28,14 @@ interface NetworkMapCache {
|
|||||||
data class Modified(override val node: NodeInfo, val previousNode: NodeInfo) : MapChange()
|
data class Modified(override val node: NodeInfo, val previousNode: NodeInfo) : MapChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A list of all nodes the cache is aware of */
|
|
||||||
val partyNodes: List<NodeInfo>
|
|
||||||
/** A list of nodes that advertise a network map service */
|
|
||||||
val networkMapNodes: List<NodeInfo>
|
|
||||||
/** A list of nodes that advertise a notary service */
|
|
||||||
val notaryNodes: List<NodeInfo> get() = getNodesWithService(ServiceType.notary)
|
|
||||||
/**
|
/**
|
||||||
* A list of nodes that advertise a regulatory service. Identifying the correct regulator for a trade is outside
|
* A list of notary services available on the network.
|
||||||
* the scope of the network map service, and this is intended solely as a sanity check on configuration stored
|
*
|
||||||
* elsewhere.
|
* Note that the identities are sorted based on legal name, and the ordering might change once new notaries are introduced.
|
||||||
*/
|
*/
|
||||||
val regulatorNodes: List<NodeInfo> get() = getNodesWithService(ServiceType.regulator)
|
// TODO this list will be taken from NetworkParameters distributed by NetworkMap.
|
||||||
/** Tracks changes to the network map cache */
|
val notaryIdentities: List<Party>
|
||||||
|
/** Tracks changes to the network map cache. */
|
||||||
val changed: Observable<MapChange>
|
val changed: Observable<MapChange>
|
||||||
/** Future to track completion of the NetworkMapService registration. */
|
/** Future to track completion of the NetworkMapService registration. */
|
||||||
val nodeReady: CordaFuture<Void?>
|
val nodeReady: CordaFuture<Void?>
|
||||||
@ -53,18 +46,6 @@ interface NetworkMapCache {
|
|||||||
*/
|
*/
|
||||||
fun track(): DataFeed<List<NodeInfo>, MapChange>
|
fun track(): DataFeed<List<NodeInfo>, MapChange>
|
||||||
|
|
||||||
/** Get the collection of nodes which advertise a specific service. */
|
|
||||||
fun getNodesWithService(serviceType: ServiceType): List<NodeInfo> {
|
|
||||||
return partyNodes.filter { it.advertisedServices.any { it.info.type.isSubTypeOf(serviceType) } }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a recommended node that advertises a service, and is suitable for the specified contract and parties.
|
|
||||||
* Implementations might understand, for example, the correct regulator to use for specific contracts/parties,
|
|
||||||
* or the appropriate oracle for a contract.
|
|
||||||
*/
|
|
||||||
fun getRecommended(type: ServiceType, contract: Contract, vararg party: Party): NodeInfo? = getNodesWithService(type).firstOrNull()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Look up the node info for a specific party. Will attempt to de-anonymise the party if applicable; if the party
|
* Look up the node info for a specific party. Will attempt to de-anonymise the party if applicable; if the party
|
||||||
* is anonymised and the well known party cannot be resolved, it is impossible ot identify the node and therefore this
|
* is anonymised and the well known party cannot be resolved, it is impossible ot identify the node and therefore this
|
||||||
@ -77,80 +58,41 @@ interface NetworkMapCache {
|
|||||||
fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo?
|
fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo?
|
||||||
|
|
||||||
/** Look up the node info for a legal name. */
|
/** Look up the node info for a legal name. */
|
||||||
fun getNodeByLegalName(principal: X500Name): NodeInfo?
|
fun getNodeByLegalName(name: CordaX500Name): NodeInfo?
|
||||||
|
|
||||||
/** Look up the node info for a host and port. */
|
/** Look up the node info for a host and port. */
|
||||||
fun getNodeByAddress(address: NetworkHostAndPort): NodeInfo?
|
fun getNodeByAddress(address: NetworkHostAndPort): NodeInfo?
|
||||||
|
|
||||||
|
fun getPeerByLegalName(name: CordaX500Name): Party? = getNodeByLegalName(name)?.let {
|
||||||
|
it.legalIdentitiesAndCerts.singleOrNull { it.name == name }?.party
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Return all [NodeInfo]s the node currently is aware of (including ourselves). */
|
||||||
|
val allNodes: List<NodeInfo>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Look up the node infos for a specific peer key.
|
||||||
* In general, nodes can advertise multiple identities: a legal identity, and separate identities for each of
|
* In general, nodes can advertise multiple identities: a legal identity, and separate identities for each of
|
||||||
* the services it provides. In case of a distributed service – run by multiple nodes – each participant advertises
|
* the services it provides. In case of a distributed service – run by multiple nodes – each participant advertises
|
||||||
* the identity of the *whole group*.
|
* the identity of the *whole group*.
|
||||||
*/
|
*/
|
||||||
|
fun getNodesByLegalIdentityKey(identityKey: PublicKey): List<NodeInfo>
|
||||||
/** Look up the node info for a specific peer key. */
|
|
||||||
fun getNodeByLegalIdentityKey(identityKey: PublicKey): NodeInfo?
|
|
||||||
|
|
||||||
/** Look up all nodes advertising the service owned by [publicKey] */
|
|
||||||
fun getNodesByAdvertisedServiceIdentityKey(publicKey: PublicKey): List<NodeInfo> {
|
|
||||||
return partyNodes.filter { it.advertisedServices.any { it.identity.owningKey == publicKey } }
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns information about the party, which may be a specific node or a service */
|
/** Returns information about the party, which may be a specific node or a service */
|
||||||
fun getPartyInfo(party: Party): PartyInfo?
|
fun getPartyInfo(party: Party): PartyInfo?
|
||||||
|
|
||||||
/** Gets a notary identity by the given name. */
|
/** Gets a notary identity by the given name. */
|
||||||
fun getNotary(principal: X500Name): Party? {
|
fun getNotary(name: CordaX500Name): Party? = notaryIdentities.firstOrNull { it.name == name }
|
||||||
val notaryNode = notaryNodes.filter {
|
|
||||||
it.advertisedServices.any { it.info.type.isSubTypeOf(ServiceType.notary) && it.info.name == principal }
|
|
||||||
}.randomOrNull()
|
|
||||||
return notaryNode?.notaryIdentity
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/** Checks whether a given party is an advertised notary identity. */
|
||||||
* Returns a notary identity advertised by any of the nodes on the network (chosen at random)
|
fun isNotary(party: Party): Boolean = party in notaryIdentities
|
||||||
* @param type Limits the result to notaries of the specified type (optional)
|
|
||||||
*/
|
|
||||||
fun getAnyNotary(type: ServiceType? = null): Party? {
|
|
||||||
val nodes = if (type == null) {
|
|
||||||
notaryNodes
|
|
||||||
} else {
|
|
||||||
require(type != ServiceType.notary && type.isSubTypeOf(ServiceType.notary)) {
|
|
||||||
"The provided type must be a specific notary sub-type"
|
|
||||||
}
|
|
||||||
notaryNodes.filter { it.advertisedServices.any { it.info.type == type } }
|
|
||||||
}
|
|
||||||
return nodes.randomOrNull()?.notaryIdentity
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/** Checks whether a given party is an validating notary identity. */
|
||||||
* Returns a service identity advertised by one of the nodes on the network
|
|
||||||
* @param type Specifies the type of the service
|
|
||||||
*/
|
|
||||||
fun getAnyServiceOfType(type: ServiceType): Party? {
|
|
||||||
for (node in partyNodes) {
|
|
||||||
val serviceIdentities = node.serviceIdentities(type)
|
|
||||||
if (serviceIdentities.isNotEmpty()) {
|
|
||||||
return serviceIdentities.randomOrNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Checks whether a given party is an advertised notary identity */
|
|
||||||
fun isNotary(party: Party): Boolean = notaryNodes.any { it.notaryIdentity == party }
|
|
||||||
|
|
||||||
/** Checks whether a given party is an advertised validating notary identity */
|
|
||||||
fun isValidatingNotary(party: Party): Boolean {
|
fun isValidatingNotary(party: Party): Boolean {
|
||||||
val notary = notaryNodes.firstOrNull { it.notaryIdentity == party }
|
require(isNotary(party)) { "No notary found with identity $party." }
|
||||||
?: throw IllegalArgumentException("No notary found with identity $party. This is most likely caused " +
|
return !party.name.toString().contains("corda.notary.simple", true) // TODO This implementation will change after introducing of NetworkParameters.
|
||||||
"by using the notary node's legal identity instead of its advertised notary identity. " +
|
|
||||||
"Your options are: ${notaryNodes.map { "\"${it.notaryIdentity.name}\"" }.joinToString()}.")
|
|
||||||
return notary.advertisedServices.any { it.info.type.isValidatingNotary() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Clear all network map data from local node cache. */
|
||||||
* Clear all network map data from local node cache.
|
|
||||||
*/
|
|
||||||
fun clearNetworkMapCache()
|
fun clearNetworkMapCache()
|
||||||
}
|
}
|
||||||
|
@ -3,28 +3,27 @@ package net.corda.core.node.services
|
|||||||
import net.corda.core.contracts.StateRef
|
import net.corda.core.contracts.StateRef
|
||||||
import net.corda.core.contracts.TimeWindow
|
import net.corda.core.contracts.TimeWindow
|
||||||
import net.corda.core.crypto.*
|
import net.corda.core.crypto.*
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.flows.NotaryError
|
|
||||||
import net.corda.core.flows.NotaryException
|
|
||||||
import net.corda.core.flows.NotaryFlow
|
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.node.ServiceHub
|
import net.corda.core.node.ServiceHub
|
||||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.core.utilities.loggerFor
|
import net.corda.core.utilities.loggerFor
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
|
import java.security.PublicKey
|
||||||
|
|
||||||
abstract class NotaryService : SingletonSerializeAsToken() {
|
abstract class NotaryService : SingletonSerializeAsToken() {
|
||||||
abstract val services: ServiceHub
|
abstract val services: ServiceHub
|
||||||
|
abstract val notaryIdentityKey: PublicKey
|
||||||
|
|
||||||
abstract fun start()
|
abstract fun start()
|
||||||
abstract fun stop()
|
abstract fun stop()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Produces a notary service flow which has the corresponding sends and receives as [NotaryFlow.Client].
|
* Produces a notary service flow which has the corresponding sends and receives as [NotaryFlow.Client].
|
||||||
* @param otherParty client [Party] making the request
|
* @param otherPartySession client [Party] making the request
|
||||||
*/
|
*/
|
||||||
abstract fun createServiceFlow(otherParty: Party): FlowLogic<Void?>
|
abstract fun createServiceFlow(otherPartySession: FlowSession): FlowLogic<Void?>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -70,11 +69,11 @@ abstract class TrustedAuthorityNotaryService : NotaryService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun sign(bits: ByteArray): DigitalSignature.WithKey {
|
fun sign(bits: ByteArray): DigitalSignature.WithKey {
|
||||||
return services.keyManagementService.sign(bits, services.notaryIdentityKey)
|
return services.keyManagementService.sign(bits, notaryIdentityKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sign(txId: SecureHash): TransactionSignature {
|
fun sign(txId: SecureHash): TransactionSignature {
|
||||||
val signableData = SignableData(txId, SignatureMetadata(services.myInfo.platformVersion, Crypto.findSignatureScheme(services.notaryIdentityKey).schemeNumberID))
|
val signableData = SignableData(txId, SignatureMetadata(services.myInfo.platformVersion, Crypto.findSignatureScheme(notaryIdentityKey).schemeNumberID))
|
||||||
return services.keyManagementService.sign(signableData, services.notaryIdentityKey)
|
return services.keyManagementService.sign(signableData, notaryIdentityKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,21 +1,13 @@
|
|||||||
package net.corda.core.node.services
|
package net.corda.core.node.services
|
||||||
|
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.identity.PartyAndCertificate
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
import net.corda.core.node.NodeInfo
|
|
||||||
import net.corda.core.node.ServiceEntry
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds information about a [Party], which may refer to either a specific node or a service.
|
* Holds information about a [Party], which may refer to either a specific node or a service.
|
||||||
*/
|
*/
|
||||||
sealed class PartyInfo {
|
sealed class PartyInfo {
|
||||||
abstract val party: PartyAndCertificate
|
abstract val party: Party
|
||||||
|
data class SingleNode(override val party: Party, val addresses: List<NetworkHostAndPort>): PartyInfo()
|
||||||
data class Node(val node: NodeInfo) : PartyInfo() {
|
data class DistributedNode(override val party: Party): PartyInfo()
|
||||||
override val party get() = node.legalIdentityAndCert
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Service(val service: ServiceEntry) : PartyInfo() {
|
|
||||||
override val party get() = service.identity
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,5 +1,6 @@
|
|||||||
package net.corda.core.node.services
|
package net.corda.core.node.services
|
||||||
|
|
||||||
|
import net.corda.core.CordaException
|
||||||
import net.corda.core.contracts.StateRef
|
import net.corda.core.contracts.StateRef
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
@ -32,5 +33,4 @@ interface UniquenessProvider {
|
|||||||
data class ConsumingTx(val id: SecureHash, val inputIndex: Int, val requestingParty: Party)
|
data class ConsumingTx(val id: SecureHash, val inputIndex: Int, val requestingParty: Party)
|
||||||
}
|
}
|
||||||
|
|
||||||
@CordaSerializable
|
class UniquenessException(val error: UniquenessProvider.Conflict) : CordaException(UniquenessException::class.java.name)
|
||||||
class UniquenessException(val error: UniquenessProvider.Conflict) : Exception()
|
|
@ -33,7 +33,7 @@ interface VaultQueryService {
|
|||||||
fun <T : ContractState> _queryBy(criteria: QueryCriteria,
|
fun <T : ContractState> _queryBy(criteria: QueryCriteria,
|
||||||
paging: PageSpecification,
|
paging: PageSpecification,
|
||||||
sorting: Sort,
|
sorting: Sort,
|
||||||
contractType: Class<out T>): Vault.Page<T>
|
contractStateType: Class<out T>): Vault.Page<T>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic vault query function which takes a [QueryCriteria] object to define filters,
|
* Generic vault query function which takes a [QueryCriteria] object to define filters,
|
||||||
@ -51,49 +51,49 @@ interface VaultQueryService {
|
|||||||
fun <T : ContractState> _trackBy(criteria: QueryCriteria,
|
fun <T : ContractState> _trackBy(criteria: QueryCriteria,
|
||||||
paging: PageSpecification,
|
paging: PageSpecification,
|
||||||
sorting: Sort,
|
sorting: Sort,
|
||||||
contractType: Class<out T>): DataFeed<Vault.Page<T>, Vault.Update<T>>
|
contractStateType: Class<out T>): DataFeed<Vault.Page<T>, Vault.Update<T>>
|
||||||
// DOCEND VaultQueryAPI
|
// DOCEND VaultQueryAPI
|
||||||
|
|
||||||
// Note: cannot apply @JvmOverloads to interfaces nor interface implementations
|
// Note: cannot apply @JvmOverloads to interfaces nor interface implementations
|
||||||
// Java Helpers
|
// Java Helpers
|
||||||
fun <T : ContractState> queryBy(contractType: Class<out T>): Vault.Page<T> {
|
fun <T : ContractState> queryBy(contractStateType: Class<out T>): Vault.Page<T> {
|
||||||
return _queryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractType)
|
return _queryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractStateType)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T : ContractState> queryBy(contractType: Class<out T>, criteria: QueryCriteria): Vault.Page<T> {
|
fun <T : ContractState> queryBy(contractStateType: Class<out T>, criteria: QueryCriteria): Vault.Page<T> {
|
||||||
return _queryBy(criteria, PageSpecification(), Sort(emptySet()), contractType)
|
return _queryBy(criteria, PageSpecification(), Sort(emptySet()), contractStateType)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T : ContractState> queryBy(contractType: 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()), contractType)
|
return _queryBy(criteria, paging, Sort(emptySet()), contractStateType)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T : ContractState> queryBy(contractType: Class<out T>, criteria: QueryCriteria, sorting: Sort): Vault.Page<T> {
|
fun <T : ContractState> queryBy(contractStateType: Class<out T>, criteria: QueryCriteria, sorting: Sort): Vault.Page<T> {
|
||||||
return _queryBy(criteria, PageSpecification(), sorting, contractType)
|
return _queryBy(criteria, PageSpecification(), sorting, contractStateType)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T : ContractState> queryBy(contractType: Class<out T>, criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.Page<T> {
|
fun <T : ContractState> queryBy(contractStateType: Class<out T>, criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.Page<T> {
|
||||||
return _queryBy(criteria, paging, sorting, contractType)
|
return _queryBy(criteria, paging, sorting, contractStateType)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T : ContractState> trackBy(contractType: Class<out T>): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
fun <T : ContractState> trackBy(contractStateType: Class<out T>): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
||||||
return _trackBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractType)
|
return _trackBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractStateType)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T : ContractState> trackBy(contractType: Class<out T>, criteria: QueryCriteria): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
fun <T : ContractState> trackBy(contractStateType: Class<out T>, criteria: QueryCriteria): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
||||||
return _trackBy(criteria, PageSpecification(), Sort(emptySet()), contractType)
|
return _trackBy(criteria, PageSpecification(), Sort(emptySet()), contractStateType)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T : ContractState> trackBy(contractType: 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()), contractType)
|
return _trackBy(criteria, paging, Sort(emptySet()), contractStateType)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T : ContractState> trackBy(contractType: Class<out T>, criteria: QueryCriteria, sorting: Sort): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
fun <T : ContractState> trackBy(contractStateType: Class<out T>, criteria: QueryCriteria, sorting: Sort): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
||||||
return _trackBy(criteria, PageSpecification(), sorting, contractType)
|
return _trackBy(criteria, PageSpecification(), sorting, contractStateType)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T : ContractState> trackBy(contractType: Class<out T>, criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
fun <T : ContractState> trackBy(contractStateType: Class<out T>, criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
||||||
return _trackBy(criteria, paging, sorting, contractType)
|
return _trackBy(criteria, paging, sorting, contractStateType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +9,6 @@ import net.corda.core.identity.AbstractParty
|
|||||||
import net.corda.core.node.services.vault.QueryCriteria
|
import net.corda.core.node.services.vault.QueryCriteria
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.toFuture
|
import net.corda.core.toFuture
|
||||||
import net.corda.core.transactions.CoreTransaction
|
|
||||||
import net.corda.core.utilities.NonEmptySet
|
import net.corda.core.utilities.NonEmptySet
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.subjects.PublishSubject
|
import rx.subjects.PublishSubject
|
||||||
@ -228,11 +227,11 @@ interface VaultService {
|
|||||||
* However, this is fully generic and can operate with custom [FungibleAsset] states.
|
* However, this is fully generic and can operate with custom [FungibleAsset] states.
|
||||||
* @param lockId The [FlowLogic.runId.uuid] of the current flow used to soft lock the states.
|
* @param lockId The [FlowLogic.runId.uuid] of the current flow used to soft lock the states.
|
||||||
* @param eligibleStatesQuery A custom query object that selects down to the appropriate subset of all states of the
|
* @param eligibleStatesQuery A custom query object that selects down to the appropriate subset of all states of the
|
||||||
* [contractType]. e.g. by selecting on account, issuer, etc. The query is internally augmented with the UNCONSUMED,
|
* [contractStateType]. e.g. by selecting on account, issuer, etc. The query is internally augmented with the UNCONSUMED,
|
||||||
* soft lock and contract type requirements.
|
* soft lock and contract type requirements.
|
||||||
* @param amount The required amount of the asset, but with the issuer stripped off.
|
* @param amount The required amount of the asset, but with the issuer stripped off.
|
||||||
* It is assumed that compatible issuer states will be filtered out by the [eligibleStatesQuery].
|
* It is assumed that compatible issuer states will be filtered out by the [eligibleStatesQuery].
|
||||||
* @param contractType class type of the result set.
|
* @param contractStateType class type of the result set.
|
||||||
* @return Returns a locked subset of the [eligibleStatesQuery] sufficient to satisfy the requested amount,
|
* @return Returns a locked subset of the [eligibleStatesQuery] sufficient to satisfy the requested amount,
|
||||||
* or else an empty list and no change in the stored lock states when their are insufficient resources available.
|
* or else an empty list and no change in the stored lock states when their are insufficient resources available.
|
||||||
*/
|
*/
|
||||||
@ -241,7 +240,7 @@ interface VaultService {
|
|||||||
fun <T : FungibleAsset<U>, U : Any> tryLockFungibleStatesForSpending(lockId: UUID,
|
fun <T : FungibleAsset<U>, U : Any> tryLockFungibleStatesForSpending(lockId: UUID,
|
||||||
eligibleStatesQuery: QueryCriteria,
|
eligibleStatesQuery: QueryCriteria,
|
||||||
amount: Amount<U>,
|
amount: Amount<U>,
|
||||||
contractType: Class<out T>): List<StateAndRef<T>>
|
contractStateType: Class<out T>): List<StateAndRef<T>>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user