mirror of
https://github.com/corda/corda.git
synced 2024-12-27 08:22:35 +00:00
Merge remote-tracking branch 'remotes/open/master' into enterprise-merge-september-26
# Conflicts: # core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt # node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt # settings.gradle
This commit is contained in:
commit
9284e731c0
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
|
||||
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.
|
||||
Thanks for your code, it's appreciated! :)
|
||||
|
16
.idea/compiler.xml
generated
16
.idea/compiler.xml
generated
@ -12,6 +12,8 @@
|
||||
<module name="buildSrc_test" target="1.8" />
|
||||
<module name="client_main" 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_test" 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="cordform-common_main" 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_test" target="1.8" />
|
||||
<module name="demobench_main" target="1.8" />
|
||||
@ -72,8 +77,12 @@
|
||||
<module name="node_test" target="1.8" />
|
||||
<module name="notary-demo_main" 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_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_main" 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_main" 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_main" target="1.8" />
|
||||
<module name="testing-node-driver_test" target="1.8" />
|
||||
|
@ -1,5 +1,5 @@
|
||||
<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" />
|
||||
<option name="RUN_TYPE" value="App" />
|
||||
<option name="VIEW_CLASS_NAME" />
|
21
build.gradle
21
build.gradle
@ -36,7 +36,7 @@ buildscript {
|
||||
ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion")
|
||||
ext.fileupload_version = '1.3.2'
|
||||
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.jansi_version = '1.14'
|
||||
ext.hibernate_version = '5.2.6.Final'
|
||||
@ -146,10 +146,16 @@ allprojects {
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
|
||||
configurations.compile {
|
||||
// We want to use SLF4J's version of these bindings: jcl-over-slf4j
|
||||
// Remove any transitive dependency on Apache's version.
|
||||
exclude group: 'commons-logging', module: 'commons-logging'
|
||||
configurations {
|
||||
compile {
|
||||
// We want to use SLF4J's version of these bindings: jcl-over-slf4j
|
||||
// Remove any transitive dependency on Apache's version.
|
||||
exclude group: 'commons-logging', module: 'commons-logging'
|
||||
}
|
||||
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:rpc')
|
||||
cordaRuntime project(':core')
|
||||
cordaRuntime project(':confidential-identities')
|
||||
cordaRuntime project(':finance')
|
||||
cordaRuntime project(':webserver')
|
||||
testCompile project(':test-utils')
|
||||
@ -252,7 +259,7 @@ bintrayConfig {
|
||||
projectUrl = 'https://github.com/corda/corda'
|
||||
gpgSign = true
|
||||
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 {
|
||||
name = 'Apache-2.0'
|
||||
url = 'https://www.apache.org/licenses/LICENSE-2.0'
|
||||
@ -287,7 +294,7 @@ artifactory {
|
||||
password = System.getenv('CORDA_ARTIFACTORY_PASSWORD')
|
||||
}
|
||||
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.identity.AbstractParty
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
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.toBase58String
|
||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import java.math.BigDecimal
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
@ -46,25 +46,25 @@ object JacksonSupport {
|
||||
// If you change this API please update the docs in the docsite (json.rst)
|
||||
|
||||
interface PartyObjectMapper {
|
||||
fun partyFromX500Name(name: X500Name): Party?
|
||||
fun wellKnownPartyFromX500Name(name: CordaX500Name): Party?
|
||||
fun partyFromKey(owningKey: PublicKey): Party?
|
||||
fun partiesFromName(query: String): Set<Party>
|
||||
}
|
||||
|
||||
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 partiesFromName(query: String) = rpc.partiesFromName(query, fuzzyIdentityMatch)
|
||||
}
|
||||
|
||||
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 partiesFromName(query: String) = identityService.partiesFromName(query, fuzzyIdentityMatch)
|
||||
}
|
||||
|
||||
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 partiesFromName(query: String) = throw UnsupportedOperationException()
|
||||
}
|
||||
@ -105,8 +105,8 @@ object JacksonSupport {
|
||||
addSerializer(OpaqueBytes::class.java, OpaqueBytesSerializer)
|
||||
|
||||
// For X.500 distinguished names
|
||||
addDeserializer(X500Name::class.java, X500NameDeserializer)
|
||||
addSerializer(X500Name::class.java, X500NameSerializer)
|
||||
addDeserializer(CordaX500Name::class.java, CordaX500NameDeserializer)
|
||||
addSerializer(CordaX500Name::class.java, CordaX500NameSerializer)
|
||||
|
||||
// Mixins for transaction types to prevent some properties from being serialized
|
||||
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
|
||||
// how to parse the content
|
||||
return if (parser.text.contains("=")) {
|
||||
val principal = X500Name(parser.text)
|
||||
mapper.partyFromX500Name(principal) ?: throw JsonParseException(parser, "Could not find a Party with name $principal")
|
||||
val principal = CordaX500Name.parse(parser.text)
|
||||
mapper.wellKnownPartyFromX500Name(principal) ?: throw JsonParseException(parser, "Could not find a Party with name $principal")
|
||||
} else {
|
||||
val nameMatches = mapper.partiesFromName(parser.text)
|
||||
if (nameMatches.isEmpty()) {
|
||||
@ -211,22 +211,22 @@ object JacksonSupport {
|
||||
}
|
||||
}
|
||||
|
||||
object X500NameSerializer : JsonSerializer<X500Name>() {
|
||||
override fun serialize(obj: X500Name, generator: JsonGenerator, provider: SerializerProvider) {
|
||||
object CordaX500NameSerializer : JsonSerializer<CordaX500Name>() {
|
||||
override fun serialize(obj: CordaX500Name, generator: JsonGenerator, provider: SerializerProvider) {
|
||||
generator.writeString(obj.toString())
|
||||
}
|
||||
}
|
||||
|
||||
object X500NameDeserializer : JsonDeserializer<X500Name>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext): X500Name {
|
||||
object CordaX500NameDeserializer : JsonDeserializer<CordaX500Name>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext): CordaX500Name {
|
||||
if (parser.currentToken == JsonToken.FIELD_NAME) {
|
||||
parser.nextToken()
|
||||
}
|
||||
|
||||
return try {
|
||||
X500Name(parser.text)
|
||||
CordaX500Name.parse(parser.text)
|
||||
} 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
|
||||
|
||||
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.finance.USD
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SignatureMetadata
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.cordapp.CordappProvider
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.finance.USD
|
||||
import net.corda.testing.ALICE_PUBKEY
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.MINI_CORP
|
||||
import net.corda.testing.TestDependencyInjectionBase
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.util.*
|
||||
import kotlin.reflect.jvm.jvmName
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class JacksonSupportTest : TestDependencyInjectionBase() {
|
||||
@ -23,6 +26,16 @@ class JacksonSupportTest : TestDependencyInjectionBase() {
|
||||
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
|
||||
fun publicKeySerializingWorks() {
|
||||
val publicKey = generateKeyPair().public
|
||||
@ -57,8 +70,12 @@ class JacksonSupportTest : TestDependencyInjectionBase() {
|
||||
|
||||
@Test
|
||||
fun writeTransaction() {
|
||||
val attachmentRef = SecureHash.randomSHA256()
|
||||
whenever(cordappProvider.getContractAttachmentID(DummyContract.PROGRAM_ID))
|
||||
.thenReturn(attachmentRef)
|
||||
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(
|
||||
ByteArray(1),
|
||||
ALICE_PUBKEY,
|
||||
|
@ -7,9 +7,6 @@ description 'Corda client JavaFX 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'
|
||||
|
||||
integrationTestCompile.extendsFrom testCompile
|
||||
integrationTestRuntime.extendsFrom testRuntime
|
||||
}
|
||||
|
@ -8,6 +8,8 @@ import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.crypto.keys
|
||||
import net.corda.core.flows.FlowInitiator
|
||||
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.messaging.CordaRPCOps
|
||||
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.node.NodeInfo
|
||||
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.transactions.SignedTransaction
|
||||
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.CashIssueFlow
|
||||
import net.corda.finance.flows.CashPaymentFlow
|
||||
import net.corda.node.services.network.NetworkMapService
|
||||
import net.corda.node.services.FlowPermissions.Companion.startFlowPermission
|
||||
import net.corda.nodeapi.internal.ServiceInfo
|
||||
import net.corda.node.services.transactions.SimpleNotaryService
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.node.DriverBasedTest
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.junit.Test
|
||||
import rx.Observable
|
||||
|
||||
class NodeMonitorModelTest : DriverBasedTest() {
|
||||
lateinit var aliceNode: NodeInfo
|
||||
lateinit var bobNode: NodeInfo
|
||||
lateinit var notaryNode: NodeInfo
|
||||
lateinit var notaryParty: Party
|
||||
|
||||
lateinit var rpc: CordaRPCOps
|
||||
lateinit var rpcBob: CordaRPCOps
|
||||
@ -50,20 +50,18 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
lateinit var transactions: Observable<SignedTransaction>
|
||||
lateinit var vaultUpdates: Observable<Vault.Update<ContractState>>
|
||||
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(
|
||||
startFlowPermission<CashIssueFlow>(),
|
||||
startFlowPermission<CashPaymentFlow>(),
|
||||
startFlowPermission<CashExitFlow>())
|
||||
)
|
||||
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 notaryNodeHandle = notaryNodeFuture.getOrThrow()
|
||||
aliceNode = aliceNodeHandle.nodeInfo
|
||||
notaryNode = notaryNodeHandle.nodeInfo
|
||||
newNode = { nodeName -> startNode(providedName = nodeName).getOrThrow().nodeInfo }
|
||||
val monitor = NodeMonitorModel()
|
||||
stateMachineTransactionMapping = monitor.stateMachineTransactionMapping.bufferUntilSubscribed()
|
||||
@ -75,6 +73,7 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
|
||||
monitor.register(aliceNodeHandle.configuration.rpcAddress!!, cashUser.username, cashUser.password, initialiseSerialization = false)
|
||||
rpc = monitor.proxyObservable.value!!
|
||||
notaryParty = notaryHandle.nodeInfo.legalIdentities[1]
|
||||
|
||||
val bobNodeHandle = startNode(providedName = BOB.name, rpcUsers = listOf(cashUser)).getOrThrow()
|
||||
bobNode = bobNodeHandle.nodeInfo
|
||||
@ -87,20 +86,20 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
|
||||
@Test
|
||||
fun `network map update`() {
|
||||
newNode(CHARLIE.name)
|
||||
networkMapUpdates.filter { !it.node.advertisedServices.any { it.info.type.isNotary() } }
|
||||
.filter { !it.node.advertisedServices.any { it.info.type == NetworkMapService.type } }
|
||||
val charlieNode = newNode(CHARLIE.name)
|
||||
val nonServiceIdentities = aliceNode.legalIdentitiesAndCerts + bobNode.legalIdentitiesAndCerts + charlieNode.legalIdentitiesAndCerts
|
||||
networkMapUpdates.filter { it.node.legalIdentitiesAndCerts.any { it in nonServiceIdentities } }
|
||||
.expectEvents(isStrict = false) {
|
||||
sequence(
|
||||
// TODO : Add test for remove when driver DSL support individual node shutdown.
|
||||
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 ->
|
||||
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 ->
|
||||
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,
|
||||
Amount(100, USD),
|
||||
OpaqueBytes(ByteArray(1, { 1 })),
|
||||
notaryNode.notaryIdentity
|
||||
notaryParty
|
||||
)
|
||||
|
||||
vaultUpdates.expectEvents(isStrict = false) {
|
||||
@ -132,9 +131,8 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
|
||||
@Test
|
||||
fun `cash issue and move`() {
|
||||
val anonymous = false
|
||||
rpc.startFlow(::CashIssueFlow, 100.DOLLARS, OpaqueBytes.of(1), notaryNode.notaryIdentity).returnValue.getOrThrow()
|
||||
rpc.startFlow(::CashPaymentFlow, 100.DOLLARS, bobNode.legalIdentity, anonymous).returnValue.getOrThrow()
|
||||
val (_, issueIdentity) = rpc.startFlow(::CashIssueFlow, 100.DOLLARS, OpaqueBytes.of(1), notaryParty).returnValue.getOrThrow()
|
||||
rpc.startFlow(::CashPaymentFlow, 100.DOLLARS, bobNode.chooseIdentity()).returnValue.getOrThrow()
|
||||
|
||||
var issueSmId: StateMachineRunId? = null
|
||||
var moveSmId: StateMachineRunId? = null
|
||||
@ -152,7 +150,7 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
require(remove.id == issueSmId)
|
||||
},
|
||||
// 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
|
||||
val initiator = add.stateMachineInfo.initiator
|
||||
require(initiator is FlowInitiator.RPC && initiator.username == "user1")
|
||||
@ -167,7 +165,7 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
// MOVE
|
||||
expect { add: StateMachineUpdate.Added ->
|
||||
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)
|
||||
val signaturePubKeys = stx.sigs.map { it.by }.toSet()
|
||||
// Only Alice signed
|
||||
val aliceKey = aliceNode.legalIdentity.owningKey
|
||||
val aliceKey = aliceNode.chooseIdentity().owningKey
|
||||
require(signaturePubKeys.size <= aliceKey.keys.size)
|
||||
require(aliceKey.isFulfilledBy(signaturePubKeys))
|
||||
issueTx = stx
|
||||
@ -191,8 +189,8 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
require(stx.tx.outputs.size == 1)
|
||||
val signaturePubKeys = stx.sigs.map { it.by }.toSet()
|
||||
// Alice and Notary signed
|
||||
require(aliceNode.legalIdentity.owningKey.isFulfilledBy(signaturePubKeys))
|
||||
require(notaryNode.notaryIdentity.owningKey.isFulfilledBy(signaturePubKeys))
|
||||
require(issueIdentity!!.owningKey.isFulfilledBy(signaturePubKeys))
|
||||
require(notaryParty.owningKey.isFulfilledBy(signaturePubKeys))
|
||||
moveTx = stx
|
||||
}
|
||||
)
|
||||
|
@ -11,20 +11,16 @@ import rx.Observer
|
||||
import rx.subjects.Subject
|
||||
import java.util.*
|
||||
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
|
||||
* are global.
|
||||
*
|
||||
* 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.
|
||||
* Global models store to allow decoupling of UI logic from stream initialisation and provide a central place to
|
||||
* inspect data flows. It also allows detecting of looping logic by constructing a stream dependency graph TODO do this.
|
||||
*
|
||||
* Usage:
|
||||
* // 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 {
|
||||
* val root = (..)
|
||||
@ -41,12 +37,13 @@ import kotlin.reflect.KProperty
|
||||
* }
|
||||
*
|
||||
* For example if I wanted to display a list of all USD cash states:
|
||||
*
|
||||
* class USDCashStatesScreen {
|
||||
* val root: Pane by fxml()
|
||||
*
|
||||
* 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 }
|
||||
*
|
||||
@ -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
|
||||
* 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 {
|
||||
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)
|
||||
}
|
||||
|
||||
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.collections.FXCollections
|
||||
import javafx.collections.ObservableList
|
||||
import net.corda.client.jfx.utils.filterNotNull
|
||||
import net.corda.client.jfx.utils.fold
|
||||
import net.corda.client.jfx.utils.map
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.services.NetworkMapCache.MapChange
|
||||
import net.corda.nodeapi.internal.ServiceType
|
||||
import java.security.PublicKey
|
||||
|
||||
class NetworkIdentityModel {
|
||||
private val networkIdentityObservable by observable(NodeMonitorModel::networkMap)
|
||||
|
||||
val networkIdentities: ObservableList<NodeInfo> =
|
||||
private val networkIdentities: ObservableList<NodeInfo> =
|
||||
networkIdentityObservable.fold(FXCollections.observableArrayList()) { list, update ->
|
||||
list.removeIf {
|
||||
when (update) {
|
||||
@ -31,19 +33,17 @@ class NetworkIdentityModel {
|
||||
private val rpcProxy by observableValue(NodeMonitorModel::proxyObservable)
|
||||
|
||||
private val identityCache = CacheBuilder.newBuilder()
|
||||
.build<PublicKey, ObservableValue<NodeInfo?>>(CacheLoader.from {
|
||||
publicKey ->
|
||||
publicKey?.let { rpcProxy.map { it?.nodeIdentityFromParty(AnonymousParty(publicKey)) } }
|
||||
.build<PublicKey, ObservableValue<NodeInfo?>>(CacheLoader.from { publicKey ->
|
||||
publicKey?.let { rpcProxy.map { it?.nodeInfoFromParty(AnonymousParty(publicKey)) } }
|
||||
})
|
||||
|
||||
val parties: ObservableList<NodeInfo> = networkIdentities.filtered { !it.isCordaService() }
|
||||
val notaries: ObservableList<NodeInfo> = networkIdentities.filtered { it.advertisedServices.any { it.info.type.isNotary() } }
|
||||
val myIdentity = rpcProxy.map { it?.nodeIdentity() }
|
||||
val notaries: ObservableList<Party> = networkIdentities.map {
|
||||
it.legalIdentitiesAndCerts.find { it.name.commonName?.let { ServiceType.parse(it).isNotary() } ?: false }
|
||||
}.map { it?.party }.filterNotNull()
|
||||
|
||||
private fun NodeInfo.isCordaService(): Boolean {
|
||||
// TODO: better way to identify Corda service?
|
||||
return advertisedServices.any { it.info.type.isNetworkMap() || it.info.type.isNotary() }
|
||||
}
|
||||
val notaryNodes: ObservableList<NodeInfo> = notaries.map { rpcProxy.value?.nodeInfoFromParty(it) }.filterNotNull()
|
||||
val parties: ObservableList<NodeInfo> = networkIdentities.filtered { it.legalIdentities.all { it !in notaries } }
|
||||
val myIdentity = rpcProxy.map { it?.nodeInfo()?.legalIdentitiesAndCerts?.first()?.party }
|
||||
|
||||
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.core.contracts.ContractState
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.messaging.*
|
||||
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.utilities.seconds
|
||||
import net.corda.core.node.services.vault.DEFAULT_PAGE_NUM
|
||||
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.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.seconds
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
|
||||
@ -45,6 +49,7 @@ class NodeMonitorModel {
|
||||
val networkMap: Observable<MapChange> = networkMapSubject
|
||||
|
||||
val proxyObservable = SimpleObjectProperty<CordaRPCOps?>()
|
||||
lateinit var notaryIdentities: List<Party>
|
||||
|
||||
/**
|
||||
* Register for updates to/from a given vault.
|
||||
@ -60,6 +65,7 @@ class NodeMonitorModel {
|
||||
)
|
||||
val connection = client.start(username, password)
|
||||
val proxy = connection.proxy
|
||||
notaryIdentities = proxy.notaryIdentities()
|
||||
|
||||
val (stateMachines, stateMachineUpdates) = proxy.stateMachinesFeed()
|
||||
// Extract the flow tracking stream
|
||||
@ -83,8 +89,13 @@ class NodeMonitorModel {
|
||||
stateMachineUpdates.startWith(currentStateMachines).subscribe(stateMachineUpdatesSubject)
|
||||
|
||||
// Vault snapshot (force single page load with MAX_PAGE_SIZE) + updates
|
||||
val (vaultSnapshot, vaultUpdates) = proxy.vaultTrackBy<ContractState>(QueryCriteria.VaultQueryCriteria(Vault.StateStatus.ALL),
|
||||
PageSpecification(DEFAULT_PAGE_NUM, MAX_PAGE_SIZE))
|
||||
val (_, vaultUpdates) = proxy.vaultTrackBy<ContractState>(QueryCriteria.VaultQueryCriteria(Vault.StateStatus.ALL),
|
||||
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())
|
||||
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 {
|
||||
private val transactions by observable(NodeMonitorModel::transactions)
|
||||
private val collectedTransactions = transactions.recordInSequence()
|
||||
private val transactionMap = collectedTransactions.associateBy(SignedTransaction::id)
|
||||
private val transactionMap = transactions.recordAsAssociation(SignedTransaction::id)
|
||||
|
||||
val partiallyResolvedTransactions = collectedTransactions.map {
|
||||
PartiallyResolvedTransaction.fromSignedTransaction(it, transactionMap)
|
||||
|
@ -353,4 +353,4 @@ fun <T : ContractState> DataFeed<Vault.Page<T>, Vault.Update<T>>.toFXListOfState
|
||||
*/
|
||||
fun <T : ContractState> DataFeed<Vault.Page<T>, Vault.Update<T>>.toFXListOfStates(): ObservableList<T> {
|
||||
return toFXListOfStateRefs().map { it.state.data }
|
||||
}
|
||||
}
|
||||
|
@ -5,12 +5,6 @@ apply plugin: 'com.jfrog.artifactory'
|
||||
|
||||
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
|
||||
// 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 {
|
||||
var error: Throwable? = null
|
||||
for (i in 0..numberOfTries - 1) {
|
||||
for (i in 0 until numberOfTries) {
|
||||
val result = generate(random)
|
||||
error = when (result) {
|
||||
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> sequence(generators: List<Generator<A>>) = Generator<List<A>> {
|
||||
fun <A> sequence(generators: List<Generator<A>>) = Generator {
|
||||
val result = mutableListOf<A>()
|
||||
for (generator in generators) {
|
||||
val element = generator.generate(it)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
when (element) {
|
||||
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)
|
||||
@ -135,12 +136,12 @@ class Generator<out A>(val generate: (SplittableRandom) -> Try<A>) {
|
||||
|
||||
fun intRange(range: IntRange) = intRange(range.first, range.last)
|
||||
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(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() }
|
||||
@ -153,7 +154,7 @@ class Generator<out A>(val generate: (SplittableRandom) -> Try<A>) {
|
||||
if (Character.isValidCodePoint(codePoint)) {
|
||||
return@Generator Try.Success(codePoint.toChar())
|
||||
} 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 result = mutableListOf<A>()
|
||||
var finish = false
|
||||
@ -190,7 +191,8 @@ class Generator<out A>(val generate: (SplittableRandom) -> Try<A>) {
|
||||
}
|
||||
}
|
||||
if (res is Try.Failure) {
|
||||
return@Generator res
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return@Generator res as Try<List<A>>
|
||||
}
|
||||
}
|
||||
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>> {
|
||||
val mask = BitSet(list.size)
|
||||
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] = true
|
||||
}
|
||||
for (i in 0..list.size - 1) {
|
||||
for (i in 0 until list.size) {
|
||||
val bit = mask[i]
|
||||
val swapIndex = i + it.nextInt(size - i)
|
||||
mask[i] = mask[swapIndex]
|
||||
|
@ -7,9 +7,6 @@ description 'Corda client RPC 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'
|
||||
|
||||
integrationTestCompile.extendsFrom testCompile
|
||||
integrationTestRuntime.extendsFrom testRuntime
|
||||
|
||||
|
@ -5,14 +5,17 @@ import net.corda.core.concurrent.CordaFuture;
|
||||
import net.corda.core.contracts.Amount;
|
||||
import net.corda.core.messaging.CordaRPCOps;
|
||||
import net.corda.core.messaging.FlowHandle;
|
||||
import net.corda.core.node.services.ServiceInfo;
|
||||
import net.corda.core.utilities.OpaqueBytes;
|
||||
import net.corda.finance.flows.AbstractCashFlow;
|
||||
import net.corda.finance.flows.CashIssueFlow;
|
||||
import net.corda.finance.flows.CashPaymentFlow;
|
||||
import net.corda.finance.schemas.*;
|
||||
import net.corda.node.internal.Node;
|
||||
import net.corda.node.internal.StartedNode;
|
||||
import net.corda.node.services.transactions.ValidatingNotaryService;
|
||||
import net.corda.nodeapi.internal.ServiceInfo;
|
||||
import net.corda.nodeapi.User;
|
||||
import net.corda.testing.CoreTestUtils;
|
||||
import net.corda.testing.node.NodeBasedTest;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
@ -22,14 +25,14 @@ import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import static java.util.Collections.emptyMap;
|
||||
import static java.util.Collections.singletonList;
|
||||
import static java.util.Collections.*;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static kotlin.test.AssertionsKt.assertEquals;
|
||||
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.node.services.FlowPermissions.startFlowPermission;
|
||||
import static net.corda.testing.CoreTestUtils.*;
|
||||
import static net.corda.testing.TestConstants.getALICE;
|
||||
|
||||
public class CordaRPCJavaClientTest extends NodeBasedTest {
|
||||
@ -37,7 +40,7 @@ public class CordaRPCJavaClientTest extends NodeBasedTest {
|
||||
private Set<String> permSet = new HashSet<>(perms);
|
||||
private User rpcUser = new User("user1", "test", permSet);
|
||||
|
||||
private Node node;
|
||||
private StartedNode<Node> node;
|
||||
private CordaRPCClient client;
|
||||
private RPCClient.RPCConnection<CordaRPCOps> connection = null;
|
||||
private CordaRPCOps rpcProxy;
|
||||
@ -49,15 +52,18 @@ public class CordaRPCJavaClientTest extends NodeBasedTest {
|
||||
|
||||
@Before
|
||||
public void setUp() throws ExecutionException, InterruptedException {
|
||||
setCordappPackages("net.corda.finance.contracts");
|
||||
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();
|
||||
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
|
||||
public void done() throws IOException {
|
||||
connection.close();
|
||||
unsetCordappPackages();
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -71,7 +77,7 @@ public class CordaRPCJavaClientTest extends NodeBasedTest {
|
||||
|
||||
FlowHandle<AbstractCashFlow.Result> flowHandle = rpcProxy.startFlowDynamic(CashIssueFlow.class,
|
||||
DOLLARS(123), OpaqueBytes.of("1".getBytes()),
|
||||
node.info.getLegalIdentity());
|
||||
CoreTestUtils.chooseIdentity(node.getInfo()));
|
||||
System.out.println("Started issuing cash, waiting on result");
|
||||
flowHandle.getReturnValue().get();
|
||||
|
||||
|
@ -2,8 +2,10 @@ package net.corda.client.rpc
|
||||
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.flows.FlowInitiator
|
||||
import net.corda.core.messaging.*
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.messaging.FlowProgressHandle
|
||||
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.getOrThrow
|
||||
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.CashIssueFlow
|
||||
import net.corda.finance.flows.CashPaymentFlow
|
||||
import net.corda.finance.schemas.CashSchemaV1
|
||||
import net.corda.node.internal.Node
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.node.services.FlowPermissions.Companion.startFlowPermission
|
||||
import net.corda.nodeapi.internal.ServiceInfo
|
||||
import net.corda.node.services.transactions.ValidatingNotaryService
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.testing.ALICE
|
||||
import net.corda.testing.chooseIdentity
|
||||
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.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||
import org.junit.After
|
||||
@ -33,7 +41,7 @@ class CordaRPCClientTest : NodeBasedTest() {
|
||||
startFlowPermission<CashIssueFlow>(),
|
||||
startFlowPermission<CashPaymentFlow>()
|
||||
))
|
||||
private lateinit var node: Node
|
||||
private lateinit var node: StartedNode<Node>
|
||||
private lateinit var client: CordaRPCClient
|
||||
private var connection: CordaRPCConnection? = null
|
||||
|
||||
@ -43,13 +51,16 @@ class CordaRPCClientTest : NodeBasedTest() {
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
setCordappPackages("net.corda.finance.contracts")
|
||||
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
|
||||
fun done() {
|
||||
connection?.close()
|
||||
unsetCordappPackages()
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -78,7 +89,7 @@ class CordaRPCClientTest : NodeBasedTest() {
|
||||
println("Creating proxy")
|
||||
println("Starting flow")
|
||||
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")
|
||||
flowHandle.progress.subscribe {
|
||||
@ -90,7 +101,7 @@ class CordaRPCClientTest : NodeBasedTest() {
|
||||
@Test
|
||||
fun `sub-type of FlowException thrown by flow`() {
|
||||
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 {
|
||||
handle.returnValue.getOrThrow()
|
||||
}
|
||||
@ -99,9 +110,8 @@ class CordaRPCClientTest : NodeBasedTest() {
|
||||
@Test
|
||||
fun `check basic flow has no progress`() {
|
||||
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<*>)
|
||||
assertTrue(it is FlowHandle<*>)
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,7 +123,7 @@ class CordaRPCClientTest : NodeBasedTest() {
|
||||
assertTrue(startCash.isEmpty(), "Should not start with any cash")
|
||||
|
||||
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")
|
||||
flowHandle.returnValue.get()
|
||||
@ -138,7 +148,7 @@ class CordaRPCClientTest : NodeBasedTest() {
|
||||
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()
|
||||
proxy.startFlow(::CashIssueFlow,
|
||||
123.DOLLARS,
|
||||
|
@ -1,18 +1,13 @@
|
||||
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.RPCClientConfiguration
|
||||
import net.corda.client.rpc.serialization.KryoClientSerializationScheme
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport
|
||||
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.SerializationFactoryImpl
|
||||
import java.time.Duration
|
||||
|
||||
/** @see RPCClient.RPCConnection */
|
||||
@ -38,9 +33,9 @@ data class CordaRPCClientConfiguration(
|
||||
}
|
||||
|
||||
/** @see RPCClient */
|
||||
//TODO Add SSL support
|
||||
class CordaRPCClient(
|
||||
hostAndPort: NetworkHostAndPort,
|
||||
sslConfiguration: SSLConfiguration? = null,
|
||||
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default,
|
||||
initialiseSerialization: Boolean = true
|
||||
) {
|
||||
@ -49,12 +44,12 @@ class CordaRPCClient(
|
||||
// others having registered first.
|
||||
// TODO: allow clients to have serialization factory etc injected and align with RPC protocol version?
|
||||
if (initialiseSerialization) {
|
||||
initialiseSerialization()
|
||||
KryoClientSerializationScheme.initialiseSerialization()
|
||||
}
|
||||
}
|
||||
|
||||
private val rpcClient = RPCClient<CordaRPCOps>(
|
||||
tcpTransport(ConnectionDirection.Outbound(), hostAndPort, sslConfiguration),
|
||||
tcpTransport(ConnectionDirection.Outbound(), hostAndPort, config = null),
|
||||
configuration.toRpcClientConfiguration(),
|
||||
KRYO_RPC_CLIENT_CONTEXT
|
||||
)
|
||||
@ -66,21 +61,4 @@ class CordaRPCClient(
|
||||
inline fun <A> use(username: String, password: String, block: (CordaRPCConnection) -> A): A {
|
||||
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
|
||||
|
||||
import net.corda.client.rpc.RPCException
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.internal.logElapsedTime
|
||||
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.ConnectionDirection
|
||||
import net.corda.nodeapi.RPCApi
|
||||
import net.corda.nodeapi.RPCException
|
||||
import net.corda.nodeapi.config.SSLConfiguration
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
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.util.concurrent.SettableFuture
|
||||
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.internal.LazyPool
|
||||
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.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.FlowHandle;
|
||||
import net.corda.core.node.NodeInfo;
|
||||
import net.corda.core.utilities.OpaqueBytes;
|
||||
import net.corda.core.utilities.X500NameUtils;
|
||||
import net.corda.finance.flows.AbstractCashFlow;
|
||||
import net.corda.finance.flows.CashIssueFlow;
|
||||
import net.corda.nodeapi.User;
|
||||
@ -41,9 +42,10 @@ public class StandaloneCordaRPCJavaClientTest {
|
||||
private CordaRPCOps rpcProxy;
|
||||
private CordaRPCConnection connection;
|
||||
private NodeInfo notaryNode;
|
||||
private Party notaryNodeIdentity;
|
||||
|
||||
private NodeConfig notaryConfig = new NodeConfig(
|
||||
X500NameUtils.getX500Name("Notary Service", "Zurich", "CH"),
|
||||
new CordaX500Name("Notary Service", "Zurich", "CH"),
|
||||
port.getAndIncrement(),
|
||||
port.getAndIncrement(),
|
||||
port.getAndIncrement(),
|
||||
@ -60,6 +62,7 @@ public class StandaloneCordaRPCJavaClientTest {
|
||||
connection = notary.connect();
|
||||
rpcProxy = connection.getProxy();
|
||||
notaryNode = fetchNotaryIdentity();
|
||||
notaryNodeIdentity = rpcProxy.nodeInfo().getLegalIdentities().get(0);
|
||||
}
|
||||
|
||||
@After
|
||||
@ -106,7 +109,7 @@ public class StandaloneCordaRPCJavaClientTest {
|
||||
|
||||
FlowHandle<AbstractCashFlow.Result> flowHandle = rpcProxy.startFlowDynamic(CashIssueFlow.class,
|
||||
dollars123, OpaqueBytes.of("1".getBytes()),
|
||||
notaryNode.getLegalIdentity());
|
||||
notaryNodeIdentity);
|
||||
System.out.println("Started issuing cash, waiting on result");
|
||||
flowHandle.getReturnValue().get();
|
||||
|
||||
|
@ -4,12 +4,17 @@ import com.google.common.hash.Hashing
|
||||
import com.google.common.hash.HashingInputStream
|
||||
import net.corda.client.rpc.CordaRPCConnection
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.messaging.*
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.node.services.vault.*
|
||||
import net.corda.core.utilities.*
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.POUNDS
|
||||
import net.corda.finance.SWISS_FRANCS
|
||||
@ -52,9 +57,10 @@ class StandaloneCordaRPClientTest {
|
||||
private lateinit var rpcProxy: CordaRPCOps
|
||||
private lateinit var connection: CordaRPCConnection
|
||||
private lateinit var notaryNode: NodeInfo
|
||||
private lateinit var notaryNodeIdentity: Party
|
||||
|
||||
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,
|
||||
rpcPort = port.andIncrement,
|
||||
webPort = port.andIncrement,
|
||||
@ -70,6 +76,7 @@ class StandaloneCordaRPClientTest {
|
||||
connection = notary.connect()
|
||||
rpcProxy = connection.proxy
|
||||
notaryNode = fetchNotaryIdentity()
|
||||
notaryNodeIdentity = rpcProxy.nodeInfo().legalIdentitiesAndCerts.first().party
|
||||
}
|
||||
|
||||
@After
|
||||
@ -106,7 +113,7 @@ class StandaloneCordaRPClientTest {
|
||||
|
||||
@Test
|
||||
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)
|
||||
}
|
||||
|
||||
@ -114,7 +121,7 @@ class StandaloneCordaRPClientTest {
|
||||
fun `test starting tracked flow`() {
|
||||
var trackCount = 0
|
||||
val handle = rpcProxy.startTrackedFlow(
|
||||
::CashIssueFlow, 429.DOLLARS, OpaqueBytes.of(0), notaryNode.notaryIdentity
|
||||
::CashIssueFlow, 429.DOLLARS, OpaqueBytes.of(0), notaryNodeIdentity
|
||||
)
|
||||
val updateLatch = CountDownLatch(1)
|
||||
handle.progress.subscribe { msg ->
|
||||
@ -129,7 +136,7 @@ class StandaloneCordaRPClientTest {
|
||||
|
||||
@Test
|
||||
fun `test network map`() {
|
||||
assertEquals(notaryConfig.legalName, notaryNode.legalIdentity.name)
|
||||
assertEquals(notaryConfig.legalName, notaryNodeIdentity.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -148,7 +155,7 @@ class StandaloneCordaRPClientTest {
|
||||
}
|
||||
|
||||
// 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)
|
||||
updateLatch.await()
|
||||
assertEquals(1, updateCount.get())
|
||||
@ -166,7 +173,7 @@ class StandaloneCordaRPClientTest {
|
||||
}
|
||||
|
||||
// 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)
|
||||
updateLatch.await()
|
||||
|
||||
@ -180,7 +187,7 @@ class StandaloneCordaRPClientTest {
|
||||
@Test
|
||||
fun `test vault query by`() {
|
||||
// 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)
|
||||
|
||||
val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL)
|
||||
@ -191,7 +198,7 @@ class StandaloneCordaRPClientTest {
|
||||
assertEquals(1, queryResults.totalStatesAvailable)
|
||||
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)
|
||||
assertEquals(3, moreResults.totalStatesAvailable) // 629 - 100 + 100
|
||||
@ -209,7 +216,7 @@ class StandaloneCordaRPClientTest {
|
||||
println(startCash)
|
||||
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")
|
||||
flowHandle.returnValue.get()
|
||||
|
||||
|
@ -7,7 +7,6 @@ import net.corda.core.internal.concurrent.thenMatch
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.services.messaging.getRpcContext
|
||||
import net.corda.nodeapi.RPCSinceVersion
|
||||
import net.corda.testing.RPCDriverExposedDSLInterface
|
||||
import net.corda.testing.rpcDriver
|
||||
import net.corda.testing.rpcTestUser
|
||||
|
@ -3,7 +3,6 @@ package net.corda.client.rpc
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.node.services.messaging.getRpcContext
|
||||
import net.corda.node.services.messaging.requirePermission
|
||||
import net.corda.nodeapi.PermissionException
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.testing.RPCDriverExposedDSLInterface
|
||||
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 {
|
||||
val until = Math.min(Math.min(offset + length, byteArray.size), offset + bytesLeft)
|
||||
for (i in offset .. until - 1) {
|
||||
val lastIdx = Math.min(Math.min(offset + length, byteArray.size), offset + bytesLeft)
|
||||
for (i in offset until lastIdx) {
|
||||
byteArray[i] = bytesToRepeat[(numberOfBytes - bytesLeft + i - offset) % bytesToRepeat.size]
|
||||
}
|
||||
val bytesRead = until - offset
|
||||
val bytesRead = lastIdx - offset
|
||||
bytesLeft -= 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 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.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
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.
|
||||
*/
|
||||
// TODO: Can this be triggered automatically from [SendTransactionFlow]
|
||||
class Send(val otherSides: Set<Party>,
|
||||
class Send(val otherSideSessions: Set<FlowSession>,
|
||||
val tx: WireTransaction,
|
||||
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 {
|
||||
object SYNCING_IDENTITIES : ProgressTracker.Step("Syncing identities")
|
||||
@ -39,14 +40,14 @@ object IdentitySyncFlow {
|
||||
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)
|
||||
val confidentialIdentities = identities
|
||||
.filter { serviceHub.networkMapCache.getNodeByLegalIdentityKey(it.owningKey) == null }
|
||||
.filter { serviceHub.networkMapCache.getNodesByLegalIdentityKey(it.owningKey).isEmpty() }
|
||||
.toList()
|
||||
val identityCertificates: Map<AbstractParty, PartyAndCertificate?> = identities
|
||||
.map { Pair(it, serviceHub.identityService.certificateFromKey(it.owningKey)) }.toMap()
|
||||
|
||||
otherSides.forEach { otherSide ->
|
||||
val requestedIdentities: List<AbstractParty> = sendAndReceive<List<AbstractParty>>(otherSide, confidentialIdentities).unwrap { req ->
|
||||
require(req.all { it in identityCertificates.keys }) { "${otherSide} requested a confidential identity not part of transaction: ${tx.id}" }
|
||||
otherSideSessions.forEach { otherSideSession ->
|
||||
val requestedIdentities: List<AbstractParty> = otherSideSession.sendAndReceive<List<AbstractParty>>(confidentialIdentities).unwrap { req ->
|
||||
require(req.all { it in identityCertificates.keys }) { "${otherSideSession.counterparty} requested a confidential identity not part of transaction: ${tx.id}" }
|
||||
req
|
||||
}
|
||||
val sendIdentities: List<PartyAndCertificate?> = requestedIdentities.map {
|
||||
@ -56,7 +57,7 @@ object IdentitySyncFlow {
|
||||
else
|
||||
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
|
||||
* we do not yet know about.
|
||||
*/
|
||||
class Receive(val otherSide: Party) : FlowLogic<Unit>() {
|
||||
class Receive(val otherSideSession: FlowSession) : FlowLogic<Unit>() {
|
||||
companion object {
|
||||
object RECEIVING_IDENTITIES : ProgressTracker.Step("Receiving confidential identities")
|
||||
object RECEIVING_CERTIFICATES : ProgressTracker.Step("Receiving certificates for unknown identities")
|
||||
@ -77,10 +78,10 @@ object IdentitySyncFlow {
|
||||
@Suspendable
|
||||
override fun call(): Unit {
|
||||
progressTracker.currentStep = RECEIVING_IDENTITIES
|
||||
val allIdentities = receive<List<AbstractParty>>(otherSide).unwrap { it }
|
||||
val unknownIdentities = allIdentities.filter { serviceHub.identityService.partyFromAnonymous(it) == null }
|
||||
val allIdentities = otherSideSession.receive<List<AbstractParty>>().unwrap { it }
|
||||
val unknownIdentities = allIdentities.filter { serviceHub.identityService.wellKnownPartyFromAnonymous(it) == null }
|
||||
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
|
||||
// the identity service
|
@ -1,23 +1,27 @@
|
||||
package net.corda.core.flows
|
||||
package net.corda.confidential
|
||||
|
||||
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.PartyAndCertificate
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.unwrap
|
||||
|
||||
/**
|
||||
* Very basic flow which exchanges transaction key and certificate paths between two parties in a transaction.
|
||||
* This is intended for use as a subflow of another flow.
|
||||
* Very basic flow which generates new confidential identities for parties in a transaction and exchanges the transaction
|
||||
* key and certificate paths between the parties. This is intended for use as a subflow of another flow which builds a
|
||||
* transaction.
|
||||
*/
|
||||
@StartableByRPC
|
||||
@InitiatingFlow
|
||||
class TransactionKeyFlow(val otherSide: Party,
|
||||
val revocationEnabled: Boolean,
|
||||
class SwapIdentitiesFlow(private val otherParty: Party,
|
||||
private val revocationEnabled: Boolean,
|
||||
override val progressTracker: ProgressTracker) : FlowLogic<LinkedHashMap<Party, AnonymousParty>>() {
|
||||
constructor(otherSide: Party) : this(otherSide, false, tracker())
|
||||
constructor(otherParty: Party) : this(otherParty, false, tracker())
|
||||
|
||||
companion object {
|
||||
object AWAITING_KEY : ProgressTracker.Step("Awaiting key")
|
||||
@ -35,18 +39,19 @@ class TransactionKeyFlow(val otherSide: Party,
|
||||
@Suspendable
|
||||
override fun call(): LinkedHashMap<Party, AnonymousParty> {
|
||||
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
|
||||
val identities = LinkedHashMap<Party, AnonymousParty>()
|
||||
if (otherSide == serviceHub.myInfo.legalIdentity) {
|
||||
identities.put(otherSide, legalIdentityAnonymous.party.anonymise())
|
||||
if (serviceHub.myInfo.isLegalIdentity(otherParty)) {
|
||||
identities.put(otherParty, legalIdentityAnonymous.party.anonymise())
|
||||
} else {
|
||||
val anonymousOtherSide = sendAndReceive<PartyAndCertificate>(otherSide, legalIdentityAnonymous).unwrap { confidentialIdentity ->
|
||||
validateAndRegisterIdentity(serviceHub.identityService, otherSide, confidentialIdentity)
|
||||
val otherSession = initiateFlow(otherParty)
|
||||
val anonymousOtherSide = otherSession.sendAndReceive<PartyAndCertificate>(legalIdentityAnonymous).unwrap { confidentialIdentity ->
|
||||
validateAndRegisterIdentity(serviceHub.identityService, otherSession.counterparty, confidentialIdentity)
|
||||
}
|
||||
identities.put(serviceHub.myInfo.legalIdentity, legalIdentityAnonymous.party.anonymise())
|
||||
identities.put(otherSide, anonymousOtherSide.party.anonymise())
|
||||
identities.put(ourIdentity, legalIdentityAnonymous.party.anonymise())
|
||||
identities.put(otherSession.counterparty, anonymousOtherSide.party.anonymise())
|
||||
}
|
||||
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 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.transactions.WireTransaction
|
||||
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.contracts.asset.Cash
|
||||
import net.corda.finance.flows.CashIssueAndPaymentFlow
|
||||
import net.corda.testing.ALICE
|
||||
import net.corda.testing.BOB
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
@ -24,6 +26,7 @@ class IdentitySyncFlowTests {
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
setCordappPackages("net.corda.finance.contracts.asset")
|
||||
// We run this in parallel threads to help catch any race conditions that may exist.
|
||||
mockNet = MockNetwork(networkSendManuallyPumped = false, threadPerNode = true)
|
||||
}
|
||||
@ -31,6 +34,7 @@ class IdentitySyncFlowTests {
|
||||
@After
|
||||
fun cleanUp() {
|
||||
mockNet.stopNodes()
|
||||
unsetCordappPackages()
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -39,25 +43,26 @@ class IdentitySyncFlowTests {
|
||||
val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name)
|
||||
val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name)
|
||||
val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name)
|
||||
val alice: Party = aliceNode.services.myInfo.legalIdentity
|
||||
val bob: Party = bobNode.services.myInfo.legalIdentity
|
||||
bobNode.registerInitiatedFlow(Receive::class.java)
|
||||
val alice: Party = aliceNode.services.myInfo.chooseIdentity()
|
||||
val bob: Party = bobNode.services.myInfo.chooseIdentity()
|
||||
bobNode.internals.registerInitiatedFlow(Receive::class.java)
|
||||
|
||||
// Alice issues then pays some cash to a new confidential identity that Bob doesn't know about
|
||||
val anonymous = true
|
||||
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 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
|
||||
aliceNode.services.startFlow(Initiator(bob, issueTx.tx)).resultFuture.getOrThrow()
|
||||
val expected = aliceNode.database.transaction {
|
||||
aliceNode.services.identityService.partyFromAnonymous(confidentialIdentity)
|
||||
aliceNode.services.identityService.wellKnownPartyFromAnonymous(confidentialIdentity)
|
||||
}
|
||||
val actual = bobNode.database.transaction {
|
||||
bobNode.services.identityService.partyFromAnonymous(confidentialIdentity)
|
||||
bobNode.services.identityService.wellKnownPartyFromAnonymous(confidentialIdentity)
|
||||
}
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
@ -69,19 +74,20 @@ class IdentitySyncFlowTests {
|
||||
class Initiator(val otherSide: Party, val tx: WireTransaction): FlowLogic<Boolean>() {
|
||||
@Suspendable
|
||||
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
|
||||
return receive<Boolean>(otherSide).unwrap { it }
|
||||
return session.receive<Boolean>().unwrap { it }
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(IdentitySyncFlowTests.Initiator::class)
|
||||
class Receive(val otherSide: Party): FlowLogic<Unit>() {
|
||||
class Receive(val otherSideSession: FlowSession): FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
subFlow(IdentitySyncFlow.Receive(otherSide))
|
||||
subFlow(IdentitySyncFlow.Receive(otherSideSession))
|
||||
// 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.AnonymousParty
|
||||
@ -7,6 +7,7 @@ import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.testing.ALICE
|
||||
import net.corda.testing.BOB
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.chooseIdentity
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
@ -14,7 +15,7 @@ import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class TransactionKeyFlowTests {
|
||||
class SwapIdentitiesFlowTests {
|
||||
@Test
|
||||
fun `issue key`() {
|
||||
// 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 aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name)
|
||||
val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name)
|
||||
val alice: Party = aliceNode.services.myInfo.legalIdentity
|
||||
val bob: Party = bobNode.services.myInfo.legalIdentity
|
||||
val alice: Party = aliceNode.services.myInfo.chooseIdentity()
|
||||
val bob: Party = bobNode.services.myInfo.chooseIdentity()
|
||||
mockNet.registerIdentities()
|
||||
|
||||
// Run the flows
|
||||
val requesterFlow = aliceNode.services.startFlow(TransactionKeyFlow(bob))
|
||||
val requesterFlow = aliceNode.services.startFlow(SwapIdentitiesFlow(bob))
|
||||
|
||||
// Get the results
|
||||
val actual: Map<Party, AnonymousParty> = requesterFlow.resultFuture.getOrThrow().toMap()
|
||||
@ -40,8 +42,8 @@ class TransactionKeyFlowTests {
|
||||
assertNotEquals<AbstractParty>(bob, bobAnonymousIdentity)
|
||||
|
||||
// Verify that the anonymous identities look sane
|
||||
assertEquals(alice.name, aliceNode.database.transaction { aliceNode.services.identityService.partyFromAnonymous(aliceAnonymousIdentity)!!.name })
|
||||
assertEquals(bob.name, bobNode.database.transaction { bobNode.services.identityService.partyFromAnonymous(bobAnonymousIdentity)!!.name })
|
||||
assertEquals(alice.name, aliceNode.database.transaction { aliceNode.services.identityService.wellKnownPartyFromAnonymous(aliceAnonymousIdentity)!!.name })
|
||||
assertEquals(bob.name, bobNode.database.transaction { bobNode.services.identityService.wellKnownPartyFromAnonymous(bobAnonymousIdentity)!!.name })
|
||||
|
||||
// Verify that the nodes have the right anonymous identities
|
||||
assertTrue { aliceAnonymousIdentity.owningKey in aliceNode.services.keyManagementService.keys }
|
@ -1,5 +1,5 @@
|
||||
gradlePluginsVersion=0.16.2
|
||||
kotlinVersion=1.1.4
|
||||
gradlePluginsVersion=1.0.0
|
||||
kotlinVersion=1.1.50
|
||||
guavaVersion=21.0
|
||||
bouncycastleVersion=1.57
|
||||
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.secureRandomBytes
|
||||
import net.corda.core.crypto.toStringShort
|
||||
import net.corda.core.flows.FlowLogicRef
|
||||
import net.corda.core.flows.FlowLogicRefFactory
|
||||
import net.corda.core.identity.AbstractParty
|
||||
@ -15,111 +16,13 @@ import net.corda.core.utilities.OpaqueBytes
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
|
||||
// DOCSTART 1
|
||||
/** Implemented by anything that can be named by a secure hash value (e.g. transactions, attachments). */
|
||||
interface NamedByHash {
|
||||
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
|
||||
|
||||
// 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.
|
||||
* 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 */
|
||||
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))
|
||||
|
||||
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. */
|
||||
@ -297,7 +200,7 @@ interface MoveCommand : CommandData {
|
||||
}
|
||||
|
||||
/** 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
|
||||
/** 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.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
@ -371,6 +274,14 @@ class PrivacySalt(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
|
||||
init {
|
||||
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)
|
||||
: 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)
|
||||
: 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
|
||||
class CompositeKey private constructor(val threshold: Int, children: List<NodeAndWeight>) : PublicKey {
|
||||
companion object {
|
||||
val KEY_ALGORITHM = "COMPOSITE"
|
||||
const val KEY_ALGORITHM = "COMPOSITE"
|
||||
/**
|
||||
* 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) {
|
||||
val curVisitedMap = IdentityHashMap<CompositeKey, Boolean>()
|
||||
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)
|
||||
node.cycleDetection(curVisitedMap)
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import java.security.spec.AlgorithmParameterSpec
|
||||
class CompositeSignature : Signature(SIGNATURE_ALGORITHM) {
|
||||
companion object {
|
||||
const val SIGNATURE_ALGORITHM = "COMPOSITESIG"
|
||||
@JvmStatic
|
||||
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
|
||||
|
||||
import net.corda.core.internal.X509EdDSAEngine
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.i2p.crypto.eddsa.EdDSAEngine
|
||||
import net.i2p.crypto.eddsa.EdDSAPrivateKey
|
||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||
import net.i2p.crypto.eddsa.EdDSASecurityProvider
|
||||
import net.i2p.crypto.eddsa.*
|
||||
import net.i2p.crypto.eddsa.math.GroupElement
|
||||
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec
|
||||
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.SubjectPublicKeyInfo
|
||||
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.BCECPublicKey
|
||||
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 java.math.BigInteger
|
||||
import java.security.*
|
||||
import java.security.KeyFactory
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.spec.InvalidKeySpecException
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
import java.security.spec.X509EncodedKeySpec
|
||||
@ -201,6 +200,8 @@ object Crypto {
|
||||
|
||||
private fun getBouncyCastleProvider() = BouncyCastleProvider().apply {
|
||||
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))
|
||||
}
|
||||
|
||||
@ -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
|
||||
* [X509CertificateHolder].
|
||||
* Convert a public key to a supported implementation.
|
||||
*
|
||||
* @param key a public key.
|
||||
* @return a supported implementation of the input public key.
|
||||
|
@ -2,11 +2,15 @@
|
||||
|
||||
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.toBase58
|
||||
import net.corda.core.utilities.toSHA256Bytes
|
||||
import java.math.BigInteger
|
||||
import net.corda.core.utilities.SgxSupport
|
||||
import java.nio.ByteBuffer
|
||||
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 {
|
||||
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.
|
||||
if (hashesToCheck.groupBy { it } != usedHashes.groupBy { it })
|
||||
return false
|
||||
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))
|
||||
val zeroHash = SecureHash.SHA256(ByteArray(32, { 0.toByte() }))
|
||||
val allOnesHash = SecureHash.SHA256(ByteArray(32, { 255.toByte() }))
|
||||
}
|
||||
|
||||
// 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.crypto.TransactionSignature
|
||||
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.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
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.
|
||||
@ -33,10 +34,8 @@ abstract class AbstractStateReplacementFlow {
|
||||
* The assembled transaction for upgrading a contract.
|
||||
*
|
||||
* @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
|
||||
@ -62,15 +61,11 @@ abstract class AbstractStateReplacementFlow {
|
||||
@Suspendable
|
||||
@Throws(StateReplacementException::class)
|
||||
override fun call(): StateAndRef<T> {
|
||||
val (stx, participantKeys, myKey) = assembleTx()
|
||||
|
||||
val (stx) = assembleTx()
|
||||
val participantSessions = getParticipantSessions()
|
||||
progressTracker.currentStep = SIGNING
|
||||
|
||||
val signatures = if (participantKeys.singleOrNull() == myKey) {
|
||||
getNotarySignatures(stx)
|
||||
} else {
|
||||
collectSignatures(participantKeys - myKey, stx)
|
||||
}
|
||||
val signatures = collectSignatures(participantSessions, stx)
|
||||
|
||||
val finalTx = stx + signatures
|
||||
serviceHub.recordTransactions(finalTx)
|
||||
@ -89,35 +84,38 @@ abstract class AbstractStateReplacementFlow {
|
||||
/**
|
||||
* Build the upgrade transaction.
|
||||
*
|
||||
* @return a triple of the transaction, the public keys of all participants, and the participating public key of
|
||||
* this node.
|
||||
* @return the transaction
|
||||
*/
|
||||
abstract protected fun assembleTx(): UpgradeTx
|
||||
|
||||
@Suspendable
|
||||
private fun collectSignatures(participants: Iterable<PublicKey>, stx: SignedTransaction): List<TransactionSignature> {
|
||||
val parties = participants.map {
|
||||
val participantNode = serviceHub.networkMapCache.getNodeByLegalIdentityKey(it) ?:
|
||||
throw IllegalStateException("Participant $it to state $originalState not found on the network")
|
||||
participantNode.legalIdentity
|
||||
}
|
||||
/**
|
||||
* Initiate sessions with parties we want signatures from.
|
||||
*/
|
||||
open fun getParticipantSessions(): List<Pair<FlowSession, List<AbstractParty>>> {
|
||||
return excludeHostNode(serviceHub, groupAbstractPartyByWellKnownParty(serviceHub, originalState.state.data.participants)).map { initiateFlow(it.key) to it.value }
|
||||
}
|
||||
|
||||
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 allSignatures = participantSignatures + getNotarySignatures(allPartySignedTx)
|
||||
parties.forEach { send(it, allSignatures) }
|
||||
sessions.forEach { it.first.send(allSignatures) }
|
||||
|
||||
return allSignatures
|
||||
}
|
||||
|
||||
@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)
|
||||
subFlow(SendTransactionFlow(party, stx))
|
||||
return sendAndReceive<TransactionSignature>(party, proposal).unwrap {
|
||||
check(party.owningKey.isFulfilledBy(it.by)) { "Not signed by the required participant" }
|
||||
subFlow(SendTransactionFlow(session, stx))
|
||||
return session.sendAndReceive<TransactionSignature>(proposal).unwrap {
|
||||
check(party.single().owningKey.isFulfilledBy(it.by)) { "Not signed by the required participant" }
|
||||
it.verify(stx.id)
|
||||
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).
|
||||
// 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?>() {
|
||||
constructor(otherSide: Party) : this(otherSide, Acceptor.tracker())
|
||||
constructor(initiatingSession: FlowSession) : this(initiatingSession, Acceptor.tracker())
|
||||
companion object {
|
||||
object VERIFYING : ProgressTracker.Step("Verifying state replacement proposal")
|
||||
object APPROVING : ProgressTracker.Step("State replacement approved")
|
||||
@ -151,9 +149,9 @@ abstract class AbstractStateReplacementFlow {
|
||||
override fun call(): Void? {
|
||||
progressTracker.currentStep = VERIFYING
|
||||
// We expect stx to have insufficient signatures here
|
||||
val stx = subFlow(ReceiveTransactionFlow(otherSide, checkSufficientSignatures = false))
|
||||
val stx = subFlow(ReceiveTransactionFlow(initiatingSession, checkSufficientSignatures = false))
|
||||
checkMySignatureRequired(stx)
|
||||
val maybeProposal: UntrustworthyData<Proposal<T>> = receive(otherSide)
|
||||
val maybeProposal: UntrustworthyData<Proposal<T>> = initiatingSession.receive()
|
||||
maybeProposal.unwrap {
|
||||
verifyProposal(stx, it)
|
||||
}
|
||||
@ -166,7 +164,7 @@ abstract class AbstractStateReplacementFlow {
|
||||
progressTracker.currentStep = APPROVING
|
||||
|
||||
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.
|
||||
val allSignatures = swapSignatures.unwrap { signatures ->
|
||||
@ -193,7 +191,8 @@ abstract class AbstractStateReplacementFlow {
|
||||
|
||||
private fun checkMySignatureRequired(stx: SignedTransaction) {
|
||||
// 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()) {
|
||||
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 net.corda.core.crypto.TransactionSignature
|
||||
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.groupPublicKeysByWellKnownParty
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
@ -57,27 +56,31 @@ import java.security.PublicKey
|
||||
* val stx = subFlow(CollectSignaturesFlow(ptx))
|
||||
*
|
||||
* @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
|
||||
* just in the states. If null, the default well known identity of the node is used.
|
||||
*/
|
||||
// TODO: AbstractStateReplacementFlow needs updating to use this flow.
|
||||
class CollectSignaturesFlow @JvmOverloads constructor (val partiallySignedTx: SignedTransaction,
|
||||
val sessionsToCollectFrom: Collection<FlowSession>,
|
||||
val myOptionalKeys: Iterable<PublicKey>?,
|
||||
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 {
|
||||
object COLLECTING : ProgressTracker.Step("Collecting signatures from counter-parties.")
|
||||
object VERIFYING : ProgressTracker.Step("Verifying collected signatures.")
|
||||
|
||||
@JvmStatic
|
||||
fun tracker() = ProgressTracker(COLLECTING, VERIFYING)
|
||||
|
||||
// TODO: Make the progress tracker adapt to the number of counter-parties to collect from.
|
||||
|
||||
}
|
||||
|
||||
@Suspendable override fun call(): SignedTransaction {
|
||||
// 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.
|
||||
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 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 (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.
|
||||
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
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup the [Party] object for each [PublicKey] using the [ServiceHub.networkMapCache].
|
||||
*
|
||||
* @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).
|
||||
*/
|
||||
@Suspendable private fun keysToParties(keys: Collection<PublicKey>): List<Pair<Party, PublicKey>> = keys.map {
|
||||
val party = serviceHub.identityService.partyFromAnonymous(AnonymousParty(it))
|
||||
?: throw IllegalStateException("Party ${it.toBase58String()} not found on the network.")
|
||||
Pair(party, it)
|
||||
}
|
||||
|
||||
// DOCSTART 1
|
||||
/**
|
||||
* 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 {
|
||||
// DOCSTART 1
|
||||
/**
|
||||
* Get and check the required signature.
|
||||
*
|
||||
* @param partiallySignedTx the transaction to sign.
|
||||
* @param session the [FlowSession] to connect to to get the signature.
|
||||
* @param signingKeys the list of keys the party should use to sign the transaction.
|
||||
*/
|
||||
@Suspendable
|
||||
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))
|
||||
@Suspendable
|
||||
override fun call(): List<TransactionSignature> {
|
||||
// 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
|
||||
// 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.
|
||||
send(counterparty, signingKey)
|
||||
return receive<TransactionSignature>(counterparty).unwrap {
|
||||
require(signingKey.isFulfilledBy(it.by)) { "Not signed by the required signing key." }
|
||||
it
|
||||
session.send(signingKeys)
|
||||
return session.receive<List<TransactionSignature>>().unwrap { signatures ->
|
||||
require(signatures.size == signingKeys.size) { "Need signature for each signing key" }
|
||||
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
|
||||
@ -159,15 +167,15 @@ class CollectSignaturesFlow @JvmOverloads constructor (val partiallySignedTx: Si
|
||||
* - Subclass [SignTransactionFlow] - this can be done inside an existing flow (as shown below)
|
||||
* - Override the [checkTransaction] method to add some custom verification logic
|
||||
* - 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
|
||||
* CollectSignaturesFlowTests.kt for further examples:
|
||||
*
|
||||
* class Responder(val otherParty: Party): FlowLogic<SignedTransaction>() {
|
||||
* class Responder(val otherPartySession: FlowSession): FlowLogic<SignedTransaction>() {
|
||||
* @Suspendable override fun call(): SignedTransaction {
|
||||
* // [SignTransactionFlow] sub-classed as a singleton object.
|
||||
* val flow = object : SignTransactionFlow(otherParty) {
|
||||
* val flow = object : SignTransactionFlow(otherPartySession) {
|
||||
* @Suspendable override fun checkTransaction(stx: SignedTransaction) = requireThat {
|
||||
* val tx = stx.tx
|
||||
* 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>() {
|
||||
|
||||
companion object {
|
||||
@ -192,23 +200,24 @@ abstract class SignTransactionFlow(val otherParty: Party,
|
||||
object VERIFYING : ProgressTracker.Step("Verifying transaction proposal.")
|
||||
object SIGNING : ProgressTracker.Step("Signing transaction proposal.")
|
||||
|
||||
@JvmStatic
|
||||
fun tracker() = ProgressTracker(RECEIVING, VERIFYING, SIGNING)
|
||||
}
|
||||
|
||||
@Suspendable override fun call(): SignedTransaction {
|
||||
progressTracker.currentStep = RECEIVING
|
||||
// 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
|
||||
// means we only have to check we own that one key, rather than matching all keys in the transaction against all
|
||||
// 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
|
||||
serviceHub.keyManagementService.filterMyKeys(listOf(it)).single()
|
||||
serviceHub.keyManagementService.filterMyKeys(keys)
|
||||
}
|
||||
progressTracker.currentStep = VERIFYING
|
||||
// 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.
|
||||
checkSignatures(stx)
|
||||
stx.tx.toLedgerTransaction(serviceHub).verify()
|
||||
@ -223,18 +232,19 @@ abstract class SignTransactionFlow(val otherParty: Party,
|
||||
}
|
||||
// Sign and send back our signature to the Initiator.
|
||||
progressTracker.currentStep = SIGNING
|
||||
val mySignature = serviceHub.createSignature(stx, signingKey)
|
||||
send(otherParty, mySignature)
|
||||
val mySignatures = signingKeys.map { key ->
|
||||
serviceHub.createSignature(stx, key)
|
||||
}
|
||||
otherSideSession.send(mySignatures)
|
||||
|
||||
// Return the fully signed transaction once it has been committed.
|
||||
return waitForLedgerCommit(stx.id)
|
||||
// Return the additionally signed transaction.
|
||||
return stx + mySignatures
|
||||
}
|
||||
|
||||
@Suspendable private fun checkSignatures(stx: SignedTransaction) {
|
||||
val signingIdentities = stx.sigs.map(TransactionSignature::by).mapNotNull(serviceHub.identityService::partyFromKey)
|
||||
val signingWellKnownIdentities = signingIdentities.mapNotNull(serviceHub.identityService::partyFromAnonymous)
|
||||
require(otherParty in signingWellKnownIdentities) {
|
||||
"The Initiator of CollectSignaturesFlow must have signed the transaction. Found ${signingWellKnownIdentities}, expected ${otherParty}"
|
||||
val signingWellKnownIdentities = groupPublicKeysByWellKnownParty(serviceHub, stx.sigs.map(TransactionSignature::by))
|
||||
require(otherSideSession.counterparty in signingWellKnownIdentities) {
|
||||
"The Initiator of CollectSignaturesFlow must have signed the transaction. Found ${signingWellKnownIdentities}, expected ${otherSideSession}"
|
||||
}
|
||||
val signed = stx.sigs.map { it.by }
|
||||
val allSigners = stx.tx.requiredSigningKeys
|
||||
@ -266,9 +276,9 @@ abstract class SignTransactionFlow(val otherParty: Party,
|
||||
@Throws(FlowException::class)
|
||||
abstract protected fun checkTransaction(stx: SignedTransaction)
|
||||
|
||||
@Suspendable private fun checkMySignatureRequired(stx: SignedTransaction, signingKey: PublicKey) {
|
||||
require(signingKey in stx.tx.requiredSigningKeys) {
|
||||
"Party is not a participant for any of the input states of transaction ${stx.id}"
|
||||
@Suspendable private fun checkMySignaturesRequired(stx: SignedTransaction, signingKeys: Iterable<PublicKey>) {
|
||||
require(signingKeys.all { it in stx.tx.requiredSigningKeys }) {
|
||||
"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 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.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import java.security.PublicKey
|
||||
|
||||
@ -18,11 +17,37 @@ import java.security.PublicKey
|
||||
*/
|
||||
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.
|
||||
* 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 method will NOT initiate the upgrade process. To start the upgrade process, see [Initiator].
|
||||
*
|
||||
* 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 flow will NOT initiate the upgrade process. To start the upgrade process, see [Initiate].
|
||||
*/
|
||||
@StartableByRPC
|
||||
class Authorise(
|
||||
@ -32,7 +57,7 @@ object ContractUpgradeFlow {
|
||||
@Suspendable
|
||||
override fun call(): Void? {
|
||||
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.")
|
||||
}
|
||||
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)
|
||||
*/
|
||||
@StartableByRPC
|
||||
class Deauthorise(
|
||||
val stateRef: StateRef
|
||||
) : FlowLogic< Void?>() {
|
||||
class Deauthorise(val stateRef: StateRef) : FlowLogic<Void?>() {
|
||||
@Suspendable
|
||||
override fun call(): Void? {
|
||||
serviceHub.contractUpgradeService.removeAuthorisedContractUpgrade(stateRef)
|
||||
@ -56,9 +79,12 @@ object ContractUpgradeFlow {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This flow begins the contract upgrade process.
|
||||
*/
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
class Initiator<OldState : ContractState, out NewState : ContractState>(
|
||||
class Initiate<OldState : ContractState, out NewState : ContractState>(
|
||||
originalState: StateAndRef<OldState>,
|
||||
newContractClass: Class<out UpgradedContract<OldState, NewState>>
|
||||
) : AbstractStateReplacementFlow.Instigator<OldState, NewState, Class<out UpgradedContract<OldState, NewState>>>(originalState, newContractClass) {
|
||||
@ -73,8 +99,8 @@ object ContractUpgradeFlow {
|
||||
return TransactionBuilder(stateRef.state.notary)
|
||||
.withItems(
|
||||
stateRef,
|
||||
contractUpgrade.upgrade(stateRef.state.data),
|
||||
Command(UpgradeCommand(upgradedContractClass), stateRef.state.data.participants.map { it.owningKey }),
|
||||
StateAndContract(contractUpgrade.upgrade(stateRef.state.data), upgradedContractClass.name),
|
||||
Command(UpgradeCommand(upgradedContractClass.name), stateRef.state.data.participants.map { it.owningKey }),
|
||||
privacySalt
|
||||
)
|
||||
}
|
||||
@ -82,65 +108,12 @@ object ContractUpgradeFlow {
|
||||
|
||||
@Suspendable
|
||||
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()
|
||||
// TODO: We need a much faster way of finding our key in the transaction
|
||||
val myKey = serviceHub.keyManagementService.filterMyKeys(participantKeys).single()
|
||||
val stx = serviceHub.signInitialTransaction(baseTx, myKey)
|
||||
return AbstractStateReplacementFlow.UpgradeTx(stx, participantKeys, myKey)
|
||||
}
|
||||
}
|
||||
|
||||
@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())
|
||||
return AbstractStateReplacementFlow.UpgradeTx(stx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,46 +1,36 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
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.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.ResolveTransactionsFlow
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.identity.groupAbstractPartyByWellKnownParty
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
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
|
||||
* are acceptable then they are 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.
|
||||
* Verifies the given transaction, then sends it to the named notary. If the notary agrees that the transaction
|
||||
* is acceptable then it is from that point onwards committed to the ledger, and will be written through to the
|
||||
* 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
|
||||
* dependers, so you don't need to do this yourself.
|
||||
* The transaction is expected to have already been resolved: if its dependencies are not available in local
|
||||
* 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
|
||||
* storage or within the given set, verification will fail. They must have signatures from all necessary parties
|
||||
* other than the notary.
|
||||
* If specified, the extra recipients are sent the given transaction. The base set of parties to inform are calculated
|
||||
* from the contract-given set of participants.
|
||||
*
|
||||
* If specified, the extra recipients are sent all the given transactions. The base set of parties to inform of each
|
||||
* transaction are calculated on a per transaction basis from the contract-given set of participants.
|
||||
* The flow returns the same transaction but with the additional signatures from the notary.
|
||||
*
|
||||
* The flow returns the same transactions, in the same order, with the additional signatures.
|
||||
*
|
||||
* @param transactions What to commit.
|
||||
* @param transaction What to commit.
|
||||
* @param extraRecipients A list of additional participants to inform of the transaction.
|
||||
*/
|
||||
open class FinalityFlow(val transactions: Iterable<SignedTransaction>,
|
||||
val extraRecipients: Set<Party>,
|
||||
override val progressTracker: ProgressTracker) : FlowLogic<List<SignedTransaction>>() {
|
||||
val extraParticipants: Set<Participant> = extraRecipients.map { it -> Participant(it, it) }.toSet()
|
||||
constructor(transaction: SignedTransaction, extraParticipants: Set<Party>) : this(listOf(transaction), extraParticipants, tracker())
|
||||
constructor(transaction: SignedTransaction) : this(listOf(transaction), emptySet(), tracker())
|
||||
constructor(transaction: SignedTransaction, progressTracker: ProgressTracker) : this(listOf(transaction), emptySet(), progressTracker)
|
||||
@InitiatingFlow
|
||||
class FinalityFlow(val transaction: SignedTransaction,
|
||||
private val extraRecipients: Set<Party>,
|
||||
override val progressTracker: ProgressTracker) : FlowLogic<SignedTransaction>() {
|
||||
constructor(transaction: SignedTransaction, extraParticipants: Set<Party>) : this(transaction, extraParticipants, tracker())
|
||||
constructor(transaction: SignedTransaction) : this(transaction, emptySet(), tracker())
|
||||
constructor(transaction: SignedTransaction, progressTracker: ProgressTracker) : this(transaction, emptySet(), progressTracker)
|
||||
|
||||
companion object {
|
||||
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")
|
||||
|
||||
// TODO: Make all tracker() methods @JvmStatic
|
||||
@JvmStatic
|
||||
fun tracker() = ProgressTracker(NOTARISING, BROADCASTING)
|
||||
}
|
||||
|
||||
open protected val me
|
||||
get() = serviceHub.myInfo.legalIdentity
|
||||
|
||||
@Suspendable
|
||||
@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
|
||||
// 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.
|
||||
// Then send to the notary if needed, record locally and distribute.
|
||||
val parties = getPartiesToSend(verifyTx())
|
||||
progressTracker.currentStep = NOTARISING
|
||||
val notarisedTxns: List<Pair<SignedTransaction, Set<Participant>>> = resolveDependenciesOf(transactions)
|
||||
.map { (stx, ltx) -> Pair(notariseAndRecord(stx), lookupParties(ltx)) }
|
||||
val notarised = notariseAndRecord()
|
||||
|
||||
// Each transaction has its own set of recipients, but extra recipients get them all.
|
||||
progressTracker.currentStep = BROADCASTING
|
||||
for ((stx, parties) in notarisedTxns) {
|
||||
broadcastTransaction(stx, (parties + extraParticipants).filter { it.wellKnown != me })
|
||||
for (party in parties) {
|
||||
if (!serviceHub.myInfo.isLegalIdentity(party)) {
|
||||
val session = initiateFlow(party)
|
||||
subFlow(SendTransactionFlow(session, notarised))
|
||||
}
|
||||
}
|
||||
return notarisedTxns.map { it.first }
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()))
|
||||
}
|
||||
return notarised
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun notariseAndRecord(stx: SignedTransaction): SignedTransaction {
|
||||
val notarised = if (needsNotarySignature(stx)) {
|
||||
val notarySignatures = subFlow(NotaryFlow.Client(stx))
|
||||
stx + notarySignatures
|
||||
private fun notariseAndRecord(): SignedTransaction {
|
||||
val notarised = if (needsNotarySignature(transaction)) {
|
||||
val notarySignatures = subFlow(NotaryFlow.Client(transaction))
|
||||
transaction + notarySignatures
|
||||
} else {
|
||||
stx
|
||||
transaction
|
||||
}
|
||||
serviceHub.recordTransactions(notarised)
|
||||
return notarised
|
||||
@ -108,7 +83,6 @@ open class FinalityFlow(val transactions: Iterable<SignedTransaction>,
|
||||
val wtx = stx.tx
|
||||
val needsNotarisation = wtx.inputs.isNotEmpty() || wtx.timeWindow != null
|
||||
return needsNotarisation && hasNoNotarySignature(stx)
|
||||
|
||||
}
|
||||
|
||||
private fun hasNoNotarySignature(stx: SignedTransaction): Boolean {
|
||||
@ -117,55 +91,17 @@ open class FinalityFlow(val transactions: Iterable<SignedTransaction>,
|
||||
return !(notaryKey?.isFulfilledBy(signers) ?: false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the parties involved in a transaction.
|
||||
*
|
||||
* @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 getPartiesToSend(ltx: LedgerTransaction): Set<Party> {
|
||||
val participants = ltx.outputStates.flatMap { it.participants } + ltx.inputStates.flatMap { it.participants }
|
||||
return groupAbstractPartyByWellKnownParty(serviceHub, participants).keys + extraRecipients
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to extract all participants from a ledger transaction. Intended to help implement [lookupParties]
|
||||
* overriding functions.
|
||||
*/
|
||||
protected fun extractParticipants(ltx: LedgerTransaction): List<AbstractParty> {
|
||||
return ltx.outputStates.flatMap { it.participants } + ltx.inputStates.flatMap { it.participants }
|
||||
private fun verifyTx(): LedgerTransaction {
|
||||
val notary = transaction.tx.notary
|
||||
// The notary signature(s) are allowed to be missing but no others.
|
||||
if (notary != null) transaction.verifySignaturesExcept(notary.owningKey) else transaction.verifyRequiredSignatures()
|
||||
val ltx = transaction.toLedgerTransaction(serviceHub, false)
|
||||
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.
|
||||
* 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).
|
||||
*
|
||||
* [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 net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.internal.FlowStateMachine
|
||||
import net.corda.core.internal.abbreviate
|
||||
import net.corda.core.messaging.DataFeed
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
@ -53,15 +55,39 @@ abstract class FlowLogic<out T> {
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
@Deprecated("Use FlowSession.getFlowInfo()", level = DeprecationLevel.WARNING)
|
||||
@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
|
||||
@ -76,6 +102,7 @@ abstract class FlowLogic<out T> {
|
||||
*
|
||||
* @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> {
|
||||
return sendAndReceive(R::class.java, otherParty, payload)
|
||||
}
|
||||
@ -91,6 +118,7 @@ abstract class FlowLogic<out T> {
|
||||
*
|
||||
* @returns an [UntrustworthyData] wrapper around the received object.
|
||||
*/
|
||||
@Deprecated("Use FlowSession.sendAndReceive()", level = DeprecationLevel.WARNING)
|
||||
@Suspendable
|
||||
open fun <R : Any> sendAndReceive(receiveType: Class<R>, otherParty: Party, payload: Any): UntrustworthyData<R> {
|
||||
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
|
||||
* 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> {
|
||||
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
|
||||
* 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)
|
||||
|
||||
/**
|
||||
@ -127,6 +167,7 @@ abstract class FlowLogic<out T> {
|
||||
*
|
||||
* @returns an [UntrustworthyData] wrapper around the received object.
|
||||
*/
|
||||
@Deprecated("Use FlowSession.receive()", level = DeprecationLevel.WARNING)
|
||||
@Suspendable
|
||||
open fun <R : Any> receive(receiveType: Class<R>, otherParty: Party): UntrustworthyData<R> {
|
||||
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
|
||||
* network's event horizon time.
|
||||
*/
|
||||
@Deprecated("Use FlowSession.send()", level = DeprecationLevel.WARNING)
|
||||
@Suspendable
|
||||
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.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class FlowContext(
|
||||
data class FlowInfo(
|
||||
/**
|
||||
* The integer flow version the other side is using.
|
||||
* @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 stx = SignedTransaction(tx, listOf(mySignature))
|
||||
|
||||
return AbstractStateReplacementFlow.UpgradeTx(stx, participantKeys, myKey)
|
||||
return AbstractStateReplacementFlow.UpgradeTx(stx)
|
||||
}
|
||||
|
||||
/** 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.UniquenessProvider
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.FilteredTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
import net.corda.core.utilities.unwrap
|
||||
import java.security.SignatureException
|
||||
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
|
||||
* 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)
|
||||
}
|
||||
|
||||
lateinit var notaryParty: Party
|
||||
|
||||
@Suspendable
|
||||
@Throws(NotaryException::class)
|
||||
override fun call(): List<TransactionSignature> {
|
||||
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 }) {
|
||||
"Input states must have the same Notary"
|
||||
}
|
||||
@ -65,16 +65,17 @@ object NotaryFlow {
|
||||
}
|
||||
|
||||
val response = try {
|
||||
val session = initiateFlow(notaryParty)
|
||||
if (serviceHub.networkMapCache.isValidatingNotary(notaryParty)) {
|
||||
subFlow(SendTransactionWithRetry(notaryParty, stx))
|
||||
receive<List<TransactionSignature>>(notaryParty)
|
||||
subFlow(SendTransactionWithRetry(session, stx))
|
||||
session.receive<List<TransactionSignature>>()
|
||||
} else {
|
||||
val tx: Any = if (stx.isNotaryChangeTransaction()) {
|
||||
stx.notaryChangeTx
|
||||
} 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) {
|
||||
if (e.error is NotaryError.Conflict) {
|
||||
@ -84,17 +85,28 @@ object NotaryFlow {
|
||||
}
|
||||
|
||||
return response.unwrap { signatures ->
|
||||
signatures.forEach { validateSignature(it, stx.id) }
|
||||
signatures.forEach { validateSignature(it, stx.id, notaryParty) }
|
||||
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" }
|
||||
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.
|
||||
*
|
||||
@ -104,13 +116,14 @@ object NotaryFlow {
|
||||
* Additional transaction validation logic can be added when implementing [receiveAndVerifyTx].
|
||||
*/
|
||||
// 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
|
||||
override fun call(): Void? {
|
||||
val (id, inputs, timeWindow) = receiveAndVerifyTx()
|
||||
val (id, inputs, timeWindow, notary) = receiveAndVerifyTx()
|
||||
checkNotary(notary)
|
||||
service.validateTimeWindow(timeWindow)
|
||||
service.commitInputStates(inputs, id, otherSide)
|
||||
service.commitInputStates(inputs, id, otherSideSession.counterparty)
|
||||
signAndSendResponse(id)
|
||||
return null
|
||||
}
|
||||
@ -122,10 +135,17 @@ object NotaryFlow {
|
||||
@Suspendable
|
||||
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
|
||||
private fun signAndSendResponse(txId: SecureHash) {
|
||||
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
|
||||
* 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")
|
||||
|
||||
@ -150,13 +170,6 @@ sealed class NotaryError {
|
||||
data class TransactionInvalid(val cause: Throwable) : NotaryError() {
|
||||
override fun toString() = cause.toString()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
object WrongNotary: NotaryError()
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.ResolveTransactionsFlow
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.unwrap
|
||||
@ -11,18 +10,26 @@ import java.security.SignatureException
|
||||
/**
|
||||
* 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]
|
||||
* and perform the resolution back-and-forth required to check the dependencies and download any missing attachments.
|
||||
* The flow will return the [SignedTransaction] after it is resolved and then verified using [SignedTransaction.verify].
|
||||
* This flow is a combination of [FlowSession.receive], resolve and [SignedTransaction.verify]. This flow will receive the
|
||||
* [SignedTransaction] and perform the resolution back-and-forth required to check the dependencies and download any missing
|
||||
* 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
|
||||
@JvmOverloads
|
||||
constructor(private val otherParty: Party, private val checkSufficientSignatures: Boolean = true) : FlowLogic<SignedTransaction>() {
|
||||
class ReceiveTransactionFlow(private val otherSideSession: FlowSession,
|
||||
private val checkSufficientSignatures: Boolean) : FlowLogic<SignedTransaction>() {
|
||||
/** Receives a [SignedTransaction] from [otherSideSession], verifies it and then records it in the vault. */
|
||||
constructor(otherSideSession: FlowSession) : this(otherSideSession, true)
|
||||
|
||||
@Suspendable
|
||||
@Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class)
|
||||
@Throws(SignatureException::class,
|
||||
AttachmentResolutionException::class,
|
||||
TransactionResolutionException::class,
|
||||
TransactionVerificationException::class)
|
||||
override fun call(): SignedTransaction {
|
||||
return receive<SignedTransaction>(otherParty).unwrap {
|
||||
subFlow(ResolveTransactionsFlow(it, otherParty))
|
||||
return otherSideSession.receive<SignedTransaction>().unwrap {
|
||||
subFlow(ResolveTransactionsFlow(it, otherSideSession))
|
||||
it.verify(serviceHub, checkSufficientSignatures)
|
||||
it
|
||||
}
|
||||
@ -32,16 +39,16 @@ constructor(private val otherParty: Party, private val checkSufficientSignatures
|
||||
/**
|
||||
* 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.
|
||||
* 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.
|
||||
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
|
||||
override fun call(): List<StateAndRef<T>> {
|
||||
return receive<List<StateAndRef<T>>>(otherParty).unwrap {
|
||||
subFlow(ResolveTransactionsFlow(it.map { it.ref.txhash }.toSet(), otherParty))
|
||||
return otherSideSession.receive<List<StateAndRef<T>>>().unwrap {
|
||||
subFlow(ResolveTransactionsFlow(it.map { it.ref.txhash }.toSet(), otherSideSession))
|
||||
it
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.FetchDataFlow
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
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.
|
||||
*
|
||||
* @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
|
||||
@ -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
|
||||
* required to check the dependencies.
|
||||
*
|
||||
* @param otherSide the target party.
|
||||
* @param stateAndRefs the list of [StateAndRef] being sent to the [otherSide].
|
||||
* @param otherSideSession the target session.
|
||||
* @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
|
||||
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
|
||||
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? {
|
||||
// The first payload will be the transaction data, subsequent payload will be the transaction/attachment data.
|
||||
var payload = payload
|
||||
// This loop will receive [FetchDataFlow.Request] continuously until the `otherSide` has all the data they need
|
||||
// to resolve the transaction, a [FetchDataFlow.EndRequest] will be sent from the `otherSide` to indicate end of
|
||||
// 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 `otherSideSession` to indicate end of
|
||||
// data request.
|
||||
while (true) {
|
||||
val dataRequest = sendPayloadAndReceiveDataRequest(otherSide, payload).unwrap { request ->
|
||||
val dataRequest = sendPayloadAndReceiveDataRequest(otherSideSession, payload).unwrap { request ->
|
||||
when (request) {
|
||||
is FetchDataFlow.Request.Data -> {
|
||||
// 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.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
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 */
|
||||
override fun equals(other: Any?): Boolean = other === this || other is AbstractParty && other.owningKey == owningKey
|
||||
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
|
||||
|
@ -3,7 +3,6 @@ package net.corda.core.identity
|
||||
import net.corda.core.contracts.PartyAndReference
|
||||
import net.corda.core.crypto.toStringShort
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
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.
|
||||
*/
|
||||
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 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
|
||||
|
||||
import net.corda.core.contracts.PartyAndReference
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
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
|
||||
@ -27,9 +26,10 @@ import java.security.PublicKey
|
||||
*
|
||||
* @see CompositeKey
|
||||
*/
|
||||
class Party(val name: X500Name, owningKey: PublicKey) : AbstractParty(owningKey) {
|
||||
constructor(certificate: X509CertificateHolder) : this(certificate.subject, Crypto.toSupportedPublicKey(certificate.subjectPublicKeyInfo))
|
||||
override fun nameOrNull(): X500Name = name
|
||||
class Party(val name: CordaX500Name, owningKey: PublicKey) : AbstractParty(owningKey) {
|
||||
constructor(certificate: X509Certificate)
|
||||
: this(CordaX500Name.build(certificate.subjectX500Principal), Crypto.toSupportedPublicKey(certificate.publicKey))
|
||||
override fun nameOrNull(): CordaX500Name = name
|
||||
fun anonymise(): AnonymousParty = AnonymousParty(owningKey)
|
||||
override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes)
|
||||
override fun toString() = name.toString()
|
||||
|
@ -1,8 +1,6 @@
|
||||
package net.corda.core.identity
|
||||
|
||||
import net.corda.core.internal.toX509CertHolder
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.security.PublicKey
|
||||
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,
|
||||
* not part of the identifier themselves.
|
||||
*/
|
||||
//TODO Is VerifiableIdentity a better name?
|
||||
@CordaSerializable
|
||||
class PartyAndCertificate(val certPath: CertPath) {
|
||||
@Transient val certificate: X509CertificateHolder
|
||||
@Transient val certificate: X509Certificate
|
||||
init {
|
||||
require(certPath.type == "X.509") { "Only X.509 certificates supported" }
|
||||
val certs = certPath.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)
|
||||
|
||||
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 component2(): X509CertificateHolder = certificate
|
||||
operator fun component2(): X509Certificate = certificate
|
||||
|
||||
override fun equals(other: Any?): Boolean = other === this || other is PartyAndCertificate && other.party == party
|
||||
override fun hashCode(): Int = party.hashCode()
|
||||
|
@ -10,6 +10,7 @@ import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.security.CodeSigner
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.jar.JarInputStream
|
||||
|
||||
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> */
|
||||
private val unsignableEntryName = "META-INF/(?:.*[.](?:SF|DSA|RSA)|SIG-.*)".toRegex()
|
||||
private val shredder = ByteArray(1024)
|
||||
}
|
||||
|
||||
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":
|
||||
var attachmentSigners: MutableSet<CodeSigner>? = null
|
||||
openAsJAR().use { jar ->
|
||||
val shredder = ByteArray(1024)
|
||||
while (true) {
|
||||
val entry = jar.nextJarEntry ?: break
|
||||
if (entry.isDirectory || unsignableEntryName.matches(entry.name)) continue
|
||||
@ -44,7 +45,7 @@ abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment {
|
||||
}
|
||||
}
|
||||
(attachmentSigners ?: emptySet<CodeSigner>()).map {
|
||||
Party(it.signerCertPath.certificates[0].toX509CertHolder())
|
||||
Party(it.signerCertPath.certificates[0] as X509Certificate)
|
||||
}.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.flows.FlowException
|
||||
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.HashNotFound
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
@ -38,7 +38,7 @@ import java.util.*
|
||||
*/
|
||||
sealed class FetchDataFlow<T : NamedByHash, in W : Any>(
|
||||
protected val requests: Set<SecureHash>,
|
||||
protected val otherSide: Party,
|
||||
protected val otherSideSession: FlowSession,
|
||||
protected val dataType: DataType) : FlowLogic<FetchDataFlow.Result<T>>() {
|
||||
|
||||
@CordaSerializable
|
||||
@ -72,7 +72,7 @@ sealed class FetchDataFlow<T : NamedByHash, in W : Any>(
|
||||
return if (toFetch.isEmpty()) {
|
||||
Result(fromDisk, emptyList())
|
||||
} else {
|
||||
logger.info("Requesting ${toFetch.size} dependency(s) for verification from ${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.
|
||||
// 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) {
|
||||
// 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.
|
||||
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.
|
||||
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)
|
||||
Result(fromDisk, downloaded)
|
||||
}
|
||||
@ -140,7 +140,7 @@ sealed class FetchDataFlow<T : NamedByHash, in W : Any>(
|
||||
* attachments are saved to local storage automatically.
|
||||
*/
|
||||
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)
|
||||
|
||||
@ -171,7 +171,7 @@ class FetchAttachmentsFlow(requests: Set<SecureHash>,
|
||||
* 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.
|
||||
*/
|
||||
class FetchTransactionsFlow(requests: Set<SecureHash>, otherSide: Party) :
|
||||
class FetchTransactionsFlow(requests: Set<SecureHash>, otherSide: FlowSession) :
|
||||
FetchDataFlow<SignedTransaction, SignedTransaction>(requests, otherSide, DataType.TRANSACTION) {
|
||||
|
||||
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.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
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]. */
|
||||
interface FlowStateMachine<R> {
|
||||
@Suspendable
|
||||
fun getFlowContext(otherParty: Party, sessionFlow: FlowLogic<*>): FlowContext
|
||||
fun getFlowInfo(otherParty: Party, sessionFlow: FlowLogic<*>): FlowInfo
|
||||
|
||||
@Suspendable
|
||||
fun initiateFlow(otherParty: Party, sessionFlow: FlowLogic<*>): FlowSession
|
||||
|
||||
@Suspendable
|
||||
fun <T : Any> sendAndReceive(receiveType: Class<T>,
|
||||
@ -46,4 +50,5 @@ interface FlowStateMachine<R> {
|
||||
val id: StateMachineRunId
|
||||
val resultFuture: CordaFuture<R>
|
||||
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.sha256
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
||||
import org.slf4j.Logger
|
||||
import rx.Observable
|
||||
import rx.Observer
|
||||
@ -15,6 +16,8 @@ import java.nio.charset.Charset
|
||||
import java.nio.charset.StandardCharsets.UTF_8
|
||||
import java.nio.file.*
|
||||
import java.nio.file.attribute.FileAttribute
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Duration
|
||||
import java.time.temporal.Temporal
|
||||
import java.util.*
|
||||
@ -26,6 +29,7 @@ import java.util.zip.Deflater
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.full.createInstance
|
||||
|
||||
val Throwable.rootCause: Throwable get() = cause?.rootCause ?: this
|
||||
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 javax.security.cert.Certificate.toX509CertHolder() = X509CertificateHolder(encoded)
|
||||
fun Certificate.toX509CertHolder() = X509CertificateHolder(encoded)
|
||||
val X509CertificateHolder.cert: X509Certificate get() = JcaX509CertificateConverter().getCertificate(this)
|
||||
|
||||
/** Convert a [ByteArrayOutputStream] to [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)
|
||||
|
||||
/** 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
|
||||
* visibility.
|
||||
@ -260,3 +269,8 @@ class DeclaredField<T>(clazz: Class<*>, name: String, private val receiver: Any?
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
@MustBeDocumented
|
||||
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 net.corda.core.crypto.SecureHash
|
||||
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.transactions.SignedTransaction
|
||||
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.
|
||||
*/
|
||||
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
|
||||
* *not* validate or store the [signedTransaction] itself.
|
||||
*
|
||||
* @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
|
||||
}
|
||||
companion object {
|
||||
@ -82,7 +82,7 @@ class ResolveTransactionsFlow(private val txHashes: Set<SecureHash>,
|
||||
// Start fetching data.
|
||||
val newTxns = downloadDependencies(txHashes)
|
||||
fetchMissingAttachments(signedTransaction?.let { newTxns + it } ?: newTxns)
|
||||
send(otherSide, FetchDataFlow.Request.End)
|
||||
otherSide.send(FetchDataFlow.Request.End)
|
||||
// Finish fetching data.
|
||||
|
||||
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.contracts.ContractState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.UpgradedContract
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowInitiator
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.NodeInfo
|
||||
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.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.Try
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import rx.Observable
|
||||
import java.io.InputStream
|
||||
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
|
||||
* 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>
|
||||
|
||||
/**
|
||||
* 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
|
||||
fun stateMachinesFeed(): DataFeed<List<StateMachineInfo>, StateMachineUpdate>
|
||||
@ -97,24 +94,24 @@ interface CordaRPCOps : RPCOps {
|
||||
fun <T : ContractState> vaultQueryBy(criteria: QueryCriteria,
|
||||
paging: PageSpecification,
|
||||
sorting: Sort,
|
||||
contractType: Class<out T>): Vault.Page<T>
|
||||
contractStateType: Class<out T>): Vault.Page<T>
|
||||
// DOCEND VaultQueryByAPI
|
||||
|
||||
// Note: cannot apply @JvmOverloads to interfaces nor interface implementations
|
||||
// Java Helpers
|
||||
|
||||
// DOCSTART VaultQueryAPIHelpers
|
||||
fun <T : ContractState> vaultQuery(contractType: Class<out T>): Vault.Page<T> {
|
||||
return vaultQueryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractType)
|
||||
fun <T : ContractState> vaultQuery(contractStateType: Class<out T>): Vault.Page<T> {
|
||||
return vaultQueryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractStateType)
|
||||
}
|
||||
fun <T : ContractState> vaultQueryByCriteria(criteria: QueryCriteria, contractType: Class<out T>): Vault.Page<T> {
|
||||
return vaultQueryBy(criteria, PageSpecification(), Sort(emptySet()), contractType)
|
||||
fun <T : ContractState> vaultQueryByCriteria(criteria: QueryCriteria, contractStateType: Class<out T>): Vault.Page<T> {
|
||||
return vaultQueryBy(criteria, PageSpecification(), Sort(emptySet()), contractStateType)
|
||||
}
|
||||
fun <T : ContractState> vaultQueryByWithPagingSpec(contractType: Class<out T>, criteria: QueryCriteria, paging: PageSpecification): Vault.Page<T> {
|
||||
return vaultQueryBy(criteria, paging, Sort(emptySet()), contractType)
|
||||
fun <T : ContractState> vaultQueryByWithPagingSpec(contractStateType: Class<out T>, criteria: QueryCriteria, paging: PageSpecification): Vault.Page<T> {
|
||||
return vaultQueryBy(criteria, paging, Sort(emptySet()), contractStateType)
|
||||
}
|
||||
fun <T : ContractState> vaultQueryByWithSorting(contractType: Class<out T>, criteria: QueryCriteria, sorting: Sort): Vault.Page<T> {
|
||||
return vaultQueryBy(criteria, PageSpecification(), sorting, contractType)
|
||||
fun <T : ContractState> vaultQueryByWithSorting(contractStateType: Class<out T>, criteria: QueryCriteria, sorting: Sort): Vault.Page<T> {
|
||||
return vaultQueryBy(criteria, PageSpecification(), sorting, contractStateType)
|
||||
}
|
||||
// DOCEND VaultQueryAPIHelpers
|
||||
|
||||
@ -135,24 +132,24 @@ interface CordaRPCOps : RPCOps {
|
||||
fun <T : ContractState> vaultTrackBy(criteria: QueryCriteria,
|
||||
paging: PageSpecification,
|
||||
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
|
||||
|
||||
// Note: cannot apply @JvmOverloads to interfaces nor interface implementations
|
||||
// Java Helpers
|
||||
|
||||
// DOCSTART VaultTrackAPIHelpers
|
||||
fun <T : ContractState> vaultTrack(contractType: Class<out T>): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
||||
return vaultTrackBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractType)
|
||||
fun <T : ContractState> vaultTrack(contractStateType: Class<out T>): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
||||
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>> {
|
||||
return vaultTrackBy(criteria, PageSpecification(), Sort(emptySet()), contractType)
|
||||
fun <T : ContractState> vaultTrackByCriteria(contractStateType: Class<out T>, criteria: QueryCriteria): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
||||
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>> {
|
||||
return vaultTrackBy(criteria, paging, Sort(emptySet()), contractType)
|
||||
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()), contractStateType)
|
||||
}
|
||||
fun <T : ContractState> vaultTrackByWithSorting(contractType: Class<out T>, criteria: QueryCriteria, sorting: Sort): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
||||
return vaultTrackBy(criteria, PageSpecification(), sorting, contractType)
|
||||
fun <T : ContractState> vaultTrackByWithSorting(contractStateType: Class<out T>, criteria: QueryCriteria, sorting: Sort): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
||||
return vaultTrackBy(criteria, PageSpecification(), sorting, contractStateType)
|
||||
}
|
||||
// DOCEND VaultTrackAPIHelpers
|
||||
|
||||
@ -173,9 +170,7 @@ interface CordaRPCOps : RPCOps {
|
||||
@RPCReturnsObservables
|
||||
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>
|
||||
|
||||
/**
|
||||
@ -185,19 +180,19 @@ interface CordaRPCOps : RPCOps {
|
||||
@RPCReturnsObservables
|
||||
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>
|
||||
|
||||
/**
|
||||
* 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
|
||||
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
|
||||
fun <T> startFlowDynamic(logicType: Class<out FlowLogic<T>>, vararg args: Any?): FlowHandle<T>
|
||||
@ -209,39 +204,32 @@ interface CordaRPCOps : RPCOps {
|
||||
@RPCReturnsObservables
|
||||
fun <T> startTrackedFlowDynamic(logicType: Class<out FlowLogic<T>>, vararg args: Any?): FlowProgressHandle<T>
|
||||
|
||||
/**
|
||||
* Returns Node's identity, assuming this will not change while the node is running.
|
||||
*/
|
||||
fun nodeIdentity(): NodeInfo
|
||||
/** Returns Node's NodeInfo, assuming this will not change while the node is running. */
|
||||
fun nodeInfo(): 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)
|
||||
|
||||
/*
|
||||
* Retrieve existing note(s) for a given Vault transaction
|
||||
*/
|
||||
/** Retrieve existing note(s) for a given Vault transaction. */
|
||||
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
|
||||
|
||||
/**
|
||||
* Download an attachment JAR by ID
|
||||
*/
|
||||
/** Download an attachment JAR by ID. */
|
||||
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
|
||||
|
||||
/**
|
||||
* Returns the node's current time.
|
||||
*/
|
||||
/** Returns the node's current time. */
|
||||
fun currentNodeTime(): Instant
|
||||
|
||||
/**
|
||||
@ -261,16 +249,12 @@ interface CordaRPCOps : RPCOps {
|
||||
* @param party identity to determine well known identity for.
|
||||
* @return well known identity, if found.
|
||||
*/
|
||||
fun partyFromAnonymous(party: AbstractParty): Party?
|
||||
/**
|
||||
* Returns the [Party] corresponding to the given key, if found.
|
||||
*/
|
||||
fun wellKnownPartyFromAnonymous(party: AbstractParty): Party?
|
||||
/** Returns the [Party] corresponding to the given key, if found. */
|
||||
fun partyFromKey(key: PublicKey): Party?
|
||||
|
||||
/**
|
||||
* Returns the [Party] with the X.500 principal as it's [Party.name]
|
||||
*/
|
||||
fun partyFromX500Name(x500Name: X500Name): Party?
|
||||
/** Returns the [Party] with the X.500 principal as it's [Party.name]. */
|
||||
fun wellKnownPartyFromX500Name(x500Name: CordaX500Name): Party?
|
||||
|
||||
/**
|
||||
* 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>
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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()
|
||||
}
|
||||
|
||||
@ -310,15 +292,9 @@ inline fun <reified T : ContractState> CordaRPCOps.vaultTrackBy(criteria: QueryC
|
||||
return vaultTrackBy(criteria, paging, sorting, T::class.java)
|
||||
}
|
||||
|
||||
/**
|
||||
* These allow type safe invocations of flows from Kotlin, e.g.:
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
// 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(
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
flowConstructor: () -> R
|
||||
@ -330,6 +306,12 @@ inline fun <T , A, reified R : FlowLogic<T>> CordaRPCOps.startFlow(
|
||||
arg0: A
|
||||
): 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(
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
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)
|
||||
|
||||
/**
|
||||
* 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")
|
||||
inline fun <T, reified R : FlowLogic<T>> CordaRPCOps.startTrackedFlow(
|
||||
|
@ -2,7 +2,6 @@ package net.corda.core.messaging
|
||||
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
|
||||
|
||||
/** The interface for a group of message recipients (which may contain only one recipient) */
|
||||
@CordaSerializable
|
||||
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.
|
||||
*/
|
||||
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.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.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.
|
||||
* @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.
|
||||
@CordaSerializable
|
||||
data class NodeInfo(val addresses: List<NetworkHostAndPort>,
|
||||
// TODO After removing of services these two fields will be merged together and made NonEmptySet.
|
||||
val legalIdentityAndCert: PartyAndCertificate,
|
||||
val legalIdentitiesAndCerts: Set<PartyAndCertificate>,
|
||||
val legalIdentitiesAndCerts: List<PartyAndCertificate>,
|
||||
val platformVersion: Int,
|
||||
val advertisedServices: List<ServiceEntry> = emptyList(),
|
||||
val serial: Long
|
||||
) {
|
||||
init {
|
||||
require(advertisedServices.none { it.identity == legalIdentityAndCert }) {
|
||||
"Service identities must be different from node legal identity"
|
||||
}
|
||||
require(legalIdentitiesAndCerts.isNotEmpty()) { "Node should have at least one legal identity" }
|
||||
}
|
||||
|
||||
val legalIdentity: Party get() = legalIdentityAndCert.party
|
||||
val notaryIdentity: Party get() = advertisedServices.single { it.info.type.isNotary() }.identity.party
|
||||
fun serviceIdentities(type: ServiceType): List<Party> {
|
||||
return advertisedServices.mapNotNull { if (it.info.type.isSubTypeOf(type)) it.identity.party else null }
|
||||
@Transient private var _legalIdentities: List<Party>? = null
|
||||
val legalIdentities: List<Party> get() {
|
||||
return _legalIdentities ?: legalIdentitiesAndCerts.map { it.party }.also { _legalIdentities = it }
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses node's owner X500 name to infer the node's location. Used in Explorer in map view.
|
||||
*/
|
||||
fun getWorldMapLocation(): WorldMapLocation? {
|
||||
val nodeOwnerLocation = legalIdentity.name.locality
|
||||
return nodeOwnerLocation.let { CityDatabase[it] }
|
||||
}
|
||||
/** Returns true if [party] is one of the identities of this node, else false. */
|
||||
fun isLegalIdentity(party: Party): Boolean = party in legalIdentities
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.cordapp.CordappProvider
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SignableData
|
||||
import net.corda.core.crypto.SignatureMetadata
|
||||
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.serialization.SerializeAsToken
|
||||
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). */
|
||||
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].
|
||||
*
|
||||
@ -123,38 +131,17 @@ interface ServiceHub : ServicesForResolution {
|
||||
fun <T : ContractState> toStateAndRef(stateRef: StateRef): StateAndRef<T> {
|
||||
val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash)
|
||||
return if (stx.isNotaryChangeTransaction()) {
|
||||
stx.resolveNotaryChangeTransaction(this).outRef<T>(stateRef.index)
|
||||
stx.resolveNotaryChangeTransaction(this).outRef(stateRef.index)
|
||||
} else {
|
||||
stx.tx.outRef<T>(stateRef.index)
|
||||
stx.tx.outRef(stateRef.index)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
private val legalIdentityKey: PublicKey get() = this.myInfo.legalIdentitiesAndCerts.first().owningKey
|
||||
|
||||
// Helper method to construct an initial partially signed transaction from a [TransactionBuilder].
|
||||
private fun signInitialTransaction(builder: TransactionBuilder, publicKey: PublicKey, signatureMetadata: SignatureMetadata): SignedTransaction {
|
||||
return builder.toSignedTransaction(keyManagementService, publicKey, signatureMetadata)
|
||||
return builder.toSignedTransaction(keyManagementService, publicKey, signatureMetadata, this)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -288,4 +275,4 @@ interface ServiceHub : ServicesForResolution {
|
||||
* @return A new [Connection]
|
||||
*/
|
||||
fun jdbcSession(): Connection
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.nio.file.FileAlreadyExistsException
|
||||
|
||||
typealias AttachmentId = SecureHash
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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
|
||||
@ -28,6 +30,6 @@ interface AttachmentStorage {
|
||||
* @throws IOException if something went wrong.
|
||||
*/
|
||||
@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
|
||||
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.serialization.SerializeAsToken
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
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.
|
||||
* Such a class needs to have a constructor with a single parameter of type [net.corda.core.node.PluginServiceHub]. This
|
||||
* construtor will be invoked during node start to initialise the service. The service hub provided can be used to get
|
||||
* information about the node that may be necessary for the service. Corda services are created as singletons within
|
||||
* the node and are available to flows via [net.corda.core.node.ServiceHub.cordaService].
|
||||
* Such a class needs to have a constructor with a single parameter of type [ServiceHub]. This constructor will be invoked
|
||||
* during node start to initialise the service. The service hub provided can be used to get information about the node
|
||||
* that may be necessary for the service. Corda services are created as singletons within the node and are available
|
||||
* to flows via [ServiceHub.cordaService].
|
||||
*
|
||||
* The service class has to implement [net.corda.core.serialization.SerializeAsToken] to ensure correct usage within flows.
|
||||
* (If possible extend [net.corda.core.serialization.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.
|
||||
* The service class has to implement [SerializeAsToken] to ensure correct usage within flows. (If possible extend
|
||||
* [SingletonSerializeAsToken] instead as it removes the boilerplate.)
|
||||
*/
|
||||
// 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
|
||||
|
@ -1,12 +1,7 @@
|
||||
package net.corda.core.node.services
|
||||
|
||||
import net.corda.core.contracts.PartyAndReference
|
||||
import net.corda.core.identity.AbstractParty
|
||||
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 net.corda.core.identity.*
|
||||
import java.security.InvalidAlgorithmParameterException
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.*
|
||||
@ -18,20 +13,9 @@ import java.security.cert.*
|
||||
*/
|
||||
interface IdentityService {
|
||||
val trustRoot: X509Certificate
|
||||
val trustRootHolder: X509CertificateHolder
|
||||
val trustAnchor: TrustAnchor
|
||||
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.
|
||||
*
|
||||
@ -58,46 +42,48 @@ interface IdentityService {
|
||||
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.
|
||||
*/
|
||||
fun certificateFromKey(owningKey: PublicKey): PartyAndCertificate?
|
||||
|
||||
/**
|
||||
* Get the certificate and path for a well known identity.
|
||||
*
|
||||
* @return the party and certificate.
|
||||
* @throws IllegalArgumentException if the certificate and path are unknown. This should never happen 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,
|
||||
* or as a part of flows creating and exchanging the identity.
|
||||
* @param key The owning [PublicKey] of the [Party].
|
||||
* @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 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
|
||||
* from a confidential identity, however it transparently handles returning the well known identity back if
|
||||
* Returns the well known identity from an [AbstractParty]. This is intended to resolve the well known identity,
|
||||
* 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.
|
||||
*
|
||||
* @param party identity to determine well known identity for.
|
||||
* @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
|
||||
* (i.e. a [Party]) this returns it as-is.
|
||||
* Returns the well known identity from a PartyAndReference. This is intended to resolve the well known identity,
|
||||
* 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.
|
||||
*/
|
||||
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.
|
||||
@ -106,7 +92,7 @@ interface IdentityService {
|
||||
* @return the well known identity.
|
||||
* @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
|
||||
|
@ -1,15 +1,13 @@
|
||||
package net.corda.core.node.services
|
||||
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.contracts.Contract
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.randomOrNull
|
||||
import net.corda.core.messaging.DataFeed
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import rx.Observable
|
||||
import java.security.PublicKey
|
||||
|
||||
@ -30,19 +28,14 @@ interface NetworkMapCache {
|
||||
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
|
||||
* the scope of the network map service, and this is intended solely as a sanity check on configuration stored
|
||||
* elsewhere.
|
||||
* A list of notary services available on the network.
|
||||
*
|
||||
* 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)
|
||||
/** Tracks changes to the network map cache */
|
||||
// TODO this list will be taken from NetworkParameters distributed by NetworkMap.
|
||||
val notaryIdentities: List<Party>
|
||||
/** Tracks changes to the network map cache. */
|
||||
val changed: Observable<MapChange>
|
||||
/** Future to track completion of the NetworkMapService registration. */
|
||||
val nodeReady: CordaFuture<Void?>
|
||||
@ -53,18 +46,6 @@ interface NetworkMapCache {
|
||||
*/
|
||||
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
|
||||
* 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?
|
||||
|
||||
/** 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. */
|
||||
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
|
||||
* the services it provides. In case of a distributed service – run by multiple nodes – each participant advertises
|
||||
* the identity of the *whole group*.
|
||||
*/
|
||||
|
||||
/** 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 } }
|
||||
}
|
||||
fun getNodesByLegalIdentityKey(identityKey: PublicKey): List<NodeInfo>
|
||||
|
||||
/** Returns information about the party, which may be a specific node or a service */
|
||||
fun getPartyInfo(party: Party): PartyInfo?
|
||||
|
||||
/** Gets a notary identity by the given name. */
|
||||
fun getNotary(principal: X500Name): Party? {
|
||||
val notaryNode = notaryNodes.filter {
|
||||
it.advertisedServices.any { it.info.type.isSubTypeOf(ServiceType.notary) && it.info.name == principal }
|
||||
}.randomOrNull()
|
||||
return notaryNode?.notaryIdentity
|
||||
}
|
||||
fun getNotary(name: CordaX500Name): Party? = notaryIdentities.firstOrNull { it.name == name }
|
||||
|
||||
/**
|
||||
* Returns a notary identity advertised by any of the nodes on the network (chosen at random)
|
||||
* @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 advertised notary identity. */
|
||||
fun isNotary(party: Party): Boolean = party in notaryIdentities
|
||||
|
||||
/**
|
||||
* 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 */
|
||||
/** Checks whether a given party is an validating notary identity. */
|
||||
fun isValidatingNotary(party: Party): Boolean {
|
||||
val notary = notaryNodes.firstOrNull { it.notaryIdentity == party }
|
||||
?: throw IllegalArgumentException("No notary found with identity $party. This is most likely caused " +
|
||||
"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() }
|
||||
require(isNotary(party)) { "No notary found with identity $party." }
|
||||
return !party.name.toString().contains("corda.notary.simple", true) // TODO This implementation will change after introducing of NetworkParameters.
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all network map data from local node cache.
|
||||
*/
|
||||
/** Clear all network map data from local node cache. */
|
||||
fun clearNetworkMapCache()
|
||||
}
|
||||
|
@ -3,28 +3,27 @@ package net.corda.core.node.services
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.NotaryError
|
||||
import net.corda.core.flows.NotaryException
|
||||
import net.corda.core.flows.NotaryFlow
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import org.slf4j.Logger
|
||||
import java.security.PublicKey
|
||||
|
||||
abstract class NotaryService : SingletonSerializeAsToken() {
|
||||
abstract val services: ServiceHub
|
||||
abstract val notaryIdentityKey: PublicKey
|
||||
|
||||
abstract fun start()
|
||||
abstract fun stop()
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
return services.keyManagementService.sign(bits, services.notaryIdentityKey)
|
||||
return services.keyManagementService.sign(bits, notaryIdentityKey)
|
||||
}
|
||||
|
||||
fun sign(txId: SecureHash): TransactionSignature {
|
||||
val signableData = SignableData(txId, SignatureMetadata(services.myInfo.platformVersion, Crypto.findSignatureScheme(services.notaryIdentityKey).schemeNumberID))
|
||||
return services.keyManagementService.sign(signableData, services.notaryIdentityKey)
|
||||
val signableData = SignableData(txId, SignatureMetadata(services.myInfo.platformVersion, Crypto.findSignatureScheme(notaryIdentityKey).schemeNumberID))
|
||||
return services.keyManagementService.sign(signableData, notaryIdentityKey)
|
||||
}
|
||||
}
|
@ -1,21 +1,13 @@
|
||||
package net.corda.core.node.services
|
||||
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.ServiceEntry
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
|
||||
/**
|
||||
* Holds information about a [Party], which may refer to either a specific node or a service.
|
||||
*/
|
||||
sealed class PartyInfo {
|
||||
abstract val party: PartyAndCertificate
|
||||
|
||||
data class Node(val node: NodeInfo) : PartyInfo() {
|
||||
override val party get() = node.legalIdentityAndCert
|
||||
}
|
||||
|
||||
data class Service(val service: ServiceEntry) : PartyInfo() {
|
||||
override val party get() = service.identity
|
||||
}
|
||||
}
|
||||
abstract val party: Party
|
||||
data class SingleNode(override val party: Party, val addresses: List<NetworkHostAndPort>): PartyInfo()
|
||||
data class DistributedNode(override val party: Party): PartyInfo()
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.core.node.services
|
||||
|
||||
import net.corda.core.CordaException
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.Party
|
||||
@ -32,5 +33,4 @@ interface UniquenessProvider {
|
||||
data class ConsumingTx(val id: SecureHash, val inputIndex: Int, val requestingParty: Party)
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
class UniquenessException(val error: UniquenessProvider.Conflict) : Exception()
|
||||
class UniquenessException(val error: UniquenessProvider.Conflict) : CordaException(UniquenessException::class.java.name)
|
@ -33,7 +33,7 @@ interface VaultQueryService {
|
||||
fun <T : ContractState> _queryBy(criteria: QueryCriteria,
|
||||
paging: PageSpecification,
|
||||
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,
|
||||
@ -51,49 +51,49 @@ interface VaultQueryService {
|
||||
fun <T : ContractState> _trackBy(criteria: QueryCriteria,
|
||||
paging: PageSpecification,
|
||||
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
|
||||
|
||||
// Note: cannot apply @JvmOverloads to interfaces nor interface implementations
|
||||
// Java Helpers
|
||||
fun <T : ContractState> queryBy(contractType: Class<out T>): Vault.Page<T> {
|
||||
return _queryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractType)
|
||||
fun <T : ContractState> queryBy(contractStateType: Class<out T>): Vault.Page<T> {
|
||||
return _queryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractStateType)
|
||||
}
|
||||
|
||||
fun <T : ContractState> queryBy(contractType: Class<out T>, criteria: QueryCriteria): Vault.Page<T> {
|
||||
return _queryBy(criteria, PageSpecification(), Sort(emptySet()), contractType)
|
||||
fun <T : ContractState> queryBy(contractStateType: Class<out T>, criteria: QueryCriteria): Vault.Page<T> {
|
||||
return _queryBy(criteria, PageSpecification(), Sort(emptySet()), contractStateType)
|
||||
}
|
||||
|
||||
fun <T : ContractState> queryBy(contractType: Class<out T>, criteria: QueryCriteria, paging: PageSpecification): Vault.Page<T> {
|
||||
return _queryBy(criteria, paging, Sort(emptySet()), contractType)
|
||||
fun <T : ContractState> queryBy(contractStateType: Class<out T>, criteria: QueryCriteria, paging: PageSpecification): Vault.Page<T> {
|
||||
return _queryBy(criteria, paging, Sort(emptySet()), contractStateType)
|
||||
}
|
||||
|
||||
fun <T : ContractState> queryBy(contractType: Class<out T>, criteria: QueryCriteria, sorting: Sort): Vault.Page<T> {
|
||||
return _queryBy(criteria, PageSpecification(), sorting, contractType)
|
||||
fun <T : ContractState> queryBy(contractStateType: Class<out T>, criteria: QueryCriteria, sorting: Sort): Vault.Page<T> {
|
||||
return _queryBy(criteria, PageSpecification(), sorting, contractStateType)
|
||||
}
|
||||
|
||||
fun <T : ContractState> queryBy(contractType: Class<out T>, criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.Page<T> {
|
||||
return _queryBy(criteria, paging, sorting, contractType)
|
||||
fun <T : ContractState> queryBy(contractStateType: Class<out T>, criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.Page<T> {
|
||||
return _queryBy(criteria, paging, sorting, contractStateType)
|
||||
}
|
||||
|
||||
fun <T : ContractState> trackBy(contractType: Class<out T>): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
||||
return _trackBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractType)
|
||||
fun <T : ContractState> trackBy(contractStateType: Class<out T>): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
||||
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>> {
|
||||
return _trackBy(criteria, PageSpecification(), Sort(emptySet()), contractType)
|
||||
fun <T : ContractState> trackBy(contractStateType: Class<out T>, criteria: QueryCriteria): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
||||
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>> {
|
||||
return _trackBy(criteria, paging, Sort(emptySet()), contractType)
|
||||
fun <T : ContractState> trackBy(contractStateType: Class<out T>, criteria: QueryCriteria, paging: PageSpecification): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
||||
return _trackBy(criteria, paging, Sort(emptySet()), contractStateType)
|
||||
}
|
||||
|
||||
fun <T : ContractState> trackBy(contractType: Class<out T>, criteria: QueryCriteria, sorting: Sort): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
||||
return _trackBy(criteria, PageSpecification(), sorting, contractType)
|
||||
fun <T : ContractState> trackBy(contractStateType: Class<out T>, criteria: QueryCriteria, sorting: Sort): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
||||
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>> {
|
||||
return _trackBy(criteria, paging, sorting, contractType)
|
||||
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, contractStateType)
|
||||
}
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user