mirror of
https://github.com/corda/corda.git
synced 2024-12-27 08:22:35 +00:00
Merge pull request #466 from corda/aslemmer-merge-19-Feb
Aslemmer merge 19 feb
This commit is contained in:
commit
089916d350
1211
.ci/api-current.txt
1211
.ci/api-current.txt
File diff suppressed because it is too large
Load Diff
@ -31,8 +31,7 @@ if [ $removalCount -gt 0 ]; then
|
||||
fi
|
||||
|
||||
# Adding new abstract methods could also break the API.
|
||||
# However, first exclude anything with the @DoNotImplement annotation.
|
||||
|
||||
# However, first exclude classes marked with the @DoNotImplement annotation
|
||||
function forUserImpl() {
|
||||
awk '/DoNotImplement/,/^##/{ next }{ print }' $1
|
||||
}
|
||||
@ -45,13 +44,28 @@ $newAbstracts
|
||||
EOF
|
||||
`
|
||||
|
||||
#Get a list of any methods that expose classes in .internal. namespaces, and any classes which extend/implement
|
||||
#an internal class
|
||||
newInternalExposures=$(echo "$userDiffContents" | grep "^+" | grep "\.internal\." )
|
||||
|
||||
internalCount=`grep -v "^$" <<EOF | wc -l
|
||||
$newInternalExposures
|
||||
EOF
|
||||
`
|
||||
|
||||
echo "Number of new internal class exposures: "$internalCount
|
||||
if [ $internalCount -gt 0 ]; then
|
||||
echo "$newInternalExposures"
|
||||
echo
|
||||
fi
|
||||
|
||||
echo "Number of new abstract APIs: "$abstractCount
|
||||
if [ $abstractCount -gt 0 ]; then
|
||||
echo "$newAbstracts"
|
||||
echo
|
||||
fi
|
||||
|
||||
badChanges=$(($removalCount + $abstractCount))
|
||||
badChanges=$(($removalCount + $abstractCount + $internalCount))
|
||||
if [ $badChanges -gt 255 ]; then
|
||||
echo "OVERFLOW! Number of bad API changes: $badChanges"
|
||||
badChanges=255
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -6,6 +6,7 @@ tags
|
||||
.DS_Store
|
||||
*.log
|
||||
*.orig
|
||||
corda-docs-only-build
|
||||
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
|
||||
|
34
.idea/compiler.xml
generated
34
.idea/compiler.xml
generated
@ -25,11 +25,6 @@
|
||||
<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-cordform-common_main" target="1.8" />
|
||||
<module name="corda-cordform-common_test" target="1.8" />
|
||||
<module name="corda-core_integrationTest" target="1.8" />
|
||||
<module name="corda-core_smokeTest" target="1.8" />
|
||||
<module name="corda-finance_integrationTest" target="1.8" />
|
||||
<module name="corda-project_main" target="1.8" />
|
||||
<module name="corda-project_test" target="1.8" />
|
||||
<module name="cordapp_integrationTest" target="1.8" />
|
||||
@ -37,9 +32,6 @@
|
||||
<module name="cordapp_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_integrationTest" target="1.8" />
|
||||
<module name="core_main" target="1.8" />
|
||||
<module name="core_smokeTest" target="1.8" />
|
||||
@ -50,9 +42,6 @@
|
||||
<module name="demobench_main" target="1.8" />
|
||||
<module name="demobench_test" target="1.8" />
|
||||
<module name="docs_main" target="1.8" />
|
||||
<module name="docs_source_example-code_integrationTest" target="1.8" />
|
||||
<module name="docs_source_example-code_main" target="1.8" />
|
||||
<module name="docs_source_example-code_test" target="1.8" />
|
||||
<module name="docs_test" target="1.8" />
|
||||
<module name="example-code_integrationTest" target="1.8" />
|
||||
<module name="example-code_main" target="1.8" />
|
||||
@ -70,21 +59,10 @@
|
||||
<module name="finance_test" target="1.8" />
|
||||
<module name="flow-hook_main" target="1.8" />
|
||||
<module name="flow-hook_test" target="1.8" />
|
||||
<module name="gradle-plugins-cordapp_main" target="1.8" />
|
||||
<module name="gradle-plugins-cordapp_test" target="1.8" />
|
||||
<module name="gradle-plugins-cordform-common_main" target="1.8" />
|
||||
<module name="gradle-plugins-cordform-common_test" target="1.8" />
|
||||
<module name="graphs_main" target="1.8" />
|
||||
<module name="graphs_test" target="1.8" />
|
||||
<module name="intellij-plugin_main" target="1.8" />
|
||||
<module name="intellij-plugin_test" target="1.8" />
|
||||
<module name="irs-demo-cordapp_integrationTest" target="1.8" />
|
||||
<module name="irs-demo-cordapp_main" target="1.8" />
|
||||
<module name="irs-demo-cordapp_main~1" target="1.8" />
|
||||
<module name="irs-demo-cordapp_test" target="1.8" />
|
||||
<module name="irs-demo-cordapp_test~1" target="1.8" />
|
||||
<module name="irs-demo-web_main" target="1.8" />
|
||||
<module name="irs-demo-web_test" target="1.8" />
|
||||
<module name="irs-demo_integrationTest" target="1.8" />
|
||||
<module name="irs-demo_main" target="1.8" />
|
||||
<module name="irs-demo_test" target="1.8" />
|
||||
@ -128,8 +106,6 @@
|
||||
<module name="perftestcordapp_integrationTest" target="1.8" />
|
||||
<module name="perftestcordapp_main" target="1.8" />
|
||||
<module name="perftestcordapp_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" />
|
||||
@ -154,16 +130,6 @@
|
||||
<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="smoke-test-utils_testDriver" target="1.8" />
|
||||
<module name="source-example-code_integrationTest" target="1.8" />
|
||||
<module name="source-example-code_main" target="1.8" />
|
||||
<module name="source-example-code_test" target="1.8" />
|
||||
<module name="test-common_main" target="1.8" />
|
||||
<module name="test-common_test" target="1.8" />
|
||||
<module name="test-utils_main" target="1.8" />
|
||||
<module name="test-utils_test" target="1.8" />
|
||||
<module name="testing-smoke-test-utils_main" target="1.8" />
|
||||
<module name="testing-smoke-test-utils_test" target="1.8" />
|
||||
<module name="testing-test-common_main" target="1.8" />
|
||||
|
25
build.gradle
25
build.gradle
@ -15,7 +15,9 @@ buildscript {
|
||||
//
|
||||
// TODO: Sort this alphabetically.
|
||||
ext.kotlin_version = constants.getProperty("kotlinVersion")
|
||||
ext.quasar_version = '0.7.9'
|
||||
// use our fork of quasar
|
||||
ext.quasar_group = 'com.github.corda.quasar'
|
||||
ext.quasar_version = '7629695563deae6cc95adcfbebcbc8322fd0241a'
|
||||
|
||||
// gradle-capsule-plugin:1.0.2 contains capsule:1.0.1
|
||||
// TODO: Upgrade gradle-capsule-plugin to a version with capsule:1.0.3
|
||||
@ -37,10 +39,9 @@ buildscript {
|
||||
* https://issues.apache.org/jira/browse/ARTEMIS-1559
|
||||
*/
|
||||
ext.artemis_version = '2.2.0'
|
||||
ext.jackson_version = '2.9.2'
|
||||
ext.jackson_version = '2.9.3'
|
||||
ext.jetty_version = '9.4.7.v20170914'
|
||||
ext.jersey_version = '2.25'
|
||||
ext.jolokia_version = '1.3.7'
|
||||
ext.assertj_version = '3.8.0'
|
||||
ext.slf4j_version = '1.7.25'
|
||||
ext.log4j_version = '2.9.1'
|
||||
@ -71,6 +72,11 @@ buildscript {
|
||||
ext.liquibase_version = '3.5.3'
|
||||
ext.shadow_version = '2.0.2'
|
||||
ext.hikari_version = '2.5.1'
|
||||
ext.snake_yaml_version = constants.getProperty('snakeYamlVersion')
|
||||
ext.docker_compose_rule_version = '0.33.0'
|
||||
ext.selenium_version = '3.8.1'
|
||||
ext.ghostdriver_version = '2.1.0'
|
||||
ext.eaagentloader_version = '1.0.3'
|
||||
ext.curator_version = '4.0.0'
|
||||
|
||||
// Update 121 is required for ObjectInputFilter and at time of writing 131 was latest:
|
||||
@ -80,6 +86,7 @@ buildscript {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
// This repository is needed for Dokka until 0.9.16 is released.
|
||||
maven {
|
||||
url 'https://dl.bintray.com/kotlin/kotlin-eap/'
|
||||
}
|
||||
@ -197,6 +204,10 @@ allprojects {
|
||||
systemProperty(it, property)
|
||||
}
|
||||
}
|
||||
|
||||
if (System.getProperty("test.maxParallelForks") != null) {
|
||||
maxParallelForks = Integer.valueOf(System.getProperty("test.maxParallelForks"))
|
||||
}
|
||||
}
|
||||
|
||||
group 'com.r3.corda'
|
||||
@ -379,8 +390,12 @@ task generateApi(type: net.corda.plugins.GenerateApi){
|
||||
}
|
||||
|
||||
// This exists to reduce CI build time when the envvar is set (can save up to 40 minutes)
|
||||
if(System.getenv('CORDA_DOCS_ONLY_BUILD') != null) {
|
||||
logger.info("Tests are disabled due to presence of envvar CORDA_DOCS_ONLY_BUILD")
|
||||
if(file('corda-docs-only-build').exists() || (System.getenv('CORDA_DOCS_ONLY_BUILD') != null)) {
|
||||
if(file('corda-docs-only-build').exists()) {
|
||||
logger.info("Tests are disabled due to presence of file 'corda-docs-only-build' in the project root")
|
||||
} else {
|
||||
logger.info("Tests are disabled due to the presence of envvar CORDA_DOCS_ONLY_BUILD")
|
||||
}
|
||||
|
||||
allprojects {
|
||||
test {
|
||||
|
@ -8,11 +8,11 @@ dependencies {
|
||||
compile project(':core')
|
||||
testCompile project(':test-utils')
|
||||
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||
|
||||
// Jackson and its plugins: parsing to/from JSON and other textual formats.
|
||||
compile "com.fasterxml.jackson.module:jackson-module-kotlin:${jackson_version}"
|
||||
compile "com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_version"
|
||||
// Yaml is useful for parsing strings to method calls.
|
||||
compile "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:$jackson_version"
|
||||
// This adds support for java.time types.
|
||||
|
@ -2,7 +2,7 @@ package net.corda.client.jfx
|
||||
|
||||
import net.corda.client.jfx.model.NodeMonitorModel
|
||||
import net.corda.client.jfx.model.ProgressTrackingEvent
|
||||
import net.corda.core.context.Origin
|
||||
import net.corda.core.context.InvocationOrigin
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
@ -29,11 +29,12 @@ import net.corda.finance.flows.CashPaymentFlow
|
||||
import net.corda.node.services.Permissions.Companion.invokeRpc
|
||||
import net.corda.node.services.Permissions.Companion.startFlow
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.node.User
|
||||
import net.corda.testing.internal.IntegrationTest
|
||||
import net.corda.testing.internal.IntegrationTestSchemas
|
||||
import net.corda.testing.internal.toDatabaseSchemaName
|
||||
import net.corda.testing.node.User
|
||||
import org.junit.ClassRule
|
||||
import org.junit.Test
|
||||
import rx.Observable
|
||||
@ -61,7 +62,7 @@ class NodeMonitorModelTest : IntegrationTest() {
|
||||
}
|
||||
|
||||
private fun setup(runTest: () -> Unit) {
|
||||
driver(extraCordappPackagesToScan = listOf("net.corda.finance")) {
|
||||
driver(DriverParameters(extraCordappPackagesToScan = listOf("net.corda.finance"))) {
|
||||
val cashUser = User("user1", "test", permissions = setOf(
|
||||
startFlow<CashIssueFlow>(),
|
||||
startFlow<CashPaymentFlow>(),
|
||||
@ -85,7 +86,7 @@ class NodeMonitorModelTest : IntegrationTest() {
|
||||
vaultUpdates = monitor.vaultUpdates.bufferUntilSubscribed()
|
||||
networkMapUpdates = monitor.networkMap.bufferUntilSubscribed()
|
||||
|
||||
monitor.register(aliceNodeHandle.configuration.rpcOptions.address!!, cashUser.username, cashUser.password)
|
||||
monitor.register(aliceNodeHandle.rpcAddress, cashUser.username, cashUser.password)
|
||||
rpc = monitor.proxyObservable.value!!
|
||||
notaryParty = defaultNotaryIdentity
|
||||
|
||||
@ -93,7 +94,7 @@ class NodeMonitorModelTest : IntegrationTest() {
|
||||
bobNode = bobNodeHandle.nodeInfo
|
||||
val monitorBob = NodeMonitorModel()
|
||||
stateMachineUpdatesBob = monitorBob.stateMachineUpdates.bufferUntilSubscribed()
|
||||
monitorBob.register(bobNodeHandle.configuration.rpcOptions.address!!, cashUser.username, cashUser.password)
|
||||
monitorBob.register(bobNodeHandle.rpcAddress, cashUser.username, cashUser.password)
|
||||
rpcBob = monitorBob.proxyObservable.value!!
|
||||
runTest()
|
||||
}
|
||||
@ -158,8 +159,8 @@ class NodeMonitorModelTest : IntegrationTest() {
|
||||
// ISSUE
|
||||
expect { add: StateMachineUpdate.Added ->
|
||||
issueSmId = add.id
|
||||
val context = add.stateMachineInfo.context()
|
||||
require(context.origin is Origin.RPC && context.principal().name == "user1")
|
||||
val context = add.stateMachineInfo.invocationContext
|
||||
require(context.origin is InvocationOrigin.RPC && context.principal().name == "user1")
|
||||
},
|
||||
expect { remove: StateMachineUpdate.Removed ->
|
||||
require(remove.id == issueSmId)
|
||||
@ -167,8 +168,8 @@ class NodeMonitorModelTest : IntegrationTest() {
|
||||
// MOVE - N.B. There are other framework flows that happen in parallel for the remote resolve transactions flow
|
||||
expect(match = { it.stateMachineInfo.flowLogicClassName == CashPaymentFlow::class.java.name }) { add: StateMachineUpdate.Added ->
|
||||
moveSmId = add.id
|
||||
val context = add.stateMachineInfo.context()
|
||||
require(context.origin is Origin.RPC && context.principal().name == "user1")
|
||||
val context = add.stateMachineInfo.invocationContext
|
||||
require(context.origin is InvocationOrigin.RPC && context.principal().name == "user1")
|
||||
},
|
||||
expect(match = { it is StateMachineUpdate.Removed && it.id == moveSmId }) {
|
||||
}
|
||||
@ -179,8 +180,8 @@ class NodeMonitorModelTest : IntegrationTest() {
|
||||
sequence(
|
||||
// MOVE
|
||||
expect { add: StateMachineUpdate.Added ->
|
||||
val context = add.stateMachineInfo.context()
|
||||
require(context.origin is Origin.Peer && aliceNode.isLegalIdentity(aliceNode.identityFromX500Name((context.origin as Origin.Peer).party)))
|
||||
val context = add.stateMachineInfo.invocationContext
|
||||
require(context.origin is InvocationOrigin.Peer && aliceNode.isLegalIdentity(aliceNode.identityFromX500Name((context.origin as InvocationOrigin.Peer).party)))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -15,6 +15,14 @@ configurations {
|
||||
smokeTestRuntime.extendsFrom runtime
|
||||
}
|
||||
|
||||
compileKotlin {
|
||||
kotlinOptions.jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
compileTestKotlin {
|
||||
kotlinOptions.jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
integrationTest {
|
||||
kotlin {
|
||||
|
@ -9,6 +9,7 @@ import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.internal.IntegrationTest
|
||||
import net.corda.testing.internal.IntegrationTestSchemas
|
||||
@ -36,7 +37,7 @@ class BlacklistKotlinClosureTest : IntegrationTest() {
|
||||
|
||||
@Test
|
||||
fun `closure sent via RPC`() {
|
||||
driver(startNodesInProcess = true) {
|
||||
driver(DriverParameters(startNodesInProcess = true)) {
|
||||
val rpc = startNode(providedName = ALICE_NAME).getOrThrow().rpc
|
||||
val packet = Packet { EVIL }
|
||||
assertThatExceptionOfType(KryoException::class.java)
|
||||
|
@ -162,11 +162,11 @@ class CordaRPCClientTest : NodeBasedTest(listOf("net.corda.finance.contracts", C
|
||||
},
|
||||
expect { update: StateMachineUpdate.Added ->
|
||||
checkRpcNotification(update.stateMachineInfo, rpcUser.username, historicalIds, externalTrace, impersonatedActor)
|
||||
sessionId = update.stateMachineInfo.context().trace.sessionId
|
||||
sessionId = update.stateMachineInfo.invocationContext.trace.sessionId
|
||||
},
|
||||
expect { update: StateMachineUpdate.Added ->
|
||||
checkRpcNotification(update.stateMachineInfo, rpcUser.username, historicalIds, externalTrace, impersonatedActor)
|
||||
assertThat(update.stateMachineInfo.context().trace.sessionId).isEqualTo(sessionId)
|
||||
assertThat(update.stateMachineInfo.invocationContext.trace.sessionId).isEqualTo(sessionId)
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -174,15 +174,13 @@ class CordaRPCClientTest : NodeBasedTest(listOf("net.corda.finance.contracts", C
|
||||
}
|
||||
|
||||
private fun checkShellNotification(info: StateMachineInfo) {
|
||||
|
||||
val context = info.context()
|
||||
assertThat(context.origin).isInstanceOf(Origin.Shell::class.java)
|
||||
val context = info.invocationContext
|
||||
assertThat(context.origin).isInstanceOf(InvocationOrigin.Shell::class.java)
|
||||
}
|
||||
|
||||
private fun checkRpcNotification(info: StateMachineInfo, rpcUsername: String, historicalIds: MutableSet<Trace.InvocationId>, externalTrace: Trace?, impersonatedActor: Actor?) {
|
||||
|
||||
val context = info.context()
|
||||
assertThat(context.origin).isInstanceOf(Origin.RPC::class.java)
|
||||
val context = info.invocationContext
|
||||
assertThat(context.origin).isInstanceOf(InvocationOrigin.RPC::class.java)
|
||||
assertThat(context.externalTrace).isEqualTo(externalTrace)
|
||||
assertThat(context.impersonatedActor).isEqualTo(impersonatedActor)
|
||||
assertThat(context.actor?.id?.value).isEqualTo(rpcUsername)
|
||||
|
@ -0,0 +1,110 @@
|
||||
package net.corda.client.rpc
|
||||
|
||||
import net.corda.core.context.Actor
|
||||
import net.corda.core.context.Trace
|
||||
import net.corda.core.internal.packageName
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.schemas.CashSchemaV1
|
||||
import net.corda.node.internal.Node
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.node.services.Permissions
|
||||
import net.corda.node.services.Permissions.Companion.invokeRpc
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.NodeHandle
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.node.User
|
||||
import net.corda.testing.node.internal.NodeBasedTest
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Assume.assumeFalse
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class FlowsExecutionModeRpcTest {
|
||||
|
||||
@Test
|
||||
fun `persistent state survives node restart`() {
|
||||
// Temporary disable this test when executed on Windows. It is known to be sporadically failing.
|
||||
// More investigation is needed to establish why.
|
||||
assumeFalse(System.getProperty("os.name").toLowerCase().startsWith("win"))
|
||||
|
||||
val user = User("mark", "dadada", setOf(invokeRpc("setFlowsDrainingModeEnabled"), invokeRpc("isFlowsDrainingModeEnabled")))
|
||||
driver(DriverParameters(isDebug = true, startNodesInProcess = true)) {
|
||||
val nodeName = {
|
||||
val nodeHandle = startNode(rpcUsers = listOf(user)).getOrThrow()
|
||||
val nodeName = nodeHandle.nodeInfo.chooseIdentity().name
|
||||
nodeHandle.rpc.setFlowsDrainingModeEnabled(true)
|
||||
nodeHandle.stop()
|
||||
nodeName
|
||||
}()
|
||||
|
||||
val nodeHandle = startNode(providedName = nodeName, rpcUsers = listOf(user)).getOrThrow()
|
||||
assertThat(nodeHandle.rpc.isFlowsDrainingModeEnabled()).isEqualTo(true)
|
||||
nodeHandle.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FlowsExecutionModeTests : NodeBasedTest(listOf("net.corda.finance.contracts", CashSchemaV1::class.packageName)) {
|
||||
|
||||
private val rpcUser = User("user1", "test", permissions = setOf(Permissions.all()))
|
||||
private lateinit var node: StartedNode<Node>
|
||||
private lateinit var client: CordaRPCClient
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
|
||||
node = startNode(ALICE_NAME, rpcUsers = listOf(rpcUser))
|
||||
client = CordaRPCClient(node.internals.configuration.rpcOptions.address!!)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flows draining mode can be enabled and queried`() {
|
||||
|
||||
asALoggerUser { rpcOps ->
|
||||
val newValue = true
|
||||
rpcOps.setFlowsDrainingModeEnabled(true)
|
||||
|
||||
val flowsExecutionMode = rpcOps.isFlowsDrainingModeEnabled()
|
||||
|
||||
assertThat(flowsExecutionMode).isEqualTo(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flows draining mode can be disabled and queried`() {
|
||||
|
||||
asALoggerUser { rpcOps ->
|
||||
rpcOps.setFlowsDrainingModeEnabled(true)
|
||||
val newValue = false
|
||||
rpcOps.setFlowsDrainingModeEnabled(newValue)
|
||||
|
||||
val flowsExecutionMode = rpcOps.isFlowsDrainingModeEnabled()
|
||||
|
||||
assertThat(flowsExecutionMode).isEqualTo(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `node starts with flows draining mode disabled`() {
|
||||
|
||||
asALoggerUser { rpcOps ->
|
||||
val defaultStartingMode = rpcOps.isFlowsDrainingModeEnabled()
|
||||
|
||||
assertThat(defaultStartingMode).isEqualTo(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun login(username: String, password: String, externalTrace: Trace? = null, impersonatedActor: Actor? = null): CordaRPCConnection {
|
||||
|
||||
return client.start(username, password, externalTrace, impersonatedActor)
|
||||
}
|
||||
|
||||
private fun asALoggerUser(action: (CordaRPCOps) -> Unit) {
|
||||
|
||||
login(rpcUser.username, rpcUser.password).use {
|
||||
action(it.proxy)
|
||||
}
|
||||
}
|
||||
}
|
@ -30,6 +30,7 @@ import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ScheduledExecutorService
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
class RPCStabilityTests {
|
||||
@Rule
|
||||
@ -127,7 +128,7 @@ class RPCStabilityTests {
|
||||
rpcDriver {
|
||||
fun startAndCloseServer(broker: RpcBrokerHandle) {
|
||||
startRpcServerWithBrokerRunning(
|
||||
configuration = RPCServerConfiguration.default.copy(consumerPoolSize = 1, producerPoolBound = 1),
|
||||
configuration = RPCServerConfiguration.default,
|
||||
ops = DummyOps,
|
||||
brokerHandle = broker
|
||||
).rpcServer.close()
|
||||
@ -148,7 +149,7 @@ class RPCStabilityTests {
|
||||
@Test
|
||||
fun `rpc client close doesnt leak broker resources`() {
|
||||
rpcDriver {
|
||||
val server = startRpcServer(configuration = RPCServerConfiguration.default.copy(consumerPoolSize = 1, producerPoolBound = 1), ops = DummyOps).get()
|
||||
val server = startRpcServer(configuration = RPCServerConfiguration.default, ops = DummyOps).get()
|
||||
RPCClient<RPCOps>(server.broker.hostAndPort!!).start(RPCOps::class.java, rpcTestUser.username, rpcTestUser.password).close()
|
||||
val initial = server.broker.getStats()
|
||||
repeat(100) {
|
||||
@ -337,11 +338,12 @@ class RPCStabilityTests {
|
||||
val request = RPCApi.ClientToServer.RpcRequest(
|
||||
clientAddress = SimpleString(myQueue),
|
||||
methodName = SlowConsumerRPCOps::streamAtInterval.name,
|
||||
serialisedArguments = listOf(10.millis, 123456).serialize(context = SerializationDefaults.RPC_SERVER_CONTEXT).bytes,
|
||||
serialisedArguments = listOf(10.millis, 123456).serialize(context = SerializationDefaults.RPC_SERVER_CONTEXT),
|
||||
replyId = Trace.InvocationId.newInstance(),
|
||||
sessionId = Trace.SessionId.newInstance()
|
||||
)
|
||||
request.writeToClientMessage(message)
|
||||
message.putLongProperty(RPCApi.DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME, 0)
|
||||
producer.send(message)
|
||||
session.commit()
|
||||
|
||||
@ -350,6 +352,79 @@ class RPCStabilityTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deduplication in the server`() {
|
||||
rpcDriver {
|
||||
val server = startRpcServer(ops = SlowConsumerRPCOpsImpl()).getOrThrow()
|
||||
|
||||
// Construct an RPC client session manually
|
||||
val myQueue = "${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.test.${random63BitValue()}"
|
||||
val session = startArtemisSession(server.broker.hostAndPort!!)
|
||||
session.createTemporaryQueue(myQueue, myQueue)
|
||||
val consumer = session.createConsumer(myQueue, null, -1, -1, false)
|
||||
val replies = ArrayList<Any>()
|
||||
consumer.setMessageHandler {
|
||||
replies.add(it)
|
||||
it.acknowledge()
|
||||
}
|
||||
|
||||
val producer = session.createProducer(RPCApi.RPC_SERVER_QUEUE_NAME)
|
||||
session.start()
|
||||
|
||||
pollUntilClientNumber(server, 1)
|
||||
|
||||
val message = session.createMessage(false)
|
||||
val request = RPCApi.ClientToServer.RpcRequest(
|
||||
clientAddress = SimpleString(myQueue),
|
||||
methodName = DummyOps::protocolVersion.name,
|
||||
serialisedArguments = emptyList<Any>().serialize(context = SerializationDefaults.RPC_SERVER_CONTEXT),
|
||||
replyId = Trace.InvocationId.newInstance(),
|
||||
sessionId = Trace.SessionId.newInstance()
|
||||
)
|
||||
request.writeToClientMessage(message)
|
||||
message.putLongProperty(RPCApi.DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME, 0)
|
||||
producer.send(message)
|
||||
// duplicate the message
|
||||
producer.send(message)
|
||||
|
||||
pollUntilTrue("Number of replies is 1") {
|
||||
replies.size == 1
|
||||
}.getOrThrow()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deduplication in the client`() {
|
||||
rpcDriver {
|
||||
val broker = startRpcBroker().getOrThrow()
|
||||
|
||||
// Construct an RPC server session manually
|
||||
val session = startArtemisSession(broker.hostAndPort!!)
|
||||
val consumer = session.createConsumer(RPCApi.RPC_SERVER_QUEUE_NAME)
|
||||
val producer = session.createProducer()
|
||||
val dedupeId = AtomicLong(0)
|
||||
consumer.setMessageHandler {
|
||||
it.acknowledge()
|
||||
val request = RPCApi.ClientToServer.fromClientMessage(it)
|
||||
when (request) {
|
||||
is RPCApi.ClientToServer.RpcRequest -> {
|
||||
val reply = RPCApi.ServerToClient.RpcReply(request.replyId, Try.Success(0), "server")
|
||||
val message = session.createMessage(false)
|
||||
reply.writeToClientMessage(SerializationDefaults.RPC_SERVER_CONTEXT, message)
|
||||
message.putLongProperty(RPCApi.DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME, dedupeId.getAndIncrement())
|
||||
producer.send(request.clientAddress, message)
|
||||
// duplicate the reply
|
||||
producer.send(request.clientAddress, message)
|
||||
}
|
||||
is RPCApi.ClientToServer.ObservablesClosed -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
session.start()
|
||||
|
||||
startRpcClient<RPCOps>(broker.hostAndPort!!).getOrThrow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun RPCDriverDSL.pollUntilClientNumber(server: RpcServerHandle, expected: Int) {
|
||||
|
@ -70,11 +70,24 @@ data class CordaRPCClientConfiguration(val connectionMaxRetryInterval: Duration)
|
||||
* @param configuration An optional configuration used to tweak client behaviour.
|
||||
* @param sslConfiguration An optional [SSLConfiguration] used to enable secure communication with the server.
|
||||
*/
|
||||
class CordaRPCClient @JvmOverloads constructor(
|
||||
class CordaRPCClient private constructor(
|
||||
hostAndPort: NetworkHostAndPort,
|
||||
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT,
|
||||
sslConfiguration: SSLConfiguration? = null
|
||||
) {
|
||||
@JvmOverloads
|
||||
constructor(hostAndPort: NetworkHostAndPort, configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT) : this(hostAndPort, configuration, null)
|
||||
|
||||
companion object {
|
||||
internal fun createWithSsl(
|
||||
hostAndPort: NetworkHostAndPort,
|
||||
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT,
|
||||
sslConfiguration: SSLConfiguration? = null
|
||||
): CordaRPCClient {
|
||||
return CordaRPCClient(hostAndPort, configuration, sslConfiguration)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
try {
|
||||
effectiveSerializationEnv
|
||||
|
@ -0,0 +1,13 @@
|
||||
package net.corda.client.rpc.internal
|
||||
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.client.rpc.CordaRPCClientConfiguration
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
|
||||
/** Utility which exposes the internal Corda RPC constructor to other internal Corda components */
|
||||
fun createCordaRPCClientWithSsl(
|
||||
hostAndPort: NetworkHostAndPort,
|
||||
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT,
|
||||
sslConfiguration: SSLConfiguration? = null
|
||||
) = CordaRPCClient.createWithSsl(hostAndPort, configuration, sslConfiguration)
|
@ -2,22 +2,22 @@ package net.corda.client.rpc.internal
|
||||
|
||||
import com.esotericsoftware.kryo.pool.KryoPool
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.nodeapi.internal.serialization.CordaSerializationMagic
|
||||
import net.corda.core.serialization.internal.SerializationEnvironment
|
||||
import net.corda.core.serialization.internal.SerializationEnvironmentImpl
|
||||
import net.corda.core.serialization.internal.nodeSerializationEnv
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
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 net.corda.nodeapi.internal.serialization.amqp.AMQPClientSerializationScheme
|
||||
import net.corda.nodeapi.internal.serialization.kryo.AbstractKryoSerializationScheme
|
||||
import net.corda.nodeapi.internal.serialization.kryo.DefaultKryoCustomizer
|
||||
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||
import net.corda.nodeapi.internal.serialization.kryo.kryoMagic
|
||||
import net.corda.nodeapi.internal.serialization.kryo.RPCKryo
|
||||
|
||||
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 canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean {
|
||||
return magic == kryoMagic && (target == SerializationContext.UseCase.RPCClient || target == SerializationContext.UseCase.P2P)
|
||||
}
|
||||
|
||||
override fun rpcClientKryoPool(context: SerializationContext): KryoPool {
|
||||
|
@ -1,5 +1,7 @@
|
||||
package net.corda.client.rpc.internal
|
||||
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.client.rpc.CordaRPCClientConfiguration
|
||||
import net.corda.client.rpc.RPCConnection
|
||||
import net.corda.client.rpc.RPCException
|
||||
import net.corda.core.context.Actor
|
||||
@ -42,8 +44,6 @@ data class RPCClientConfiguration(
|
||||
val reapInterval: Duration,
|
||||
/** The number of threads to use for observations (for executing [Observable.onNext]) */
|
||||
val observationExecutorPoolSize: Int,
|
||||
/** The maximum number of producers to create to handle outgoing messages */
|
||||
val producerPoolBound: Int,
|
||||
/**
|
||||
* Determines the concurrency level of the Observable Cache. This is exposed because it implicitly determines
|
||||
* the limit on the number of leaked observables reaped because of garbage collection per reaping.
|
||||
@ -56,9 +56,12 @@ data class RPCClientConfiguration(
|
||||
val connectionRetryIntervalMultiplier: Double,
|
||||
/** Maximum retry interval */
|
||||
val connectionMaxRetryInterval: Duration,
|
||||
/** Maximum reconnect attempts on failover */
|
||||
val maxReconnectAttempts: Int,
|
||||
/** Maximum file size */
|
||||
val maxFileSize: Int
|
||||
val maxFileSize: Int,
|
||||
/** The cache expiry of a deduplication watermark per client. */
|
||||
val deduplicationCacheExpiry: Duration
|
||||
) {
|
||||
companion object {
|
||||
val unlimitedReconnectAttempts = -1
|
||||
@ -68,14 +71,14 @@ data class RPCClientConfiguration(
|
||||
trackRpcCallSites = false,
|
||||
reapInterval = 1.seconds,
|
||||
observationExecutorPoolSize = 4,
|
||||
producerPoolBound = 1,
|
||||
cacheConcurrencyLevel = 8,
|
||||
connectionRetryInterval = 5.seconds,
|
||||
connectionRetryIntervalMultiplier = 1.5,
|
||||
connectionMaxRetryInterval = 3.minutes,
|
||||
maxReconnectAttempts = unlimitedReconnectAttempts,
|
||||
/** 10 MiB maximum allowed file size for attachments, including message headers. TODO: acquire this value from Network Map when supported. */
|
||||
maxFileSize = 10485760
|
||||
maxFileSize = 10485760,
|
||||
deduplicationCacheExpiry = 1.days
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,6 @@ import net.corda.client.rpc.RPCSinceVersion
|
||||
import net.corda.core.context.Actor
|
||||
import net.corda.core.context.Trace
|
||||
import net.corda.core.context.Trace.InvocationId
|
||||
import net.corda.core.internal.LazyPool
|
||||
import net.corda.core.internal.LazyStickyPool
|
||||
import net.corda.core.internal.LifeCycle
|
||||
import net.corda.core.internal.ThreadBox
|
||||
@ -26,14 +25,12 @@ import net.corda.core.utilities.Try
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.nodeapi.ArtemisConsumer
|
||||
import net.corda.nodeapi.ArtemisProducer
|
||||
import net.corda.nodeapi.RPCApi
|
||||
import net.corda.nodeapi.internal.DeduplicationChecker
|
||||
import org.apache.activemq.artemis.api.core.RoutingType
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
import org.apache.activemq.artemis.api.core.client.*
|
||||
import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE
|
||||
import org.apache.activemq.artemis.api.core.client.ClientMessage
|
||||
import org.apache.activemq.artemis.api.core.client.ServerLocator
|
||||
import rx.Notification
|
||||
import rx.Observable
|
||||
import rx.subjects.UnicastSubject
|
||||
@ -43,6 +40,7 @@ import java.time.Instant
|
||||
import java.util.*
|
||||
import java.util.concurrent.*
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import kotlin.reflect.jvm.javaMethod
|
||||
|
||||
/**
|
||||
@ -111,6 +109,8 @@ class RPCClientProxyHandler(
|
||||
|
||||
// Used for reaping
|
||||
private var reaperExecutor: ScheduledExecutorService? = null
|
||||
// Used for sending
|
||||
private var sendExecutor: ExecutorService? = null
|
||||
|
||||
// A sticky pool for running Observable.onNext()s. We need the stickiness to preserve the observation ordering.
|
||||
private val observationExecutorThreadFactory = ThreadFactoryBuilder().setNameFormat("rpc-client-observation-pool-%d").setDaemon(true).build()
|
||||
@ -161,22 +161,14 @@ class RPCClientProxyHandler(
|
||||
build()
|
||||
}
|
||||
|
||||
// We cannot pool consumers as we need to preserve the original muxed message order.
|
||||
// TODO We may need to pool these somehow anyway, otherwise if the server sends many big messages in parallel a
|
||||
// single consumer may be starved for flow control credits. Recheck this once Artemis's large message streaming is
|
||||
// integrated properly.
|
||||
private var sessionAndConsumer: ArtemisConsumer? = null
|
||||
// Pool producers to reduce contention on the client side.
|
||||
private val sessionAndProducerPool = LazyPool(bound = rpcConfiguration.producerPoolBound) {
|
||||
// Note how we create new sessions *and* session factories per producer.
|
||||
// We cannot simply pool producers on one session because sessions are single threaded.
|
||||
// We cannot simply pool sessions on one session factory because flow control credits are tied to factories, so
|
||||
// sessions tend to starve each other when used concurrently.
|
||||
val sessionFactory = serverLocator.createSessionFactory()
|
||||
val session = sessionFactory.createSession(rpcUsername, rpcPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE)
|
||||
session.start()
|
||||
ArtemisProducer(sessionFactory, session, session.createProducer(RPCApi.RPC_SERVER_QUEUE_NAME))
|
||||
}
|
||||
private var sessionFactory: ClientSessionFactory? = null
|
||||
private var producerSession: ClientSession? = null
|
||||
private var consumerSession: ClientSession? = null
|
||||
private var rpcProducer: ClientProducer? = null
|
||||
private var rpcConsumer: ClientConsumer? = null
|
||||
|
||||
private val deduplicationChecker = DeduplicationChecker(rpcConfiguration.deduplicationCacheExpiry)
|
||||
private val deduplicationSequenceNumber = AtomicLong(0)
|
||||
|
||||
/**
|
||||
* Start the client. This creates the per-client queue, starts the consumer session and the reaper.
|
||||
@ -187,22 +179,25 @@ class RPCClientProxyHandler(
|
||||
1,
|
||||
ThreadFactoryBuilder().setNameFormat("rpc-client-reaper-%d").setDaemon(true).build()
|
||||
)
|
||||
sendExecutor = Executors.newSingleThreadExecutor(
|
||||
ThreadFactoryBuilder().setNameFormat("rpc-client-sender-%d").build()
|
||||
)
|
||||
reaperScheduledFuture = reaperExecutor!!.scheduleAtFixedRate(
|
||||
this::reapObservablesAndNotify,
|
||||
rpcConfiguration.reapInterval.toMillis(),
|
||||
rpcConfiguration.reapInterval.toMillis(),
|
||||
TimeUnit.MILLISECONDS
|
||||
)
|
||||
sessionAndProducerPool.run {
|
||||
it.session.createTemporaryQueue(clientAddress, RoutingType.ANYCAST, clientAddress)
|
||||
}
|
||||
val sessionFactory = serverLocator.createSessionFactory()
|
||||
val session = sessionFactory.createSession(rpcUsername, rpcPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE)
|
||||
val consumer = session.createConsumer(clientAddress)
|
||||
consumer.setMessageHandler(this@RPCClientProxyHandler::artemisMessageHandler)
|
||||
sessionAndConsumer = ArtemisConsumer(sessionFactory, session, consumer)
|
||||
sessionFactory = serverLocator.createSessionFactory()
|
||||
producerSession = sessionFactory!!.createSession(rpcUsername, rpcPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE)
|
||||
rpcProducer = producerSession!!.createProducer(RPCApi.RPC_SERVER_QUEUE_NAME)
|
||||
consumerSession = sessionFactory!!.createSession(rpcUsername, rpcPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE)
|
||||
consumerSession!!.createTemporaryQueue(clientAddress, RoutingType.ANYCAST, clientAddress)
|
||||
rpcConsumer = consumerSession!!.createConsumer(clientAddress)
|
||||
rpcConsumer!!.setMessageHandler(this::artemisMessageHandler)
|
||||
lifeCycle.transition(State.UNSTARTED, State.SERVER_VERSION_NOT_SET)
|
||||
session.start()
|
||||
consumerSession!!.start()
|
||||
producerSession!!.start()
|
||||
}
|
||||
|
||||
// This is the general function that transforms a client side RPC to internal Artemis messages.
|
||||
@ -212,7 +207,7 @@ class RPCClientProxyHandler(
|
||||
if (method == toStringMethod) {
|
||||
return "Client RPC proxy for $rpcOpsClass"
|
||||
}
|
||||
if (sessionAndConsumer!!.session.isClosed) {
|
||||
if (consumerSession!!.isClosed) {
|
||||
throw RPCException("RPC Proxy is closed")
|
||||
}
|
||||
|
||||
@ -220,23 +215,20 @@ class RPCClientProxyHandler(
|
||||
callSiteMap?.set(replyId, Throwable("<Call site of root RPC '${method.name}'>"))
|
||||
try {
|
||||
val serialisedArguments = (arguments?.toList() ?: emptyList()).serialize(context = serializationContextWithObservableContext)
|
||||
val request = RPCApi.ClientToServer.RpcRequest(clientAddress, method.name, serialisedArguments.bytes, replyId, sessionId, externalTrace, impersonatedActor)
|
||||
val request = RPCApi.ClientToServer.RpcRequest(
|
||||
clientAddress,
|
||||
method.name,
|
||||
serialisedArguments,
|
||||
replyId,
|
||||
sessionId,
|
||||
externalTrace,
|
||||
impersonatedActor
|
||||
)
|
||||
val replyFuture = SettableFuture.create<Any>()
|
||||
sessionAndProducerPool.run {
|
||||
val message = it.session.createMessage(false)
|
||||
request.writeToClientMessage(message)
|
||||
|
||||
log.debug {
|
||||
val argumentsString = arguments?.joinToString() ?: ""
|
||||
"-> RPC(${replyId.value}) -> ${method.name}($argumentsString): ${method.returnType}"
|
||||
}
|
||||
|
||||
require(rpcReplyMap.put(replyId, replyFuture) == null) {
|
||||
"Generated several RPC requests with same ID $replyId"
|
||||
}
|
||||
it.producer.send(message)
|
||||
it.session.commit()
|
||||
require(rpcReplyMap.put(replyId, replyFuture) == null) {
|
||||
"Generated several RPC requests with same ID $replyId"
|
||||
}
|
||||
sendMessage(request)
|
||||
return replyFuture.getOrThrow()
|
||||
} catch (e: RuntimeException) {
|
||||
// Already an unchecked exception, so just rethrow it
|
||||
@ -249,9 +241,24 @@ class RPCClientProxyHandler(
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendMessage(message: RPCApi.ClientToServer) {
|
||||
val artemisMessage = producerSession!!.createMessage(false)
|
||||
message.writeToClientMessage(artemisMessage)
|
||||
sendExecutor!!.submit {
|
||||
artemisMessage.putLongProperty(RPCApi.DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME, deduplicationSequenceNumber.getAndIncrement())
|
||||
log.debug { "-> RPC -> $message" }
|
||||
rpcProducer!!.send(artemisMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// The handler for Artemis messages.
|
||||
private fun artemisMessageHandler(message: ClientMessage) {
|
||||
val serverToClient = RPCApi.ServerToClient.fromClientMessage(serializationContextWithObservableContext, message)
|
||||
val deduplicationSequenceNumber = message.getLongProperty(RPCApi.DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME)
|
||||
if (deduplicationChecker.checkDuplicateMessageId(serverToClient.deduplicationIdentity, deduplicationSequenceNumber)) {
|
||||
log.info("Message duplication detected, discarding message")
|
||||
return
|
||||
}
|
||||
log.debug { "Got message from RPC server $serverToClient" }
|
||||
when (serverToClient) {
|
||||
is RPCApi.ServerToClient.RpcReply -> {
|
||||
@ -325,14 +332,12 @@ class RPCClientProxyHandler(
|
||||
* @param notify whether to notify observables or not.
|
||||
*/
|
||||
private fun close(notify: Boolean = true) {
|
||||
sessionAndConsumer?.sessionFactory?.close()
|
||||
sessionFactory?.close()
|
||||
reaperScheduledFuture?.cancel(false)
|
||||
observableContext.observableMap.invalidateAll()
|
||||
reapObservables(notify)
|
||||
reaperExecutor?.shutdownNow()
|
||||
sessionAndProducerPool.close().forEach {
|
||||
it.sessionFactory.close()
|
||||
}
|
||||
sendExecutor?.shutdownNow()
|
||||
// Note the ordering is important, we shut down the consumer *before* the observation executor, otherwise we may
|
||||
// leak borrowed executors.
|
||||
val observationExecutors = observationExecutorPool.close()
|
||||
@ -385,11 +390,7 @@ class RPCClientProxyHandler(
|
||||
}
|
||||
if (observableIds != null) {
|
||||
log.debug { "Reaping ${observableIds.size} observables" }
|
||||
sessionAndProducerPool.run {
|
||||
val message = it.session.createMessage(false)
|
||||
RPCApi.ClientToServer.ObservablesClosed(observableIds).writeToClientMessage(message)
|
||||
it.producer.send(message)
|
||||
}
|
||||
sendMessage(RPCApi.ClientToServer.ObservablesClosed(observableIds))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import com.google.common.base.Stopwatch
|
||||
import net.corda.client.rpc.internal.RPCClientConfiguration
|
||||
import net.corda.core.internal.concurrent.doneFuture
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.core.utilities.minutes
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.node.services.messaging.RPCServerConfiguration
|
||||
@ -88,13 +89,10 @@ class RPCPerformanceTests : AbstractRPCTest() {
|
||||
val proxy = testProxy(
|
||||
RPCClientConfiguration.default.copy(
|
||||
cacheConcurrencyLevel = 16,
|
||||
observationExecutorPoolSize = 2,
|
||||
producerPoolBound = 2
|
||||
observationExecutorPoolSize = 2
|
||||
),
|
||||
RPCServerConfiguration.default.copy(
|
||||
rpcThreadPoolSize = 8,
|
||||
consumerPoolSize = 2,
|
||||
producerPoolBound = 8
|
||||
rpcThreadPoolSize = 8
|
||||
)
|
||||
)
|
||||
|
||||
@ -131,13 +129,10 @@ class RPCPerformanceTests : AbstractRPCTest() {
|
||||
val proxy = testProxy(
|
||||
RPCClientConfiguration.default.copy(
|
||||
reapInterval = 1.seconds,
|
||||
cacheConcurrencyLevel = 16,
|
||||
producerPoolBound = 8
|
||||
cacheConcurrencyLevel = 16
|
||||
),
|
||||
RPCServerConfiguration.default.copy(
|
||||
rpcThreadPoolSize = 8,
|
||||
consumerPoolSize = 1,
|
||||
producerPoolBound = 8
|
||||
rpcThreadPoolSize = 8
|
||||
)
|
||||
)
|
||||
startPublishingFixedRateInjector(
|
||||
@ -167,9 +162,7 @@ class RPCPerformanceTests : AbstractRPCTest() {
|
||||
rpcDriver {
|
||||
val proxy = testProxy(
|
||||
RPCClientConfiguration.default,
|
||||
RPCServerConfiguration.default.copy(
|
||||
consumerPoolSize = 1
|
||||
)
|
||||
RPCServerConfiguration.default
|
||||
)
|
||||
val numberOfMessages = 1000
|
||||
val bigSize = 10_000_000
|
||||
|
@ -14,7 +14,7 @@ dependencies {
|
||||
compile project(':core')
|
||||
|
||||
// Quasar, for suspendable fibres.
|
||||
compileOnly "co.paralleluniverse:quasar-core:$quasar_version:jdk8"
|
||||
compileOnly "$quasar_group:quasar-core:$quasar_version:jdk8"
|
||||
|
||||
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||
testCompile "junit:junit:$junit_version"
|
||||
|
@ -18,7 +18,7 @@ import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.CHARLIE_NAME
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.internal.InternalMockNetwork
|
||||
import net.corda.testing.node.startFlow
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
@ -28,15 +28,15 @@ import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class IdentitySyncFlowTests {
|
||||
private lateinit var mockNet: MockNetwork
|
||||
private lateinit var mockNet: InternalMockNetwork
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
// We run this in parallel threads to help catch any race conditions that may exist.
|
||||
mockNet = MockNetwork(
|
||||
mockNet = InternalMockNetwork(
|
||||
cordappPackages = listOf("net.corda.finance.contracts.asset", "net.corda.finance.schemas"),
|
||||
networkSendManuallyPumped = false,
|
||||
threadPerNode = true,
|
||||
cordappPackages = listOf("net.corda.finance.contracts.asset", "net.corda.finance.schemas")
|
||||
threadPerNode = true
|
||||
)
|
||||
}
|
||||
|
||||
@ -58,12 +58,12 @@ class IdentitySyncFlowTests {
|
||||
val anonymous = true
|
||||
val ref = OpaqueBytes.of(0x01)
|
||||
val issueFlow = aliceNode.services.startFlow(CashIssueAndPaymentFlow(1000.DOLLARS, ref, alice, anonymous, notary))
|
||||
val issueTx = issueFlow.resultFuture.getOrThrow().stx
|
||||
val issueTx = issueFlow.getOrThrow().stx
|
||||
val confidentialIdentity = issueTx.tx.outputs.map { it.data }.filterIsInstance<Cash.State>().single().owner
|
||||
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()
|
||||
aliceNode.services.startFlow(Initiator(bob, issueTx.tx)).getOrThrow()
|
||||
val expected = aliceNode.database.transaction {
|
||||
aliceNode.services.identityService.wellKnownPartyFromAnonymous(confidentialIdentity)
|
||||
}
|
||||
@ -88,7 +88,7 @@ class IdentitySyncFlowTests {
|
||||
val anonymous = true
|
||||
val ref = OpaqueBytes.of(0x01)
|
||||
val issueFlow = charlieNode.services.startFlow(CashIssueAndPaymentFlow(1000.DOLLARS, ref, charlie, anonymous, notary))
|
||||
val issueTx = issueFlow.resultFuture.getOrThrow().stx
|
||||
val issueTx = issueFlow.getOrThrow().stx
|
||||
val confidentialIdentity = issueTx.tx.outputs.map { it.data }.filterIsInstance<Cash.State>().single().owner
|
||||
val confidentialIdentCert = charlieNode.services.identityService.certificateFromKey(confidentialIdentity.owningKey)!!
|
||||
|
||||
@ -97,11 +97,11 @@ class IdentitySyncFlowTests {
|
||||
assertNotNull(aliceNode.database.transaction { aliceNode.services.identityService.wellKnownPartyFromAnonymous(confidentialIdentity) })
|
||||
|
||||
// Generate a payment from Charlie to Alice, including the confidential state
|
||||
val payTx = charlieNode.services.startFlow(CashPaymentFlow(1000.DOLLARS, alice, anonymous)).resultFuture.getOrThrow().stx
|
||||
val payTx = charlieNode.services.startFlow(CashPaymentFlow(1000.DOLLARS, alice, anonymous)).getOrThrow().stx
|
||||
|
||||
// Run the flow to sync up the identities, and confirm Charlie's confidential identity doesn't leak
|
||||
assertNull(bobNode.database.transaction { bobNode.services.identityService.wellKnownPartyFromAnonymous(confidentialIdentity) })
|
||||
aliceNode.services.startFlow(Initiator(bob, payTx.tx)).resultFuture.getOrThrow()
|
||||
aliceNode.services.startFlow(Initiator(bob, payTx.tx)).getOrThrow()
|
||||
assertNull(bobNode.database.transaction { bobNode.services.identityService.wellKnownPartyFromAnonymous(confidentialIdentity) })
|
||||
}
|
||||
|
||||
|
@ -3,19 +3,19 @@ package net.corda.confidential
|
||||
import net.corda.core.identity.*
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.internal.InternalMockNetwork
|
||||
import org.junit.Before
|
||||
import net.corda.testing.node.startFlow
|
||||
import org.junit.Test
|
||||
import kotlin.test.*
|
||||
|
||||
class SwapIdentitiesFlowTests {
|
||||
private lateinit var mockNet: MockNetwork
|
||||
private lateinit var mockNet: InternalMockNetwork
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
// We run this in parallel threads to help catch any race conditions that may exist.
|
||||
mockNet = MockNetwork(emptyList(), networkSendManuallyPumped = false, threadPerNode = true)
|
||||
mockNet = InternalMockNetwork(emptyList(), networkSendManuallyPumped = false, threadPerNode = true)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -30,7 +30,7 @@ class SwapIdentitiesFlowTests {
|
||||
val requesterFlow = aliceNode.services.startFlow(SwapIdentitiesFlow(bob))
|
||||
|
||||
// Get the results
|
||||
val actual: Map<Party, AnonymousParty> = requesterFlow.resultFuture.getOrThrow().toMap()
|
||||
val actual: Map<Party, AnonymousParty> = requesterFlow.getOrThrow().toMap()
|
||||
assertEquals(2, actual.size)
|
||||
// Verify that the generated anonymous identities do not match the well known identities
|
||||
val aliceAnonymousIdentity = actual[alice] ?: throw IllegalStateException()
|
||||
|
@ -1,8 +1,9 @@
|
||||
gradlePluginsVersion=3.0.5
|
||||
kotlinVersion=1.1.60
|
||||
platformVersion=3
|
||||
gradlePluginsVersion=4.0.0
|
||||
kotlinVersion=1.2.20
|
||||
platformVersion=2
|
||||
guavaVersion=21.0
|
||||
bouncycastleVersion=1.57
|
||||
typesafeConfigVersion=1.3.1
|
||||
jsr305Version=3.0.2
|
||||
artifactoryPluginVersion=4.4.18
|
||||
artifactoryPluginVersion=4.4.18
|
||||
snakeYamlVersion=1.19
|
||||
|
@ -78,7 +78,7 @@ dependencies {
|
||||
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||
|
||||
// Quasar, for suspendable fibres.
|
||||
compileOnly "co.paralleluniverse:quasar-core:$quasar_version:jdk8"
|
||||
compileOnly "$quasar_group:quasar-core:$quasar_version:jdk8"
|
||||
|
||||
// Thread safety annotations
|
||||
compile "com.google.code.findbugs:jsr305:$jsr305_version"
|
||||
@ -107,7 +107,7 @@ dependencies {
|
||||
// Apache JEXL: An embeddable expression evaluation library.
|
||||
// This may be temporary until we experiment with other ways to do on-the-fly contract specialisation via an API.
|
||||
compile "org.apache.commons:commons-jexl3:3.0"
|
||||
|
||||
compile 'commons-lang:commons-lang:2.6'
|
||||
// For JSON
|
||||
compile "com.fasterxml.jackson.core:jackson-databind:${jackson_version}"
|
||||
|
||||
|
11
core/src/main/kotlin/net/corda/core/CordaInternal.kt
Normal file
11
core/src/main/kotlin/net/corda/core/CordaInternal.kt
Normal file
@ -0,0 +1,11 @@
|
||||
package net.corda.core
|
||||
|
||||
/**
|
||||
* These methods are not part of Corda's API compatibility guarantee and applications should not use them.
|
||||
*
|
||||
* These fields are only meant to be used by Corda internally, and are not intended to be part of the public API.
|
||||
*/
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
@Target(AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
|
||||
@MustBeDocumented
|
||||
annotation class CordaInternal
|
@ -1,9 +0,0 @@
|
||||
package net.corda.core.context
|
||||
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
|
||||
/**
|
||||
* Authentication / Authorisation Service ID.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class AuthServiceId(val value: String)
|
@ -9,52 +9,50 @@ import java.security.Principal
|
||||
* Models the information needed to trace an invocation in Corda.
|
||||
* Includes initiating actor, origin, trace information, and optional external trace information to correlate clients' IDs.
|
||||
*
|
||||
* @param origin origin of the invocation.
|
||||
* @param trace Corda invocation trace.
|
||||
* @param actor acting agent of the invocation, used to derive the security principal.
|
||||
* @param externalTrace optional external invocation trace for cross-system logs correlation.
|
||||
* @param impersonatedActor optional impersonated actor, used for logging but not for authorisation.
|
||||
* @property origin Origin of the invocation.
|
||||
* @property trace Corda invocation trace.
|
||||
* @property actor Acting agent of the invocation, used to derive the security principal.
|
||||
* @property externalTrace Optional external invocation trace for cross-system logs correlation.
|
||||
* @property impersonatedActor Optional impersonated actor, used for logging but not for authorisation.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class InvocationContext(val origin: Origin, val trace: Trace, val actor: Actor?, val externalTrace: Trace? = null, val impersonatedActor: Actor? = null) {
|
||||
|
||||
data class InvocationContext(val origin: InvocationOrigin, val trace: Trace, val actor: Actor?, val externalTrace: Trace? = null, val impersonatedActor: Actor? = null) {
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Creates an [InvocationContext] with a [Trace] that defaults to a [java.util.UUID] as value and [java.time.Instant.now] timestamp.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun newInstance(origin: Origin, trace: Trace = Trace.newInstance(), actor: Actor? = null, externalTrace: Trace? = null, impersonatedActor: Actor? = null) = InvocationContext(origin, trace, actor, externalTrace, impersonatedActor)
|
||||
fun newInstance(origin: InvocationOrigin, trace: Trace = Trace.newInstance(), actor: Actor? = null, externalTrace: Trace? = null, impersonatedActor: Actor? = null) = InvocationContext(origin, trace, actor, externalTrace, impersonatedActor)
|
||||
|
||||
/**
|
||||
* Creates an [InvocationContext] with [Origin.RPC] origin.
|
||||
* Creates an [InvocationContext] with [InvocationOrigin.RPC] origin.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun rpc(actor: Actor, trace: Trace = Trace.newInstance(), externalTrace: Trace? = null, impersonatedActor: Actor? = null): InvocationContext = newInstance(Origin.RPC(actor), trace, actor, externalTrace, impersonatedActor)
|
||||
fun rpc(actor: Actor, trace: Trace = Trace.newInstance(), externalTrace: Trace? = null, impersonatedActor: Actor? = null): InvocationContext = newInstance(InvocationOrigin.RPC(actor), trace, actor, externalTrace, impersonatedActor)
|
||||
|
||||
/**
|
||||
* Creates an [InvocationContext] with [Origin.Peer] origin.
|
||||
* Creates an [InvocationContext] with [InvocationOrigin.Peer] origin.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun peer(party: CordaX500Name, trace: Trace = Trace.newInstance(), externalTrace: Trace? = null, impersonatedActor: Actor? = null): InvocationContext = newInstance(Origin.Peer(party), trace, null, externalTrace, impersonatedActor)
|
||||
fun peer(party: CordaX500Name, trace: Trace = Trace.newInstance(), externalTrace: Trace? = null, impersonatedActor: Actor? = null): InvocationContext = newInstance(InvocationOrigin.Peer(party), trace, null, externalTrace, impersonatedActor)
|
||||
|
||||
/**
|
||||
* Creates an [InvocationContext] with [Origin.Service] origin.
|
||||
* Creates an [InvocationContext] with [InvocationOrigin.Service] origin.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun service(serviceClassName: String, owningLegalIdentity: CordaX500Name, trace: Trace = Trace.newInstance(), externalTrace: Trace? = null): InvocationContext = newInstance(Origin.Service(serviceClassName, owningLegalIdentity), trace, null, externalTrace)
|
||||
fun service(serviceClassName: String, owningLegalIdentity: CordaX500Name, trace: Trace = Trace.newInstance(), externalTrace: Trace? = null): InvocationContext = newInstance(InvocationOrigin.Service(serviceClassName, owningLegalIdentity), trace, null, externalTrace)
|
||||
|
||||
/**
|
||||
* Creates an [InvocationContext] with [Origin.Scheduled] origin.
|
||||
* Creates an [InvocationContext] with [InvocationOrigin.Scheduled] origin.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun scheduled(scheduledState: ScheduledStateRef, trace: Trace = Trace.newInstance(), externalTrace: Trace? = null): InvocationContext = newInstance(Origin.Scheduled(scheduledState), trace, null, externalTrace)
|
||||
fun scheduled(scheduledState: ScheduledStateRef, trace: Trace = Trace.newInstance(), externalTrace: Trace? = null): InvocationContext = newInstance(InvocationOrigin.Scheduled(scheduledState), trace, null, externalTrace)
|
||||
|
||||
/**
|
||||
* Creates an [InvocationContext] with [Origin.Shell] origin.
|
||||
* Creates an [InvocationContext] with [InvocationOrigin.Shell] origin.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun shell(trace: Trace = Trace.newInstance(), externalTrace: Trace? = null): InvocationContext = InvocationContext(Origin.Shell, trace, null, externalTrace)
|
||||
fun shell(trace: Trace = Trace.newInstance(), externalTrace: Trace? = null): InvocationContext = InvocationContext(InvocationOrigin.Shell, trace, null, externalTrace)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -83,11 +81,10 @@ data class Actor(val id: Id, val serviceId: AuthServiceId, val owningLegalIdenti
|
||||
}
|
||||
|
||||
/**
|
||||
* Invocation origin for tracing purposes.
|
||||
* Represents the source of an action such as a flow start, an RPC, a shell command etc.
|
||||
*/
|
||||
@CordaSerializable
|
||||
sealed class Origin {
|
||||
|
||||
sealed class InvocationOrigin {
|
||||
/**
|
||||
* Returns the [Principal] for a given [Actor].
|
||||
*/
|
||||
@ -96,32 +93,28 @@ sealed class Origin {
|
||||
/**
|
||||
* Origin was an RPC call.
|
||||
*/
|
||||
data class RPC(private val actor: Actor) : Origin() {
|
||||
|
||||
data class RPC(private val actor: Actor) : InvocationOrigin() {
|
||||
override fun principal() = Principal { actor.id.value }
|
||||
}
|
||||
|
||||
/**
|
||||
* Origin was a message sent by a [Peer].
|
||||
*/
|
||||
data class Peer(val party: CordaX500Name) : Origin() {
|
||||
|
||||
data class Peer(val party: CordaX500Name) : InvocationOrigin() {
|
||||
override fun principal() = Principal { party.toString() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Origin was a Corda Service.
|
||||
*/
|
||||
data class Service(val serviceClassName: String, val owningLegalIdentity: CordaX500Name) : Origin() {
|
||||
|
||||
data class Service(val serviceClassName: String, val owningLegalIdentity: CordaX500Name) : InvocationOrigin() {
|
||||
override fun principal() = Principal { serviceClassName }
|
||||
}
|
||||
|
||||
/**
|
||||
* Origin was a scheduled activity.
|
||||
*/
|
||||
data class Scheduled(val scheduledState: ScheduledStateRef) : Origin() {
|
||||
|
||||
data class Scheduled(val scheduledState: ScheduledStateRef) : InvocationOrigin() {
|
||||
override fun principal() = Principal { "Scheduler" }
|
||||
}
|
||||
|
||||
@ -129,8 +122,13 @@ sealed class Origin {
|
||||
/**
|
||||
* Origin was the Shell.
|
||||
*/
|
||||
object Shell : Origin() {
|
||||
|
||||
object Shell : InvocationOrigin() {
|
||||
override fun principal() = Principal { "Shell User" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication / Authorisation Service ID.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class AuthServiceId(val value: String)
|
@ -50,4 +50,9 @@ interface Attachment : NamedByHash {
|
||||
* Can be empty, for example non-contract attachments won't be necessarily be signed.
|
||||
*/
|
||||
val signers: List<Party>
|
||||
|
||||
/**
|
||||
* Attachment size in bytes.
|
||||
*/
|
||||
val size: Int
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
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
|
||||
|
@ -46,7 +46,7 @@ interface NamedByHash {
|
||||
@CordaSerializable
|
||||
data class Issued<out P : Any>(val issuer: PartyAndReference, val product: P) {
|
||||
init {
|
||||
require(issuer.reference.bytes.size <= MAX_ISSUER_REF_SIZE) { "Maximum issuer reference size is $MAX_ISSUER_REF_SIZE." }
|
||||
require(issuer.reference.size <= MAX_ISSUER_REF_SIZE) { "Maximum issuer reference size is $MAX_ISSUER_REF_SIZE." }
|
||||
}
|
||||
override fun toString() = "$product issued by $issuer"
|
||||
}
|
||||
|
@ -0,0 +1,6 @@
|
||||
package net.corda.core.cordapp
|
||||
|
||||
/**
|
||||
* Thrown if an exception occurs in accessing or parsing cordapp configuration
|
||||
*/
|
||||
class CordappConfigException(msg: String, e: Throwable) : Exception(msg, e)
|
70
core/src/main/kotlin/net/corda/core/cordapp/CordappConfig.kt
Normal file
70
core/src/main/kotlin/net/corda/core/cordapp/CordappConfig.kt
Normal file
@ -0,0 +1,70 @@
|
||||
package net.corda.core.cordapp
|
||||
|
||||
import net.corda.core.DoNotImplement
|
||||
|
||||
/**
|
||||
* Provides access to cordapp configuration independent of the configuration provider.
|
||||
*/
|
||||
@DoNotImplement
|
||||
interface CordappConfig {
|
||||
/**
|
||||
* Check if a config exists at path
|
||||
*/
|
||||
fun exists(path: String): Boolean
|
||||
|
||||
/**
|
||||
* Get the value of the configuration at "path".
|
||||
*
|
||||
* @throws CordappConfigException If the configuration fails to load, parse, or find a value.
|
||||
*/
|
||||
fun get(path: String): Any
|
||||
|
||||
/**
|
||||
* Get the int value of the configuration at "path".
|
||||
*
|
||||
* @throws CordappConfigException If the configuration fails to load, parse, or find a value.
|
||||
*/
|
||||
fun getInt(path: String): Int
|
||||
|
||||
/**
|
||||
* Get the long value of the configuration at "path".
|
||||
*
|
||||
* @throws CordappConfigException If the configuration fails to load, parse, or find a value.
|
||||
*/
|
||||
fun getLong(path: String): Long
|
||||
|
||||
/**
|
||||
* Get the float value of the configuration at "path".
|
||||
*
|
||||
* @throws CordappConfigException If the configuration fails to load, parse, or find a value.
|
||||
*/
|
||||
fun getFloat(path: String): Float
|
||||
|
||||
/**
|
||||
* Get the double value of the configuration at "path".
|
||||
*
|
||||
* @throws CordappConfigException If the configuration fails to load, parse, or find a value.
|
||||
*/
|
||||
fun getDouble(path: String): Double
|
||||
|
||||
/**
|
||||
* Get the number value of the configuration at "path".
|
||||
*
|
||||
* @throws CordappConfigException If the configuration fails to load, parse, or find a value.
|
||||
*/
|
||||
fun getNumber(path: String): Number
|
||||
|
||||
/**
|
||||
* Get the string value of the configuration at "path".
|
||||
*
|
||||
* @throws CordappConfigException If the configuration fails to load, parse, or find a value.
|
||||
*/
|
||||
fun getString(path: String): String
|
||||
|
||||
/**
|
||||
* Get the boolean value of the configuration at "path".
|
||||
*
|
||||
* @throws CordappConfigException If the configuration fails to load, parse, or find a value.
|
||||
*/
|
||||
fun getBoolean(path: String): Boolean
|
||||
}
|
@ -2,8 +2,6 @@ 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.
|
||||
@ -15,5 +13,11 @@ import net.corda.core.crypto.SecureHash
|
||||
* @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
|
||||
* @property config Configuration for this CorDapp
|
||||
*/
|
||||
class CordappContext(val cordapp: Cordapp, val attachmentId: SecureHash?, val classLoader: ClassLoader)
|
||||
class CordappContext internal constructor(
|
||||
val cordapp: Cordapp,
|
||||
val attachmentId: SecureHash?,
|
||||
val classLoader: ClassLoader,
|
||||
val config: CordappConfig
|
||||
)
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.crypto.CompositeKey.NodeAndWeight
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.exactAdd
|
||||
import net.corda.core.utilities.sequence
|
||||
@ -12,7 +11,7 @@ import java.util.*
|
||||
|
||||
/**
|
||||
* A tree data structure that enables the representation of composite public keys, which are used to represent
|
||||
* the signing requirements for multisignature scenarios such as RAFT notary services. A composite key is a list
|
||||
* the signing requirements for multi-signature scenarios such as RAFT notary services. A composite key is a list
|
||||
* of leaf keys and their contributing weight, and each leaf can be a conventional single key or a composite key.
|
||||
* Keys contribute their weight to the total if they are matched by the signature.
|
||||
*
|
||||
@ -53,9 +52,19 @@ class CompositeKey private constructor(val threshold: Int, children: List<NodeAn
|
||||
}
|
||||
return builder.build(threshold)
|
||||
}
|
||||
// Required for sorting [children] list. To ensure a deterministic way of adding children required for equality
|
||||
// checking, [children] list is sorted during construction. A DESC ordering in the [NodeAndWeight.weight] field
|
||||
// will improve efficiency, because keys with bigger "weights" are the first to be checked and thus the
|
||||
// threshold requirement might be met earlier without requiring a full [children] scan.
|
||||
// TODO: node.encoded.sequence() might be expensive, consider a faster deterministic compareTo implementation
|
||||
// for public keys in general.
|
||||
private val descWeightComparator = compareBy<NodeAndWeight>({ -it.weight }, { it.node.encoded.sequence() })
|
||||
}
|
||||
|
||||
val children: List<NodeAndWeight> = children.sorted()
|
||||
/**
|
||||
* Τhe order of the children may not be the same to what was provided in the builder.
|
||||
*/
|
||||
val children: List<NodeAndWeight> = children.sortedWith(descWeightComparator)
|
||||
|
||||
init {
|
||||
// TODO: replace with the more extensive, but slower, checkValidity() test.
|
||||
@ -103,9 +112,9 @@ class CompositeKey private constructor(val threshold: Int, children: List<NodeAn
|
||||
* requirements are met, while it tests for aggregated-weight integer overflow.
|
||||
* In practice, this method should be always invoked on the root [CompositeKey], as it inherently
|
||||
* validates the child nodes (all the way till the leaves).
|
||||
* TODO: Always call this method when deserialising [CompositeKey]s.
|
||||
*/
|
||||
fun checkValidity() {
|
||||
if (validated) return
|
||||
val visitedMap = IdentityHashMap<CompositeKey, Boolean>()
|
||||
visitedMap.put(this, true)
|
||||
cycleDetection(visitedMap) // Graph cycle testing on the root node.
|
||||
@ -143,6 +152,7 @@ class CompositeKey private constructor(val threshold: Int, children: List<NodeAn
|
||||
|
||||
override fun compareTo(other: NodeAndWeight): Int {
|
||||
return if (weight == other.weight)
|
||||
// TODO: this might be expensive, consider a faster deterministic compareTo implementation when weights are equal.
|
||||
node.encoded.sequence().compareTo(other.node.encoded.sequence())
|
||||
else
|
||||
weight.compareTo(other.weight)
|
||||
@ -180,17 +190,18 @@ class CompositeKey private constructor(val threshold: Int, children: List<NodeAn
|
||||
|
||||
override fun getFormat() = ASN1Encoding.DER
|
||||
|
||||
// Extracted method from isFulfilledBy.
|
||||
// Return true when and if the threshold requirement is met.
|
||||
private fun checkFulfilledBy(keysToCheck: Iterable<PublicKey>): Boolean {
|
||||
if (keysToCheck.any { it is CompositeKey }) return false
|
||||
val totalWeight = children.map { (node, weight) ->
|
||||
var totalWeight = 0
|
||||
children.forEach { (node, weight) ->
|
||||
if (node is CompositeKey) {
|
||||
if (node.checkFulfilledBy(keysToCheck)) weight else 0
|
||||
if (node.checkFulfilledBy(keysToCheck)) totalWeight += weight
|
||||
} else {
|
||||
if (keysToCheck.contains(node)) weight else 0
|
||||
if (node in keysToCheck) totalWeight += weight
|
||||
}
|
||||
}.sum()
|
||||
return totalWeight >= threshold
|
||||
if (totalWeight >= threshold) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
@ -201,8 +212,8 @@ class CompositeKey private constructor(val threshold: Int, children: List<NodeAn
|
||||
fun isFulfilledBy(keysToCheck: Iterable<PublicKey>): Boolean {
|
||||
// We validate keys only when checking if they're matched, as this checks subkeys as a result.
|
||||
// Doing these checks at deserialization/construction time would result in duplicate checks.
|
||||
if (!validated)
|
||||
checkValidity() // TODO: remove when checkValidity() will be eventually invoked during/after deserialization.
|
||||
checkValidity()
|
||||
if (keysToCheck.any { it is CompositeKey }) return false
|
||||
return checkFulfilledBy(keysToCheck)
|
||||
}
|
||||
|
||||
|
@ -55,7 +55,7 @@ import javax.crypto.spec.SecretKeySpec
|
||||
* However, only the schemes returned by {@link #listSupportedSignatureSchemes()} are supported.
|
||||
* Note that Corda currently supports the following signature schemes by their code names:
|
||||
* <p><ul>
|
||||
* <li>RSA_SHA256 (RSA using SHA256 as hash algorithm and MGF1 (with SHA256) as mask generation function).
|
||||
* <li>RSA_SHA256 (RSA PKCS#1 using SHA256 as hash algorithm).
|
||||
* <li>ECDSA_SECP256K1_SHA256 (ECDSA using the secp256k1 Koblitz curve and SHA256 as hash algorithm).
|
||||
* <li>ECDSA_SECP256R1_SHA256 (ECDSA using the secp256r1 (NIST P-256) curve and SHA256 as hash algorithm).
|
||||
* <li>EDDSA_ED25519_SHA512 (EdDSA using the ed255519 twisted Edwards curve and SHA512 as hash algorithm).
|
||||
@ -64,7 +64,8 @@ import javax.crypto.spec.SecretKeySpec
|
||||
*/
|
||||
object Crypto {
|
||||
/**
|
||||
* RSA signature scheme using SHA256 for message hashing.
|
||||
* RSA PKCS#1 signature scheme using SHA256 for message hashing.
|
||||
* The actual algorithm id is 1.2.840.113549.1.1.1
|
||||
* Note: Recommended key size >= 3072 bits.
|
||||
*/
|
||||
@JvmField
|
||||
@ -75,7 +76,7 @@ object Crypto {
|
||||
listOf(AlgorithmIdentifier(PKCSObjectIdentifiers.rsaEncryption, null)),
|
||||
BouncyCastleProvider.PROVIDER_NAME,
|
||||
"RSA",
|
||||
"SHA256WITHRSAEncryption",
|
||||
"SHA256WITHRSA",
|
||||
null,
|
||||
3072,
|
||||
"RSA_SHA256 signature scheme using SHA256 as hash algorithm."
|
||||
@ -547,7 +548,7 @@ object Crypto {
|
||||
/**
|
||||
* Utility to simplify the act of verifying a [TransactionSignature].
|
||||
* It returns true if it succeeds, but it always throws an exception if verification fails.
|
||||
* @param txId transaction's id (Merkle root).
|
||||
* @param txId transaction's id.
|
||||
* @param transactionSignature the signature on the transaction.
|
||||
* @return true if verification passes or throw exception if verification fails.
|
||||
* @throws InvalidKeyException if the key is invalid.
|
||||
@ -559,7 +560,7 @@ object Crypto {
|
||||
@JvmStatic
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun doVerify(txId: SecureHash, transactionSignature: TransactionSignature): Boolean {
|
||||
val signableData = SignableData(txId, transactionSignature.signatureMetadata)
|
||||
val signableData = SignableData(originalSignedHash(txId, transactionSignature.partialMerkleTree), transactionSignature.signatureMetadata)
|
||||
return Crypto.doVerify(transactionSignature.by, transactionSignature.bytes, signableData.serialize().bytes)
|
||||
}
|
||||
|
||||
@ -569,7 +570,7 @@ object Crypto {
|
||||
* It returns true if it succeeds and false if not. In comparison to [doVerify] if the key and signature
|
||||
* do not match it returns false rather than throwing an exception. Normally you should use the function which throws,
|
||||
* as it avoids the risk of failing to test the result.
|
||||
* @param txId transaction's id (Merkle root).
|
||||
* @param txId transaction's id.
|
||||
* @param transactionSignature the signature on the transaction.
|
||||
* @throws SignatureException if this signatureData object is not initialized properly,
|
||||
* the passed-in signatureData is improperly encoded or of the wrong type,
|
||||
@ -578,7 +579,7 @@ object Crypto {
|
||||
@JvmStatic
|
||||
@Throws(SignatureException::class)
|
||||
fun isValid(txId: SecureHash, transactionSignature: TransactionSignature): Boolean {
|
||||
val signableData = SignableData(txId, transactionSignature.signatureMetadata)
|
||||
val signableData = SignableData(originalSignedHash(txId, transactionSignature.partialMerkleTree), transactionSignature.signatureMetadata)
|
||||
return isValid(
|
||||
findSignatureScheme(transactionSignature.by),
|
||||
transactionSignature.by,
|
||||
@ -1011,4 +1012,21 @@ object Crypto {
|
||||
else -> decodePrivateKey(key.encoded)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the hash value that is actually signed.
|
||||
* The txId is returned when [partialMerkleTree] is null,
|
||||
* else the root of the tree is computed and returned.
|
||||
* Note that the hash of the txId should be a leaf in the tree, not the txId itself.
|
||||
*/
|
||||
private fun originalSignedHash(txId: SecureHash, partialMerkleTree: PartialMerkleTree?): SecureHash {
|
||||
return if (partialMerkleTree != null) {
|
||||
val usedHashes = mutableListOf<SecureHash>()
|
||||
val root = PartialMerkleTree.rootAndUsedHashes(partialMerkleTree.root, usedHashes)
|
||||
require(txId.sha256() in usedHashes) { "Transaction with id:$txId is not a leaf in the provided partial Merkle tree" }
|
||||
root
|
||||
} else {
|
||||
txId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -140,23 +140,24 @@ class PartialMerkleTree(val root: PartialTree) {
|
||||
is PartialTree.Node -> {
|
||||
val leftHash = rootAndUsedHashes(node.left, usedHashes)
|
||||
val rightHash = rootAndUsedHashes(node.right, usedHashes)
|
||||
return leftHash.hashConcat(rightHash)
|
||||
leftHash.hashConcat(rightHash)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to verify a [PartialMerkleTree] against an input Merkle root and a list of leaves.
|
||||
* The tree should only contain the leaves defined in [hashesToCheck].
|
||||
* @param merkleRootHash Hash that should be checked for equality with root calculated from this partial tree.
|
||||
* @param hashesToCheck List of included leaves hashes that should be found in this partial tree.
|
||||
*/
|
||||
fun verify(merkleRootHash: SecureHash, hashesToCheck: List<SecureHash>): Boolean {
|
||||
val usedHashes = ArrayList<SecureHash>()
|
||||
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)
|
||||
return verifyRoot == merkleRootHash // Tree roots match.
|
||||
&& hashesToCheck.size == usedHashes.size // Obtained the same number of hashes (leaves).
|
||||
&& hashesToCheck.toSet().containsAll(usedHashes) // Lists contain the same elements.
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -5,8 +5,10 @@ import net.corda.core.serialization.CordaSerializable
|
||||
/**
|
||||
* A [SignableData] object is the packet actually signed.
|
||||
* It works as a wrapper over transaction id and signature metadata.
|
||||
* Note that when multi-transaction signing (signing a block of transactions) is used, the root of the Merkle tree
|
||||
* (having transaction IDs as leaves) is actually signed and thus [txId] refers to this root and not a specific transaction.
|
||||
*
|
||||
* @param txId transaction's id.
|
||||
* @param txId transaction's id or root of multi-transaction Merkle tree in case of multi-transaction signing.
|
||||
* @param signatureMetadata meta data required.
|
||||
*/
|
||||
@CordaSerializable
|
||||
|
@ -8,13 +8,24 @@ import java.util.*
|
||||
|
||||
/**
|
||||
* A wrapper over the signature output accompanied by signer's public key and signature metadata.
|
||||
* This is similar to [DigitalSignature.WithKey], but targeted to DLT transaction signatures.
|
||||
* This is similar to [DigitalSignature.WithKey], but targeted to DLT transaction (or block of transactions) signatures.
|
||||
* @property bytes actual bytes of the cryptographic signature.
|
||||
* @property by [PublicKey] of the signer.
|
||||
* @property signatureMetadata attached [SignatureMetadata] for this signature.
|
||||
* @property partialMerkleTree required when multi-transaction signing is utilised.
|
||||
*/
|
||||
@CordaSerializable
|
||||
class TransactionSignature(bytes: ByteArray, val by: PublicKey, val signatureMetadata: SignatureMetadata) : DigitalSignature(bytes) {
|
||||
class TransactionSignature(bytes: ByteArray, val by: PublicKey, val signatureMetadata: SignatureMetadata, val partialMerkleTree: PartialMerkleTree?) : DigitalSignature(bytes) {
|
||||
/**
|
||||
* Construct a [TransactionSignature] with [partialMerkleTree] set to null.
|
||||
* This is the recommended constructor when signing over a single transaction.
|
||||
* */
|
||||
constructor(bytes: ByteArray, by: PublicKey, signatureMetadata: SignatureMetadata) : this(bytes, by, signatureMetadata, null)
|
||||
|
||||
/**
|
||||
* Function to verify a [SignableData] object's signature.
|
||||
* Note that [SignableData] contains the id of the transaction and extra metadata, such as DLT's platform version.
|
||||
* A non-null [partialMerkleTree] implies multi-transaction signing and the signature is over the root of this tree.
|
||||
*
|
||||
* @param txId transaction's id (Merkle root), which along with [signatureMetadata] will be used to construct the [SignableData] object to be signed.
|
||||
* @throws InvalidKeyException if the key is invalid.
|
||||
|
@ -1,45 +1,69 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import net.corda.core.context.Actor
|
||||
import net.corda.core.context.AuthServiceId
|
||||
import net.corda.core.context.InvocationContext
|
||||
import net.corda.core.context.InvocationOrigin
|
||||
import net.corda.core.contracts.ScheduledStateRef
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.security.Principal
|
||||
|
||||
/**
|
||||
* FlowInitiator holds information on who started the flow. We have different ways of doing that: via RPC [FlowInitiator.RPC],
|
||||
* communication started by peer node [FlowInitiator.Peer], scheduled flows [FlowInitiator.Scheduled]
|
||||
* or via the Corda Shell [FlowInitiator.Shell].
|
||||
* Please note that [FlowInitiator] has been superceded by [net.corda.core.context.InvocationContext], which offers
|
||||
* more detail for the same event.
|
||||
*
|
||||
* FlowInitiator holds information on who started the flow. We have different ways of doing that: via [FlowInitiator.RPC],
|
||||
* communication started by peer nodes ([FlowInitiator.Peer]), scheduled flows ([FlowInitiator.Scheduled])
|
||||
* or via the Corda Shell ([FlowInitiator.Shell]).
|
||||
*/
|
||||
@Deprecated("Do not use these types. Future releases might remove them.")
|
||||
@CordaSerializable
|
||||
sealed class FlowInitiator : Principal {
|
||||
/** Started using [net.corda.core.messaging.CordaRPCOps.startFlowDynamic]. */
|
||||
@Deprecated("Do not use this type. Future releases might remove it.")
|
||||
data class RPC(val username: String) : FlowInitiator() {
|
||||
override fun getName(): String = username
|
||||
}
|
||||
|
||||
/** Started when we get new session initiation request. */
|
||||
@Deprecated("Do not use this type. Future releases might remove it.")
|
||||
data class Peer(val party: Party) : FlowInitiator() {
|
||||
override fun getName(): String = party.name.toString()
|
||||
}
|
||||
|
||||
/** Started by a CordaService. */
|
||||
@Deprecated("Do not use this type. Future releases might remove it.")
|
||||
data class Service(val serviceClassName: String) : FlowInitiator() {
|
||||
override fun getName(): String = serviceClassName
|
||||
}
|
||||
|
||||
/** Started as scheduled activity. */
|
||||
@Deprecated("Do not use this type. Future releases might remove it.")
|
||||
data class Scheduled(val scheduledState: ScheduledStateRef) : FlowInitiator() {
|
||||
override fun getName(): String = "Scheduler"
|
||||
}
|
||||
|
||||
// TODO When proper ssh access enabled, add username/use RPC?
|
||||
@Deprecated("Do not use this type. Future releases might remove it.")
|
||||
object Shell : FlowInitiator() {
|
||||
override fun getName(): String = "Shell User"
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an [InvocationContext], which is equivalent to this object but expressed using the successor to this
|
||||
* class hierarchy (which is now deprecated). The returned object has less information than it could have, so
|
||||
* prefer to use fetch an invocation context directly if you can (e.g. in [net.corda.core.messaging.StateMachineInfo])
|
||||
*/
|
||||
val invocationContext: InvocationContext get() {
|
||||
val unknownName = CordaX500Name("UNKNOWN", "UNKNOWN", "GB")
|
||||
var actor: Actor? = null
|
||||
val origin: InvocationOrigin
|
||||
when (this) {
|
||||
is FlowInitiator.RPC -> {
|
||||
actor = Actor(Actor.Id(this.username), AuthServiceId("UNKNOWN"), unknownName)
|
||||
origin = InvocationOrigin.RPC(actor)
|
||||
}
|
||||
is FlowInitiator.Peer -> origin = InvocationOrigin.Peer(this.party.name)
|
||||
is FlowInitiator.Service -> origin = InvocationOrigin.Service(this.serviceClassName, unknownName)
|
||||
FlowInitiator.Shell -> origin = InvocationOrigin.Shell
|
||||
is FlowInitiator.Scheduled -> origin = InvocationOrigin.Scheduled(this.scheduledState)
|
||||
}
|
||||
return InvocationContext.newInstance(origin = origin, actor = actor)
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import co.paralleluniverse.strands.Strand
|
||||
import net.corda.core.CordaInternal
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
@ -19,7 +20,6 @@ import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.*
|
||||
import org.slf4j.Logger
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* A sub-class of [FlowLogic<T>] implements a flow using direct, straight line blocking code. Thus you
|
||||
@ -43,7 +43,7 @@ import java.time.Instant
|
||||
* also has a version property to allow you to version your flow and enables a node to restrict support for the flow to
|
||||
* that particular version.
|
||||
*
|
||||
* Functions that suspend the flow (including all functions on [FlowSession]) accept a [maySkipCheckpoint] parameter
|
||||
* Functions that suspend the flow (including all functions on [FlowSession]) accept a maySkipCheckpoint parameter
|
||||
* defaulting to false, false meaning a checkpoint should always be created on suspend. This parameter may be set to
|
||||
* true which allows the implementation to potentially optimise away the checkpoint, saving a roundtrip to the database.
|
||||
*
|
||||
@ -53,6 +53,7 @@ import java.time.Instant
|
||||
* parameter the flow must be prepared for scenarios where a previous running of the flow *already committed its
|
||||
* relevant database transactions*. Only set this option to true if you know what you're doing.
|
||||
*/
|
||||
@Suppress("DEPRECATION", "DeprecatedCallableAddReplaceWith")
|
||||
abstract class FlowLogic<out T> {
|
||||
/** This is where you should log things to. */
|
||||
val logger: Logger get() = stateMachine.logger
|
||||
@ -61,14 +62,14 @@ abstract class FlowLogic<out T> {
|
||||
/**
|
||||
* Return the outermost [FlowLogic] instance, or null if not in a flow.
|
||||
*/
|
||||
@JvmStatic
|
||||
@Suppress("unused") @JvmStatic
|
||||
val currentTopLevel: FlowLogic<*>? get() = (Strand.currentStrand() as? FlowStateMachine<*>)?.logic
|
||||
|
||||
/**
|
||||
* If on a flow, suspends the flow and only wakes it up after at least [duration] time has passed. Otherwise,
|
||||
* just sleep for [duration]. This sleep function is not designed to aid scheduling, for which you should
|
||||
* consider using [SchedulableState]. It is designed to aid with managing contention for which you have not
|
||||
* managed via another means.
|
||||
* consider using [net.corda.core.contracts.SchedulableState]. It is designed to aid with managing contention
|
||||
* for which you have not managed via another means.
|
||||
*
|
||||
* Warning: long sleeps and in general long running flows are highly discouraged, as there is currently no
|
||||
* support for flow migration! This method will throw an exception if you attempt to sleep for longer than
|
||||
@ -79,7 +80,7 @@ abstract class FlowLogic<out T> {
|
||||
@JvmOverloads
|
||||
@Throws(FlowException::class)
|
||||
fun sleep(duration: Duration, maySkipCheckpoint: Boolean = false) {
|
||||
if (duration.compareTo(Duration.ofMinutes(5)) > 0) {
|
||||
if (duration > Duration.ofMinutes(5)) {
|
||||
throw FlowException("Attempt to sleep for longer than 5 minutes is not supported. Consider using SchedulableState.")
|
||||
}
|
||||
val fiber = (Strand.currentStrand() as? FlowStateMachine<*>)
|
||||
@ -343,7 +344,9 @@ abstract class FlowLogic<out T> {
|
||||
* is public only because it must be accessed across module boundaries.
|
||||
*/
|
||||
var stateMachine: FlowStateMachine<*>
|
||||
@CordaInternal
|
||||
get() = _stateMachine ?: throw IllegalStateException("This can only be done after the flow has been started.")
|
||||
@CordaInternal
|
||||
set(value) {
|
||||
_stateMachine = value
|
||||
}
|
||||
|
101
core/src/main/kotlin/net/corda/core/flows/NotarisationRequest.kt
Normal file
101
core/src/main/kotlin/net/corda/core/flows/NotarisationRequest.kt
Normal file
@ -0,0 +1,101 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.CoreTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.SignatureException
|
||||
|
||||
/**
|
||||
* A notarisation request specifies a list of states to consume and the id of the consuming transaction. Its primary
|
||||
* purpose is for notarisation traceability – a signature over the notarisation request, [NotarisationRequestSignature],
|
||||
* allows a notary to prove that a certain party requested the consumption of a particular state.
|
||||
*
|
||||
* While the signature must be retained, the notarisation request does not need to be transferred or stored anywhere - it
|
||||
* can be built from a [SignedTransaction] or a [CoreTransaction]. The notary can recompute it from the committed states index.
|
||||
*
|
||||
* In case there is a need to prove that a party spent a particular state, the notary will:
|
||||
* 1) Locate the consuming transaction id in the index, along with all other states consumed in the same transaction.
|
||||
* 2) Build a [NotarisationRequest].
|
||||
* 3) Locate the [NotarisationRequestSignature] for the transaction id. The signature will contain the signing public key.
|
||||
* 4) Demonstrate the signature verifies against the serialized request. The provided states are always sorted internally,
|
||||
* to ensure the serialization does not get affected by the order.
|
||||
*/
|
||||
@CordaSerializable
|
||||
class NotarisationRequest(statesToConsume: List<StateRef>, val transactionId: SecureHash) {
|
||||
companion object {
|
||||
/** Sorts in ascending order first by transaction hash, then by output index. */
|
||||
private val stateRefComparator = compareBy<StateRef>({ it.txhash }, { it.index })
|
||||
}
|
||||
|
||||
private val _statesToConsumeSorted = statesToConsume.sortedWith(stateRefComparator)
|
||||
|
||||
/** States this request specifies to be consumed. Sorted to ensure the serialized form does not get affected by the state order. */
|
||||
val statesToConsume: List<StateRef> get() = _statesToConsumeSorted // Getter required for AMQP serialization
|
||||
|
||||
/** Verifies the signature against this notarisation request. Checks that the signature is issued by the right party. */
|
||||
fun verifySignature(requestSignature: NotarisationRequestSignature, intendedSigner: Party) {
|
||||
val signature = requestSignature.digitalSignature
|
||||
if (intendedSigner.owningKey != signature.by) {
|
||||
val errorMessage = "Expected a signature by ${intendedSigner.owningKey}, but received by ${signature.by}}"
|
||||
throw NotaryException(NotaryError.RequestSignatureInvalid(IllegalArgumentException(errorMessage)))
|
||||
}
|
||||
// TODO: if requestSignature was generated over an old version of NotarisationRequest, we need to be able to
|
||||
// reserialize it in that version to get the exact same bytes. Modify the serialization logic once that's
|
||||
// available.
|
||||
val expectedSignedBytes = this.serialize().bytes
|
||||
verifyCorrectBytesSigned(signature, expectedSignedBytes)
|
||||
}
|
||||
|
||||
private fun verifyCorrectBytesSigned(signature: DigitalSignature.WithKey, bytes: ByteArray) {
|
||||
try {
|
||||
signature.verify(bytes)
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is InvalidKeyException, is SignatureException -> {
|
||||
val error = NotaryError.RequestSignatureInvalid(e)
|
||||
throw NotaryException(error)
|
||||
}
|
||||
else -> throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper around a digital signature used for notarisation requests.
|
||||
*
|
||||
* The [platformVersion] is required so the notary can verify the signature against the right version of serialized
|
||||
* bytes of the [NotarisationRequest]. Otherwise, the request may be rejected.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class NotarisationRequestSignature(val digitalSignature: DigitalSignature.WithKey, val platformVersion: Int)
|
||||
|
||||
/**
|
||||
* Container for the transaction and notarisation request signature.
|
||||
* This is the payload that gets sent by a client to a notary service for committing the input states of the [transaction].
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class NotarisationPayload(val transaction: Any, val requestSignature: NotarisationRequestSignature) {
|
||||
init {
|
||||
require(transaction is SignedTransaction || transaction is CoreTransaction) {
|
||||
"Unsupported transaction type in the notarisation payload: ${transaction.javaClass.simpleName}"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper for automatically casting the underlying [transaction] payload to a [SignedTransaction].
|
||||
* Should only be used by validating notaries.
|
||||
*/
|
||||
val signedTransaction get() = transaction as SignedTransaction
|
||||
/**
|
||||
* A helper for automatically casting the underlying [transaction] payload to a [CoreTransaction].
|
||||
* Should only be used by non-validating notaries.
|
||||
*/
|
||||
val coreTransaction get() = transaction as CoreTransaction
|
||||
}
|
@ -9,10 +9,12 @@ import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.crypto.keys
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.FetchDataFlow
|
||||
import net.corda.core.internal.generateSignature
|
||||
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.CoreTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
@ -73,15 +75,17 @@ class NotaryFlow {
|
||||
return notaryParty
|
||||
}
|
||||
|
||||
/** Notarises the transaction with the [notaryParty], obtains the notary's signature(s). */
|
||||
@Throws(NotaryException::class)
|
||||
@Suspendable
|
||||
protected fun notarise(notaryParty: Party): UntrustworthyData<List<TransactionSignature>> {
|
||||
return try {
|
||||
val session = initiateFlow(notaryParty)
|
||||
val requestSignature = NotarisationRequest(stx.inputs, stx.id).generateSignature(serviceHub)
|
||||
if (serviceHub.networkMapCache.isValidatingNotary(notaryParty)) {
|
||||
sendAndReceiveValidating(session)
|
||||
sendAndReceiveValidating(session, requestSignature)
|
||||
} else {
|
||||
sendAndReceiveNonValidating(notaryParty, session)
|
||||
sendAndReceiveNonValidating(notaryParty, session, requestSignature)
|
||||
}
|
||||
} catch (e: NotaryException) {
|
||||
if (e.error is NotaryError.Conflict) {
|
||||
@ -92,21 +96,23 @@ class NotaryFlow {
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
protected open fun sendAndReceiveValidating(session: FlowSession): UntrustworthyData<List<TransactionSignature>> {
|
||||
subFlow(SendTransactionWithRetry(session, stx))
|
||||
private fun sendAndReceiveValidating(session: FlowSession, signature: NotarisationRequestSignature): UntrustworthyData<List<TransactionSignature>> {
|
||||
val payload = NotarisationPayload(stx, signature)
|
||||
subFlow(NotarySendTransactionFlow(session, payload))
|
||||
return session.receive()
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
protected open fun sendAndReceiveNonValidating(notaryParty: Party, session: FlowSession): UntrustworthyData<List<TransactionSignature>> {
|
||||
val tx: Any = if (stx.isNotaryChangeTransaction()) {
|
||||
private fun sendAndReceiveNonValidating(notaryParty: Party, session: FlowSession, signature: NotarisationRequestSignature): UntrustworthyData<List<TransactionSignature>> {
|
||||
val tx: CoreTransaction = if (stx.isNotaryChangeTransaction()) {
|
||||
stx.notaryChangeTx // Notary change transactions do not support filtering
|
||||
} else {
|
||||
stx.buildFilteredTransaction(Predicate { it is StateRef || it is TimeWindow || it == notaryParty })
|
||||
}
|
||||
return session.sendAndReceiveWithRetry(tx)
|
||||
return session.sendAndReceiveWithRetry(NotarisationPayload(tx, signature))
|
||||
}
|
||||
|
||||
/** Checks that the notary's signature(s) is/are valid. */
|
||||
protected fun validateResponse(response: UntrustworthyData<List<TransactionSignature>>, notaryParty: Party): List<TransactionSignature> {
|
||||
return response.unwrap { signatures ->
|
||||
signatures.forEach { validateSignature(it, stx.id, notaryParty) }
|
||||
@ -118,16 +124,16 @@ class NotaryFlow {
|
||||
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)
|
||||
/**
|
||||
* The [NotarySendTransactionFlow] flow is similar to [SendTransactionFlow], but uses [NotarisationPayload] as the
|
||||
* initial message, and retries message delivery.
|
||||
*/
|
||||
private class NotarySendTransactionFlow(otherSide: FlowSession, payload: NotarisationPayload) : DataVendingFlow(otherSide, payload) {
|
||||
@Suspendable
|
||||
override fun sendPayloadAndReceiveDataRequest(otherSideSession: FlowSession, payload: Any): UntrustworthyData<FetchDataFlow.Request> {
|
||||
return otherSideSession.sendAndReceiveWithRetry(payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -186,10 +192,16 @@ class NotaryFlow {
|
||||
*/
|
||||
data class TransactionParts(val id: SecureHash, val inputs: List<StateRef>, val timestamp: TimeWindow?, val notary: Party?)
|
||||
|
||||
/**
|
||||
* Exception thrown by the notary service if any issues are encountered while trying to commit a transaction. The
|
||||
* underlying [error] specifies the cause of failure.
|
||||
*/
|
||||
class NotaryException(val error: NotaryError) : FlowException("Unable to notarise: $error")
|
||||
|
||||
/** Specifies the cause for notarisation request failure. */
|
||||
@CordaSerializable
|
||||
sealed class NotaryError {
|
||||
/** Occurs when one or more input states of transaction with [txId] have already been consumed by another transaction. */
|
||||
data class Conflict(val txId: SecureHash, val conflict: SignedData<UniquenessProvider.Conflict>) : NotaryError() {
|
||||
override fun toString() = "One or more input states for transaction $txId have been used in another transaction"
|
||||
}
|
||||
@ -199,18 +211,27 @@ sealed class NotaryError {
|
||||
override fun toString() = "Current time $currentTime is outside the time bounds specified by the transaction: $txTimeWindow"
|
||||
|
||||
companion object {
|
||||
@JvmField @Deprecated("Here only for binary compatibility purposes, do not use.")
|
||||
@JvmField
|
||||
@Deprecated("Here only for binary compatibility purposes, do not use.")
|
||||
val INSTANCE = TimeWindowInvalid(Instant.EPOCH, TimeWindow.fromOnly(Instant.EPOCH))
|
||||
}
|
||||
}
|
||||
|
||||
/** Occurs when the provided transaction fails to verify. */
|
||||
data class TransactionInvalid(val cause: Throwable) : NotaryError() {
|
||||
override fun toString() = cause.toString()
|
||||
}
|
||||
|
||||
/** Occurs when the transaction sent for notarisation is assigned to a different notary identity. */
|
||||
object WrongNotary : NotaryError()
|
||||
|
||||
data class General(val cause: String): NotaryError() {
|
||||
override fun toString() = cause
|
||||
/** Occurs when the notarisation request signature does not verify for the provided transaction. */
|
||||
data class RequestSignatureInvalid(val cause: Throwable) : NotaryError() {
|
||||
override fun toString() = "Request signature invalid: $cause"
|
||||
}
|
||||
|
||||
/** Occurs when the notary service encounters an unexpected issue or becomes temporarily unavailable. */
|
||||
data class General(val cause: Throwable) : NotaryError() {
|
||||
override fun toString() = cause.toString()
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ open class SendTransactionFlow(otherSide: FlowSession, stx: SignedTransaction) :
|
||||
*/
|
||||
open class SendStateAndRefFlow(otherSideSession: FlowSession, stateAndRefs: List<StateAndRef<*>>) : DataVendingFlow(otherSideSession, stateAndRefs)
|
||||
|
||||
sealed class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any) : FlowLogic<Void?>() {
|
||||
open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any) : FlowLogic<Void?>() {
|
||||
@Suspendable
|
||||
protected open fun sendPayloadAndReceiveDataRequest(otherSideSession: FlowSession, payload: Any) = otherSideSession.sendAndReceive<FetchDataFlow.Request>(payload)
|
||||
|
||||
|
@ -50,9 +50,6 @@ data class CordaX500Name(val commonName: String?,
|
||||
// Legal name checks.
|
||||
LegalNameValidator.validateOrganization(organisation, LegalNameValidator.Validation.MINIMAL)
|
||||
|
||||
// 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) {
|
||||
@ -74,6 +71,7 @@ data class CordaX500Name(val commonName: String?,
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Deprecated("Not Used")
|
||||
const val LENGTH_COUNTRY = 2
|
||||
const val MAX_LENGTH_ORGANISATION = 128
|
||||
const val MAX_LENGTH_LOCALITY = 64
|
||||
|
@ -27,6 +27,10 @@ abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment {
|
||||
}
|
||||
|
||||
protected val attachmentData: ByteArray by lazy(dataLoader)
|
||||
|
||||
// TODO: read file size information from metadata instead of loading the data.
|
||||
override val size: Int get() = attachmentData.size
|
||||
|
||||
override fun open(): InputStream = attachmentData.inputStream()
|
||||
override val signers by lazy {
|
||||
// Can't start with empty set if we're doing intersections. Logically the null means "all possible signers":
|
||||
|
@ -2,17 +2,24 @@
|
||||
|
||||
package net.corda.core.internal
|
||||
|
||||
import net.corda.core.cordapp.Cordapp
|
||||
import net.corda.core.cordapp.CordappConfig
|
||||
import net.corda.core.cordapp.CordappContext
|
||||
import net.corda.core.cordapp.CordappProvider
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.flows.NotarisationRequest
|
||||
import net.corda.core.flows.NotarisationRequestSignature
|
||||
import net.corda.core.flows.NotaryFlow
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x500.X500NameBuilder
|
||||
import org.bouncycastle.asn1.x500.style.BCStyle
|
||||
@ -26,10 +33,12 @@ import java.lang.reflect.Field
|
||||
import java.math.BigDecimal
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.nio.ByteBuffer
|
||||
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.KeyPair
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Duration
|
||||
@ -307,6 +316,16 @@ val KClass<*>.packageName: String get() = java.`package`.name
|
||||
|
||||
fun URL.openHttpConnection(): HttpURLConnection = openConnection() as HttpURLConnection
|
||||
|
||||
fun URL.post(serializedData: OpaqueBytes) {
|
||||
openHttpConnection().apply {
|
||||
doOutput = true
|
||||
requestMethod = "POST"
|
||||
setRequestProperty("Content-Type", "application/octet-stream")
|
||||
outputStream.use { serializedData.open().copyTo(it) }
|
||||
checkOkResponse()
|
||||
}
|
||||
}
|
||||
|
||||
fun HttpURLConnection.checkOkResponse() {
|
||||
if (responseCode != 200) {
|
||||
val message = errorStream.use { it.reader().readText() }
|
||||
@ -353,3 +372,33 @@ fun <T : Any> T.signWithCert(privateKey: PrivateKey, certificate: X509Certificat
|
||||
val signature = Crypto.doSign(privateKey, serialised.bytes)
|
||||
return SignedDataWithCert(serialised, DigitalSignatureWithCert(certificate, signature))
|
||||
}
|
||||
|
||||
inline fun <T : Any> SerializedBytes<T>.sign(signer: (SerializedBytes<T>) -> DigitalSignature.WithKey): SignedData<T> {
|
||||
return SignedData(this, signer(this))
|
||||
}
|
||||
|
||||
inline fun <T : Any> SerializedBytes<T>.sign(keyPair: KeyPair): SignedData<T> {
|
||||
return SignedData(this, keyPair.sign(this.bytes))
|
||||
}
|
||||
|
||||
fun ByteBuffer.copyBytes() = ByteArray(remaining()).also { get(it) }
|
||||
|
||||
fun createCordappContext(cordapp: Cordapp, attachmentId: SecureHash?, classLoader: ClassLoader, config: CordappConfig): CordappContext {
|
||||
return CordappContext(cordapp, attachmentId, classLoader, config)
|
||||
}
|
||||
/** Verifies that the correct notarisation request was signed by the counterparty. */
|
||||
fun NotaryFlow.Service.validateRequest(request: NotarisationRequest, signature: NotarisationRequestSignature) {
|
||||
val requestingParty = otherSideSession.counterparty
|
||||
request.verifySignature(signature, requestingParty)
|
||||
// TODO: persist the signature for traceability. Do we need to persist the request as well?
|
||||
}
|
||||
|
||||
/** Creates a signature over the notarisation request using the legal identity key. */
|
||||
fun NotarisationRequest.generateSignature(serviceHub: ServiceHub): NotarisationRequestSignature {
|
||||
val serializedRequest = this.serialize().bytes
|
||||
val signature = with(serviceHub) {
|
||||
val myLegalIdentity = myInfo.legalIdentitiesAndCerts.first().owningKey
|
||||
keyManagementService.sign(serializedRequest, myLegalIdentity)
|
||||
}
|
||||
return NotarisationRequestSignature(signature, serviceHub.myInfo.platformVersion)
|
||||
}
|
@ -91,7 +91,8 @@ object LegalNameValidator {
|
||||
CapitalLetterRule()
|
||||
)
|
||||
val legalNameRules: List<Rule<String>> = attributeRules + listOf(
|
||||
WordRule("node", "server"),
|
||||
// Removal of word restriction was requested in https://github.com/corda/corda/issues/2326
|
||||
// WordRule("node", "server"),
|
||||
X500NameRule()
|
||||
)
|
||||
val legalNameFullRules: List<Rule<String>> = legalNameRules + listOf(
|
||||
|
@ -27,12 +27,11 @@ class X509EdDSAEngine : Signature {
|
||||
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
|
||||
val parsedKey = try {
|
||||
publicKey as? EdDSAPublicKey ?: EdDSAPublicKey(X509EncodedKeySpec(publicKey.encoded))
|
||||
} catch(e: Exception) {
|
||||
throw (InvalidKeyException(e.message))
|
||||
}
|
||||
|
||||
engine.initVerify(parsedKey)
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,7 @@
|
||||
package net.corda.core.messaging
|
||||
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.context.Actor
|
||||
import net.corda.core.context.AuthServiceId
|
||||
import net.corda.core.context.InvocationContext
|
||||
import net.corda.core.context.Origin
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowInitiator
|
||||
@ -13,6 +10,7 @@ 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.NetworkParameters
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.node.services.NetworkMapCache
|
||||
@ -23,48 +21,45 @@ import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.Try
|
||||
import rx.Observable
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
|
||||
private val unknownName = CordaX500Name("UNKNOWN", "UNKNOWN", "GB")
|
||||
|
||||
/**
|
||||
* Represents information about a flow (the name "state machine" is legacy, Kotlin users can use the [FlowInfo] type
|
||||
* alias). You can access progress tracking, information about why the flow was started and so on.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class StateMachineInfo @JvmOverloads constructor(
|
||||
/** A univerally unique ID ([java.util.UUID]) representing this particular instance of the named flow. */
|
||||
val id: StateMachineRunId,
|
||||
/** The JVM class name of the flow code. */
|
||||
val flowLogicClassName: String,
|
||||
val initiator: FlowInitiator,
|
||||
/**
|
||||
* An object representing information about the initiator of the flow. Note that this field is
|
||||
* superceded by the [invocationContext] property, which has more detail.
|
||||
*/
|
||||
@Deprecated("There is more info available using 'context'") val initiator: FlowInitiator,
|
||||
/** A [DataFeed] of the current progress step as a human readable string, and updates to that string. */
|
||||
val progressTrackerStepAndUpdates: DataFeed<String, String>?,
|
||||
val context: InvocationContext? = null
|
||||
/** An [InvocationContext] describing why and by whom the flow was started. */
|
||||
val invocationContext: InvocationContext = initiator.invocationContext
|
||||
) {
|
||||
fun context(): InvocationContext = context ?: contextFrom(initiator)
|
||||
|
||||
private fun contextFrom(initiator: FlowInitiator): InvocationContext {
|
||||
var actor: Actor? = null
|
||||
val origin: Origin
|
||||
when (initiator) {
|
||||
is FlowInitiator.RPC -> {
|
||||
actor = Actor(Actor.Id(initiator.username), AuthServiceId("UNKNOWN"), unknownName)
|
||||
origin = Origin.RPC(actor)
|
||||
}
|
||||
is FlowInitiator.Peer -> origin = Origin.Peer(initiator.party.name)
|
||||
is FlowInitiator.Service -> origin = Origin.Service(initiator.serviceClassName, unknownName)
|
||||
is FlowInitiator.Shell -> origin = Origin.Shell
|
||||
is FlowInitiator.Scheduled -> origin = Origin.Scheduled(initiator.scheduledState)
|
||||
}
|
||||
return InvocationContext.newInstance(origin = origin, actor = actor)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun copy(id: StateMachineRunId = this.id,
|
||||
flowLogicClassName: String = this.flowLogicClassName,
|
||||
initiator: FlowInitiator = this.initiator,
|
||||
progressTrackerStepAndUpdates: DataFeed<String, String>? = this.progressTrackerStepAndUpdates): StateMachineInfo {
|
||||
return copy(id = id, flowLogicClassName = flowLogicClassName, initiator = initiator, progressTrackerStepAndUpdates = progressTrackerStepAndUpdates, context = context)
|
||||
return copy(id = id, flowLogicClassName = flowLogicClassName, initiator = initiator, progressTrackerStepAndUpdates = progressTrackerStepAndUpdates, invocationContext = invocationContext)
|
||||
}
|
||||
|
||||
override fun toString(): String = "${javaClass.simpleName}($id, $flowLogicClassName)"
|
||||
}
|
||||
|
||||
/** An alias for [StateMachineInfo] which uses more modern terminology. */
|
||||
typealias FlowInfo = StateMachineInfo
|
||||
|
||||
@CordaSerializable
|
||||
sealed class StateMachineUpdate {
|
||||
abstract val id: StateMachineRunId
|
||||
@ -76,6 +71,24 @@ sealed class StateMachineUpdate {
|
||||
data class Removed(override val id: StateMachineRunId, val result: Try<*>) : StateMachineUpdate()
|
||||
}
|
||||
|
||||
// DOCSTART 1
|
||||
/**
|
||||
* Data class containing information about the scheduled network parameters update. The info is emitted every time node
|
||||
* receives network map with [ParametersUpdate] which wasn't seen before. For more information see: [CordaRPCOps.networkParametersFeed] and [CordaRPCOps.acceptNewNetworkParameters].
|
||||
* @property hash new [NetworkParameters] hash
|
||||
* @property parameters new [NetworkParameters] data structure
|
||||
* @property description description of the update
|
||||
* @property updateDeadline deadline for accepting this update using [CordaRPCOps.acceptNewNetworkParameters]
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class ParametersUpdateInfo(
|
||||
val hash: SecureHash,
|
||||
val parameters: NetworkParameters,
|
||||
val description: String,
|
||||
val updateDeadline: Instant
|
||||
)
|
||||
// DOCEND 1
|
||||
|
||||
@CordaSerializable
|
||||
data class StateMachineTransactionMapping(val stateMachineRunId: StateMachineRunId, val transactionId: SecureHash)
|
||||
|
||||
@ -209,6 +222,29 @@ interface CordaRPCOps : RPCOps {
|
||||
@RPCReturnsObservables
|
||||
fun networkMapFeed(): DataFeed<List<NodeInfo>, NetworkMapCache.MapChange>
|
||||
|
||||
/**
|
||||
* Returns [DataFeed] object containing information on currently scheduled parameters update (null if none are currently scheduled)
|
||||
* and observable with future update events. Any update that occurs before the deadline automatically cancels the current one.
|
||||
* Only the latest update can be accepted.
|
||||
* Note: This operation may be restricted only to node administrators.
|
||||
*/
|
||||
// TODO This operation should be restricted to just node admins.
|
||||
@RPCReturnsObservables
|
||||
fun networkParametersFeed(): DataFeed<ParametersUpdateInfo?, ParametersUpdateInfo>
|
||||
|
||||
/**
|
||||
* Accept network parameters with given hash, hash is obtained through [networkParametersFeed] method.
|
||||
* Information is sent back to the zone operator that the node accepted the parameters update - this process cannot be
|
||||
* undone.
|
||||
* Only parameters that are scheduled for update can be accepted, if different hash is provided this method will fail.
|
||||
* Note: This operation may be restricted only to node administrators.
|
||||
* @param parametersHash hash of network parameters to accept
|
||||
* @throws IllegalArgumentException if network map advertises update with different parameters hash then the one accepted by node's operator.
|
||||
* @throws IOException if failed to send the approval to network map
|
||||
*/
|
||||
// TODO This operation should be restricted to just node admins.
|
||||
fun acceptNewNetworkParameters(parametersHash: SecureHash)
|
||||
|
||||
/**
|
||||
* Start the given flow with the given arguments. [logicType] must be annotated
|
||||
* with [net.corda.core.flows.StartableByRPC].
|
||||
@ -321,6 +357,21 @@ interface CordaRPCOps : RPCOps {
|
||||
|
||||
/** Clear all network map data from local node cache. */
|
||||
fun clearNetworkMapCache()
|
||||
|
||||
/** Sets the value of the node's flows draining mode.
|
||||
* If this mode is [enabled], the node will reject new flows through RPC, ignore scheduled flows, and do not process
|
||||
* initial session messages, meaning that P2P counter-parties will not be able to initiate new flows involving the node.
|
||||
*
|
||||
* @param enabled whether the flows draining mode will be enabled.
|
||||
* */
|
||||
fun setFlowsDrainingModeEnabled(enabled: Boolean)
|
||||
|
||||
/**
|
||||
* Returns whether the flows draining mode is enabled.
|
||||
*
|
||||
* @see setFlowsDrainingModeEnabled
|
||||
*/
|
||||
fun isFlowsDrainingModeEnabled(): Boolean
|
||||
}
|
||||
|
||||
inline fun <reified T : ContractState> CordaRPCOps.vaultQueryBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(),
|
||||
|
@ -0,0 +1,44 @@
|
||||
package net.corda.core.node
|
||||
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Network parameters are a set of values that every node participating in the zone needs to agree on and use to
|
||||
* correctly interoperate with each other.
|
||||
* @property minimumPlatformVersion Minimum version of Corda platform that is required for nodes in the network.
|
||||
* @property notaries List of well known and trusted notary identities with information on validation type.
|
||||
* @property maxMessageSize Maximum P2P message sent over the wire in bytes.
|
||||
* @property maxTransactionSize Maximum permitted transaction size in bytes.
|
||||
* @property modifiedTime Last modification time of network parameters set.
|
||||
* @property epoch Version number of the network parameters. Starting from 1, this will always increment on each new set
|
||||
* of parameters.
|
||||
*/
|
||||
// TODO Add eventHorizon - how many days a node can be offline before being automatically ejected from the network.
|
||||
// It needs separate design.
|
||||
@CordaSerializable
|
||||
data class NetworkParameters(
|
||||
val minimumPlatformVersion: Int,
|
||||
val notaries: List<NotaryInfo>,
|
||||
val maxMessageSize: Int,
|
||||
val maxTransactionSize: Int,
|
||||
val modifiedTime: Instant,
|
||||
val epoch: Int
|
||||
) {
|
||||
init {
|
||||
require(minimumPlatformVersion > 0) { "minimumPlatformVersion must be at least 1" }
|
||||
require(notaries.distinctBy { it.identity } == notaries) { "Duplicate notary identities" }
|
||||
require(epoch > 0) { "epoch must be at least 1" }
|
||||
require(maxMessageSize > 0) { "maxMessageSize must be at least 1" }
|
||||
require(maxTransactionSize > 0) { "maxTransactionSize must be at least 1" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data class storing information about notaries available in the network.
|
||||
* @property identity Identity of the notary (note that it can be an identity of the distributed node).
|
||||
* @property validating Indicates if the notary is validating.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class NotaryInfo(val identity: Party, val validating: Boolean)
|
@ -26,7 +26,9 @@ data class NodeInfo(val addresses: List<NetworkHostAndPort>,
|
||||
) {
|
||||
// TODO We currently don't support multi-IP/multi-identity nodes, we only left slots in the data structures.
|
||||
init {
|
||||
require(legalIdentitiesAndCerts.isNotEmpty()) { "Node should have at least one legal identity" }
|
||||
require(addresses.isNotEmpty()) { "Node must have at least one address" }
|
||||
require(legalIdentitiesAndCerts.isNotEmpty()) { "Node must have at least one legal identity" }
|
||||
require(platformVersion > 0) { "Platform version must be at least 1" }
|
||||
}
|
||||
|
||||
@Transient private var _legalIdentities: List<Party>? = null
|
||||
|
@ -2,6 +2,7 @@ package net.corda.core.node
|
||||
|
||||
import net.corda.core.DoNotImplement
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.cordapp.CordappContext
|
||||
import net.corda.core.cordapp.CordappProvider
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SignableData
|
||||
@ -60,6 +61,9 @@ interface ServicesForResolution : StateLoader {
|
||||
|
||||
/** Provides access to anything relating to cordapps including contract attachment resolution and app context */
|
||||
val cordappProvider: CordappProvider
|
||||
|
||||
/** Returns the network parameters the node is operating under. */
|
||||
val networkParameters: NetworkParameters
|
||||
}
|
||||
|
||||
/**
|
||||
@ -369,4 +373,9 @@ interface ServiceHub : ServicesForResolution {
|
||||
* node starts.
|
||||
*/
|
||||
fun registerUnloadHandler(runOnStop: () -> Unit)
|
||||
|
||||
/**
|
||||
* See [CordappProvider.getAppContext]
|
||||
*/
|
||||
fun getAppContext(): CordappContext = cordappProvider.getAppContext()
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ package net.corda.core.node.services
|
||||
|
||||
import net.corda.core.DoNotImplement
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
@ -68,12 +67,22 @@ interface NetworkMapCacheBase {
|
||||
fun track(): DataFeed<List<NodeInfo>, NetworkMapCache.MapChange>
|
||||
|
||||
/**
|
||||
* Look up the node info for a legal name.
|
||||
* Notice that when there are more than one node for a given name (in case of distributed services) first service node
|
||||
* found will be returned.
|
||||
* Return a [NodeInfo] which has the given legal name for one of its identities, or null if no such node is found.
|
||||
*
|
||||
* @throws IllegalArgumentException If more than one matching node is found, in the case of a distributed service identity
|
||||
* (such as with a notary cluster). For such a scenerio use [getNodesByLegalName] instead.
|
||||
*/
|
||||
fun getNodeByLegalName(name: CordaX500Name): NodeInfo?
|
||||
|
||||
/**
|
||||
* Return a list of [NodeInfo]s which have the given legal name for one of their identities, or an empty list if no
|
||||
* such nodes are found.
|
||||
*
|
||||
* Normally there is at most one node for a legal name, but for distributed service identities (such as with a notary
|
||||
* cluster) there can be multiple nodes sharing the same identity.
|
||||
*/
|
||||
fun getNodesByLegalName(name: CordaX500Name): List<NodeInfo>
|
||||
|
||||
/** Look up the node info for a host and port. */
|
||||
fun getNodeByAddress(address: NetworkHostAndPort): NodeInfo?
|
||||
|
||||
@ -101,13 +110,6 @@ interface NetworkMapCacheBase {
|
||||
*/
|
||||
fun getNodesByLegalIdentityKey(identityKey: PublicKey): List<NodeInfo>
|
||||
|
||||
/**
|
||||
* Look up the node information entries for a legal name.
|
||||
* Note that normally there will be only one node for a legal name, but for clusters of nodes or distributed services there
|
||||
* can be multiple nodes.
|
||||
*/
|
||||
fun getNodesByLegalName(name: CordaX500Name): List<NodeInfo>
|
||||
|
||||
/** Returns information about the party, which may be a specific node or a service */
|
||||
fun getPartyInfo(party: Party): PartyInfo?
|
||||
|
||||
|
@ -94,7 +94,7 @@ abstract class TrustedAuthorityNotaryService : NotaryService() {
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log.error("Internal error", e)
|
||||
throw NotaryException(NotaryError.General("Service unavailable, please try again later"))
|
||||
throw NotaryException(NotaryError.General(Exception("Service unavailable, please try again later")))
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,15 +104,19 @@ abstract class TrustedAuthorityNotaryService : NotaryService() {
|
||||
return NotaryException(NotaryError.Conflict(txId, signedConflict))
|
||||
}
|
||||
|
||||
/** Sign a [ByteArray] input. */
|
||||
fun sign(bits: ByteArray): DigitalSignature.WithKey {
|
||||
return services.keyManagementService.sign(bits, notaryIdentityKey)
|
||||
}
|
||||
|
||||
/** Sign a single transaction. */
|
||||
fun sign(txId: SecureHash): TransactionSignature {
|
||||
val signableData = SignableData(txId, SignatureMetadata(services.myInfo.platformVersion, Crypto.findSignatureScheme(notaryIdentityKey).schemeNumberID))
|
||||
return services.keyManagementService.sign(signableData, notaryIdentityKey)
|
||||
}
|
||||
|
||||
// TODO: Sign multiple transactions at once by building their Merkle tree and then signing over its root.
|
||||
|
||||
@Deprecated("This property is no longer used") @Suppress("DEPRECATION")
|
||||
protected open val timeWindowChecker: TimeWindowChecker get() = throw UnsupportedOperationException("No default implementation, need to override")
|
||||
}
|
@ -98,9 +98,7 @@ abstract class SerializationFactory {
|
||||
val currentFactory: SerializationFactory? get() = _currentFactory.get()
|
||||
}
|
||||
}
|
||||
|
||||
typealias VersionHeader = ByteSequence
|
||||
|
||||
typealias SerializationMagic = ByteSequence
|
||||
/**
|
||||
* Parameters to serialization and deserialization.
|
||||
*/
|
||||
@ -108,7 +106,7 @@ interface SerializationContext {
|
||||
/**
|
||||
* When serializing, use the format this header sequence represents.
|
||||
*/
|
||||
val preferredSerializationVersion: VersionHeader
|
||||
val preferredSerializationVersion: SerializationMagic
|
||||
/**
|
||||
* The class loader to use for deserialization.
|
||||
*/
|
||||
@ -161,7 +159,7 @@ interface SerializationContext {
|
||||
/**
|
||||
* Helper method to return a new context based on this context but with serialization using the format this header sequence represents.
|
||||
*/
|
||||
fun withPreferredSerializationVersion(versionHeader: VersionHeader): SerializationContext
|
||||
fun withPreferredSerializationVersion(magic: SerializationMagic): SerializationContext
|
||||
|
||||
/**
|
||||
* The use case that we are serializing for, since it influences the implementations chosen.
|
||||
@ -225,6 +223,7 @@ fun <T : Any> T.serialize(serializationFactory: SerializationFactory = Serializa
|
||||
* A type safe wrapper around a byte array that contains a serialised object. You can call [SerializedBytes.deserialize]
|
||||
* to get the original object back.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
class SerializedBytes<T : Any>(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
// It's OK to use lazy here because SerializedBytes is configured to use the ImmutableClassSerializer.
|
||||
val hash: SecureHash by lazy { bytes.sha256() }
|
||||
|
@ -3,12 +3,14 @@ package net.corda.core.transactions
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
|
||||
/**
|
||||
* A transaction with the minimal amount of information required to compute the unique transaction [id], and
|
||||
* resolve a [FullTransaction]. This type of transaction, wrapped in [SignedTransaction], gets transferred across the
|
||||
* wire and recorded to storage.
|
||||
*/
|
||||
@CordaSerializable
|
||||
abstract class CoreTransaction : BaseTransaction() {
|
||||
/** The inputs of this transaction, containing state references only **/
|
||||
abstract override val inputs: List<StateRef>
|
||||
|
@ -68,15 +68,15 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
|
||||
*/
|
||||
fun buildFilteredTransaction(filtering: Predicate<Any>) = tx.buildFilteredTransaction(filtering)
|
||||
|
||||
/** Helper to access the inputs of the contained transaction */
|
||||
/** Helper to access the inputs of the contained transaction. */
|
||||
val inputs: List<StateRef> get() = transaction.inputs
|
||||
/** Helper to access the notary of the contained transaction */
|
||||
/** Helper to access the notary of the contained transaction. */
|
||||
val notary: Party? get() = transaction.notary
|
||||
|
||||
override val requiredSigningKeys: Set<PublicKey> get() = tx.requiredSigningKeys
|
||||
|
||||
override fun getKeyDescriptions(keys: Set<PublicKey>): ArrayList<String> {
|
||||
// TODO: We need a much better way of structuring this data
|
||||
// TODO: We need a much better way of structuring this data.
|
||||
val descriptions = ArrayList<String>()
|
||||
this.tx.commands.forEach { command ->
|
||||
if (command.signers.any { it in keys })
|
||||
@ -134,8 +134,18 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
|
||||
@JvmOverloads
|
||||
@Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class)
|
||||
fun toLedgerTransaction(services: ServiceHub, checkSufficientSignatures: Boolean = true): LedgerTransaction {
|
||||
checkSignaturesAreValid()
|
||||
if (checkSufficientSignatures) verifyRequiredSignatures()
|
||||
// TODO: We could probably optimise the below by
|
||||
// a) not throwing if threshold is eventually satisfied, but some of the rest of the signatures are failing.
|
||||
// b) omit verifying signatures when threshold requirement is met.
|
||||
// c) omit verifying signatures from keys not included in [requiredSigningKeys].
|
||||
// For the above to work, [checkSignaturesAreValid] should take the [requiredSigningKeys] as input
|
||||
// and probably combine logic from signature validation and key-fulfilment
|
||||
// in [TransactionWithSignatures.verifySignaturesExcept].
|
||||
if (checkSufficientSignatures) {
|
||||
verifyRequiredSignatures() // It internally invokes checkSignaturesAreValid().
|
||||
} else {
|
||||
checkSignaturesAreValid()
|
||||
}
|
||||
return tx.toLedgerTransaction(services)
|
||||
}
|
||||
|
||||
@ -153,28 +163,25 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
|
||||
@Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class)
|
||||
fun verify(services: ServiceHub, checkSufficientSignatures: Boolean = true) {
|
||||
if (isNotaryChangeTransaction()) {
|
||||
verifyNotaryChangeTransaction(checkSufficientSignatures, services)
|
||||
verifyNotaryChangeTransaction(services, checkSufficientSignatures)
|
||||
} else {
|
||||
verifyRegularTransaction(checkSufficientSignatures, services)
|
||||
verifyRegularTransaction(services, checkSufficientSignatures)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Verify contract constraints here as well as in LedgerTransaction to ensure that anything being deserialised
|
||||
* from the attachment is trusted. This will require some partial serialisation work to not load the ContractState
|
||||
* objects from the TransactionState.
|
||||
*/
|
||||
private fun verifyRegularTransaction(checkSufficientSignatures: Boolean, services: ServiceHub) {
|
||||
checkSignaturesAreValid()
|
||||
if (checkSufficientSignatures) verifyRequiredSignatures()
|
||||
val ltx = tx.toLedgerTransaction(services)
|
||||
// TODO: allow non-blocking verification
|
||||
// TODO: Verify contract constraints here as well as in LedgerTransaction to ensure that anything being deserialised
|
||||
// from the attachment is trusted. This will require some partial serialisation work to not load the ContractState
|
||||
// objects from the TransactionState.
|
||||
private fun verifyRegularTransaction(services: ServiceHub, checkSufficientSignatures: Boolean) {
|
||||
val ltx = toLedgerTransaction(services, checkSufficientSignatures)
|
||||
// TODO: allow non-blocking verification.
|
||||
services.transactionVerifierService.verify(ltx).getOrThrow()
|
||||
}
|
||||
|
||||
private fun verifyNotaryChangeTransaction(checkSufficientSignatures: Boolean, services: ServiceHub) {
|
||||
private fun verifyNotaryChangeTransaction(services: ServiceHub, checkSufficientSignatures: Boolean) {
|
||||
val ntx = resolveNotaryChangeTransaction(services)
|
||||
if (checkSufficientSignatures) ntx.verifyRequiredSignatures()
|
||||
else checkSignaturesAreValid()
|
||||
}
|
||||
|
||||
fun isNotaryChangeTransaction() = transaction is NotaryChangeWireTransaction
|
||||
|
@ -11,7 +11,7 @@ import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
import java.util.*
|
||||
|
||||
/** An interface for transactions containing signatures, with logic for signature verification */
|
||||
/** An interface for transactions containing signatures, with logic for signature verification. */
|
||||
@DoNotImplement
|
||||
interface TransactionWithSignatures : NamedByHash {
|
||||
/**
|
||||
@ -21,7 +21,7 @@ interface TransactionWithSignatures : NamedByHash {
|
||||
*/
|
||||
val sigs: List<TransactionSignature>
|
||||
|
||||
/** Specifies all the public keys that require signatures for the transaction to be valid */
|
||||
/** Specifies all the public keys that require signatures for the transaction to be valid. */
|
||||
val requiredSigningKeys: Set<PublicKey>
|
||||
|
||||
/**
|
||||
@ -65,11 +65,10 @@ interface TransactionWithSignatures : NamedByHash {
|
||||
*/
|
||||
@Throws(SignatureException::class)
|
||||
fun verifySignaturesExcept(allowedToBeMissing: Collection<PublicKey>) {
|
||||
checkSignaturesAreValid()
|
||||
|
||||
val needed = getMissingSigners() - allowedToBeMissing
|
||||
if (needed.isNotEmpty())
|
||||
throw SignaturesMissingException(needed.toNonEmptySet(), getKeyDescriptions(needed), id)
|
||||
checkSignaturesAreValid()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -84,11 +84,12 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
*/
|
||||
@Throws(AttachmentResolutionException::class, TransactionResolutionException::class)
|
||||
fun toLedgerTransaction(services: ServicesForResolution): LedgerTransaction {
|
||||
return toLedgerTransaction(
|
||||
return toLedgerTransactionInternal(
|
||||
resolveIdentity = { services.identityService.partyFromKey(it) },
|
||||
resolveAttachment = { services.attachments.openAttachment(it) },
|
||||
resolveStateRef = { services.loadState(it) },
|
||||
resolveContractAttachment = { services.cordappProvider.getContractAttachmentID(it.contract) }
|
||||
resolveContractAttachment = { services.cordappProvider.getContractAttachmentID(it.contract) },
|
||||
maxTransactionSize = services.networkParameters.maxTransactionSize
|
||||
)
|
||||
}
|
||||
|
||||
@ -99,12 +100,23 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
* @throws AttachmentResolutionException if a required attachment was not found using [resolveAttachment].
|
||||
* @throws TransactionResolutionException if an input was not found not using [resolveStateRef].
|
||||
*/
|
||||
@Deprecated("Use toLedgerTransaction(ServicesForTransaction) instead")
|
||||
@Throws(AttachmentResolutionException::class, TransactionResolutionException::class)
|
||||
fun toLedgerTransaction(
|
||||
resolveIdentity: (PublicKey) -> Party?,
|
||||
resolveAttachment: (SecureHash) -> Attachment?,
|
||||
resolveStateRef: (StateRef) -> TransactionState<*>?,
|
||||
resolveContractAttachment: (TransactionState<ContractState>) -> AttachmentId?
|
||||
): LedgerTransaction {
|
||||
return toLedgerTransactionInternal(resolveIdentity, resolveAttachment, resolveStateRef, resolveContractAttachment, 10485760)
|
||||
}
|
||||
|
||||
private fun toLedgerTransactionInternal(
|
||||
resolveIdentity: (PublicKey) -> Party?,
|
||||
resolveAttachment: (SecureHash) -> Attachment?,
|
||||
resolveStateRef: (StateRef) -> TransactionState<*>?,
|
||||
resolveContractAttachment: (TransactionState<ContractState>) -> AttachmentId?,
|
||||
maxTransactionSize: Int
|
||||
): LedgerTransaction {
|
||||
// Look up public keys to authenticated identities. This is just a stub placeholder and will all change in future.
|
||||
val authenticatedArgs = commands.map {
|
||||
@ -118,7 +130,24 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
val contractAttachments = findAttachmentContracts(resolvedInputs, resolveContractAttachment, resolveAttachment)
|
||||
// Order of attachments is important since contracts may refer to indexes so only append automatic attachments
|
||||
val attachments = (attachments.map { resolveAttachment(it) ?: throw AttachmentResolutionException(it) } + contractAttachments).distinct()
|
||||
return LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, timeWindow, privacySalt)
|
||||
val ltx = LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, timeWindow, privacySalt)
|
||||
checkTransactionSize(ltx, maxTransactionSize)
|
||||
return ltx
|
||||
}
|
||||
|
||||
private fun checkTransactionSize(ltx: LedgerTransaction, maxTransactionSize: Int) {
|
||||
var remainingTransactionSize = maxTransactionSize
|
||||
|
||||
fun minus(size: Int) {
|
||||
require(remainingTransactionSize > size) { "Transaction exceeded network's maximum transaction size limit : $maxTransactionSize bytes." }
|
||||
remainingTransactionSize -= size
|
||||
}
|
||||
|
||||
// Check attachment size first as they are most likely to go over the limit.
|
||||
ltx.attachments.associateBy(Attachment::id).values.forEach { minus(it.size) }
|
||||
minus(ltx.inputs.serialize().size)
|
||||
minus(ltx.commands.serialize().size)
|
||||
minus(ltx.outputs.serialize().size)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -4,44 +4,27 @@ package net.corda.core.utilities
|
||||
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.OutputStream
|
||||
import java.lang.Math.max
|
||||
import java.lang.Math.min
|
||||
import java.nio.ByteBuffer
|
||||
import javax.xml.bind.DatatypeConverter
|
||||
|
||||
/**
|
||||
* An abstraction of a byte array, with offset and size that does no copying of bytes unless asked to.
|
||||
*
|
||||
* The data of interest typically starts at position [offset] within the [bytes] and is [size] bytes long.
|
||||
*
|
||||
* @property offset The start position of the sequence within the byte array.
|
||||
* @property size The number of bytes this sequence represents.
|
||||
*/
|
||||
@CordaSerializable
|
||||
sealed class ByteSequence : Comparable<ByteSequence> {
|
||||
constructor() {
|
||||
this._bytes = COPY_BYTES
|
||||
}
|
||||
|
||||
/**
|
||||
* This constructor allows to bypass calls to [bytes] for functions in this class if the implementation
|
||||
* of [bytes] makes a copy of the underlying [ByteArray] (as [OpaqueBytes] does for safety). This improves
|
||||
* performance. It is recommended to use this constructor rather than the default constructor.
|
||||
*/
|
||||
constructor(uncopiedBytes: ByteArray) {
|
||||
this._bytes = uncopiedBytes
|
||||
}
|
||||
|
||||
sealed class ByteSequence(private val _bytes: ByteArray, val offset: Int, val size: Int) : Comparable<ByteSequence> {
|
||||
/**
|
||||
* The underlying bytes. Some implementations may choose to make a copy of the underlying [ByteArray] for
|
||||
* security reasons. For example, [OpaqueBytes].
|
||||
*/
|
||||
abstract val bytes: ByteArray
|
||||
/**
|
||||
* The number of bytes this sequence represents.
|
||||
*/
|
||||
abstract val size: Int
|
||||
/**
|
||||
* The start position of the sequence within the byte array.
|
||||
*/
|
||||
abstract val offset: Int
|
||||
|
||||
private val _bytes: ByteArray
|
||||
get() = if (field === COPY_BYTES) bytes else field
|
||||
|
||||
/** Returns a [ByteArrayInputStream] of the bytes */
|
||||
fun open() = ByteArrayInputStream(_bytes, offset, size)
|
||||
@ -53,6 +36,7 @@ sealed class ByteSequence : Comparable<ByteSequence> {
|
||||
* @param offset The offset within this sequence to start the new sequence. Note: not the offset within the backing array.
|
||||
* @param size The size of the intended sub sequence.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanPrivate")
|
||||
fun subSequence(offset: Int, size: Int): ByteSequence {
|
||||
require(offset >= 0)
|
||||
require(offset + size <= this.size)
|
||||
@ -71,23 +55,40 @@ sealed class ByteSequence : Comparable<ByteSequence> {
|
||||
fun of(bytes: ByteArray, offset: Int = 0, size: Int = bytes.size): ByteSequence {
|
||||
return OpaqueBytesSubSequence(bytes, offset, size)
|
||||
}
|
||||
|
||||
private val COPY_BYTES: ByteArray = ByteArray(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Take the first n bytes of this sequence as a sub-sequence. See [subSequence] for further semantics.
|
||||
*/
|
||||
fun take(n: Int): ByteSequence {
|
||||
require(size >= n)
|
||||
return subSequence(0, n)
|
||||
fun take(n: Int): ByteSequence = subSequence(0, n)
|
||||
|
||||
/**
|
||||
* A new read-only [ByteBuffer] view of this sequence or part of it.
|
||||
* If [start] or [end] are negative then [IllegalArgumentException] is thrown, otherwise they are clamped if necessary.
|
||||
* This method cannot be used to get bytes before [offset] or after [offset]+[size], and never makes a new array.
|
||||
*/
|
||||
fun slice(start: Int = 0, end: Int = size): ByteBuffer {
|
||||
require(start >= 0)
|
||||
require(end >= 0)
|
||||
val clampedStart = min(start, size)
|
||||
val clampedEnd = min(end, size)
|
||||
return ByteBuffer.wrap(_bytes, offset + clampedStart, max(0, clampedEnd - clampedStart)).asReadOnlyBuffer()
|
||||
}
|
||||
|
||||
/** Write this sequence to an [OutputStream]. */
|
||||
fun writeTo(output: OutputStream) = output.write(_bytes, offset, size)
|
||||
|
||||
/** Write this sequence to a [ByteBuffer]. */
|
||||
fun putTo(buffer: ByteBuffer): ByteBuffer = buffer.put(_bytes, offset, size)
|
||||
|
||||
/**
|
||||
* Copy this sequence, complete with new backing array. This can be helpful to break references to potentially
|
||||
* large backing arrays from small sub-sequences.
|
||||
*/
|
||||
fun copy(): ByteSequence = of(_bytes.copyOfRange(offset, offset + size))
|
||||
fun copy(): ByteSequence = of(copyBytes())
|
||||
|
||||
/** Same as [copy] but returns just the new byte array. */
|
||||
fun copyBytes(): ByteArray = _bytes.copyOfRange(offset, offset + size)
|
||||
|
||||
/**
|
||||
* Compare byte arrays byte by byte. Arrays that are shorter are deemed less than longer arrays if all the bytes
|
||||
@ -135,7 +136,7 @@ sealed class ByteSequence : Comparable<ByteSequence> {
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String = "[${_bytes.copyOfRange(offset, offset + size).toHexString()}]"
|
||||
override fun toString(): String = "[${copyBytes().toHexString()}]"
|
||||
}
|
||||
|
||||
/**
|
||||
@ -143,7 +144,7 @@ sealed class ByteSequence : Comparable<ByteSequence> {
|
||||
* In an ideal JVM this would be a value type and be completely overhead free. Project Valhalla is adding such
|
||||
* functionality to Java, but it won't arrive for a few years yet!
|
||||
*/
|
||||
open class OpaqueBytes(bytes: ByteArray) : ByteSequence(bytes) {
|
||||
open class OpaqueBytes(bytes: ByteArray) : ByteSequence(bytes, 0, bytes.size) {
|
||||
companion object {
|
||||
/**
|
||||
* Create [OpaqueBytes] from a sequence of [Byte] values.
|
||||
@ -158,8 +159,8 @@ open class OpaqueBytes(bytes: ByteArray) : ByteSequence(bytes) {
|
||||
|
||||
/**
|
||||
* The bytes are always cloned so that this object becomes immutable. This has been done
|
||||
* to prevent tampering with entities such as [SecureHash] and [PrivacySalt], as well as
|
||||
* preserve the integrity of our hash constants [zeroHash] and [allOnesHash].
|
||||
* to prevent tampering with entities such as [net.corda.core.crypto.SecureHash] and [net.corda.core.contracts.PrivacySalt], as well as
|
||||
* preserve the integrity of our hash constants [net.corda.core.crypto.SecureHash.zeroHash] and [net.corda.core.crypto.SecureHash.allOnesHash].
|
||||
*
|
||||
* Cloning like this may become a performance issue, depending on whether or not the JIT
|
||||
* compiler is ever able to optimise away the clone. In which case we may need to revisit
|
||||
@ -167,8 +168,6 @@ open class OpaqueBytes(bytes: ByteArray) : ByteSequence(bytes) {
|
||||
*/
|
||||
override final val bytes: ByteArray = bytes
|
||||
get() = field.clone()
|
||||
override val size: Int = bytes.size
|
||||
override val offset: Int = 0
|
||||
}
|
||||
|
||||
/**
|
||||
@ -190,7 +189,7 @@ fun String.parseAsHex(): ByteArray = DatatypeConverter.parseHexBinary(this)
|
||||
/**
|
||||
* Class is public for serialization purposes
|
||||
*/
|
||||
class OpaqueBytesSubSequence(override val bytes: ByteArray, override val offset: Int, override val size: Int) : ByteSequence(bytes) {
|
||||
class OpaqueBytesSubSequence(override val bytes: ByteArray, offset: Int, size: Int) : ByteSequence(bytes, offset, size) {
|
||||
init {
|
||||
require(offset >= 0 && offset < bytes.size)
|
||||
require(size >= 0 && size <= bytes.size)
|
||||
|
@ -5,6 +5,7 @@ import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import rx.Observable
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.Future
|
||||
|
@ -3,9 +3,9 @@ package net.corda.core.flows;
|
||||
import co.paralleluniverse.fibers.Suspendable;
|
||||
import com.google.common.primitives.Primitives;
|
||||
import net.corda.core.identity.Party;
|
||||
import net.corda.node.internal.StartedNode;
|
||||
import net.corda.testing.core.TestConstants;
|
||||
import net.corda.testing.node.MockNetwork;
|
||||
import net.corda.testing.node.StartedMockNode;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@ -21,8 +21,8 @@ import static org.junit.Assert.fail;
|
||||
|
||||
public class FlowsInJavaTest {
|
||||
private final MockNetwork mockNet = new MockNetwork(emptyList());
|
||||
private StartedNode<MockNetwork.MockNode> aliceNode;
|
||||
private StartedNode<MockNetwork.MockNode> bobNode;
|
||||
private StartedMockNode aliceNode;
|
||||
private StartedMockNode bobNode;
|
||||
private Party bob;
|
||||
|
||||
@Before
|
||||
@ -40,7 +40,7 @@ public class FlowsInJavaTest {
|
||||
@Test
|
||||
public void suspendableActionInsideUnwrap() throws Exception {
|
||||
bobNode.registerInitiatedFlow(SendHelloAndThenReceive.class);
|
||||
Future<String> result = startFlow(aliceNode.getServices(), new SendInUnwrapFlow(bob)).getResultFuture();
|
||||
Future<String> result = startFlow(aliceNode.getServices(), new SendInUnwrapFlow(bob));
|
||||
mockNet.runNetwork();
|
||||
assertThat(result.get()).isEqualTo("Hello");
|
||||
}
|
||||
@ -56,7 +56,7 @@ public class FlowsInJavaTest {
|
||||
|
||||
private void primitiveReceiveTypeTest(Class<?> receiveType) throws InterruptedException {
|
||||
PrimitiveReceiveFlow flow = new PrimitiveReceiveFlow(bob, receiveType);
|
||||
Future<?> result = startFlow(aliceNode.getServices(), flow).getResultFuture();
|
||||
Future<?> result = startFlow(aliceNode.getServices(), flow);
|
||||
mockNet.runNetwork();
|
||||
try {
|
||||
result.get();
|
||||
|
@ -31,6 +31,7 @@ class AttachmentTest {
|
||||
override val id get() = throw UnsupportedOperationException()
|
||||
override fun open() = inputStream
|
||||
override val signers get() = throw UnsupportedOperationException()
|
||||
override val size: Int = 512
|
||||
}
|
||||
try {
|
||||
attachment.openAsJAR()
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import org.apache.commons.lang.ArrayUtils.EMPTY_BYTE_ARRAY
|
||||
import org.junit.Test
|
||||
import java.math.BigInteger
|
||||
import java.util.*
|
||||
@ -26,7 +27,7 @@ class Base58Test {
|
||||
assertEquals("1111111", Base58.encode(zeroBytes7))
|
||||
|
||||
// test empty encode
|
||||
assertEquals("", Base58.encode(ByteArray(0)))
|
||||
assertEquals("", Base58.encode(EMPTY_BYTE_ARRAY))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -8,6 +8,7 @@ import net.i2p.crypto.eddsa.math.GroupElement
|
||||
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec
|
||||
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable
|
||||
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec
|
||||
import org.apache.commons.lang.ArrayUtils.EMPTY_BYTE_ARRAY
|
||||
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
||||
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey
|
||||
@ -77,7 +78,7 @@ class CryptoUtilsTest {
|
||||
|
||||
// test for empty data signing
|
||||
try {
|
||||
Crypto.doSign(privKey, ByteArray(0))
|
||||
Crypto.doSign(privKey, EMPTY_BYTE_ARRAY)
|
||||
fail()
|
||||
} catch (e: Exception) {
|
||||
// expected
|
||||
@ -85,7 +86,7 @@ class CryptoUtilsTest {
|
||||
|
||||
// test for empty source data when verifying
|
||||
try {
|
||||
Crypto.doVerify(pubKey, testBytes, ByteArray(0))
|
||||
Crypto.doVerify(pubKey, testBytes, EMPTY_BYTE_ARRAY)
|
||||
fail()
|
||||
} catch (e: Exception) {
|
||||
// expected
|
||||
@ -93,7 +94,7 @@ class CryptoUtilsTest {
|
||||
|
||||
// test for empty signed data when verifying
|
||||
try {
|
||||
Crypto.doVerify(pubKey, ByteArray(0), testBytes)
|
||||
Crypto.doVerify(pubKey, EMPTY_BYTE_ARRAY, testBytes)
|
||||
fail()
|
||||
} catch (e: Exception) {
|
||||
// expected
|
||||
@ -132,7 +133,7 @@ class CryptoUtilsTest {
|
||||
|
||||
// test for empty data signing
|
||||
try {
|
||||
Crypto.doSign(privKey, ByteArray(0))
|
||||
Crypto.doSign(privKey, EMPTY_BYTE_ARRAY)
|
||||
fail()
|
||||
} catch (e: Exception) {
|
||||
// expected
|
||||
@ -140,7 +141,7 @@ class CryptoUtilsTest {
|
||||
|
||||
// test for empty source data when verifying
|
||||
try {
|
||||
Crypto.doVerify(pubKey, testBytes, ByteArray(0))
|
||||
Crypto.doVerify(pubKey, testBytes, EMPTY_BYTE_ARRAY)
|
||||
fail()
|
||||
} catch (e: Exception) {
|
||||
// expected
|
||||
@ -148,7 +149,7 @@ class CryptoUtilsTest {
|
||||
|
||||
// test for empty signed data when verifying
|
||||
try {
|
||||
Crypto.doVerify(pubKey, ByteArray(0), testBytes)
|
||||
Crypto.doVerify(pubKey, EMPTY_BYTE_ARRAY, testBytes)
|
||||
fail()
|
||||
} catch (e: Exception) {
|
||||
// expected
|
||||
@ -187,7 +188,7 @@ class CryptoUtilsTest {
|
||||
|
||||
// test for empty data signing
|
||||
try {
|
||||
Crypto.doSign(privKey, ByteArray(0))
|
||||
Crypto.doSign(privKey, EMPTY_BYTE_ARRAY)
|
||||
fail()
|
||||
} catch (e: Exception) {
|
||||
// expected
|
||||
@ -195,7 +196,7 @@ class CryptoUtilsTest {
|
||||
|
||||
// test for empty source data when verifying
|
||||
try {
|
||||
Crypto.doVerify(pubKey, testBytes, ByteArray(0))
|
||||
Crypto.doVerify(pubKey, testBytes, EMPTY_BYTE_ARRAY)
|
||||
fail()
|
||||
} catch (e: Exception) {
|
||||
// expected
|
||||
@ -203,7 +204,7 @@ class CryptoUtilsTest {
|
||||
|
||||
// test for empty signed data when verifying
|
||||
try {
|
||||
Crypto.doVerify(pubKey, ByteArray(0), testBytes)
|
||||
Crypto.doVerify(pubKey, EMPTY_BYTE_ARRAY, testBytes)
|
||||
fail()
|
||||
} catch (e: Exception) {
|
||||
// expected
|
||||
@ -242,7 +243,7 @@ class CryptoUtilsTest {
|
||||
|
||||
// test for empty data signing
|
||||
try {
|
||||
Crypto.doSign(privKey, ByteArray(0))
|
||||
Crypto.doSign(privKey, EMPTY_BYTE_ARRAY)
|
||||
fail()
|
||||
} catch (e: Exception) {
|
||||
// expected
|
||||
@ -250,7 +251,7 @@ class CryptoUtilsTest {
|
||||
|
||||
// test for empty source data when verifying
|
||||
try {
|
||||
Crypto.doVerify(pubKey, testBytes, ByteArray(0))
|
||||
Crypto.doVerify(pubKey, testBytes, EMPTY_BYTE_ARRAY)
|
||||
fail()
|
||||
} catch (e: Exception) {
|
||||
// expected
|
||||
@ -258,7 +259,7 @@ class CryptoUtilsTest {
|
||||
|
||||
// test for empty signed data when verifying
|
||||
try {
|
||||
Crypto.doVerify(pubKey, ByteArray(0), testBytes)
|
||||
Crypto.doVerify(pubKey, EMPTY_BYTE_ARRAY, testBytes)
|
||||
fail()
|
||||
} catch (e: Exception) {
|
||||
// expected
|
||||
@ -297,7 +298,7 @@ class CryptoUtilsTest {
|
||||
|
||||
// test for empty data signing
|
||||
try {
|
||||
Crypto.doSign(privKey, ByteArray(0))
|
||||
Crypto.doSign(privKey, EMPTY_BYTE_ARRAY)
|
||||
fail()
|
||||
} catch (e: Exception) {
|
||||
// expected
|
||||
@ -305,7 +306,7 @@ class CryptoUtilsTest {
|
||||
|
||||
// test for empty source data when verifying
|
||||
try {
|
||||
Crypto.doVerify(pubKey, testBytes, ByteArray(0))
|
||||
Crypto.doVerify(pubKey, testBytes, EMPTY_BYTE_ARRAY)
|
||||
fail()
|
||||
} catch (e: Exception) {
|
||||
// expected
|
||||
@ -313,7 +314,7 @@ class CryptoUtilsTest {
|
||||
|
||||
// test for empty signed data when verifying
|
||||
try {
|
||||
Crypto.doVerify(pubKey, ByteArray(0), testBytes)
|
||||
Crypto.doVerify(pubKey, EMPTY_BYTE_ARRAY, testBytes)
|
||||
fail()
|
||||
} catch (e: Exception) {
|
||||
// expected
|
||||
|
@ -58,19 +58,19 @@ class PartialMerkleTreeTest {
|
||||
hashed = nodes.map { it.serialize().sha256() }
|
||||
expectedRoot = MerkleTree.getMerkleTree(hashed.toMutableList() + listOf(zeroHash, zeroHash)).hash
|
||||
merkleTree = MerkleTree.getMerkleTree(hashed)
|
||||
testLedger = MockServices(emptyList(), rigorousMock<IdentityServiceInternal>().also {
|
||||
testLedger = MockServices(emptyList(), MEGA_CORP.name, rigorousMock<IdentityServiceInternal>().also {
|
||||
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
|
||||
}, MEGA_CORP.name).ledger(DUMMY_NOTARY) {
|
||||
}).ledger(DUMMY_NOTARY) {
|
||||
unverifiedTransaction {
|
||||
attachments(Cash.PROGRAM_ID)
|
||||
output(Cash.PROGRAM_ID, "MEGA_CORP cash",
|
||||
Cash.State(
|
||||
amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
|
||||
owner = MEGA_CORP))
|
||||
Cash.State(
|
||||
amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
|
||||
owner = MEGA_CORP))
|
||||
output(Cash.PROGRAM_ID, "dummy cash 1",
|
||||
Cash.State(
|
||||
amount = 900.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
|
||||
owner = MINI_CORP))
|
||||
Cash.State(
|
||||
amount = 900.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
|
||||
owner = MINI_CORP))
|
||||
}
|
||||
transaction {
|
||||
attachments(Cash.PROGRAM_ID)
|
||||
|
@ -3,17 +3,21 @@ package net.corda.core.crypto
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.math.BigInteger
|
||||
import java.security.KeyPair
|
||||
import java.security.SignatureException
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Digital signature MetaData tests.
|
||||
* Transaction signature tests.
|
||||
*/
|
||||
class TransactionSignatureTest {
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
val testBytes = "12345678901234567890123456789012".toByteArray()
|
||||
private val testBytes = "12345678901234567890123456789012".toByteArray()
|
||||
|
||||
/** Valid sign and verify. */
|
||||
@Test
|
||||
@ -41,4 +45,83 @@ class TransactionSignatureTest {
|
||||
val transactionSignature = keyPair.sign(signableData)
|
||||
Crypto.doVerify((testBytes + testBytes).sha256(), transactionSignature)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Verify multi-tx signature`() {
|
||||
val keyPair = Crypto.deriveKeyPairFromEntropy(Crypto.EDDSA_ED25519_SHA512, BigInteger.valueOf(1234567890L))
|
||||
// Deterministically create 5 txIds.
|
||||
val txIds: List<SecureHash> = IntRange(0, 4).map { byteArrayOf(it.toByte()).sha256() }
|
||||
// Multi-tx signature.
|
||||
val txSignature = signMultipleTx(txIds, keyPair)
|
||||
|
||||
// The hash of all txIds are used as leaves.
|
||||
val merkleTree = MerkleTree.getMerkleTree(txIds.map { it.sha256() })
|
||||
|
||||
// We haven't added the partial tree yet.
|
||||
assertNull(txSignature.partialMerkleTree)
|
||||
// Because partial tree is still null, but we signed over a block of txs, verifying a single tx will fail.
|
||||
assertFailsWith<SignatureException> { Crypto.doVerify(txIds[3], txSignature) }
|
||||
|
||||
// Create a partial tree for one tx.
|
||||
val pmt = PartialMerkleTree.build(merkleTree, listOf(txIds[0].sha256()))
|
||||
// Add the partial Merkle tree to the tx signature.
|
||||
val txSignatureWithTree = TransactionSignature(txSignature.bytes, txSignature.by, txSignature.signatureMetadata, pmt)
|
||||
|
||||
// Verify the corresponding txId with every possible way.
|
||||
assertTrue(Crypto.doVerify(txIds[0], txSignatureWithTree))
|
||||
assertTrue(txSignatureWithTree.verify(txIds[0]))
|
||||
assertTrue(Crypto.isValid(txIds[0], txSignatureWithTree))
|
||||
assertTrue(txSignatureWithTree.isValid(txIds[0]))
|
||||
|
||||
// Verify the rest txs in the block, which are not included in the partial Merkle tree.
|
||||
txIds.subList(1, txIds.size).forEach {
|
||||
assertFailsWith<IllegalArgumentException> { Crypto.doVerify(it, txSignatureWithTree) }
|
||||
}
|
||||
|
||||
// Test that the Merkle tree consists of hash(txId), not txId.
|
||||
assertFailsWith<MerkleTreeException> { PartialMerkleTree.build(merkleTree, listOf(txIds[0])) }
|
||||
|
||||
// What if we send the Full tree. This could be used if notaries didn't want to create a per tx partial tree.
|
||||
// Create a partial tree for all txs, thus all leaves are included.
|
||||
val pmtFull = PartialMerkleTree.build(merkleTree, txIds.map { it.sha256() })
|
||||
// Add the partial Merkle tree to the tx.
|
||||
val txSignatureWithFullTree = TransactionSignature(txSignature.bytes, txSignature.by, txSignature.signatureMetadata, pmtFull)
|
||||
|
||||
// All txs can be verified, as they are all included in the provided partial tree.
|
||||
txIds.forEach {
|
||||
assertTrue(Crypto.doVerify(it, txSignatureWithFullTree))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Verify one-tx signature`() {
|
||||
val keyPair = Crypto.deriveKeyPairFromEntropy(Crypto.EDDSA_ED25519_SHA512, BigInteger.valueOf(1234567890L))
|
||||
val txId = "aTransaction".toByteArray().sha256()
|
||||
// One-tx signature.
|
||||
val txSignature = signOneTx(txId, keyPair)
|
||||
|
||||
// partialMerkleTree should be null.
|
||||
assertNull(txSignature.partialMerkleTree)
|
||||
// Verify the corresponding txId with every possible way.
|
||||
assertTrue(Crypto.doVerify(txId, txSignature))
|
||||
assertTrue(txSignature.verify(txId))
|
||||
assertTrue(Crypto.isValid(txId, txSignature))
|
||||
assertTrue(txSignature.isValid(txId))
|
||||
|
||||
// We signed the txId itself, not its hash (because it was a signature over one tx only and no partial tree has been received).
|
||||
assertFailsWith<SignatureException> { Crypto.doVerify(txId.sha256(), txSignature) }
|
||||
}
|
||||
|
||||
// Returns a TransactionSignature over the Merkle root, but the partial tree is null.
|
||||
private fun signMultipleTx(txIds: List<SecureHash>, keyPair: KeyPair): TransactionSignature {
|
||||
val merkleTreeRoot = MerkleTree.getMerkleTree(txIds.map { it.sha256() }).hash
|
||||
return signOneTx(merkleTreeRoot, keyPair)
|
||||
}
|
||||
|
||||
// Returns a TransactionSignature over one SecureHash.
|
||||
// Note that if one tx is to be signed, we don't create a Merkle tree and we directly sign over the txId.
|
||||
private fun signOneTx(txId: SecureHash, keyPair: KeyPair): TransactionSignature {
|
||||
val signableData = SignableData(txId, SignatureMetadata(3, Crypto.findSignatureScheme(keyPair.public).schemeNumberID))
|
||||
return keyPair.sign(signableData)
|
||||
}
|
||||
}
|
||||
|
@ -13,8 +13,8 @@ import net.corda.node.services.persistence.NodeAttachmentService
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.MockNodeParameters
|
||||
import net.corda.testing.node.internal.InternalMockNetwork
|
||||
import net.corda.testing.node.startFlow
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
@ -27,11 +27,11 @@ import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class AttachmentTests {
|
||||
lateinit var mockNet: MockNetwork
|
||||
lateinit var mockNet: InternalMockNetwork
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
mockNet = MockNetwork(emptyList())
|
||||
mockNet = InternalMockNetwork(emptyList())
|
||||
}
|
||||
|
||||
@After
|
||||
@ -65,7 +65,7 @@ class AttachmentTests {
|
||||
mockNet.runNetwork()
|
||||
val bobFlow = bobNode.startAttachmentFlow(setOf(id), alice)
|
||||
mockNet.runNetwork()
|
||||
assertEquals(0, bobFlow.resultFuture.getOrThrow().fromDisk.size)
|
||||
assertEquals(0, bobFlow.getOrThrow().fromDisk.size)
|
||||
|
||||
// Verify it was inserted into node one's store.
|
||||
val attachment = bobNode.database.transaction {
|
||||
@ -77,7 +77,7 @@ class AttachmentTests {
|
||||
// Shut down node zero and ensure node one can still resolve the attachment.
|
||||
aliceNode.dispose()
|
||||
|
||||
val response: FetchDataFlow.Result<Attachment> = bobNode.startAttachmentFlow(setOf(id), alice).resultFuture.getOrThrow()
|
||||
val response: FetchDataFlow.Result<Attachment> = bobNode.startAttachmentFlow(setOf(id), alice).getOrThrow()
|
||||
assertEquals(attachment, response.fromDisk[0])
|
||||
}
|
||||
|
||||
@ -92,7 +92,7 @@ class AttachmentTests {
|
||||
val alice = aliceNode.info.singleIdentity()
|
||||
val bobFlow = bobNode.startAttachmentFlow(setOf(hash), alice)
|
||||
mockNet.runNetwork()
|
||||
val e = assertFailsWith<FetchDataFlow.HashNotFound> { bobFlow.resultFuture.getOrThrow() }
|
||||
val e = assertFailsWith<FetchDataFlow.HashNotFound> { bobFlow.getOrThrow() }
|
||||
assertEquals(hash, e.requested)
|
||||
}
|
||||
|
||||
@ -100,7 +100,7 @@ class AttachmentTests {
|
||||
fun maliciousResponse() {
|
||||
// Make a node that doesn't do sanity checking at load time.
|
||||
val aliceNode = mockNet.createNode(MockNodeParameters(legalName = ALICE_NAME), nodeFactory = { args ->
|
||||
object : MockNetwork.MockNode(args) {
|
||||
object : InternalMockNetwork.MockNode(args) {
|
||||
override fun start() = super.start().apply { attachments.checkAttachmentsOnLoad = false }
|
||||
}
|
||||
})
|
||||
@ -127,7 +127,7 @@ class AttachmentTests {
|
||||
mockNet.runNetwork()
|
||||
val bobFlow = bobNode.startAttachmentFlow(setOf(id), alice)
|
||||
mockNet.runNetwork()
|
||||
assertFailsWith<FetchDataFlow.DownloadedVsRequestedDataMismatch> { bobFlow.resultFuture.getOrThrow() }
|
||||
assertFailsWith<FetchDataFlow.DownloadedVsRequestedDataMismatch> { bobFlow.getOrThrow() }
|
||||
}
|
||||
|
||||
private fun StartedNode<*>.startAttachmentFlow(hashes: Set<SecureHash>, otherSide: Party) = services.startFlow(InitiatingFetchAttachmentsFlow(otherSide, hashes))
|
||||
|
@ -15,8 +15,9 @@ import net.corda.node.internal.StartedNode
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.internal.InternalMockNetwork
|
||||
import net.corda.testing.node.internal.InternalMockNetwork.MockNode
|
||||
import net.corda.testing.node.startFlow
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
@ -29,10 +30,10 @@ class CollectSignaturesFlowTests {
|
||||
private val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB"))
|
||||
}
|
||||
|
||||
private lateinit var mockNet: MockNetwork
|
||||
private lateinit var aliceNode: StartedNode<MockNetwork.MockNode>
|
||||
private lateinit var bobNode: StartedNode<MockNetwork.MockNode>
|
||||
private lateinit var charlieNode: StartedNode<MockNetwork.MockNode>
|
||||
private lateinit var mockNet: InternalMockNetwork
|
||||
private lateinit var aliceNode: StartedNode<MockNode>
|
||||
private lateinit var bobNode: StartedNode<MockNode>
|
||||
private lateinit var charlieNode: StartedNode<MockNode>
|
||||
private lateinit var alice: Party
|
||||
private lateinit var bob: Party
|
||||
private lateinit var charlie: Party
|
||||
@ -40,7 +41,7 @@ class CollectSignaturesFlowTests {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts"))
|
||||
mockNet = InternalMockNetwork(cordappPackages = listOf("net.corda.testing.contracts"))
|
||||
aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
||||
bobNode = mockNet.createPartyNode(BOB_NAME)
|
||||
charlieNode = mockNet.createPartyNode(CHARLIE_NAME)
|
||||
@ -115,7 +116,7 @@ class CollectSignaturesFlowTests {
|
||||
val state = DummyContract.MultiOwnerState(magicNumber, parties)
|
||||
val flow = aliceNode.services.startFlow(TestFlow.Initiator(state, notary))
|
||||
mockNet.runNetwork()
|
||||
val result = flow.resultFuture.getOrThrow()
|
||||
val result = flow.getOrThrow()
|
||||
result.verifyRequiredSignatures()
|
||||
println(result.tx)
|
||||
println(result.sigs)
|
||||
@ -127,7 +128,7 @@ class CollectSignaturesFlowTests {
|
||||
val ptx = aliceNode.services.signInitialTransaction(onePartyDummyContract)
|
||||
val flow = aliceNode.services.startFlow(CollectSignaturesFlow(ptx, emptySet()))
|
||||
mockNet.runNetwork()
|
||||
val result = flow.resultFuture.getOrThrow()
|
||||
val result = flow.getOrThrow()
|
||||
result.verifyRequiredSignatures()
|
||||
println(result.tx)
|
||||
println(result.sigs)
|
||||
@ -136,12 +137,12 @@ class CollectSignaturesFlowTests {
|
||||
@Test
|
||||
fun `fails when not signed by initiator`() {
|
||||
val onePartyDummyContract = DummyContract.generateInitial(1337, notary, alice.ref(1))
|
||||
val miniCorpServices = MockServices(listOf("net.corda.testing.contracts"), rigorousMock(), miniCorp)
|
||||
val miniCorpServices = MockServices(listOf("net.corda.testing.contracts"), miniCorp, rigorousMock())
|
||||
val ptx = miniCorpServices.signInitialTransaction(onePartyDummyContract)
|
||||
val flow = aliceNode.services.startFlow(CollectSignaturesFlow(ptx, emptySet()))
|
||||
mockNet.runNetwork()
|
||||
assertFailsWith<IllegalArgumentException>("The Initiator of CollectSignaturesFlow must have signed the transaction.") {
|
||||
flow.resultFuture.getOrThrow()
|
||||
flow.getOrThrow()
|
||||
}
|
||||
}
|
||||
|
||||
@ -155,7 +156,7 @@ class CollectSignaturesFlowTests {
|
||||
val signedByBoth = bobNode.services.addSignature(signedByA)
|
||||
val flow = aliceNode.services.startFlow(CollectSignaturesFlow(signedByBoth, emptySet()))
|
||||
mockNet.runNetwork()
|
||||
val result = flow.resultFuture.getOrThrow()
|
||||
val result = flow.getOrThrow()
|
||||
println(result.tx)
|
||||
println(result.sigs)
|
||||
}
|
||||
|
@ -19,17 +19,14 @@ import net.corda.finance.flows.CashIssueFlow
|
||||
import net.corda.node.internal.SecureCordaRPCOps
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.node.services.Permissions.Companion.startFlow
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.node.User
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.contracts.DummyContractV2
|
||||
import net.corda.testing.node.internal.RPCDriverDSL
|
||||
import net.corda.testing.node.internal.rpcDriver
|
||||
import net.corda.testing.node.internal.rpcTestUser
|
||||
import net.corda.testing.node.internal.startRpcClient
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.node.User
|
||||
import net.corda.testing.node.internal.*
|
||||
import net.corda.testing.node.internal.InternalMockNetwork.MockNode
|
||||
import net.corda.testing.node.startFlow
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
@ -40,16 +37,16 @@ import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ContractUpgradeFlowTest {
|
||||
private lateinit var mockNet: MockNetwork
|
||||
private lateinit var aliceNode: StartedNode<MockNetwork.MockNode>
|
||||
private lateinit var bobNode: StartedNode<MockNetwork.MockNode>
|
||||
private lateinit var mockNet: InternalMockNetwork
|
||||
private lateinit var aliceNode: StartedNode<MockNode>
|
||||
private lateinit var bobNode: StartedNode<MockNode>
|
||||
private lateinit var notary: Party
|
||||
private lateinit var alice: Party
|
||||
private lateinit var bob: Party
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts", "net.corda.finance.contracts.asset", "net.corda.core.flows", "net.corda.finance.schemas"))
|
||||
mockNet = InternalMockNetwork(cordappPackages = listOf("net.corda.testing.contracts", "net.corda.finance.contracts.asset", "net.corda.core.flows", "net.corda.finance.schemas"))
|
||||
aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
||||
bobNode = mockNet.createPartyNode(BOB_NAME)
|
||||
notary = mockNet.defaultNotaryIdentity
|
||||
@ -81,29 +78,29 @@ class ContractUpgradeFlowTest {
|
||||
requireNotNull(btx)
|
||||
|
||||
// The request is expected to be rejected because party B hasn't authorised the upgrade yet.
|
||||
val rejectedFuture = aliceNode.services.startFlow(ContractUpgradeFlow.Initiate(atx!!.tx.outRef(0), DummyContractV2::class.java)).resultFuture
|
||||
val rejectedFuture = aliceNode.services.startFlow(ContractUpgradeFlow.Initiate(atx!!.tx.outRef(0), DummyContractV2::class.java))
|
||||
mockNet.runNetwork()
|
||||
assertFailsWith(UnexpectedFlowEndException::class) { rejectedFuture.getOrThrow() }
|
||||
|
||||
// Party B authorise the contract state upgrade, and immediately deauthorise the same.
|
||||
bobNode.services.startFlow(ContractUpgradeFlow.Authorise(btx!!.tx.outRef<ContractState>(0), DummyContractV2::class.java)).resultFuture.getOrThrow()
|
||||
bobNode.services.startFlow(ContractUpgradeFlow.Deauthorise(btx.tx.outRef<ContractState>(0).ref)).resultFuture.getOrThrow()
|
||||
bobNode.services.startFlow(ContractUpgradeFlow.Authorise(btx!!.tx.outRef<ContractState>(0), DummyContractV2::class.java)).getOrThrow()
|
||||
bobNode.services.startFlow(ContractUpgradeFlow.Deauthorise(btx.tx.outRef<ContractState>(0).ref)).getOrThrow()
|
||||
|
||||
// The request is expected to be rejected because party B has subsequently deauthorised and a previously authorised upgrade.
|
||||
val deauthorisedFuture = aliceNode.services.startFlow(ContractUpgradeFlow.Initiate(atx.tx.outRef(0), DummyContractV2::class.java)).resultFuture
|
||||
val deauthorisedFuture = aliceNode.services.startFlow(ContractUpgradeFlow.Initiate(atx.tx.outRef(0), DummyContractV2::class.java))
|
||||
mockNet.runNetwork()
|
||||
assertFailsWith(UnexpectedFlowEndException::class) { deauthorisedFuture.getOrThrow() }
|
||||
|
||||
// Party B authorise the contract state upgrade
|
||||
bobNode.services.startFlow(ContractUpgradeFlow.Authorise(btx.tx.outRef<ContractState>(0), DummyContractV2::class.java)).resultFuture.getOrThrow()
|
||||
bobNode.services.startFlow(ContractUpgradeFlow.Authorise(btx.tx.outRef<ContractState>(0), DummyContractV2::class.java)).getOrThrow()
|
||||
|
||||
// Party A initiates contract upgrade flow, expected to succeed this time.
|
||||
val resultFuture = aliceNode.services.startFlow(ContractUpgradeFlow.Initiate(atx.tx.outRef(0), DummyContractV2::class.java)).resultFuture
|
||||
val resultFuture = aliceNode.services.startFlow(ContractUpgradeFlow.Initiate(atx.tx.outRef(0), DummyContractV2::class.java))
|
||||
mockNet.runNetwork()
|
||||
|
||||
val result = resultFuture.getOrThrow()
|
||||
|
||||
fun check(node: StartedNode<*>) {
|
||||
fun check(node: StartedNode<MockNode>) {
|
||||
val nodeStx = node.database.transaction {
|
||||
node.services.validatedTransactions.getTransaction(result.ref.txhash)
|
||||
}
|
||||
@ -123,7 +120,7 @@ class ContractUpgradeFlowTest {
|
||||
check(bobNode)
|
||||
}
|
||||
|
||||
private fun RPCDriverDSL.startProxy(node: StartedNode<*>, user: User): CordaRPCOps {
|
||||
private fun RPCDriverDSL.startProxy(node: StartedNode<MockNode>, user: User): CordaRPCOps {
|
||||
return startRpcClient<CordaRPCOps>(
|
||||
rpcAddress = startRpcServer(
|
||||
rpcUser = user,
|
||||
@ -213,7 +210,7 @@ class ContractUpgradeFlowTest {
|
||||
fun `upgrade Cash to v2`() {
|
||||
// Create some cash.
|
||||
val chosenIdentity = alice
|
||||
val result = aliceNode.services.startFlow(CashIssueFlow(Amount(1000, USD), OpaqueBytes.of(1), notary)).resultFuture
|
||||
val result = aliceNode.services.startFlow(CashIssueFlow(Amount(1000, USD), OpaqueBytes.of(1), notary))
|
||||
mockNet.runNetwork()
|
||||
val stx = result.getOrThrow().stx
|
||||
val anonymisedRecipient = result.get().recipient!!
|
||||
@ -221,7 +218,7 @@ class ContractUpgradeFlowTest {
|
||||
val baseState = aliceNode.database.transaction { aliceNode.services.vaultService.queryBy<ContractState>().states.single() }
|
||||
assertTrue(baseState.state.data is Cash.State, "Contract state is old version.")
|
||||
// Starts contract upgrade flow.
|
||||
val upgradeResult = aliceNode.services.startFlow(ContractUpgradeFlow.Initiate(stateAndRef, CashV2::class.java)).resultFuture
|
||||
val upgradeResult = aliceNode.services.startFlow(ContractUpgradeFlow.Initiate(stateAndRef, CashV2::class.java))
|
||||
mockNet.runNetwork()
|
||||
upgradeResult.getOrThrow()
|
||||
// Get contract state from the vault.
|
||||
|
@ -53,7 +53,7 @@ class FinalityFlowTests {
|
||||
val stx = aliceServices.signInitialTransaction(builder)
|
||||
val flow = aliceServices.startFlow(FinalityFlow(stx))
|
||||
mockNet.runNetwork()
|
||||
val notarisedTx = flow.resultFuture.getOrThrow()
|
||||
val notarisedTx = flow.getOrThrow()
|
||||
notarisedTx.verifyRequiredSignatures()
|
||||
val transactionSeenByB = bobServices.database.transaction {
|
||||
bobServices.validatedTransactions.getTransaction(notarisedTx.id)
|
||||
@ -71,7 +71,7 @@ class FinalityFlowTests {
|
||||
val flow = aliceServices.startFlow(FinalityFlow(stx))
|
||||
mockNet.runNetwork()
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
flow.resultFuture.getOrThrow()
|
||||
flow.getOrThrow()
|
||||
}
|
||||
}
|
||||
}
|
@ -5,6 +5,8 @@ import net.corda.core.utilities.UntrustworthyData
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.node.internal.InitiatedFlowFactory
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.testing.node.StartedMockNode
|
||||
import net.corda.testing.node.internal.InternalMockNetwork
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
@ -37,14 +39,14 @@ class NoAnswer(private val closure: () -> Unit = {}) : FlowLogic<Unit>() {
|
||||
/**
|
||||
* Allows to register a flow of type [R] against an initiating flow of type [I].
|
||||
*/
|
||||
inline fun <I : FlowLogic<*>, reified R : FlowLogic<*>> StartedNode<*>.registerInitiatedFlow(initiatingFlowType: KClass<I>, crossinline construct: (session: FlowSession) -> R) {
|
||||
inline fun <I : FlowLogic<*>, reified R : FlowLogic<*>> StartedNode<InternalMockNetwork.MockNode>.registerInitiatedFlow(initiatingFlowType: KClass<I>, crossinline construct: (session: FlowSession) -> R) {
|
||||
internalRegisterFlowFactory(initiatingFlowType.java, InitiatedFlowFactory.Core { session -> construct(session) }, R::class.javaObjectType, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to register a flow of type [Answer] against an initiating flow of type [I], returning a valure of type [R].
|
||||
*/
|
||||
inline fun <I : FlowLogic<*>, reified R : Any> StartedNode<*>.registerAnswer(initiatingFlowType: KClass<I>, value: R) {
|
||||
inline fun <I : FlowLogic<*>, reified R : Any> StartedNode<InternalMockNetwork.MockNode>.registerAnswer(initiatingFlowType: KClass<I>, value: R) {
|
||||
internalRegisterFlowFactory(initiatingFlowType.java, InitiatedFlowFactory.Core { session -> Answer(session, value) }, Answer::class.javaObjectType, true)
|
||||
}
|
||||
|
||||
|
@ -5,15 +5,15 @@ import net.corda.core.identity.Party
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.node.internal.InternalMockNetwork
|
||||
import net.corda.testing.node.startFlow
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.After
|
||||
import org.junit.Test
|
||||
|
||||
class ReceiveMultipleFlowTests {
|
||||
private val mockNet = MockNetwork(emptyList())
|
||||
private val mockNet = InternalMockNetwork(emptyList())
|
||||
private val nodes = (0..2).map { mockNet.createPartyNode() }
|
||||
@After
|
||||
fun stopNodes() {
|
||||
@ -52,7 +52,7 @@ class ReceiveMultipleFlowTests {
|
||||
|
||||
val flow = nodes[0].services.startFlow(initiatingFlow)
|
||||
mockNet.runNetwork()
|
||||
val receivedAnswer = flow.resultFuture.getOrThrow()
|
||||
val receivedAnswer = flow.getOrThrow()
|
||||
assertThat(receivedAnswer).isEqualTo(answer)
|
||||
}
|
||||
|
||||
@ -64,7 +64,7 @@ class ReceiveMultipleFlowTests {
|
||||
nodes[2].registerAnswer(AlgorithmDefinition::class, stringValue)
|
||||
val flow = nodes[0].services.startFlow(ParallelAlgorithmMap(nodes[1].info.singleIdentity(), nodes[2].info.singleIdentity()))
|
||||
mockNet.runNetwork()
|
||||
val result = flow.resultFuture.getOrThrow()
|
||||
val result = flow.getOrThrow()
|
||||
assertThat(result).isEqualTo(doubleValue * stringValue.length)
|
||||
}
|
||||
|
||||
@ -76,7 +76,7 @@ class ReceiveMultipleFlowTests {
|
||||
nodes[2].registerAnswer(ParallelAlgorithmList::class, value2)
|
||||
val flow = nodes[0].services.startFlow(ParallelAlgorithmList(nodes[1].info.singleIdentity(), nodes[2].info.singleIdentity()))
|
||||
mockNet.runNetwork()
|
||||
val data = flow.resultFuture.getOrThrow()
|
||||
val data = flow.getOrThrow()
|
||||
assertThat(data[0]).isEqualTo(value1)
|
||||
assertThat(data[1]).isEqualTo(value2)
|
||||
assertThat(data.fold(1.0) { a, b -> a * b }).isEqualTo(value1 * value2)
|
||||
|
@ -7,9 +7,9 @@ import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.nodeapi.internal.crypto.X509KeyStore
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import net.corda.testing.core.DEV_ROOT_CA
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import net.corda.testing.core.getTestPartyAndCertificate
|
||||
import net.corda.testing.internal.DEV_ROOT_CA
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
@ -27,13 +27,6 @@ class LegalNameValidatorTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `blacklisted words`() {
|
||||
assertFailsWith(IllegalArgumentException::class) {
|
||||
LegalNameValidator.validateOrganization("Test Server", LegalNameValidator.Validation.FULL)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `blacklisted characters`() {
|
||||
LegalNameValidator.validateOrganization("Test", LegalNameValidator.Validation.FULL)
|
||||
|
@ -10,8 +10,9 @@ import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.sequence
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.node.internal.InternalMockNetwork
|
||||
import net.corda.testing.node.internal.InternalMockNetwork.MockNode
|
||||
import net.corda.testing.node.startFlow
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
@ -27,17 +28,17 @@ import kotlin.test.assertNull
|
||||
|
||||
// DOCSTART 3
|
||||
class ResolveTransactionsFlowTest {
|
||||
private lateinit var mockNet: MockNetwork
|
||||
private lateinit var notaryNode: StartedNode<MockNetwork.MockNode>
|
||||
private lateinit var megaCorpNode: StartedNode<MockNetwork.MockNode>
|
||||
private lateinit var miniCorpNode: StartedNode<MockNetwork.MockNode>
|
||||
private lateinit var mockNet: InternalMockNetwork
|
||||
private lateinit var notaryNode: StartedNode<MockNode>
|
||||
private lateinit var megaCorpNode: StartedNode<MockNode>
|
||||
private lateinit var miniCorpNode: StartedNode<MockNode>
|
||||
private lateinit var megaCorp: Party
|
||||
private lateinit var miniCorp: Party
|
||||
private lateinit var notary: Party
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts"))
|
||||
mockNet = InternalMockNetwork(cordappPackages = listOf("net.corda.testing.contracts"))
|
||||
notaryNode = mockNet.defaultNotaryNode
|
||||
megaCorpNode = mockNet.createPartyNode(CordaX500Name("MegaCorp", "London", "GB"))
|
||||
miniCorpNode = mockNet.createPartyNode(CordaX500Name("MiniCorp", "London", "GB"))
|
||||
@ -52,14 +53,14 @@ class ResolveTransactionsFlowTest {
|
||||
fun tearDown() {
|
||||
mockNet.stopNodes()
|
||||
}
|
||||
// DOCEND 3
|
||||
// DOCEND 3
|
||||
|
||||
// DOCSTART 1
|
||||
@Test
|
||||
fun `resolve from two hashes`() {
|
||||
val (stx1, stx2) = makeTransactions()
|
||||
val p = TestFlow(setOf(stx2.id), megaCorp)
|
||||
val future = miniCorpNode.services.startFlow(p).resultFuture
|
||||
val future = miniCorpNode.services.startFlow(p)
|
||||
mockNet.runNetwork()
|
||||
val results = future.getOrThrow()
|
||||
assertEquals(listOf(stx1.id, stx2.id), results.map { it.id })
|
||||
@ -74,7 +75,7 @@ class ResolveTransactionsFlowTest {
|
||||
fun `dependency with an error`() {
|
||||
val stx = makeTransactions(signFirstTX = false).second
|
||||
val p = TestFlow(setOf(stx.id), megaCorp)
|
||||
val future = miniCorpNode.services.startFlow(p).resultFuture
|
||||
val future = miniCorpNode.services.startFlow(p)
|
||||
mockNet.runNetwork()
|
||||
assertFailsWith(SignedTransaction.SignaturesMissingException::class) { future.getOrThrow() }
|
||||
}
|
||||
@ -83,7 +84,7 @@ class ResolveTransactionsFlowTest {
|
||||
fun `resolve from a signed transaction`() {
|
||||
val (stx1, stx2) = makeTransactions()
|
||||
val p = TestFlow(stx2, megaCorp)
|
||||
val future = miniCorpNode.services.startFlow(p).resultFuture
|
||||
val future = miniCorpNode.services.startFlow(p)
|
||||
mockNet.runNetwork()
|
||||
future.getOrThrow()
|
||||
miniCorpNode.database.transaction {
|
||||
@ -108,7 +109,7 @@ class ResolveTransactionsFlowTest {
|
||||
cursor = stx
|
||||
}
|
||||
val p = TestFlow(setOf(cursor.id), megaCorp, 40)
|
||||
val future = miniCorpNode.services.startFlow(p).resultFuture
|
||||
val future = miniCorpNode.services.startFlow(p)
|
||||
mockNet.runNetwork()
|
||||
assertFailsWith<ResolveTransactionsFlow.ExcessivelyLargeTransactionGraph> { future.getOrThrow() }
|
||||
}
|
||||
@ -132,7 +133,7 @@ class ResolveTransactionsFlowTest {
|
||||
}
|
||||
|
||||
val p = TestFlow(setOf(stx3.id), megaCorp)
|
||||
val future = miniCorpNode.services.startFlow(p).resultFuture
|
||||
val future = miniCorpNode.services.startFlow(p)
|
||||
mockNet.runNetwork()
|
||||
future.getOrThrow()
|
||||
}
|
||||
@ -154,7 +155,7 @@ class ResolveTransactionsFlowTest {
|
||||
}
|
||||
val stx2 = makeTransactions(withAttachment = id).second
|
||||
val p = TestFlow(stx2, megaCorp)
|
||||
val future = miniCorpNode.services.startFlow(p).resultFuture
|
||||
val future = miniCorpNode.services.startFlow(p)
|
||||
mockNet.runNetwork()
|
||||
future.getOrThrow()
|
||||
|
||||
|
@ -114,4 +114,12 @@ class X509EdDSAEngineTest {
|
||||
engine.verify(signature)
|
||||
}
|
||||
}
|
||||
|
||||
/** Verify will fail if the input public key cannot be converted to EdDSA public key. */
|
||||
@Test
|
||||
fun `verify with non-supported key type fails`() {
|
||||
val engine = EdDSAEngine()
|
||||
val keyPair = Crypto.deriveKeyPairFromEntropy(Crypto.ECDSA_SECP256K1_SHA256, BigInteger.valueOf(SEED))
|
||||
assertFailsWith<InvalidKeyException> { engine.initVerify(keyPair.public) }
|
||||
}
|
||||
}
|
@ -18,9 +18,9 @@ import net.corda.node.services.persistence.NodeAttachmentService
|
||||
import net.corda.nodeapi.internal.persistence.currentDBSession
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.MockNodeParameters
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.node.internal.InternalMockNetwork
|
||||
import net.corda.testing.node.startFlow
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
@ -63,14 +63,14 @@ private fun updateAttachment(attachmentId: SecureHash, data: ByteArray) {
|
||||
}
|
||||
|
||||
class AttachmentSerializationTest {
|
||||
private lateinit var mockNet: MockNetwork
|
||||
private lateinit var server: StartedNode<MockNetwork.MockNode>
|
||||
private lateinit var client: StartedNode<MockNetwork.MockNode>
|
||||
private lateinit var mockNet: InternalMockNetwork
|
||||
private lateinit var server: StartedNode<InternalMockNetwork.MockNode>
|
||||
private lateinit var client: StartedNode<InternalMockNetwork.MockNode>
|
||||
private lateinit var serverIdentity: Party
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
mockNet = MockNetwork(emptyList())
|
||||
mockNet = InternalMockNetwork(emptyList())
|
||||
server = mockNet.createNode(MockNodeParameters(legalName = ALICE_NAME))
|
||||
client = mockNet.createNode(MockNodeParameters(legalName = BOB_NAME))
|
||||
client.internals.disableDBCloseOnStop() // Otherwise the in-memory database may disappear (taking the checkpoint with it) while we reboot the client.
|
||||
@ -114,6 +114,7 @@ class AttachmentSerializationTest {
|
||||
private class CustomAttachment(override val id: SecureHash, internal val customContent: String) : Attachment {
|
||||
override fun open() = throw UnsupportedOperationException("Not implemented.")
|
||||
override val signers get() = throw UnsupportedOperationException()
|
||||
override val size get() = throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
private class CustomAttachmentLogic(serverIdentity: Party, private val attachmentId: SecureHash, private val customContent: String) : ClientLogic(serverIdentity) {
|
||||
@ -160,7 +161,7 @@ class AttachmentSerializationTest {
|
||||
private fun rebootClientAndGetAttachmentContent(checkAttachmentsOnLoad: Boolean = true): String {
|
||||
client.dispose()
|
||||
client = mockNet.createNode(MockNodeParameters(client.internals.id, client.internals.configuration.myLegalName), { args ->
|
||||
object : MockNetwork.MockNode(args) {
|
||||
object : InternalMockNetwork.MockNode(args) {
|
||||
override fun start() = super.start().apply { attachments.checkAttachmentsOnLoad = checkAttachmentsOnLoad }
|
||||
}
|
||||
})
|
||||
|
@ -62,8 +62,8 @@ class TransactionSerializationTests {
|
||||
val inputState = StateAndRef(TransactionState(TestCash.State(depositRef, 100.POUNDS, MEGA_CORP), TEST_CASH_PROGRAM_ID, DUMMY_NOTARY), fakeStateRef)
|
||||
val outputState = TransactionState(TestCash.State(depositRef, 600.POUNDS, MEGA_CORP), TEST_CASH_PROGRAM_ID, DUMMY_NOTARY)
|
||||
val changeState = TransactionState(TestCash.State(depositRef, 400.POUNDS, MEGA_CORP), TEST_CASH_PROGRAM_ID, DUMMY_NOTARY)
|
||||
val megaCorpServices = MockServices(listOf("net.corda.core.serialization"), rigorousMock(), MEGA_CORP.name, MEGA_CORP_KEY)
|
||||
val notaryServices = MockServices(listOf("net.corda.core.serialization"), rigorousMock(), DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||
val megaCorpServices = MockServices(listOf("net.corda.core.serialization"), MEGA_CORP.name, rigorousMock(), MEGA_CORP_KEY)
|
||||
val notaryServices = MockServices(listOf("net.corda.core.serialization"), DUMMY_NOTARY.name, rigorousMock(), DUMMY_NOTARY_KEY)
|
||||
lateinit var tx: TransactionBuilder
|
||||
|
||||
@Before
|
||||
@ -107,7 +107,7 @@ class TransactionSerializationTests {
|
||||
Command(TestCash.Commands.Move(), DUMMY_KEY_2.public))
|
||||
|
||||
val ptx2 = notaryServices.signInitialTransaction(tx2)
|
||||
val dummyServices = MockServices(emptyList(), rigorousMock(), MEGA_CORP.name, DUMMY_KEY_2)
|
||||
val dummyServices = MockServices(emptyList(), MEGA_CORP.name, rigorousMock(), DUMMY_KEY_2)
|
||||
val stx2 = dummyServices.addSignature(ptx2)
|
||||
|
||||
stx.copy(sigs = stx2.sigs).verifyRequiredSignatures()
|
||||
|
@ -10,6 +10,7 @@ import net.corda.core.identity.Party
|
||||
import net.corda.node.services.api.IdentityServiceInternal
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.internal.MockCordappProvider
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import net.corda.testing.node.MockServices
|
||||
import org.junit.Before
|
||||
@ -29,14 +30,15 @@ class LedgerTransactionQueryTests {
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
private val keyPair = generateKeyPair()
|
||||
private val services = MockServices(emptyList(), rigorousMock<IdentityServiceInternal>().also {
|
||||
doReturn(null).whenever(it).partyFromKey(keyPair.public)
|
||||
}, CordaX500Name("MegaCorp", "London", "GB"), keyPair)
|
||||
private val services = MockServices(emptyList(), CordaX500Name("MegaCorp", "London", "GB"),
|
||||
rigorousMock<IdentityServiceInternal>().also {
|
||||
doReturn(null).whenever(it).partyFromKey(keyPair.public)
|
||||
}, keyPair)
|
||||
private val identity: Party = services.myInfo.singleIdentity()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
services.mockCordappProvider.addMockCordapp(DummyContract.PROGRAM_ID, services.attachments)
|
||||
services.addMockCordapp(DummyContract.PROGRAM_ID)
|
||||
}
|
||||
|
||||
interface Commands {
|
||||
|
@ -51,7 +51,8 @@ class TransactionEncumbranceTests {
|
||||
class DummyTimeLock : Contract {
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
val timeLockInput = tx.inputsOfType<State>().singleOrNull() ?: return
|
||||
val time = tx.timeWindow?.untilTime ?: throw IllegalArgumentException("Transactions containing time-locks must have a time-window")
|
||||
val time = tx.timeWindow?.untilTime
|
||||
?: throw IllegalArgumentException("Transactions containing time-locks must have a time-window")
|
||||
requireThat {
|
||||
"the time specified in the time-lock has passed" using (time >= timeLockInput.validFrom)
|
||||
}
|
||||
@ -64,9 +65,10 @@ class TransactionEncumbranceTests {
|
||||
}
|
||||
}
|
||||
|
||||
private val ledgerServices = MockServices(emptyList(), rigorousMock<IdentityServiceInternal>().also {
|
||||
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
|
||||
}, MEGA_CORP.name)
|
||||
private val ledgerServices = MockServices(emptyList(), MEGA_CORP.name,
|
||||
rigorousMock<IdentityServiceInternal>().also {
|
||||
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
|
||||
})
|
||||
|
||||
@Test
|
||||
fun `state can be encumbered`() {
|
||||
|
@ -0,0 +1,45 @@
|
||||
package net.corda.core.utilities
|
||||
|
||||
import net.corda.core.internal.declaredField
|
||||
import org.assertj.core.api.Assertions.catchThrowable
|
||||
import org.junit.Assert.assertSame
|
||||
import org.junit.Test
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ReadOnlyBufferException
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class ByteArraysTest {
|
||||
@Test
|
||||
fun `slice works`() {
|
||||
byteArrayOf(9, 9, 0, 1, 2, 3, 4, 9, 9).let {
|
||||
sliceWorksImpl(it, OpaqueBytesSubSequence(it, 2, 5))
|
||||
}
|
||||
byteArrayOf(0, 1, 2, 3, 4).let {
|
||||
sliceWorksImpl(it, OpaqueBytes(it))
|
||||
}
|
||||
}
|
||||
|
||||
private fun sliceWorksImpl(array: ByteArray, seq: ByteSequence) {
|
||||
// Python-style negative indices can be implemented later if needed:
|
||||
assertSame(IllegalArgumentException::class.java, catchThrowable { seq.slice(-1) }.javaClass)
|
||||
assertSame(IllegalArgumentException::class.java, catchThrowable { seq.slice(end = -1) }.javaClass)
|
||||
fun check(expected: ByteArray, actual: ByteBuffer) {
|
||||
assertEquals(ByteBuffer.wrap(expected), actual)
|
||||
assertSame(ReadOnlyBufferException::class.java, catchThrowable { actual.array() }.javaClass)
|
||||
assertSame(array, actual.declaredField<ByteArray>(ByteBuffer::class, "hb").value)
|
||||
}
|
||||
check(byteArrayOf(0, 1, 2, 3, 4), seq.slice())
|
||||
check(byteArrayOf(0, 1, 2, 3, 4), seq.slice(0, 5))
|
||||
check(byteArrayOf(0, 1, 2, 3, 4), seq.slice(0, 6))
|
||||
check(byteArrayOf(0, 1, 2, 3), seq.slice(0, 4))
|
||||
check(byteArrayOf(1, 2, 3), seq.slice(1, 4))
|
||||
check(byteArrayOf(1, 2, 3, 4), seq.slice(1, 5))
|
||||
check(byteArrayOf(1, 2, 3, 4), seq.slice(1, 6))
|
||||
check(byteArrayOf(4), seq.slice(4))
|
||||
check(byteArrayOf(), seq.slice(5))
|
||||
check(byteArrayOf(), seq.slice(6))
|
||||
check(byteArrayOf(2), seq.slice(2, 3))
|
||||
check(byteArrayOf(), seq.slice(2, 2))
|
||||
check(byteArrayOf(), seq.slice(2, 1))
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package net.corda.core.utilities
|
||||
|
||||
import net.corda.core.crypto.AddressFormatException
|
||||
import org.apache.commons.lang.ArrayUtils.EMPTY_BYTE_ARRAY
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.fail
|
||||
@ -23,10 +24,9 @@ class EncodingUtilsTest {
|
||||
|
||||
@Test
|
||||
fun `empty encoding`() {
|
||||
val emptyByteArray = ByteArray(0)
|
||||
assertEquals("", emptyByteArray.toBase58())
|
||||
assertEquals("", emptyByteArray.toBase64())
|
||||
assertEquals("", emptyByteArray.toHex())
|
||||
assertEquals("", EMPTY_BYTE_ARRAY.toBase58())
|
||||
assertEquals("", EMPTY_BYTE_ARRAY.toBase64())
|
||||
assertEquals("", EMPTY_BYTE_ARRAY.toHex())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -7,55 +7,135 @@ API: Contract Constraints
|
||||
|
||||
Contract constraints
|
||||
--------------------
|
||||
Transaction states specify a constraint over the contract that will be used to verify it. For a transaction to be
|
||||
valid, the ``verify`` function associated with each state must run successfully. However, for this to be secure, it is
|
||||
not sufficient to specify the ``verify`` function by name as there may exist multiple different implementations with
|
||||
the same method signature and enclosing class. Contract constraints solve this problem by allowing a contract developer
|
||||
to constrain which ``verify`` functions out of the universe of implementations can be used (i.e. the universe is
|
||||
everything that matches the signature and contract constraints restrict this universe to a subset).
|
||||
|
||||
A typical constraint is the hash of the CorDapp JAR that contains the contract and states but will in future releases
|
||||
include constraints that require specific signers of the JAR, or both the signer and the hash. Constraints can be
|
||||
specified when constructing a transaction; if unspecified, an automatic constraint is used.
|
||||
Corda separates verification of states from their definition. Whilst you might have expected the ``ContractState``
|
||||
interface to define a verify method, or perhaps to do verification logic in the constructor, instead it is primarily
|
||||
done by a method on a ``Contract`` class. This is because what we're actually checking is the
|
||||
validity of a *transaction*, which is more than just whether the individual states are internally consistent.
|
||||
The transition between two valid states may be invalid, if the rules of the application are not being respected.
|
||||
For instance, two cash states of $100 and $200 may both be internally valid, but replacing the first with the second
|
||||
isn't allowed unless you're a cash issuer - otherwise you could print money for free.
|
||||
|
||||
For a transaction to be valid, the ``verify`` function associated with each state must run successfully. However,
|
||||
for this to be secure, it is not sufficient to specify the ``verify`` function by name as there may exist multiple
|
||||
different implementations with the same method signature and enclosing class. This normally will happen as applications
|
||||
evolve, but could also happen maliciously.
|
||||
|
||||
Contract constraints solve this problem by allowing a contract developer to constrain which ``verify`` functions out of
|
||||
the universe of implementations can be used (i.e. the universe is everything that matches the signature and contract
|
||||
constraints restrict this universe to a subset). Constraints are satisfied by attachments (JARs). You are not allowed to
|
||||
attach two JARs that both define the same application due to the *no overlap rule*. This rule specifies that two
|
||||
attachment JARs may not provide the same file path. If they do, the transaction is considered invalid. Because each
|
||||
state specifies both a constraint over attachments *and* a Contract class name to use, the specified class must appear
|
||||
in only one attachment.
|
||||
|
||||
So who picks the attachment to use? It is chosen by the creator of the transaction that has to satisfy input constraints.
|
||||
The transaction creator also gets to pick the constraints used by any output states, but the contract logic itself may
|
||||
have opinions about what those constraints are - a typical contract would require that the constraints are propagated,
|
||||
that is, the contract will not just enforce the validity of the next transaction that uses a state, but *all successive
|
||||
transactions as well*. The constraints mechanism creates a balance of power between the creator of data on
|
||||
the ledger and the user who is trying to edit it, which can be very useful when managing upgrades to Corda applications.
|
||||
|
||||
There are two ways of handling upgrades to a smart contract in Corda:
|
||||
|
||||
1. *Implicit:* By allowing multiple implementations of the contract ahead of time, using constraints.
|
||||
2. *Explicit:* By creating a special *contract upgrade transaction* and getting all participants of a state to sign it using the
|
||||
contract upgrade flows.
|
||||
|
||||
This article focuses on the first approach. To learn about the second please see :doc:`upgrading-cordapps`.
|
||||
|
||||
The advantage of pre-authorising upgrades using constraints is that you don't need the heavyweight process of creating
|
||||
upgrade transactions for every state on the ledger. The disadvantage is that you place more faith in third parties,
|
||||
who could potentially change the app in ways you did not expect or agree with. The advantage of using the explicit
|
||||
upgrade approach is that you can upgrade states regardless of their constraint, including in cases where you didn't
|
||||
anticipate a need to do so. But it requires everyone to sign, requires everyone to manually authorise the upgrade,
|
||||
consumes notary and ledger resources, and is just in general more complex.
|
||||
|
||||
How constraints work
|
||||
--------------------
|
||||
|
||||
Starting from Corda 3 there are two types of constraint that can be used: hash and zone whitelist. In future
|
||||
releases a third type will be added, the signature constraint.
|
||||
|
||||
**Hash constraints.** The behaviour provided by public blockchain systems like Bitcoin and Ethereum is that once data is placed on the ledger,
|
||||
the program that controls it is fixed and cannot be changed. There is no support for upgrades at all. This implements a
|
||||
form of "code is law", assuming you trust the community of that blockchain to not release a new version of the platform
|
||||
that invalidates or changes the meaning of your program.
|
||||
|
||||
This is supported by Corda using a hash constraint. This specifies exactly one hash of a CorDapp JAR that contains the
|
||||
contract and states any consuming transaction is allowed to use. Once such a state is created, other nodes will only
|
||||
accept a transaction if it uses that exact JAR file as an attachment. By implication, any bugs in the contract code
|
||||
or state definitions cannot be fixed except by using an explicit upgrade process via ``ContractUpgradeFlow``.
|
||||
|
||||
.. note:: Corda does not support any way to create states that can never be upgraded at all, but the same effect can be
|
||||
obtained by using a hash constraint and then simply refusing to agree to any explicit upgrades. Hash
|
||||
constraints put you in control by requiring an explicit agreement to any upgrade.
|
||||
|
||||
**Zone constraints.** Often a hash constraint will be too restrictive. You do want the ability to upgrade an app,
|
||||
and you don't mind the upgrade taking effect "just in time" when a transaction happens to be required for other business
|
||||
reasons. In this case you can use a zone constraint. This specifies that the network parameters of a compatibility zone
|
||||
(see :doc:`network-map`) is expected to contain a map of class name to hashes of JARs that are allowed to provide that
|
||||
class. The process for upgrading an app then involves asking the zone operator to add the hash of your new JAR to the
|
||||
parameters file, and trigger the network parameters upgrade process. This involves each node operator running a shell
|
||||
command to accept the new parameters file and then restarting the node. Node owners who do not restart their node in
|
||||
time effectively stop being a part of the network.
|
||||
|
||||
**Signature constraints.** These are not yet supported, but once implemented they will allow a state to require a JAR
|
||||
signed by a specified identity, via the regular Java jarsigner tool. This will be the most flexible type
|
||||
and the smoothest to deploy: no restarts or contract upgrade transactions are needed.
|
||||
|
||||
**Defaults.** The default constraint type is either a zone constraint, if the network parameters in effect when the
|
||||
transaction is built contain an entry for that contract class, or a hash constraint if not.
|
||||
|
||||
A ``TransactionState`` has a ``constraint`` field that represents that state's attachment constraint. When a party
|
||||
constructs a ``TransactionState`` without specifying the constraint parameter a default value
|
||||
(``AutomaticHashConstraint``) is used. This default will be automatically resolved to a specific
|
||||
``HashAttachmentConstraint`` that contains the hash of the attachment which contains the contract of that
|
||||
``TransactionState``. This automatic resolution occurs when a ``TransactionBuilder`` is converted to a
|
||||
``WireTransaction``. This reduces the boilerplate involved in finding a specific hash constraint when building a
|
||||
transaction.
|
||||
constructs a ``TransactionState``, or adds a state using ``TransactionBuilder.addOutput(ContractState)`` without
|
||||
specifying the constraint parameter, a default value (``AutomaticHashConstraint``) is used. This default will be
|
||||
automatically resolved to a specific ``HashAttachmentConstraint`` or a ``WhitelistedByZoneAttachmentConstraint``.
|
||||
This automatic resolution occurs when a ``TransactionBuilder`` is converted to a ``WireTransaction``. This reduces
|
||||
the boilerplate that would otherwise be involved.
|
||||
|
||||
It is possible to specify the constraint explicitly with any other class that implements the ``AttachmentConstraint``
|
||||
interface. To specify a hash manually the ``HashAttachmentConstraint`` can be used and to not provide any constraint
|
||||
the ``AlwaysAcceptAttachmentConstraint`` can be used - though this is intended for testing only. An example below
|
||||
shows how to construct a ``TransactionState`` with an explicitly specified hash constraint from within a flow:
|
||||
Finally, an ``AlwaysAcceptAttachmentConstraint`` can be used which accepts anything, though this is intended for
|
||||
testing only.
|
||||
|
||||
Please note that the ``AttachmentConstraint`` interface is marked as ``@DoNotImplement``. You are not allowed to write
|
||||
new constraint types. Only the platform may implement this interface. If you tried, other nodes would not understand
|
||||
your constraint type and your transaction would not verify.
|
||||
|
||||
.. warning:: An AlwaysAccept constraint is effectively the same as disabling security for those states entirely.
|
||||
Nothing stops you using this constraint in production, but that degrades Corda to being effectively a form
|
||||
of distributed messaging with optional contract logic being useful only to catch mistakes, rather than potentially
|
||||
malicious action. If you are deploying an app for which malicious actors aren't in your threat model, using an
|
||||
AlwaysAccept constraint might simplify things operationally.
|
||||
|
||||
An example below shows how to construct a ``TransactionState`` with an explicitly specified hash constraint from within
|
||||
a flow:
|
||||
|
||||
.. sourcecode:: java
|
||||
|
||||
// Constructing a transaction with a custom hash constraint on a state
|
||||
TransactionBuilder tx = new TransactionBuilder()
|
||||
// Constructing a transaction with a custom hash constraint on a state
|
||||
TransactionBuilder tx = new TransactionBuilder();
|
||||
|
||||
Party notaryParty = ... // a notary party
|
||||
DummyState contractState = new DummyState()
|
||||
SecureHash myAttachmentsHash = serviceHub.cordappProvider.getContractAttachmentID(DummyContract.PROGRAM_ID)
|
||||
TransactionState transactionState = new TransactionState(contractState, DummyContract.Companion.getPROGRAMID(), notaryParty, new AttachmentHashConstraint(myAttachmentsHash))
|
||||
Party notaryParty = ... // a notary party
|
||||
DummyState contractState = new DummyState();
|
||||
|
||||
tx.addOutputState(transactionState)
|
||||
WireTransaction wtx = tx.toWireTransaction(serviceHub) // This is where an automatic constraint would be resolved
|
||||
LedgerTransaction ltx = wtx.toLedgerTransaction(serviceHub)
|
||||
ltx.verify() // Verifies both the attachment constraints and contracts
|
||||
SecureHash myAttachmentHash = SecureHash.parse("2b4042aed7e0e39d312c4c477dca1d96ec5a878ddcfd5583251a8367edbd4a5f");
|
||||
TransactionState transactionState = new TransactionState(contractState, DummyContract.Companion.getPROGRAMID(), notaryParty, new AttachmentHashConstraint(myAttachmentHash));
|
||||
|
||||
This mechanism exists both for integrity and security reasons. It is important not to verify against the wrong contract,
|
||||
which could happen if the wrong version of the contract is attached. More importantly when resolving transaction chains
|
||||
there will, in a future release, be attachments loaded from the network into the attachment sandbox that are used
|
||||
to verify the transaction chain. Ensuring the attachment used is the correct one ensures that the verification is
|
||||
tamper-proof by providing a fake contract.
|
||||
tx.addOutputState(transactionState);
|
||||
WireTransaction wtx = tx.toWireTransaction(serviceHub); // This is where an automatic constraint would be resolved.
|
||||
LedgerTransaction ltx = wtx.toLedgerTransaction(serviceHub);
|
||||
ltx.verify(); // Verifies both the attachment constraints and contracts
|
||||
|
||||
Hard-coding the hash of your app in the code itself can be pretty awkward, so the API also offers the ``AutomaticHashConstraint``.
|
||||
This isn't a real constraint that will appear in a transaction: it acts as a marker to the ``TransactionBuilder`` that
|
||||
you require the hash of the node's installed app which supplies the specified contract to be used. In practice, when using
|
||||
hash constraints, you almost always want "whatever the current code is" and not a hard-coded hash. So this automatic
|
||||
constraint placeholder is useful.
|
||||
|
||||
CorDapps as attachments
|
||||
-----------------------
|
||||
|
||||
CorDapp JARs (:doc:`cordapp-overview`) that are installed to the node and contain classes implementing the ``Contract``
|
||||
CorDapp JARs (see :doc:`cordapp-overview`) that are installed to the node and contain classes implementing the ``Contract``
|
||||
interface are automatically loaded into the ``AttachmentStorage`` of a node at startup.
|
||||
|
||||
After CorDapps are loaded into the attachment store the node creates a link between contract classes and the attachment
|
||||
@ -63,28 +143,12 @@ that they were loaded from. This makes it possible to find the attachment for an
|
||||
automatic resolution of attachments is done by the ``TransactionBuilder`` and how, when verifying the constraints and
|
||||
contracts, attachments are associated with their respective contracts.
|
||||
|
||||
Implementations of AttachmentConstraint
|
||||
---------------------------------------
|
||||
|
||||
There are three implementations of ``AttachmentConstraint`` with more planned in the future.
|
||||
|
||||
``AlwaysAcceptAttachmentConstraint``: Any attachment (except a missing one) will satisfy this constraint.
|
||||
|
||||
``AutomaticHashConstraint``: This will be resolved to a ``HashAttachmentConstraint`` when a ``TransactionBuilder`` is
|
||||
converted to a ``WireTransaction``. The ``HashAttachmentConstraint`` will include the attachment hash of the CorDapp
|
||||
that contains the ``ContractState`` on the ``TransactionState.contract`` field.
|
||||
|
||||
``HashAttachmentConstraint``: Will require that the hash of the attachment containing the contract matches the hash
|
||||
stored in the constraint.
|
||||
|
||||
We plan to add a future ``AttachmentConstraint`` that will only be satisfied by the presence of signatures on the
|
||||
attachment JAR. This allows for trusting of attachments from trusted entities.
|
||||
|
||||
Limitations
|
||||
-----------
|
||||
|
||||
An ``AttachmentConstraint`` is verified by running the ``AttachmentConstraint.isSatisfiedBy`` method. When this is called
|
||||
it is provided only the relevant attachment by the transaction that is verifying it.
|
||||
.. note:: The obvious way to write a CorDapp is to put all you states, contracts, flows and support code into a single
|
||||
Java module. This will work but it will effectively publish your entire app onto the ledger. That has two problems:
|
||||
(1) it is inefficient, and (2) it means changes to your flows or other parts of the app will be seen by the ledger
|
||||
as a "new app", which may end up requiring essentially unnecessary upgrade procedures. It's better to split your
|
||||
app into multiple modules: one which contains just states, contracts and core data types. And another which contains
|
||||
the rest of the app. See :ref:`cordapp-structure`.
|
||||
|
||||
Testing
|
||||
-------
|
||||
@ -93,7 +157,8 @@ Since all tests involving transactions now require attachments it is also requir
|
||||
for tests. Unit test environments in JVM ecosystems tend to use class directories rather than JARs, and so CorDapp JARs
|
||||
typically aren't built for testing. Requiring this would add significant complexity to the build systems of Corda
|
||||
and CorDapps, so the test suite has a set of convenient functions to generate CorDapps from package names or
|
||||
to specify JAR URLs in the case that the CorDapp(s) involved in testing already exist.
|
||||
to specify JAR URLs in the case that the CorDapp(s) involved in testing already exist. You can also just use
|
||||
``AlwaysAcceptAttachmentConstraint`` in your tests to disable the constraints mechanism.
|
||||
|
||||
MockNetwork/MockNode
|
||||
********************
|
||||
@ -102,12 +167,14 @@ The simplest way to ensure that a vanilla instance of a MockNode generates the c
|
||||
``cordappPackages`` constructor parameter (Kotlin) or the ``setCordappPackages`` method on ``MockNetworkParameters`` (Java)
|
||||
when creating the MockNetwork. This will cause the ``AbstractNode`` to use the named packages as sources for CorDapps. All files
|
||||
within those packages will be zipped into a JAR and added to the attachment store and loaded as CorDapps by the
|
||||
``CordappLoader``. An example of this usage would be:
|
||||
``CordappLoader``.
|
||||
|
||||
An example of this usage would be:
|
||||
|
||||
.. sourcecode:: java
|
||||
|
||||
class SomeTestClass {
|
||||
MockNetwork network = null
|
||||
MockNetwork network = null;
|
||||
|
||||
@Before
|
||||
void setup() {
|
||||
@ -117,6 +184,7 @@ within those packages will be zipped into a JAR and added to the attachment stor
|
||||
... // Your tests go here
|
||||
}
|
||||
|
||||
|
||||
MockServices
|
||||
************
|
||||
|
||||
@ -127,6 +195,10 @@ to use as CorDapps using the ``cordappPackages`` parameter.
|
||||
|
||||
MockServices mockServices = new MockServices(Arrays.asList("com.domain.cordapp"))
|
||||
|
||||
However - there is an easier way! If your unit tests are in the same package as the contract code itself, then you
|
||||
can use the no-args constructor of ``MockServices``. The package to be scanned for CorDapps will be the same as the
|
||||
the package of the class that constructed the object. This is a convenient default.
|
||||
|
||||
Driver
|
||||
******
|
||||
|
||||
|
@ -411,7 +411,6 @@ Our side of the flow must mirror these calls. We could do this as follows:
|
||||
|
||||
Subflows
|
||||
--------
|
||||
|
||||
Subflows are pieces of reusable flows that may be run by calling ``FlowLogic.subFlow``. There are two broad categories
|
||||
of subflows, inlined and initiating ones. The main difference lies in the counter-flow's starting method, initiating
|
||||
ones initiate counter-flows automatically, while inlined ones expect some parent counter-flow to run the inlined
|
||||
@ -419,7 +418,6 @@ counter-part.
|
||||
|
||||
Inlined subflows
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
Inlined subflows inherit their calling flow's type when initiating a new session with a counterparty. For example, say
|
||||
we have flow A calling an inlined subflow B, which in turn initiates a session with a party. The FlowLogic type used to
|
||||
determine which counter-flow should be kicked off will be A, not B. Note that this means that the other side of this
|
||||
@ -437,7 +435,6 @@ In the code inlined subflows appear as regular ``FlowLogic`` instances, `without
|
||||
|
||||
Initiating subflows
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Initiating subflows are ones annotated with the ``@InitiatingFlow`` annotation. When such a flow initiates a session its
|
||||
type will be used to determine which ``@InitiatedBy`` flow to kick off on the counterparty.
|
||||
|
||||
@ -446,32 +443,38 @@ An example is the ``@InitiatingFlow InitiatorFlow``/``@InitiatedBy ResponderFlow
|
||||
.. note:: Initiating flows are versioned separately from their parents.
|
||||
|
||||
Core initiating subflows
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Corda-provided initiating subflows are a little different to standard ones as they are versioned together with the
|
||||
platform, and their initiated counter-flows are registered explicitly, so there is no need for the ``InitiatedBy``
|
||||
annotation.
|
||||
|
||||
An example is the ``FinalityFlow``/``FinalityHandler`` flow pair.
|
||||
Library flows
|
||||
^^^^^^^^^^^^^
|
||||
Corda installs four initiating subflow pairs on each node by default:
|
||||
|
||||
Built-in subflows
|
||||
^^^^^^^^^^^^^^^^^
|
||||
* ``FinalityFlow``/``FinalityHandler``, which should be used to notarise and record a transaction and broadcast it to
|
||||
all relevant parties
|
||||
* ``NotaryChangeFlow``/``NotaryChangeHandler``, which should be used to change a state's notary
|
||||
* ``ContractUpgradeFlow.Initiate``/``ContractUpgradeHandler``, which should be used to change a state's contract
|
||||
* ``SwapIdentitiesFlow``/``SwapIdentitiesHandler``, which is used to exchange confidential identities with a
|
||||
counterparty
|
||||
|
||||
Corda provides a number of built-in flows that should be used for handling common tasks. The most important are:
|
||||
.. warning:: ``SwapIdentitiesFlow``/``SwapIdentitiesHandler`` are only installed if the ``confidential-identities`` module
|
||||
is included. The ``confidential-identities`` module is still not stabilised, so the
|
||||
``SwapIdentitiesFlow``/``SwapIdentitiesHandler`` API may change in future releases. See :doc:`corda-api`.
|
||||
|
||||
Corda also provides a number of built-in inlined subflows that should be used for handling common tasks. The most
|
||||
important are:
|
||||
|
||||
* ``CollectSignaturesFlow`` (inlined), which should be used to collect a transaction's required signatures
|
||||
* ``FinalityFlow`` (initiating), which should be used to notarise and record a transaction as well as to broadcast it to
|
||||
all relevant parties
|
||||
* ``SendTransactionFlow`` (inlined), which should be used to send a signed transaction if it needed to be resolved on
|
||||
the other side.
|
||||
* ``ReceiveTransactionFlow`` (inlined), which should be used receive a signed transaction
|
||||
* ``ContractUpgradeFlow`` (initiating), which should be used to change a state's contract
|
||||
* ``NotaryChangeFlow`` (initiating), which should be used to change a state's notary
|
||||
|
||||
Let's look at three very common examples.
|
||||
Let's look at some of these flows in more detail.
|
||||
|
||||
FinalityFlow
|
||||
^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~
|
||||
``FinalityFlow`` allows us to notarise the transaction and get it recorded in the vault of the participants of all
|
||||
the transaction's states:
|
||||
|
||||
@ -509,7 +512,7 @@ Only one party has to call ``FinalityFlow`` for a given transaction to be record
|
||||
**not** need to be called by each participant individually.
|
||||
|
||||
CollectSignaturesFlow/SignTransactionFlow
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
The list of parties who need to sign a transaction is dictated by the transaction's commands. Once we've signed a
|
||||
transaction ourselves, we can automatically gather the signatures of the other required signers using
|
||||
``CollectSignaturesFlow``:
|
||||
@ -546,7 +549,7 @@ transaction and provide their signature if they are satisfied:
|
||||
:dedent: 12
|
||||
|
||||
SendTransactionFlow/ReceiveTransactionFlow
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Verifying a transaction received from a counterparty also requires verification of every transaction in its
|
||||
dependency chain. This means the receiving party needs to be able to ask the sender all the details of the chain.
|
||||
The sender will use ``SendTransactionFlow`` for sending the transaction and then for processing all subsequent
|
||||
@ -601,7 +604,6 @@ We can also send and receive a ``StateAndRef`` dependency chain and automaticall
|
||||
|
||||
Why inlined subflows?
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Inlined subflows provide a way to share commonly used flow code `while forcing users to create a parent flow`. Take for
|
||||
example ``CollectSignaturesFlow``. Say we made it an initiating flow that automatically kicks off
|
||||
``SignTransactionFlow`` that signs the transaction. This would mean malicious nodes can just send any old transaction to
|
||||
|
@ -3,6 +3,9 @@ API: Identity
|
||||
|
||||
.. note:: Before reading this page, you should be familiar with the key concepts of :doc:`key-concepts-identity`.
|
||||
|
||||
.. warning:: The ``confidential-identities`` module is still not stabilised, so this API may change in future releases.
|
||||
See :doc:`corda-api`.
|
||||
|
||||
.. contents::
|
||||
|
||||
Party
|
||||
|
@ -615,6 +615,24 @@ which the signatures are allowed to be missing:
|
||||
:end-before: DOCEND 36
|
||||
:dedent: 16
|
||||
|
||||
There is also an overload of ``SignedTransaction.verifySignaturesExcept``, which takes a ``Collection`` of the
|
||||
public keys for which the signatures are allowed to be missing:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 54
|
||||
:end-before: DOCEND 54
|
||||
:dedent: 8
|
||||
|
||||
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java
|
||||
:language: java
|
||||
:start-after: DOCSTART 54
|
||||
:end-before: DOCEND 54
|
||||
:dedent: 16
|
||||
|
||||
|
||||
If the transaction is missing any signatures without the corresponding public keys being passed in, a
|
||||
``SignaturesMissingException`` is thrown.
|
||||
|
||||
|
@ -6,6 +6,22 @@ from previous releases. Please refer to :doc:`upgrade-notes` for detailed instru
|
||||
|
||||
UNRELEASED
|
||||
----------
|
||||
|
||||
* Added ``NetworkMapCache.getNodesByLegalName`` for querying nodes belonging to a distributed service such as a notary cluster
|
||||
where they all share a common identity. ``NetworkMapCache.getNodeByLegalName`` has been tightened to throw if more than
|
||||
one node with the legal name is found.
|
||||
|
||||
* Per CorDapp configuration is now exposed. ``CordappContext`` now exposes a ``CordappConfig`` object that is populated
|
||||
at CorDapp context creation time from a file source during runtime.
|
||||
|
||||
* Introduced Flow Draining mode, in which a node continues executing existing flows, but does not start new. This is to support graceful node shutdown/restarts.
|
||||
In particular, when this mode is on, new flows through RPC will be rejected, scheduled flows will be ignored, and initial session messages will not be consumed.
|
||||
This will ensure that the number of checkpoints will strictly diminish with time, allowing for a clean shutdown.
|
||||
|
||||
* Make the serialisation finger-printer a pluggable entity rather than hard wiring into the factory
|
||||
|
||||
* Removed blacklisted word checks in Corda X.500 name to allow "Server" or "Node" to be use as part of the legal name.
|
||||
|
||||
* Separated our pre-existing Artemis broker into an RPC broker and a P2P broker.
|
||||
|
||||
* Refactored ``NodeConfiguration`` to expose ``NodeRpcOptions`` (using top-level "rpcAddress" property still works with warning).
|
||||
@ -71,7 +87,10 @@ R3 Corda 3.0 Developer Preview
|
||||
:doc:`corda-configuration-file` for more details.
|
||||
|
||||
* Introducing the concept of network parameters which are a set of constants which all nodes on a network must agree on
|
||||
to correctly interop.
|
||||
to correctly interop. These can be retrieved from ``ServiceHub.networkParameters``.
|
||||
|
||||
* One of these parameters, ``maxTransactionSize``, limits the size of a transaction, including its attachments, so that
|
||||
all nodes have sufficient memory to validate transactions.
|
||||
|
||||
* The set of valid notaries has been moved to the network parameters. Notaries are no longer identified by the CN in
|
||||
their X.500 name.
|
||||
@ -196,6 +215,26 @@ R3 Corda 3.0 Developer Preview
|
||||
* Move to a message based control of peer to peer bridge formation to allow for future out of process bridging components.
|
||||
This removes the legacy Artemis bridges completely, so the ``useAMQPBridges`` configuration property has been removed.
|
||||
|
||||
* A ``CordaInternal`` attribute has been added to identify properties that are not intended to form part of the
|
||||
public api and as such are not intended for public use. This is alongside the existing ``DoNotImplement`` attribute for classes which
|
||||
provide Corda functionality to user applications, but should not be implemented by consumers, and any classes which
|
||||
are defined in ``.internal`` packages, which are also not for public use.
|
||||
|
||||
* Marked ``stateMachine`` on ``FlowLogic`` as ``CordaInternal`` to make clear that is it not part of the public api and is
|
||||
only for internal use
|
||||
|
||||
* Provided experimental support for specifying your own webserver to be used instead of the default development
|
||||
webserver in ``Cordform`` using the ``webserverJar`` argument
|
||||
|
||||
* Created new ``StartedMockNode`` and ``UnstartedMockNode`` classes which are wrappers around our MockNode implementation
|
||||
that expose relevant methods for testing without exposing internals, create these using a ``MockNetwork``.
|
||||
|
||||
* The test utils in ``Expect.kt``, ``SerializationTestHelpers.kt``, ``TestConstants.kt`` and ``TestUtils.kt`` have moved
|
||||
from the ``net.corda.testing`` package to the ``net.corda.testing.core`` package, and ``FlowStackSnapshot.kt`` has moved to the
|
||||
``net.corda.testing.services`` package. Moving existing classes out of the ``net.corda.testing.*`` package
|
||||
will help make it clearer which parts of the api are stable. Scripts have been provided to smooth the upgrade
|
||||
process for existing projects in the ``tools\scripts`` directory of the Corda repo.
|
||||
|
||||
.. _changelog_v2:
|
||||
|
||||
Corda 2.0
|
||||
|
@ -203,8 +203,8 @@ path to the node's base directory.
|
||||
:publicKeyFile: Path to the public key file for SSH authentication.
|
||||
:sshPort: Port to be used for SSH connection, default ``22``.
|
||||
|
||||
:exportJMXTo: If set to ``http``, will enable JMX metrics reporting via the Jolokia HTTP/JSON agent.
|
||||
Default Jolokia access url is http://127.0.0.1:7005/jolokia/
|
||||
:jmxMonitoringHttpPort: If set, will enable JMX metrics reporting via the Jolokia HTTP/JSON agent on the corresponding port.
|
||||
Default Jolokia access url is http://127.0.0.1:port/jolokia/
|
||||
|
||||
:transactionCacheSizeMegaBytes: Optionally specify how much memory should be used for caching of ledger transactions in memory.
|
||||
Otherwise defaults to 8MB plus 5% of all heap memory above 300MB.
|
||||
|
@ -157,3 +157,20 @@ Installing the CorDapp JAR
|
||||
At runtime, nodes will load any CorDapps present in their ``cordapps`` folder. Therefore in order to install a CorDapp on
|
||||
a node, the CorDapp JAR must be added to the ``<node_dir>/cordapps/`` folder, where ``node_dir`` is the folder in which
|
||||
the node's JAR and configuration files are stored.
|
||||
|
||||
CorDapp configuration files
|
||||
---------------------------
|
||||
|
||||
CorDapp configuration files should be placed in ``<node_dir>/cordapps/config``. The name of the file should match the
|
||||
name of the JAR of the CorDapp (eg; if your CorDapp is called ``hello-0.1.jar`` the config should be ``config/hello-0.1.conf``).
|
||||
|
||||
Config files are currently only available in the `Typesafe/Lightbend <https://github.com/lightbend/config>`_ config format.
|
||||
These files are loaded when a CorDapp context is created and so can change during runtime.
|
||||
|
||||
CorDapp configuration can be accessed from ``CordappContext::config`` whenever a ``CordappContext`` is available.
|
||||
|
||||
There is an example project that demonstrates in ``samples` called ``cordapp-configuration`` and API documentation in
|
||||
<api/kotlin/corda/net.corda.core.cordapp/index.html>`_.
|
||||
|
||||
|
||||
|
||||
|
@ -30,8 +30,8 @@ handling, and ensures the Corda service is run at boot.
|
||||
4. (Optional) Download the `Corda webserver jar <http://r3.bintray.com/corda/net/corda/corda-webserver/>`_
|
||||
(under ``/VERSION_NUMBER/corda-VERSION_NUMBER.jar``) and place it in ``/opt/corda``
|
||||
|
||||
5. Create a directory called ``plugins`` in ``/opt/corda`` and save your CorDapp jar file to it. Alternatively, download one of
|
||||
our `sample CorDapps <https://www.corda.net/samples/>`_ to the ``plugins`` directory
|
||||
5. Create a directory called ``cordapps`` in ``/opt/corda`` and save your CorDapp jar file to it. Alternatively, download one of
|
||||
our `sample CorDapps <https://www.corda.net/samples/>`_ to the ``cordapps`` directory
|
||||
|
||||
6. Save the below as ``/opt/corda/node.conf``. See :doc:`corda-configuration-file` for a description of these options
|
||||
|
||||
@ -199,8 +199,8 @@ at boot, and means the Corda service stays running with no users connected to th
|
||||
mkdir C:\Corda
|
||||
wget http://jcenter.bintray.com/net/corda/corda/VERSION_NUMBER/corda-VERSION_NUMBER.jar -OutFile C:\Corda\corda.jar
|
||||
|
||||
2. Create a directory called ``plugins`` in ``/opt/corda`` and save your CorDapp jar file to it. Alternatively,
|
||||
download one of our `sample CorDapps <https://www.corda.net/samples/>`_ to the ``plugins`` directory
|
||||
2. Create a directory called ``cordapps`` in ``/opt/corda`` and save your CorDapp jar file to it. Alternatively,
|
||||
download one of our `sample CorDapps <https://www.corda.net/samples/>`_ to the ``cordapps`` directory
|
||||
|
||||
3. Save the below as ``C:\Corda\node.conf``. See :doc:`corda-configuration-file` for a description of these options
|
||||
|
||||
@ -282,4 +282,4 @@ You can verify Corda is running by connecting to your RPC port from another host
|
||||
``telnet your-hostname.example.com 10002``
|
||||
|
||||
If you receive the message "Escape character is ^]", Corda is running and accessible. Press Ctrl-] and Ctrl-D to exit
|
||||
telnet.
|
||||
telnet.
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.docs
|
||||
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.core.internal.concurrent.transpose
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.startFlow
|
||||
@ -15,11 +16,12 @@ import net.corda.finance.flows.CashPaymentFlow
|
||||
import net.corda.node.services.Permissions.Companion.invokeRpc
|
||||
import net.corda.node.services.Permissions.Companion.startFlow
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.node.User
|
||||
import net.corda.testing.internal.IntegrationTest
|
||||
import net.corda.testing.internal.IntegrationTestSchemas
|
||||
import net.corda.testing.internal.toDatabaseSchemaName
|
||||
import net.corda.testing.node.User
|
||||
import org.junit.ClassRule
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
@ -34,8 +36,10 @@ class IntegrationTestingTutorial : IntegrationTest() {
|
||||
@Test
|
||||
fun `alice bob cash exchange example`() {
|
||||
// START 1
|
||||
driver(startNodesInProcess = true,
|
||||
extraCordappPackagesToScan = listOf("net.corda.finance.contracts.asset","net.corda.finance.schemas")) {
|
||||
driver(DriverParameters(
|
||||
startNodesInProcess = true,
|
||||
extraCordappPackagesToScan = listOf("net.corda.finance.contracts.asset","net.corda.finance.schemas")
|
||||
)) {
|
||||
val aliceUser = User("aliceUser", "testPassword1", permissions = setOf(
|
||||
startFlow<CashIssueFlow>(),
|
||||
startFlow<CashPaymentFlow>(),
|
||||
@ -56,10 +60,10 @@ class IntegrationTestingTutorial : IntegrationTest() {
|
||||
// END 1
|
||||
|
||||
// START 2
|
||||
val aliceClient = alice.rpcClientToNode()
|
||||
val aliceClient = CordaRPCClient(alice.rpcAddress)
|
||||
val aliceProxy = aliceClient.start("aliceUser", "testPassword1").proxy
|
||||
|
||||
val bobClient = bob.rpcClientToNode()
|
||||
val bobClient = CordaRPCClient(bob.rpcAddress)
|
||||
val bobProxy = bobClient.start("bobUser", "testPassword2").proxy
|
||||
// END 2
|
||||
|
||||
|
@ -540,12 +540,21 @@ public class FlowCookbookJava {
|
||||
// DOCEND 35
|
||||
|
||||
// If the transaction is only partially signed, we have to pass in
|
||||
// a list of the public keys corresponding to the missing
|
||||
// a vararg of the public keys corresponding to the missing
|
||||
// signatures, explicitly telling the system not to check them.
|
||||
// DOCSTART 36
|
||||
onceSignedTx.verifySignaturesExcept(counterpartyPubKey);
|
||||
// DOCEND 36
|
||||
|
||||
// There is also an overload of ``verifySignaturesExcept`` which accepts
|
||||
// a ``Collection`` of the public keys corresponding to the missing
|
||||
// signatures. In the example below, we could also use
|
||||
// ``Arrays.asList(counterpartyPubKey)`` instead of
|
||||
// ``Collections.singletonList(counterpartyPubKey)``.
|
||||
// DOCSTART 54
|
||||
onceSignedTx.verifySignaturesExcept(Collections.singletonList(counterpartyPubKey));
|
||||
// DOCEND 54
|
||||
|
||||
// We can also choose to only check the signatures that are
|
||||
// present. BE VERY CAREFUL - this function provides no guarantees
|
||||
// that the signatures are correct, or that none are missing.
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
package net.corda.docs
|
||||
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.startFlow
|
||||
@ -18,6 +19,7 @@ import net.corda.finance.flows.CashPaymentFlow
|
||||
import net.corda.node.services.Permissions.Companion.invokeRpc
|
||||
import net.corda.node.services.Permissions.Companion.startFlow
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.node.User
|
||||
import net.corda.testing.driver.driver
|
||||
import org.graphstream.graph.Edge
|
||||
@ -48,12 +50,12 @@ fun main(args: Array<String>) {
|
||||
startFlow<CashExitFlow>(),
|
||||
invokeRpc(CordaRPCOps::nodeInfo)
|
||||
))
|
||||
driver(driverDirectory = baseDirectory, extraCordappPackagesToScan = listOf("net.corda.finance"), waitForAllNodesToFinish = true) {
|
||||
driver(DriverParameters(driverDirectory = baseDirectory, extraCordappPackagesToScan = listOf("net.corda.finance"), waitForAllNodesToFinish = true)) {
|
||||
val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).get()
|
||||
// END 1
|
||||
|
||||
// START 2
|
||||
val client = node.rpcClientToNode()
|
||||
val client = CordaRPCClient(node.rpcAddress)
|
||||
val proxy = client.start("user", "password").proxy
|
||||
|
||||
thread {
|
||||
|
@ -522,12 +522,19 @@ class InitiatorFlow(val arg1: Boolean, val arg2: Int, private val counterparty:
|
||||
// DOCEND 35
|
||||
|
||||
// If the transaction is only partially signed, we have to pass in
|
||||
// a list of the public keys corresponding to the missing
|
||||
// a vararg of the public keys corresponding to the missing
|
||||
// signatures, explicitly telling the system not to check them.
|
||||
// DOCSTART 36
|
||||
onceSignedTx.verifySignaturesExcept(counterpartyPubKey)
|
||||
// DOCEND 36
|
||||
|
||||
// There is also an overload of ``verifySignaturesExcept`` which accepts
|
||||
// a ``Collection`` of the public keys corresponding to the missing
|
||||
// signatures.
|
||||
// DOCSTART 54
|
||||
onceSignedTx.verifySignaturesExcept(listOf(counterpartyPubKey))
|
||||
// DOCEND 54
|
||||
|
||||
// We can also choose to only check the signatures that are
|
||||
// present. BE VERY CAREFUL - this function provides no guarantees
|
||||
// that the signatures are correct, or that none are missing.
|
||||
|
@ -13,15 +13,10 @@ import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.node.services.messaging.Message
|
||||
import net.corda.node.services.statemachine.DataSessionMessage
|
||||
import net.corda.node.services.statemachine.ExistingSessionMessage
|
||||
import net.corda.testing.node.InMemoryMessagingNetwork
|
||||
import net.corda.testing.node.MessagingServiceSpy
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.setMessagingServiceSpy
|
||||
import net.corda.testing.node.startFlow
|
||||
import net.corda.testing.node.*
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
@ -58,8 +53,8 @@ class TutorialMockNetwork {
|
||||
}
|
||||
|
||||
lateinit private var mockNet: MockNetwork
|
||||
lateinit private var nodeA: StartedNode<MockNetwork.MockNode>
|
||||
lateinit private var nodeB: StartedNode<MockNetwork.MockNode>
|
||||
lateinit private var nodeA: StartedMockNode
|
||||
lateinit private var nodeB: StartedMockNode
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
@ -104,6 +99,6 @@ class TutorialMockNetwork {
|
||||
|
||||
expectedEx.expect(IllegalArgumentException::class.java)
|
||||
expectedEx.expectMessage("Expected to receive 1")
|
||||
initiatingReceiveFlow.resultFuture.getOrThrow()
|
||||
initiatingReceiveFlow.getOrThrow()
|
||||
}
|
||||
}
|
@ -7,9 +7,9 @@ import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.*
|
||||
import net.corda.finance.contracts.getCashBalances
|
||||
import net.corda.finance.flows.CashIssueFlow
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.testing.core.chooseIdentity
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.StartedMockNode
|
||||
import net.corda.testing.node.startFlow
|
||||
import org.junit.After
|
||||
import org.junit.Assert
|
||||
@ -19,8 +19,8 @@ import java.util.*
|
||||
|
||||
class CustomVaultQueryTest {
|
||||
private lateinit var mockNet: MockNetwork
|
||||
private lateinit var nodeA: StartedNode<MockNetwork.MockNode>
|
||||
private lateinit var nodeB: StartedNode<MockNetwork.MockNode>
|
||||
private lateinit var nodeA: StartedMockNode
|
||||
private lateinit var nodeB: StartedMockNode
|
||||
private lateinit var notary: Party
|
||||
|
||||
@Before
|
||||
@ -60,7 +60,7 @@ class CustomVaultQueryTest {
|
||||
OpaqueBytes.of(0x01),
|
||||
notary))
|
||||
// Wait for the flow to stop and print
|
||||
flowHandle1.resultFuture.getOrThrow()
|
||||
flowHandle1.getOrThrow()
|
||||
}
|
||||
|
||||
private fun topUpCurrencies() {
|
||||
@ -69,21 +69,19 @@ class CustomVaultQueryTest {
|
||||
OpaqueBytes.of(0x01),
|
||||
nodeA.info.chooseIdentity(),
|
||||
notary))
|
||||
flowHandle1.resultFuture.getOrThrow()
|
||||
flowHandle1.getOrThrow()
|
||||
}
|
||||
|
||||
private fun getBalances(): Pair<Map<Currency, Amount<Currency>>, Map<Currency, Amount<Currency>>> {
|
||||
// Print out the balances
|
||||
val balancesNodesA =
|
||||
nodeA.database.transaction {
|
||||
nodeA.services.getCashBalances()
|
||||
}
|
||||
val balancesNodesA = nodeA.transaction {
|
||||
nodeA.services.getCashBalances()
|
||||
}
|
||||
println("BalanceA\n" + balancesNodesA)
|
||||
|
||||
val balancesNodesB =
|
||||
nodeB.database.transaction {
|
||||
nodeB.services.getCashBalances()
|
||||
}
|
||||
val balancesNodesB = nodeB.transaction {
|
||||
nodeB.services.getCashBalances()
|
||||
}
|
||||
println("BalanceB\n" + balancesNodesB)
|
||||
|
||||
return Pair(balancesNodesA, balancesNodesB)
|
||||
|
@ -7,19 +7,20 @@ import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.*
|
||||
import net.corda.finance.contracts.getCashBalances
|
||||
import net.corda.finance.flows.CashIssueFlow
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.testing.core.chooseIdentity
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.StartedMockNode
|
||||
import net.corda.testing.node.startFlow
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class FxTransactionBuildTutorialTest {
|
||||
private lateinit var mockNet: MockNetwork
|
||||
private lateinit var nodeA: StartedNode<MockNetwork.MockNode>
|
||||
private lateinit var nodeB: StartedNode<MockNetwork.MockNode>
|
||||
private lateinit var nodeA: StartedMockNode
|
||||
private lateinit var nodeB: StartedMockNode
|
||||
private lateinit var notary: Party
|
||||
|
||||
@Before
|
||||
@ -36,6 +37,7 @@ class FxTransactionBuildTutorialTest {
|
||||
mockNet.stopNodes()
|
||||
}
|
||||
|
||||
@Ignore("Pending fix from corda")
|
||||
@Test
|
||||
fun `Run ForeignExchangeFlow to completion`() {
|
||||
// Use NodeA as issuer and create some dollars
|
||||
@ -43,7 +45,7 @@ class FxTransactionBuildTutorialTest {
|
||||
OpaqueBytes.of(0x01),
|
||||
notary))
|
||||
// Wait for the flow to stop and print
|
||||
flowHandle1.resultFuture.getOrThrow()
|
||||
flowHandle1.getOrThrow()
|
||||
printBalances()
|
||||
|
||||
// Using NodeB as Issuer create some pounds.
|
||||
@ -51,7 +53,7 @@ class FxTransactionBuildTutorialTest {
|
||||
OpaqueBytes.of(0x01),
|
||||
notary))
|
||||
// Wait for flow to come to an end and print
|
||||
flowHandle2.resultFuture.getOrThrow()
|
||||
flowHandle2.getOrThrow()
|
||||
printBalances()
|
||||
|
||||
// Setup some futures on the vaults to await the arrival of the exchanged funds at both nodes
|
||||
@ -65,16 +67,17 @@ class FxTransactionBuildTutorialTest {
|
||||
nodeB.info.chooseIdentity(),
|
||||
weAreBaseCurrencySeller = false))
|
||||
// wait for the flow to finish and the vault updates to be done
|
||||
doIt.resultFuture.getOrThrow()
|
||||
doIt.getOrThrow()
|
||||
// Get the balances when the vault updates
|
||||
nodeAVaultUpdate.get()
|
||||
val balancesA = nodeA.database.transaction {
|
||||
val balancesA = nodeA.transaction {
|
||||
nodeA.services.getCashBalances()
|
||||
}
|
||||
nodeBVaultUpdate.get()
|
||||
val balancesB = nodeB.database.transaction {
|
||||
val balancesB = nodeB.transaction {
|
||||
nodeB.services.getCashBalances()
|
||||
}
|
||||
|
||||
println("BalanceA\n" + balancesA)
|
||||
println("BalanceB\n" + balancesB)
|
||||
// Verify the transfers occurred as expected
|
||||
@ -86,10 +89,10 @@ class FxTransactionBuildTutorialTest {
|
||||
|
||||
private fun printBalances() {
|
||||
// Print out the balances
|
||||
nodeA.database.transaction {
|
||||
nodeA.transaction {
|
||||
println("BalanceA\n" + nodeA.services.getCashBalances())
|
||||
}
|
||||
nodeB.database.transaction {
|
||||
nodeB.transaction {
|
||||
println("BalanceB\n" + nodeB.services.getCashBalances())
|
||||
}
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ class WorkflowTransactionBuildTutorialTest {
|
||||
// Kick of the proposal flow
|
||||
val flow1 = aliceServices.startFlow(SubmitTradeApprovalFlow("1234", bob))
|
||||
// Wait for the flow to finish
|
||||
val proposalRef = flow1.resultFuture.getOrThrow()
|
||||
val proposalRef = flow1.getOrThrow()
|
||||
val proposalLinearId = proposalRef.state.data.linearId
|
||||
// Wait for NodeB to include it's copy in the vault
|
||||
nodeBVaultUpdate.get()
|
||||
@ -80,7 +80,7 @@ class WorkflowTransactionBuildTutorialTest {
|
||||
// Run the manual completion flow from NodeB
|
||||
val flow2 = bobServices.startFlow(SubmitCompletionFlow(latestFromB.ref, WorkflowState.APPROVED))
|
||||
// wait for the flow to end
|
||||
val completedRef = flow2.resultFuture.getOrThrow()
|
||||
val completedRef = flow2.getOrThrow()
|
||||
// wait for the vault updates to stabilise
|
||||
nodeAVaultUpdate.get()
|
||||
secondNodeBVaultUpdate.get()
|
||||
|
@ -5,6 +5,7 @@ import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.contracts.TransactionVerificationException
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.`issued by`
|
||||
@ -13,7 +14,6 @@ import net.corda.finance.contracts.CommercialPaper
|
||||
import net.corda.finance.contracts.ICommercialPaperState
|
||||
import net.corda.finance.contracts.asset.CASH
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.node.services.api.IdentityServiceInternal
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import net.corda.testing.node.MockServices
|
||||
@ -38,11 +38,11 @@ class CommercialPaperTest {
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
private val ledgerServices = MockServices(emptyList(), rigorousMock<IdentityServiceInternal>().also {
|
||||
private val ledgerServices = MockServices(emptyList(), MEGA_CORP.name, rigorousMock<IdentityService>().also {
|
||||
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
|
||||
doReturn(null).whenever(it).partyFromKey(BIG_CORP_PUBKEY)
|
||||
doReturn(null).whenever(it).partyFromKey(ALICE_PUBKEY)
|
||||
}, MEGA_CORP.name)
|
||||
})
|
||||
|
||||
// DOCSTART 1
|
||||
fun getPaper(): ICommercialPaperState = CommercialPaper.State(
|
||||
|
@ -63,7 +63,6 @@ The name must also obey the following constraints:
|
||||
* The organisation field of the name also obeys the following constraints:
|
||||
|
||||
* No double-spacing
|
||||
* Does not contain the words "node" or "server"
|
||||
|
||||
* This is to avoid right-to-left issues, debugging issues when we can't pronounce names over the phone, and
|
||||
character confusability attacks
|
||||
@ -138,6 +137,25 @@ a single node to run the network map service, by putting its name in the ``netwo
|
||||
|
||||
.. warning:: When adding nodes, make sure that there are no port clashes!
|
||||
|
||||
Specifying a custom webserver
|
||||
-----------------------------
|
||||
By default, any node listing a webport will use the default development webserver, which is not production-ready. You
|
||||
can use your own webserver JAR instead by using the ``webserverJar`` argument in a ``Cordform`` ``node`` configuration
|
||||
block:
|
||||
|
||||
.. sourcecode:: groovy
|
||||
|
||||
node {
|
||||
name "O=PartyA,L=New York,C=US"
|
||||
webPort 10005
|
||||
webserverJar "lib/my_webserver.jar"
|
||||
}
|
||||
|
||||
The webserver JAR will be copied into the node's ``build`` folder with the name ``corda-webserver.jar``.
|
||||
|
||||
.. warning:: This is an experimental feature. There is currently no support for reading the webserver's port from the
|
||||
node's ``node.conf`` file.
|
||||
|
||||
Running deployNodes
|
||||
-------------------
|
||||
To create the nodes defined in our ``deployNodes`` task, run the following command in a terminal window from the root
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user