mirror of
https://github.com/corda/corda.git
synced 2025-04-05 10:27:11 +00:00
Merge commit '86fb1ed852c69121f989c9eeea92cfb4c27f9d13' into aslemmer-merge-19-Feb
This commit is contained in:
commit
1d7b0fc499
1251
.ci/api-current.txt
1251
.ci/api-current.txt
File diff suppressed because it is too large
Load Diff
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)
|
||||
|
||||
|
1
.idea/compiler.xml
generated
1
.idea/compiler.xml
generated
@ -87,6 +87,7 @@
|
||||
<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_systemTest" target="1.8" />
|
||||
<module name="irs-demo_test" target="1.8" />
|
||||
<module name="isolated_main" target="1.8" />
|
||||
<module name="isolated_test" target="1.8" />
|
||||
|
15
build.gradle
15
build.gradle
@ -37,7 +37,7 @@ 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'
|
||||
@ -71,6 +71,10 @@ 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'
|
||||
|
||||
// Update 121 is required for ObjectInputFilter and at time of writing 131 was latest:
|
||||
ext.java8_minUpdateVersion = '131'
|
||||
@ -79,6 +83,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/'
|
||||
}
|
||||
@ -378,8 +383,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
|
||||
@ -158,8 +158,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 +167,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 +179,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)))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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) })
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
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
|
@ -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"
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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":
|
||||
|
@ -0,0 +1,14 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
import net.corda.core.node.NetworkParameters
|
||||
|
||||
// TODO: This will cause problems when we run tests in parallel, make each node have its own properties.
|
||||
object GlobalProperties {
|
||||
private var _networkParameters: NetworkParameters? = null
|
||||
|
||||
var networkParameters: NetworkParameters
|
||||
get() = checkNotNull(_networkParameters) { "Property 'networkParameters' has not been initialised." }
|
||||
set(value) {
|
||||
_networkParameters = value
|
||||
}
|
||||
}
|
@ -3,16 +3,16 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
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.identity.CordaX500Name
|
||||
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 +26,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 +309,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 +365,13 @@ 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) }
|
||||
|
@ -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(
|
||||
|
@ -4,7 +4,7 @@ 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.context.InvocationOrigin
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowInitiator
|
||||
@ -13,6 +13,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 +24,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 +74,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 +225,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].
|
||||
|
@ -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)
|
@ -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
|
||||
|
@ -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() }
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -5,6 +5,7 @@ import net.corda.core.contracts.ComponentGroupEnum.*
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.Emoji
|
||||
import net.corda.core.internal.GlobalProperties
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
@ -118,7 +119,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)
|
||||
return ltx
|
||||
}
|
||||
|
||||
private fun checkTransactionSize(ltx: LedgerTransaction) {
|
||||
var remainingTransactionSize = GlobalProperties.networkParameters.maxTransactionSize
|
||||
|
||||
fun minus(size: Int) {
|
||||
require(remainingTransactionSize > size) { "Transaction exceeded network's maximum transaction size limit : ${GlobalProperties.networkParameters.maxTransactionSize} bytes." }
|
||||
remainingTransactionSize -= size
|
||||
}
|
||||
|
||||
// Check attachment size first as they are most likely to go over the limit.
|
||||
ltx.attachments.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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
@ -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))
|
||||
|
@ -115,7 +115,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 +127,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)
|
||||
@ -141,7 +141,7 @@ class CollectSignaturesFlowTests {
|
||||
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 +155,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)
|
||||
}
|
||||
|
@ -81,24 +81,24 @@ 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()
|
||||
@ -213,7 +213,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 +221,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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -52,14 +52,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 +74,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 +83,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 +108,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 +132,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 +154,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,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) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -6,6 +6,8 @@ from previous releases. Please refer to :doc:`upgrade-notes` for detailed instru
|
||||
|
||||
UNRELEASED
|
||||
----------
|
||||
* 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).
|
||||
@ -84,6 +86,9 @@ R3 Corda 3.0 Developer Preview
|
||||
* Moved ``NodeInfoSchema`` to internal package as the node info's database schema is not part of the public API. This
|
||||
was needed to allow changes to the schema.
|
||||
|
||||
* Introduced max transaction size limit on transactions. The max transaction size parameter is set by the compatibility zone
|
||||
operator. The parameter is distributed to Corda nodes by network map service as part of the ``NetworkParameters``.
|
||||
|
||||
* Support for external user credentials data source and password encryption [CORDA-827].
|
||||
|
||||
* Integrate database migration tool: http://www.liquibase.org/ :
|
||||
@ -196,6 +201,14 @@ 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
|
||||
|
||||
.. _changelog_v2:
|
||||
|
||||
Corda 2.0
|
||||
|
@ -104,6 +104,6 @@ class TutorialMockNetwork {
|
||||
|
||||
expectedEx.expect(IllegalArgumentException::class.java)
|
||||
expectedEx.expectMessage("Expected to receive 1")
|
||||
initiatingReceiveFlow.resultFuture.getOrThrow()
|
||||
initiatingReceiveFlow.getOrThrow()
|
||||
}
|
||||
}
|
@ -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,7 +69,7 @@ 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>>> {
|
||||
|
@ -43,7 +43,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 +51,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,7 +65,7 @@ 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 {
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -22,6 +22,8 @@ The set of REST end-points for the network map service are as follows.
|
||||
+================+=========================================+==============================================================================================================================================+
|
||||
| POST | /network-map/publish | For the node to upload its signed ``NodeInfo`` object to the network map. |
|
||||
+----------------+-----------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
| POST | /network-map/ack-parameters | For the node operator to acknowledge network map that new parameters were accepted for future update. |
|
||||
+----------------+-----------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
| GET | /network-map | Retrieve the current signed network map object. The entire object is signed with the network map certificate which is also attached. |
|
||||
+----------------+-----------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
| GET | /network-map/node-info/{hash} | Retrieve a signed ``NodeInfo`` as specified in the network map object. |
|
||||
@ -69,6 +71,7 @@ The current set of network parameters:
|
||||
:maxMessageSize: Maximum allowed size in bytes of an individual message sent over the wire. Note that attachments are
|
||||
a special case and may be fragmented for streaming transfer, however, an individual transaction or flow message
|
||||
may not be larger than this value.
|
||||
:maxTransactionSize: Maximum allowed size in bytes of a transaction. This is the size of the transaction object and its attachments.
|
||||
:modifiedTime: The time when the network parameters were last modified by the compatibility zone operator.
|
||||
:epoch: Version number of the network parameters. Starting from 1, this will always increment whenever any of the
|
||||
parameters change.
|
||||
@ -77,3 +80,35 @@ More parameters will be added in future releases to regulate things like allowed
|
||||
offline before it is evicted from the zone, whether or not IPv6 connectivity is required for zone members, required
|
||||
cryptographic algorithms and rollout schedules (e.g. for moving to post quantum cryptography), parameters related to
|
||||
SGX and so on.
|
||||
|
||||
Network parameters update process
|
||||
---------------------------------
|
||||
|
||||
In case of the need to change network parameters Corda zone operator will start the update process. There are many reasons
|
||||
that may lead to this decision: we discovered that some new fields have to be added to enable smooth network interoperability or change
|
||||
of the existing compatibility constants is required due to upgrade or security reasons.
|
||||
|
||||
To synchronize all nodes in the compatibility zone to use the new set of the network parameters two RPC methods exist. The process
|
||||
requires human interaction and approval of the change.
|
||||
|
||||
When the update is about to happen the network map service starts to advertise the additional information with the usual network map
|
||||
data. It includes new network parameters hash, description of the change and the update deadline. Node queries network map server
|
||||
for the new set of parameters and emits ``ParametersUpdateInfo`` via ``CordaRPCOps::networkParametersFeed`` method to inform
|
||||
node operator about the event.
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. literalinclude:: ../../core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 1
|
||||
:end-before: DOCEND 1
|
||||
|
||||
Node administrator can review the change and decide if is going to accept it. The approval should be done before ``updateDeadline``.
|
||||
Nodes that don't approve before the deadline will be removed from the network map.
|
||||
If the network operator starts advertising a different set of new parameters then that new set overrides the previous set. Only the latest update can be accepted.
|
||||
To send back parameters approval to the zone operator RPC method ``fun acceptNewNetworkParameters(parametersHash: SecureHash)``
|
||||
has to be called with ``parametersHash`` from update. Notice that the process cannot be undone.
|
||||
|
||||
Next time the node polls network map after the deadline the advertised network parameters will be the updated ones. Previous set
|
||||
of parameters will no longer be valid. At this point the node will automatically shutdown and will require the node operator
|
||||
to bring it back again.
|
||||
|
@ -19,7 +19,7 @@ Start the nodes with ``runnodes`` by running the following command from the root
|
||||
* Linux/macOS: ``build/nodes/runnodes``
|
||||
* Windows: ``call build\nodes\runnodes.bat``
|
||||
|
||||
.. warn:: On macOS, do not click/change focus until all the node terminal windows have opened, or some processes may
|
||||
.. warning:: On macOS, do not click/change focus until all the node terminal windows have opened, or some processes may
|
||||
fail to start.
|
||||
|
||||
If you receive an ``OutOfMemoryError`` exception when interacting with the nodes, you need to increase the amount of
|
||||
|
@ -80,10 +80,14 @@ searching the vault via the ``VaultService`` interface on the
|
||||
To give a few more specific details consider two simplified real world
|
||||
scenarios. First, a basic foreign exchange cash transaction. This
|
||||
transaction needs to locate a set of funds to exchange. A flow
|
||||
modelling this is implemented in ``FxTransactionBuildTutorial.kt``.
|
||||
modelling this is implemented in ``FxTransactionBuildTutorial.kt``
|
||||
(see ``docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt`` in the
|
||||
`main Corda repo <https://github.com/corda/corda>`_).
|
||||
Second, a simple business model in which parties manually accept or
|
||||
reject each other's trade proposals, which is implemented in
|
||||
``WorkflowTransactionBuildTutorial.kt``. To run and explore these
|
||||
``WorkflowTransactionBuildTutorial.kt`` (see
|
||||
``docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt`` in the
|
||||
`main Corda repo <https://github.com/corda/corda>`_). To run and explore these
|
||||
examples using the IntelliJ IDE one can run/step through the respective unit
|
||||
tests in ``FxTransactionBuildTutorialTest.kt`` and
|
||||
``WorkflowTransactionBuildTutorialTest.kt``, which drive the flows as
|
||||
|
@ -9,13 +9,12 @@ The example CorDapp
|
||||
|
||||
.. contents::
|
||||
|
||||
The example CorDapp allows nodes to agree IOUs with each other. Nodes will always agree to the creation of a new IOU
|
||||
if:
|
||||
The example CorDapp allows nodes to agree IOUs with each other, as long as they obey the following contract rules:
|
||||
|
||||
* Its value is strictly positive
|
||||
* The node is not trying to issue the IOU to itself
|
||||
* The IOU's value is strictly positive
|
||||
* A node is not trying to issue an IOU to itself
|
||||
|
||||
We will deploy the CorDapp on 4 test nodes:
|
||||
We will deploy and run the CorDapp on four test nodes:
|
||||
|
||||
* **NetworkMapAndNotary**, which hosts a validating notary service
|
||||
* **PartyA**
|
||||
@ -27,7 +26,7 @@ facts" between PartyA and PartyB only. PartyC won't be aware of these IOUs.
|
||||
|
||||
Downloading the example CorDapp
|
||||
-------------------------------
|
||||
We need to download the example CorDapp from GitHub.
|
||||
Start by downloading the example CorDapp from GitHub:
|
||||
|
||||
* Set up your machine by following the :doc:`quickstart guide <getting-set-up>`
|
||||
|
||||
@ -36,14 +35,11 @@ We need to download the example CorDapp from GitHub.
|
||||
|
||||
* Change directories to the freshly cloned repo: ``cd cordapp-example``
|
||||
|
||||
.. note:: If you wish to build off the latest, unstable version of the codebase, follow the instructions in
|
||||
:doc:`building against Master <building-against-master>` instead.
|
||||
|
||||
Opening the example CorDapp in IntelliJ
|
||||
---------------------------------------
|
||||
Let's open the example CorDapp in IntelliJ IDEA.
|
||||
Let's open the example CorDapp in IntelliJ IDEA:
|
||||
|
||||
**If opening a fresh IntelliJ instance**
|
||||
**If opening a fresh IntelliJ instance**:
|
||||
|
||||
* Open IntelliJ
|
||||
* A dialogue box will appear:
|
||||
@ -60,12 +56,12 @@ Let's open the example CorDapp in IntelliJ IDEA.
|
||||
|
||||
* Click the 'import gradle project' link. Press OK on the dialogue that pops up
|
||||
|
||||
* Gradle will now download all the project dependencies and perform some indexing. This usually takes a minute or so.
|
||||
* Gradle will now download all the project dependencies and perform some indexing. This usually takes a minute or so
|
||||
|
||||
* If the 'import gradle project' pop-up does not appear, click the small green speech bubble at the bottom-right of
|
||||
the IDE, or simply close and re-open IntelliJ again to make it reappear.
|
||||
the IDE, or simply close and re-open IntelliJ again to make it reappear
|
||||
|
||||
**If you already have IntelliJ open**
|
||||
**If you already have IntelliJ open**:
|
||||
|
||||
* Open the ``File`` menu
|
||||
|
||||
@ -76,8 +72,8 @@ Let's open the example CorDapp in IntelliJ IDEA.
|
||||
* Click OK
|
||||
|
||||
Project structure
|
||||
-----------------
|
||||
The example CorDapp has the following directory structure:
|
||||
~~~~~~~~~~~~~~~~~
|
||||
The example CorDapp has the following structure:
|
||||
|
||||
.. sourcecode:: none
|
||||
|
||||
@ -175,11 +171,11 @@ There are two ways to run the example CorDapp:
|
||||
* Via the terminal
|
||||
* Via IntelliJ
|
||||
|
||||
In both cases, we will deploy a set of test nodes with our CorDapp installed, then run the nodes. You can read more
|
||||
about how we define the nodes to be deployed :doc:`here <generating-a-node>`.
|
||||
Both approaches will create a set of test nodes, install the CorDapp on these nodes, and then run the nodes. You can
|
||||
read more about how we generate nodes :doc:`here <generating-a-node>`.
|
||||
|
||||
Terminal
|
||||
~~~~~~~~
|
||||
Running the example CorDapp from the terminal
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Building the example CorDapp
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
@ -191,29 +187,26 @@ Building the example CorDapp
|
||||
|
||||
* Windows: ``gradlew.bat deployNodes``
|
||||
|
||||
This will automatically build four pre-configured nodes with our CorDapp installed. These nodes are meant for local
|
||||
testing only
|
||||
This will automatically build four nodes with our CorDapp already installed
|
||||
|
||||
.. note:: CorDapps can be written in any language targeting the JVM. In our case, we've provided the example source in
|
||||
both Kotlin (``/kotlin-source/src``) and Java (``/java-source/src``) Since both sets of source files are
|
||||
functionally identical, we will refer to the Kotlin build throughout the documentation.
|
||||
both Kotlin (``/kotlin-source/src``) and Java (``/java-source/src``). Since both sets of source files are
|
||||
functionally identical, we will refer to the Kotlin version throughout the documentation.
|
||||
|
||||
* After the build process has finished, you will see the newly-build nodes in the ``kotlin-source/build/nodes`` folder
|
||||
* After the build finishes, you will see the generated nodes in the ``kotlin-source/build/nodes`` folder
|
||||
|
||||
* There will be one folder generated for each node you built, plus a ``runnodes`` shell script (or batch file on
|
||||
Windows) to run all the nodes simultaneously
|
||||
* There will be a folder for each generated node, plus a ``runnodes`` shell script (or batch file on Windows) to run
|
||||
all the nodes simultaneously
|
||||
|
||||
* Each node in the ``nodes`` folder has the following structure:
|
||||
|
||||
.. sourcecode:: none
|
||||
|
||||
. nodeName
|
||||
├── corda.jar
|
||||
├── node.conf
|
||||
└── cordapps
|
||||
|
||||
``corda.jar`` is the Corda runtime, ``cordapps`` contains our node's CorDapps, and the node's configuration is
|
||||
given by ``node.conf``
|
||||
├── corda.jar // The Corda node runtime.
|
||||
├── corda-webserver.jar // The node development webserver.
|
||||
├── node.conf // The node configuration file.
|
||||
└── cordapps // The node's CorDapps.
|
||||
|
||||
Running the example CorDapp
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
@ -222,7 +215,7 @@ Start the nodes by running the following command from the root of the ``cordapp-
|
||||
* Unix/Mac OSX: ``kotlin-source/build/nodes/runnodes``
|
||||
* Windows: ``call kotlin-source\build\nodes\runnodes.bat``
|
||||
|
||||
.. warn:: On Unix/Mac OSX, do not click/change focus until all seven additional terminal windows have opened, or some
|
||||
.. warning:: On Unix/Mac OSX, do not click/change focus until all seven additional terminal windows have opened, or some
|
||||
nodes may fail to start.
|
||||
|
||||
For each node, the ``runnodes`` script creates a node tab/window:
|
||||
@ -239,7 +232,7 @@ For each node, the ``runnodes`` script creates a node tab/window:
|
||||
|
||||
📚 New! Training now available worldwide, see https://corda.net/corda-training/
|
||||
|
||||
Logs can be found in : /Users/joeldudley/Desktop/cordapp-example/kotlin-source/build/nodes/PartyA/logs
|
||||
Logs can be found in : /Users/username/Desktop/cordapp-example/kotlin-source/build/nodes/PartyA/logs
|
||||
Database connection url is : jdbc:h2:tcp://10.163.199.132:54763/node
|
||||
Listening on address : 127.0.0.1:10005
|
||||
RPC service listening on address : localhost:10006
|
||||
@ -256,16 +249,16 @@ For every node except the network map/notary, the script also creates a webserve
|
||||
|
||||
.. sourcecode:: none
|
||||
|
||||
Logs can be found in /Users/joeldudley/Desktop/cordapp-example/kotlin-source/build/nodes/PartyA/logs/web
|
||||
Logs can be found in /Users/username/Desktop/cordapp-example/kotlin-source/build/nodes/PartyA/logs/web
|
||||
Starting as webserver: localhost:10007
|
||||
Webserver started up in 42.02 sec
|
||||
|
||||
It usually takes around 60 seconds for the nodes to finish starting up. To ensure that all the nodes are running OK,
|
||||
you can query the 'status' end-point located at ``http://localhost:[port]/api/status`` (e.g.
|
||||
It usually takes around 60 seconds for the nodes to finish starting up. To ensure that all the nodes are running, you
|
||||
can query the 'status' end-point located at ``http://localhost:[port]/api/status`` (e.g.
|
||||
``http://localhost:10007/api/status`` for ``PartyA``).
|
||||
|
||||
IntelliJ
|
||||
~~~~~~~~
|
||||
Running the example CorDapp from IntelliJ
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
* Select the ``Run Example CorDapp - Kotlin`` run configuration from the drop-down menu at the top right-hand side of
|
||||
the IDE
|
||||
|
||||
@ -274,66 +267,38 @@ IntelliJ
|
||||
.. image:: resources/run-config-drop-down.png
|
||||
:width: 400
|
||||
|
||||
The node driver defined in ``/src/test/kotlin/com/example/Main.kt`` allows you to specify how many nodes you would like
|
||||
to run and the configuration settings for each node. For the example CorDapp, the driver starts up four nodes
|
||||
and adds an RPC user for all but the network map/notary node:
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
// No permissions required as we are not invoking flows.
|
||||
val user = User("user1", "test", permissions = setOf())
|
||||
driver(isDebug = true, waitForNodesToFinish = true) {
|
||||
startNode(getX500Name(O="NetworkMapAndNotary",L="London",C='GB"), setOf(ServiceInfo(ValidatingNotaryService.type)))
|
||||
val (nodeA, nodeB, nodeC) = Futures.allAsList(
|
||||
startNode(getX500Name(O="PartyA",L="London",C="GB"), rpcUsers = listOf(user)),
|
||||
startNode(getX500Name(O="PartyB",L="New York",C="US"), rpcUsers = listOf(user)),
|
||||
startNode(getX500Name(O="PartyC",L="Paris",C="FR"), rpcUsers = listOf(user))).getOrThrow()
|
||||
|
||||
startWebserver(nodeA)
|
||||
startWebserver(nodeB)
|
||||
startWebserver(nodeC)
|
||||
}
|
||||
}
|
||||
|
||||
* To stop the nodes, press the red square button at the top right-hand side of the IDE, next to the run configurations
|
||||
|
||||
Later, we'll look at how the node driver can be useful for `debugging your CorDapp`_.
|
||||
|
||||
Interacting with the example CorDapp
|
||||
------------------------------------
|
||||
|
||||
Via HTTP
|
||||
~~~~~~~~
|
||||
The CorDapp defines several HTTP API end-points and a web front-end. The end-points allow you to list the IOUs a node
|
||||
is involved in, agree new IOUs, and see who is on the network.
|
||||
The nodes' webservers run locally on the following ports:
|
||||
|
||||
The nodes are running locally on the following ports:
|
||||
* PartyA: ``localhost:10007``
|
||||
* PartyB: ``localhost:10010``
|
||||
* PartyC: ``localhost:10013``
|
||||
|
||||
* PartyA: ``localhost:10007``
|
||||
* PartyB: ``localhost:10010``
|
||||
* PartyC: ``localhost:10013``
|
||||
These ports are defined in each node's node.conf file under ``kotlin-source/build/nodes/NodeX/node.conf``.
|
||||
|
||||
These ports are defined in build.gradle and in each node's node.conf file under ``kotlin-source/build/nodes/NodeX``.
|
||||
|
||||
As the nodes start up, they should tell you which port their embedded web server is running on. The available API
|
||||
endpoints are:
|
||||
Each node webserver exposes the following endpoints:
|
||||
|
||||
* ``/api/example/me``
|
||||
* ``/api/example/peers``
|
||||
* ``/api/example/ious``
|
||||
* ``/api/example/create-iou`` with parameters ``iouValue`` and ``partyName`` which is CN name of a node
|
||||
|
||||
The web front-end is served from ``/web/example``.
|
||||
|
||||
An IOU can be created by sending a PUT request to the ``api/example/create-iou`` end-point directly, or by using the
|
||||
the web form hosted at ``/web/example``.
|
||||
There is also a web front-end served from ``/web/example``.
|
||||
|
||||
.. warning:: The content in ``web/example`` is only available for demonstration purposes and does not implement
|
||||
anti-XSS, anti-XSRF or any other security techniques. Do not use this code in production.
|
||||
anti-XSS, anti-XSRF or other security techniques. Do not use this code in production.
|
||||
|
||||
Creating an IOU via the endpoint
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
An IOU can be created by sending a PUT request to the ``api/example/create-iou`` endpoint directly, or by using the
|
||||
the web form served from ``/web/example``.
|
||||
|
||||
To create an IOU between PartyA and PartyB, run the following command from the command line:
|
||||
|
||||
.. sourcecode:: bash
|
||||
@ -356,8 +321,8 @@ of the page, and enter the IOU details into the web-form. The IOU must have a po
|
||||
|
||||
And click submit. Upon clicking submit, the modal dialogue will close, and the nodes will agree the IOU.
|
||||
|
||||
Once an IOU has been submitted
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Checking the output
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
Assuming all went well, you should see some activity in PartyA's web-server terminal window:
|
||||
|
||||
.. sourcecode:: none
|
||||
@ -387,7 +352,7 @@ You can view the newly-created IOU by accessing the vault of PartyA or PartyB:
|
||||
* PartyA: Navigate to http://localhost:10007/web/example and hit the "refresh" button
|
||||
* PartyA: Navigate to http://localhost:10010/web/example and hit the "refresh" button
|
||||
|
||||
The vault and web front-end of PartyC (on ``localhost:10013``) will not display any IOUs. This is because PartyC was
|
||||
The vault and web front-end of PartyC (at ``localhost:10013``) will not display any IOUs. This is because PartyC was
|
||||
not involved in this transaction.
|
||||
|
||||
Via the interactive shell (terminal only)
|
||||
@ -414,6 +379,8 @@ following list:
|
||||
net.corda.finance.flows.CashIssueFlow
|
||||
net.corda.finance.flows.CashPaymentFlow
|
||||
|
||||
Creating an IOU via the interactive shell
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
We can create a new IOU using the ``ExampleFlow$Initiator`` flow. For example, from the interactive shell of PartyA,
|
||||
you can agree an IOU of 50 with PartyB by running
|
||||
``flow start ExampleFlow$Initiator iouValue: 50, otherParty: "O=PartyB,L=New York,C=US"``.
|
||||
@ -435,9 +402,15 @@ This will print out the following progress steps:
|
||||
✅ Broadcasting transaction to participants
|
||||
✅ Done
|
||||
|
||||
Checking the output
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
We can also issue RPC operations to the node via the interactive shell. Type ``run`` to see the full list of available
|
||||
operations.
|
||||
|
||||
You can see the newly-created IOU by running ``run vaultQuery contractStateType: com.example.state.IOUState``.
|
||||
|
||||
As before, the interactive shell of PartyC will not display any IOUs.
|
||||
|
||||
Via the h2 web console
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
You can connect directly to your node's database to see its stored states, transactions and attachments. To do so,
|
||||
@ -445,17 +418,17 @@ please follow the instructions in :doc:`node-database`.
|
||||
|
||||
Using the example RPC client
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
The ``/src/main/kotlin-source/com/example/client/ExampleClientRPC.kt`` file is a simple utility that uses the client
|
||||
RPC library to connect to a node. It will log any existing IOUs and listen for any future IOUs. If you haven't created
|
||||
``/src/main/kotlin-source/com/example/client/ExampleClientRPC.kt`` defines a simple RPC client that connects to a node,
|
||||
logs any existing IOUs and listens for any future IOUs. If you haven't created
|
||||
any IOUs when you first connect to one of the nodes, the client will simply log any future IOUs that are agreed.
|
||||
|
||||
*Running the client via IntelliJ:*
|
||||
|
||||
Select the 'Run Example RPC Client' run configuration which, by default, connects to PartyA. Click the green arrow to
|
||||
run the client. You can edit the run configuration to connect on a different port.
|
||||
|
||||
*Running the client via the command line:*
|
||||
Running the client via IntelliJ
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Run the 'Run Example RPC Client' run configuration. By default, this run configuration is configured to connect to
|
||||
PartyA. You can edit the run configuration to connect on a different port.
|
||||
|
||||
Running the client via the command line
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Run the following gradle task:
|
||||
|
||||
``./gradlew runExampleClientRPCKotlin``
|
||||
@ -469,7 +442,7 @@ For more information on the client RPC interface and how to build an RPC client
|
||||
* :doc:`Client RPC documentation <clientrpc>`
|
||||
* :doc:`Client RPC tutorial <tutorial-clientrpc-api>`
|
||||
|
||||
Running Nodes Across Machines
|
||||
Running nodes across machines
|
||||
-----------------------------
|
||||
The nodes can be split across machines and configured to communicate across the network.
|
||||
|
||||
@ -490,35 +463,32 @@ and make the following changes:
|
||||
|
||||
After starting each node, the nodes will be able to see one another and agree IOUs among themselves.
|
||||
|
||||
Debugging your CorDapp
|
||||
----------------------
|
||||
Testing and debugging
|
||||
---------------------
|
||||
|
||||
Testing a CorDapp
|
||||
~~~~~~~~~~~~~~~~~
|
||||
Corda provides several frameworks for writing unit and integration tests for CorDapps.
|
||||
|
||||
Contract tests
|
||||
^^^^^^^^^^^^^^
|
||||
You can run the CorDapp's contract tests by running the ``Run Contract Tests - Kotlin`` run configuration.
|
||||
|
||||
Flow tests
|
||||
^^^^^^^^^^
|
||||
You can run the CorDapp's flow tests by running the ``Run Flow Tests - Kotlin`` run configuration.
|
||||
|
||||
Integration tests
|
||||
^^^^^^^^^^^^^^^^^
|
||||
You can run the CorDapp's integration tests by running the ``Run Integration Tests - Kotlin`` run configuration.
|
||||
|
||||
Debugging Corda nodes
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
Debugging is done via IntelliJ as follows:
|
||||
|
||||
1. Edit the node driver code in ``Main.kt`` based on the number of nodes you wish to start, along with any other
|
||||
configuration options. For example, the code below starts 4 nodes, with one being the network map service and
|
||||
notary. It also sets up RPC credentials for the three non-notary nodes
|
||||
1. Start the nodes using the “Run Example CorDapp” run configuration in IntelliJ
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
// No permissions required as we are not invoking flows.
|
||||
val user = User("user1", "test", permissions = setOf())
|
||||
driver(isDebug = true, waitForNodesToFinish = true) {
|
||||
startNode(getX500Name(O="NetworkMapAndNotary",L="London",C="GB"), setOf(ServiceInfo(ValidatingNotaryService.type)))
|
||||
val (nodeA, nodeB, nodeC) = Futures.allAsList(
|
||||
startNode(getX500Name(O="PartyA",L=London,C=GB"), rpcUsers = listOf(user)),
|
||||
startNode(getX500Name(O="PartyB",L=New York,C=US"), rpcUsers = listOf(user)),
|
||||
startNode(getX500Name(O="PartyC",L=Paris,C=FR"), rpcUsers = listOf(user))).getOrThrow()
|
||||
|
||||
startWebserver(nodeA)
|
||||
startWebserver(nodeB)
|
||||
startWebserver(nodeC)
|
||||
}
|
||||
}
|
||||
|
||||
2. Select and run the “Run Example CorDapp” run configuration in IntelliJ
|
||||
|
||||
3. IntelliJ will build and run the CorDapp. The remote debug ports for each node will be automatically generated and
|
||||
2. IntelliJ will build and run the CorDapp. The remote debug ports for each node will be automatically generated and
|
||||
printed to the terminal. For example:
|
||||
|
||||
.. sourcecode:: none
|
||||
@ -526,9 +496,11 @@ Debugging is done via IntelliJ as follows:
|
||||
[INFO ] 15:27:59.533 [main] Node.logStartupInfo - Working Directory: /Users/joeldudley/cordapp-example/build/20170707142746/PartyA
|
||||
[INFO ] 15:27:59.533 [main] Node.logStartupInfo - Debug port: dt_socket:5007
|
||||
|
||||
4. Edit the “Debug CorDapp” run configuration with the port of the node you wish to connect to
|
||||
3. Edit the “Debug CorDapp” run configuration with the port of the node you wish to connect to
|
||||
|
||||
5. Run the “Debug CorDapp” run configuration
|
||||
4. Run the “Debug CorDapp” run configuration
|
||||
|
||||
6. Set your breakpoints and start interacting with the node you wish to connect to. When the node hits a breakpoint,
|
||||
execution will pause
|
||||
5. Set your breakpoints and interact with the node you've connected to. When the node hits a breakpoint, execution will
|
||||
pause
|
||||
|
||||
* The node webserver runs in a separate process, and is not attached to by the debugger
|
@ -31,10 +31,10 @@ class CashSelectionH2ImplTest {
|
||||
// spend operation below.
|
||||
// Issuing Integer.MAX_VALUE will not cause an exception since PersistentCashState.pennies is a long
|
||||
nCopies(2, Integer.MAX_VALUE).map { issueAmount ->
|
||||
node.services.startFlow(CashIssueFlow(issueAmount.POUNDS, OpaqueBytes.of(1), mockNet.defaultNotaryIdentity)).resultFuture
|
||||
node.services.startFlow(CashIssueFlow(issueAmount.POUNDS, OpaqueBytes.of(1), mockNet.defaultNotaryIdentity))
|
||||
}.transpose().getOrThrow()
|
||||
// The spend must be more than the size of a single cash state to force the accumulator onto the second state.
|
||||
node.services.startFlow(CashPaymentFlow((Integer.MAX_VALUE + 1L).POUNDS, node.info.legalIdentities[0])).resultFuture.getOrThrow()
|
||||
node.services.startFlow(CashPaymentFlow((Integer.MAX_VALUE + 1L).POUNDS, node.info.legalIdentities[0])).getOrThrow()
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -50,8 +50,8 @@ class CashSelectionH2ImplTest {
|
||||
val flow2 = bankA.services.startFlow(CashPaymentFlow(amount = 100.DOLLARS, anonymous = false, recipient = notary))
|
||||
val flow3 = bankA.services.startFlow(CashPaymentFlow(amount = 100.DOLLARS, anonymous = false, recipient = notary))
|
||||
|
||||
assertThatThrownBy { flow1.resultFuture.getOrThrow() }.isInstanceOf(CashException::class.java)
|
||||
assertThatThrownBy { flow2.resultFuture.getOrThrow() }.isInstanceOf(CashException::class.java)
|
||||
assertThatThrownBy { flow3.resultFuture.getOrThrow() }.isInstanceOf(CashException::class.java)
|
||||
assertThatThrownBy { flow1.getOrThrow() }.isInstanceOf(CashException::class.java)
|
||||
assertThatThrownBy { flow2.getOrThrow() }.isInstanceOf(CashException::class.java)
|
||||
assertThatThrownBy { flow3.getOrThrow() }.isInstanceOf(CashException::class.java)
|
||||
}
|
||||
}
|
@ -33,7 +33,7 @@ class CashExitFlowTests {
|
||||
bankOfCordaNode = mockNet.createPartyNode(BOC_NAME)
|
||||
bankOfCorda = bankOfCordaNode.info.identityFromX500Name(BOC_NAME)
|
||||
notary = mockNet.defaultNotaryIdentity
|
||||
val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref, notary)).resultFuture
|
||||
val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref, notary))
|
||||
mockNet.runNetwork()
|
||||
future.getOrThrow()
|
||||
}
|
||||
@ -46,7 +46,7 @@ class CashExitFlowTests {
|
||||
@Test
|
||||
fun `exit some cash`() {
|
||||
val exitAmount = 500.DOLLARS
|
||||
val future = bankOfCordaNode.services.startFlow(CashExitFlow(exitAmount, ref)).resultFuture
|
||||
val future = bankOfCordaNode.services.startFlow(CashExitFlow(exitAmount, ref))
|
||||
mockNet.runNetwork()
|
||||
val exitTx = future.getOrThrow().stx.tx
|
||||
val expected = (initialBalance - exitAmount).`issued by`(bankOfCorda.ref(ref))
|
||||
@ -59,7 +59,7 @@ class CashExitFlowTests {
|
||||
@Test
|
||||
fun `exit zero cash`() {
|
||||
val expected = 0.DOLLARS
|
||||
val future = bankOfCordaNode.services.startFlow(CashExitFlow(expected, ref)).resultFuture
|
||||
val future = bankOfCordaNode.services.startFlow(CashExitFlow(expected, ref))
|
||||
mockNet.runNetwork()
|
||||
assertFailsWith<CashException> {
|
||||
future.getOrThrow()
|
||||
|
@ -43,7 +43,7 @@ class CashIssueFlowTests {
|
||||
fun `issue some cash`() {
|
||||
val expected = 500.DOLLARS
|
||||
val ref = OpaqueBytes.of(0x01)
|
||||
val future = bankOfCordaNode.services.startFlow(CashIssueFlow(expected, ref, notary)).resultFuture
|
||||
val future = bankOfCordaNode.services.startFlow(CashIssueFlow(expected, ref, notary))
|
||||
mockNet.runNetwork()
|
||||
val issueTx = future.getOrThrow().stx
|
||||
val output = issueTx.tx.outputsOfType<Cash.State>().single()
|
||||
@ -54,7 +54,7 @@ class CashIssueFlowTests {
|
||||
fun `issue zero cash`() {
|
||||
val expected = 0.DOLLARS
|
||||
val ref = OpaqueBytes.of(0x01)
|
||||
val future = bankOfCordaNode.services.startFlow(CashIssueFlow(expected, ref, notary)).resultFuture
|
||||
val future = bankOfCordaNode.services.startFlow(CashIssueFlow(expected, ref, notary))
|
||||
mockNet.runNetwork()
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
future.getOrThrow()
|
||||
|
@ -35,7 +35,7 @@ class CashPaymentFlowTests {
|
||||
bankOfCordaNode = mockNet.createPartyNode(BOC_NAME)
|
||||
bankOfCorda = bankOfCordaNode.info.identityFromX500Name(BOC_NAME)
|
||||
aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
||||
val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref, mockNet.defaultNotaryIdentity)).resultFuture
|
||||
val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref, mockNet.defaultNotaryIdentity))
|
||||
future.getOrThrow()
|
||||
}
|
||||
|
||||
@ -56,8 +56,7 @@ class CashPaymentFlowTests {
|
||||
val (_, vaultUpdatesBoc) = bankOfCordaNode.services.vaultService.trackBy<Cash.State>(criteria)
|
||||
val (_, vaultUpdatesBankClient) = aliceNode.services.vaultService.trackBy<Cash.State>(criteria)
|
||||
|
||||
val future = bankOfCordaNode.services.startFlow(CashPaymentFlow(expectedPayment,
|
||||
payTo)).resultFuture
|
||||
val future = bankOfCordaNode.services.startFlow(CashPaymentFlow(expectedPayment, payTo))
|
||||
mockNet.runNetwork()
|
||||
future.getOrThrow()
|
||||
|
||||
@ -89,7 +88,7 @@ class CashPaymentFlowTests {
|
||||
val payTo = aliceNode.info.chooseIdentity()
|
||||
val expected = 4000.DOLLARS
|
||||
val future = bankOfCordaNode.services.startFlow(CashPaymentFlow(expected,
|
||||
payTo)).resultFuture
|
||||
payTo))
|
||||
mockNet.runNetwork()
|
||||
assertFailsWith<CashException> {
|
||||
future.getOrThrow()
|
||||
@ -101,7 +100,7 @@ class CashPaymentFlowTests {
|
||||
val payTo = aliceNode.info.chooseIdentity()
|
||||
val expected = 0.DOLLARS
|
||||
val future = bankOfCordaNode.services.startFlow(CashPaymentFlow(expected,
|
||||
payTo)).resultFuture
|
||||
payTo))
|
||||
mockNet.runNetwork()
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
future.getOrThrow()
|
||||
|
@ -14,6 +14,7 @@ buildscript {
|
||||
jsr305_version = constants.getProperty("jsr305Version")
|
||||
kotlin_version = constants.getProperty("kotlinVersion")
|
||||
artifactory_plugin_version = constants.getProperty('artifactoryPluginVersion')
|
||||
snake_yaml_version = constants.getProperty('snakeYamlVersion')
|
||||
}
|
||||
|
||||
repositories {
|
||||
|
@ -20,7 +20,8 @@ public class CordformNode implements NodeDefinition {
|
||||
protected static final String DEFAULT_HOST = "localhost";
|
||||
|
||||
/**
|
||||
* Name of the node.
|
||||
* Name of the node. Node will be placed in directory based on this name - all lowercase with whitespaces removed.
|
||||
* Actual node name inside node.conf will be as set here.
|
||||
*/
|
||||
private String name;
|
||||
|
||||
@ -28,6 +29,20 @@ public class CordformNode implements NodeDefinition {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* p2p Port.
|
||||
*/
|
||||
private int p2pPort = 10002;
|
||||
|
||||
public int getP2pPort() { return p2pPort; }
|
||||
|
||||
/**
|
||||
* RPC Port.
|
||||
*/
|
||||
private int rpcPort = 10003;
|
||||
|
||||
public int getRpcPort() { return rpcPort; }
|
||||
|
||||
/**
|
||||
* Set the RPC users for this node. This configuration block allows arbitrary configuration.
|
||||
* The recommended current structure is:
|
||||
@ -79,6 +94,7 @@ public class CordformNode implements NodeDefinition {
|
||||
*/
|
||||
public void p2pPort(int p2pPort) {
|
||||
p2pAddress(DEFAULT_HOST + ':' + p2pPort);
|
||||
this.p2pPort = p2pPort;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -110,6 +126,7 @@ public class CordformNode implements NodeDefinition {
|
||||
@Deprecated
|
||||
public void rpcPort(int rpcPort) {
|
||||
rpcAddress(DEFAULT_HOST + ':' + rpcPort);
|
||||
this.rpcPort = rpcPort;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -8,6 +8,17 @@ public final class RpcSettings {
|
||||
|
||||
private Config config = ConfigFactory.empty();
|
||||
|
||||
private int port = 10003;
|
||||
private int adminPort = 10005;
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public int getAdminPort() {
|
||||
return adminPort;
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC address for the node.
|
||||
*/
|
||||
@ -15,6 +26,14 @@ public final class RpcSettings {
|
||||
setValue("address", value);
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC Port for the node
|
||||
*/
|
||||
public final void port(final int value) {
|
||||
this.port = value;
|
||||
setValue("address", "localhost:"+port);
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC admin address for the node (necessary if [useSsl] is false or unset).
|
||||
*/
|
||||
@ -22,6 +41,11 @@ public final class RpcSettings {
|
||||
setValue("adminAddress", value);
|
||||
}
|
||||
|
||||
public final void adminPort(final int value) {
|
||||
this.adminPort = value;
|
||||
setValue("adminAddress", "localhost:"+adminPort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies whether the node RPC layer will require SSL from clients.
|
||||
*/
|
||||
@ -43,7 +67,7 @@ public final class RpcSettings {
|
||||
config = options.addTo("ssl", config);
|
||||
}
|
||||
|
||||
final Config addTo(final String key, final Config config) {
|
||||
public final Config addTo(final String key, final Config config) {
|
||||
if (this.config.isEmpty()) {
|
||||
return config;
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ public final class SslOptions {
|
||||
setValue("trustStoreFile", value);
|
||||
}
|
||||
|
||||
final Config addTo(final String key, final Config config) {
|
||||
public final Config addTo(final String key, final Config config) {
|
||||
if (this.config.isEmpty()) {
|
||||
return config;
|
||||
}
|
||||
|
@ -40,6 +40,8 @@ dependencies {
|
||||
noderunner "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
|
||||
|
||||
compile project(':cordform-common')
|
||||
// Docker-compose file generation
|
||||
compile "org.yaml:snakeyaml:$snake_yaml_version"
|
||||
}
|
||||
|
||||
task createNodeRunner(type: Jar, dependsOn: [classes]) {
|
||||
|
@ -0,0 +1,174 @@
|
||||
package net.corda.plugins
|
||||
|
||||
import groovy.lang.Closure
|
||||
import net.corda.cordform.CordformDefinition
|
||||
import org.apache.tools.ant.filters.FixCrLfFilter
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.plugins.JavaPluginConvention
|
||||
import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
import java.io.File
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.net.URLClassLoader
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.util.jar.JarInputStream
|
||||
|
||||
/**
|
||||
* Creates nodes based on the configuration of this task in the gradle configuration DSL.
|
||||
*
|
||||
* See documentation for examples.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
open class Baseform : DefaultTask() {
|
||||
private companion object {
|
||||
val nodeJarName = "corda.jar"
|
||||
private val defaultDirectory: Path = Paths.get("build", "nodes")
|
||||
}
|
||||
|
||||
/**
|
||||
* Optionally the name of a CordformDefinition subclass to which all configuration will be delegated.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanPrivate")
|
||||
var definitionClass: String? = null
|
||||
var directory = defaultDirectory
|
||||
protected val nodes = mutableListOf<Node>()
|
||||
|
||||
/**
|
||||
* Set the directory to install nodes into.
|
||||
*
|
||||
* @param directory The directory the nodes will be installed into.
|
||||
*/
|
||||
fun directory(directory: String) {
|
||||
this.directory = Paths.get(directory)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a node configuration.
|
||||
*
|
||||
* @param configureClosure A node configuration that will be deployed.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanPrivate")
|
||||
fun node(configureClosure: Closure<in Node>) {
|
||||
nodes += project.configure(Node(project), configureClosure) as Node
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a node configuration
|
||||
*
|
||||
* @param configureFunc A node configuration that will be deployed
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanPrivate")
|
||||
fun node(configureFunc: Node.() -> Any?): Node {
|
||||
val node = Node(project).apply { configureFunc() }
|
||||
nodes += node
|
||||
return node
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a node by name.
|
||||
*
|
||||
* @param name The name of the node as specified in the node configuration DSL.
|
||||
* @return A node instance.
|
||||
*/
|
||||
private fun getNodeByName(name: String): Node? = nodes.firstOrNull { it.name == name }
|
||||
|
||||
/**
|
||||
* The definitionClass needn't be compiled until just before our build method, so we load it manually via sourceSets.main.runtimeClasspath.
|
||||
*/
|
||||
private fun loadCordformDefinition(): CordformDefinition {
|
||||
val plugin = project.convention.getPlugin(JavaPluginConvention::class.java)
|
||||
val classpath = plugin.sourceSets.getByName(MAIN_SOURCE_SET_NAME).runtimeClasspath
|
||||
val urls = classpath.files.map { it.toURI().toURL() }.toTypedArray()
|
||||
return URLClassLoader(urls, CordformDefinition::class.java.classLoader)
|
||||
.loadClass(definitionClass)
|
||||
.asSubclass(CordformDefinition::class.java)
|
||||
.newInstance()
|
||||
}
|
||||
|
||||
/**
|
||||
* The NetworkBootstrapper needn't be compiled until just before our build method, so we load it manually via sourceSets.main.runtimeClasspath.
|
||||
*/
|
||||
private fun loadNetworkBootstrapperClass(): Class<*> {
|
||||
val plugin = project.convention.getPlugin(JavaPluginConvention::class.java)
|
||||
val classpath = plugin.sourceSets.getByName(MAIN_SOURCE_SET_NAME).runtimeClasspath
|
||||
val urls = classpath.files.map { it.toURI().toURL() }.toTypedArray()
|
||||
return URLClassLoader(urls, javaClass.classLoader).loadClass("net.corda.nodeapi.internal.network.NetworkBootstrapper")
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs the corda fat JAR to the root directory, for the network bootstrapper to use.
|
||||
*/
|
||||
protected fun installCordaJar() {
|
||||
val cordaJar = Cordformation.verifyAndGetRuntimeJar(project, "corda")
|
||||
project.copy {
|
||||
it.apply {
|
||||
from(cordaJar)
|
||||
into(directory)
|
||||
rename(cordaJar.name, nodeJarName)
|
||||
fileMode = Cordformation.executableFileMode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun initializeConfiguration() {
|
||||
if (definitionClass != null) {
|
||||
val cd = loadCordformDefinition()
|
||||
// If the user has specified their own directory (even if it's the same default path) then let them know
|
||||
// it's not used and should just rely on the one in CordformDefinition
|
||||
require(directory === defaultDirectory) {
|
||||
"'directory' cannot be used when 'definitionClass' is specified. Use CordformDefinition.nodesDirectory instead."
|
||||
}
|
||||
directory = cd.nodesDirectory
|
||||
val cordapps = cd.getMatchingCordapps()
|
||||
cd.nodeConfigurers.forEach {
|
||||
val node = node { }
|
||||
it.accept(node)
|
||||
node.additionalCordapps.addAll(cordapps)
|
||||
node.rootDir(directory)
|
||||
}
|
||||
cd.setup { nodeName -> project.projectDir.toPath().resolve(getNodeByName(nodeName)!!.nodeDir.toPath()) }
|
||||
} else {
|
||||
nodes.forEach {
|
||||
it.rootDir(directory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun bootstrapNetwork() {
|
||||
val networkBootstrapperClass = loadNetworkBootstrapperClass()
|
||||
val networkBootstrapper = networkBootstrapperClass.newInstance()
|
||||
val bootstrapMethod = networkBootstrapperClass.getMethod("bootstrap", Path::class.java).apply { isAccessible = true }
|
||||
// Call NetworkBootstrapper.bootstrap
|
||||
try {
|
||||
val rootDir = project.projectDir.toPath().resolve(directory).toAbsolutePath().normalize()
|
||||
bootstrapMethod.invoke(networkBootstrapper, rootDir)
|
||||
} catch (e: InvocationTargetException) {
|
||||
throw e.cause!!
|
||||
}
|
||||
}
|
||||
|
||||
private fun CordformDefinition.getMatchingCordapps(): List<File> {
|
||||
val cordappJars = project.configuration("cordapp").files
|
||||
return cordappPackages.map { `package` ->
|
||||
val cordappsWithPackage = cordappJars.filter { it.containsPackage(`package`) }
|
||||
when (cordappsWithPackage.size) {
|
||||
0 -> throw IllegalArgumentException("There are no cordapp dependencies containing the package $`package`")
|
||||
1 -> cordappsWithPackage[0]
|
||||
else -> throw IllegalArgumentException("More than one cordapp dependency contains the package $`package`: $cordappsWithPackage")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun File.containsPackage(`package`: String): Boolean {
|
||||
JarInputStream(inputStream()).use {
|
||||
while (true) {
|
||||
val name = it.nextJarEntry?.name ?: break
|
||||
if (name.endsWith(".class") && name.replace('/', '.').startsWith(`package`)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
@ -1,18 +1,12 @@
|
||||
package net.corda.plugins
|
||||
|
||||
import groovy.lang.Closure
|
||||
import net.corda.cordform.CordformDefinition
|
||||
import org.apache.tools.ant.filters.FixCrLfFilter
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.plugins.JavaPluginConvention
|
||||
import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
import java.io.File
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.net.URLClassLoader
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.util.jar.JarInputStream
|
||||
|
||||
/**
|
||||
* Creates nodes based on the configuration of this task in the gradle configuration DSL.
|
||||
@ -20,59 +14,12 @@ import java.util.jar.JarInputStream
|
||||
* See documentation for examples.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
open class Cordform : DefaultTask() {
|
||||
open class Cordform : Baseform() {
|
||||
private companion object {
|
||||
val nodeJarName = "corda.jar"
|
||||
private val defaultDirectory: Path = Paths.get("build", "nodes")
|
||||
}
|
||||
|
||||
/**
|
||||
* Optionally the name of a CordformDefinition subclass to which all configuration will be delegated.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanPrivate")
|
||||
var definitionClass: String? = null
|
||||
private var directory = defaultDirectory
|
||||
private val nodes = mutableListOf<Node>()
|
||||
|
||||
/**
|
||||
* Set the directory to install nodes into.
|
||||
*
|
||||
* @param directory The directory the nodes will be installed into.
|
||||
*/
|
||||
fun directory(directory: String) {
|
||||
this.directory = Paths.get(directory)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a node configuration.
|
||||
*
|
||||
* @param configureClosure A node configuration that will be deployed.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanPrivate")
|
||||
fun node(configureClosure: Closure<in Node>) {
|
||||
nodes += project.configure(Node(project), configureClosure) as Node
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a node configuration
|
||||
*
|
||||
* @param configureFunc A node configuration that will be deployed
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanPrivate")
|
||||
fun node(configureFunc: Node.() -> Any?): Node {
|
||||
val node = Node(project).apply { configureFunc() }
|
||||
nodes += node
|
||||
return node
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a node by name.
|
||||
*
|
||||
* @param name The name of the node as specified in the node configuration DSL.
|
||||
* @return A node instance.
|
||||
*/
|
||||
private fun getNodeByName(name: String): Node? = nodes.firstOrNull { it.name == name }
|
||||
|
||||
/**
|
||||
* Installs the run script into the nodes directory.
|
||||
*/
|
||||
@ -103,29 +50,6 @@ open class Cordform : DefaultTask() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The definitionClass needn't be compiled until just before our build method, so we load it manually via sourceSets.main.runtimeClasspath.
|
||||
*/
|
||||
private fun loadCordformDefinition(): CordformDefinition {
|
||||
val plugin = project.convention.getPlugin(JavaPluginConvention::class.java)
|
||||
val classpath = plugin.sourceSets.getByName(MAIN_SOURCE_SET_NAME).runtimeClasspath
|
||||
val urls = classpath.files.map { it.toURI().toURL() }.toTypedArray()
|
||||
return URLClassLoader(urls, CordformDefinition::class.java.classLoader)
|
||||
.loadClass(definitionClass)
|
||||
.asSubclass(CordformDefinition::class.java)
|
||||
.newInstance()
|
||||
}
|
||||
|
||||
/**
|
||||
* The NetworkBootstrapper needn't be compiled until just before our build method, so we load it manually via sourceSets.main.runtimeClasspath.
|
||||
*/
|
||||
private fun loadNetworkBootstrapperClass(): Class<*> {
|
||||
val plugin = project.convention.getPlugin(JavaPluginConvention::class.java)
|
||||
val classpath = plugin.sourceSets.getByName(MAIN_SOURCE_SET_NAME).runtimeClasspath
|
||||
val urls = classpath.files.map { it.toURI().toURL() }.toTypedArray()
|
||||
return URLClassLoader(urls, javaClass.classLoader).loadClass("net.corda.nodeapi.internal.network.NetworkBootstrapper")
|
||||
}
|
||||
|
||||
/**
|
||||
* This task action will create and install the nodes based on the node configurations added.
|
||||
*/
|
||||
@ -139,80 +63,4 @@ open class Cordform : DefaultTask() {
|
||||
bootstrapNetwork()
|
||||
nodes.forEach(Node::build)
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs the corda fat JAR to the root directory, for the network bootstrapper to use.
|
||||
*/
|
||||
private fun installCordaJar() {
|
||||
val cordaJar = Cordformation.verifyAndGetRuntimeJar(project, "corda")
|
||||
project.copy {
|
||||
it.apply {
|
||||
from(cordaJar)
|
||||
into(directory)
|
||||
rename(cordaJar.name, nodeJarName)
|
||||
fileMode = Cordformation.executableFileMode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeConfiguration() {
|
||||
if (definitionClass != null) {
|
||||
val cd = loadCordformDefinition()
|
||||
// If the user has specified their own directory (even if it's the same default path) then let them know
|
||||
// it's not used and should just rely on the one in CordformDefinition
|
||||
require(directory === defaultDirectory) {
|
||||
"'directory' cannot be used when 'definitionClass' is specified. Use CordformDefinition.nodesDirectory instead."
|
||||
}
|
||||
directory = cd.nodesDirectory
|
||||
val cordapps = cd.getMatchingCordapps()
|
||||
cd.nodeConfigurers.forEach {
|
||||
val node = node { }
|
||||
it.accept(node)
|
||||
node.additionalCordapps.addAll(cordapps)
|
||||
node.rootDir(directory)
|
||||
}
|
||||
cd.setup { nodeName -> project.projectDir.toPath().resolve(getNodeByName(nodeName)!!.nodeDir.toPath()) }
|
||||
} else {
|
||||
nodes.forEach {
|
||||
it.rootDir(directory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun bootstrapNetwork() {
|
||||
val networkBootstrapperClass = loadNetworkBootstrapperClass()
|
||||
val networkBootstrapper = networkBootstrapperClass.newInstance()
|
||||
val bootstrapMethod = networkBootstrapperClass.getMethod("bootstrap", Path::class.java).apply { isAccessible = true }
|
||||
// Call NetworkBootstrapper.bootstrap
|
||||
try {
|
||||
val rootDir = project.projectDir.toPath().resolve(directory).toAbsolutePath().normalize()
|
||||
bootstrapMethod.invoke(networkBootstrapper, rootDir)
|
||||
} catch (e: InvocationTargetException) {
|
||||
throw e.cause!!
|
||||
}
|
||||
}
|
||||
|
||||
private fun CordformDefinition.getMatchingCordapps(): List<File> {
|
||||
val cordappJars = project.configuration("cordapp").files
|
||||
return cordappPackages.map { `package` ->
|
||||
val cordappsWithPackage = cordappJars.filter { it.containsPackage(`package`) }
|
||||
when (cordappsWithPackage.size) {
|
||||
0 -> throw IllegalArgumentException("There are no cordapp dependencies containing the package $`package`")
|
||||
1 -> cordappsWithPackage[0]
|
||||
else -> throw IllegalArgumentException("More than one cordapp dependency contains the package $`package`: $cordappsWithPackage")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun File.containsPackage(`package`: String): Boolean {
|
||||
JarInputStream(inputStream()).use {
|
||||
while (true) {
|
||||
val name = it.nextJarEntry?.name ?: break
|
||||
if (name.endsWith(".class") && name.replace('/', '.').startsWith(`package`)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,66 @@
|
||||
package net.corda.plugins
|
||||
|
||||
import org.apache.tools.ant.filters.FixCrLfFilter
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.plugins.JavaPluginConvention
|
||||
import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
import org.yaml.snakeyaml.DumperOptions
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import org.yaml.snakeyaml.Yaml
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.Files
|
||||
|
||||
/**
|
||||
* Creates docker-compose file and image definitions based on the configuration of this task in the gradle configuration DSL.
|
||||
*
|
||||
* See documentation for examples.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
open class Dockerform : Baseform() {
|
||||
private companion object {
|
||||
val nodeJarName = "corda.jar"
|
||||
private val defaultDirectory: Path = Paths.get("build", "docker")
|
||||
|
||||
private val dockerComposeFileVersion = "3"
|
||||
|
||||
private val yamlOptions = DumperOptions().apply {
|
||||
indent = 2
|
||||
defaultFlowStyle = DumperOptions.FlowStyle.BLOCK
|
||||
}
|
||||
private val yaml = Yaml(yamlOptions)
|
||||
}
|
||||
|
||||
private val directoryPath = project.projectDir.toPath().resolve(directory)
|
||||
|
||||
val dockerComposePath = directoryPath.resolve("docker-compose.yml")
|
||||
|
||||
/**
|
||||
* This task action will create and install the nodes based on the node configurations added.
|
||||
*/
|
||||
@TaskAction
|
||||
fun build() {
|
||||
project.logger.info("Running Cordform task")
|
||||
initializeConfiguration()
|
||||
nodes.forEach(Node::installDockerConfig)
|
||||
installCordaJar()
|
||||
bootstrapNetwork()
|
||||
nodes.forEach(Node::buildDocker)
|
||||
|
||||
|
||||
// Transform nodes path the absolute ones
|
||||
val services = nodes.map { it.containerName to mapOf(
|
||||
"build" to directoryPath.resolve(it.nodeDir.name).toAbsolutePath().toString(),
|
||||
"ports" to listOf(it.rpcPort)) }.toMap()
|
||||
|
||||
|
||||
val dockerComposeObject = mapOf(
|
||||
"version" to dockerComposeFileVersion,
|
||||
"services" to services)
|
||||
|
||||
val dockerComposeContent = yaml.dump(dockerComposeObject)
|
||||
|
||||
Files.write(dockerComposePath, dockerComposeContent.toByteArray(StandardCharsets.UTF_8))
|
||||
}
|
||||
}
|
@ -3,8 +3,10 @@ package net.corda.plugins
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import com.typesafe.config.ConfigRenderOptions
|
||||
import com.typesafe.config.ConfigValueFactory
|
||||
import com.typesafe.config.ConfigObject
|
||||
import groovy.lang.Closure
|
||||
import net.corda.cordform.CordformNode
|
||||
import net.corda.cordform.RpcSettings
|
||||
import org.gradle.api.Project
|
||||
import java.io.File
|
||||
import java.nio.charset.StandardCharsets
|
||||
@ -34,6 +36,11 @@ class Node(private val project: Project) : CordformNode() {
|
||||
private set
|
||||
internal lateinit var rootDir: File
|
||||
private set
|
||||
internal lateinit var containerName: String
|
||||
private set
|
||||
|
||||
internal var rpcSettings: RpcSettings = RpcSettings()
|
||||
private set
|
||||
|
||||
/**
|
||||
* Sets whether this node will use HTTPS communication.
|
||||
@ -59,7 +66,7 @@ class Node(private val project: Project) : CordformNode() {
|
||||
* Specifies RPC settings for the node.
|
||||
*/
|
||||
fun rpcSettings(configureClosure: Closure<in RpcSettings>) {
|
||||
val rpcSettings = project.configure(RpcSettings(project), configureClosure) as RpcSettings
|
||||
rpcSettings = project.configure(RpcSettings(), configureClosure) as RpcSettings
|
||||
config = rpcSettings.addTo("rpcSettings", config)
|
||||
}
|
||||
|
||||
@ -81,6 +88,19 @@ class Node(private val project: Project) : CordformNode() {
|
||||
installCordapps()
|
||||
}
|
||||
|
||||
internal fun buildDocker() {
|
||||
project.copy {
|
||||
it.apply {
|
||||
from(Cordformation.getPluginFile(project, "net/corda/plugins/Dockerfile"))
|
||||
from(Cordformation.getPluginFile(project, "net/corda/plugins/run-corda.sh"))
|
||||
into("$nodeDir/")
|
||||
}
|
||||
}
|
||||
installAgentJar()
|
||||
installBuiltCordapp()
|
||||
installCordapps()
|
||||
}
|
||||
|
||||
internal fun rootDir(rootDir: Path) {
|
||||
if (name == null) {
|
||||
project.logger.error("Node has a null name - cannot create node")
|
||||
@ -90,6 +110,7 @@ class Node(private val project: Project) : CordformNode() {
|
||||
// with loading our custom X509EdDSAEngine.
|
||||
val organizationName = name.trim().split(",").firstOrNull { it.startsWith("O=") }?.substringAfter("=")
|
||||
val dirName = organizationName ?: name
|
||||
containerName = dirName.replace("\\s+".toRegex(), "-").toLowerCase()
|
||||
this.rootDir = rootDir.toFile()
|
||||
nodeDir = File(this.rootDir, dirName.replace("\\s", ""))
|
||||
Files.createDirectories(nodeDir.toPath())
|
||||
@ -157,14 +178,14 @@ class Node(private val project: Project) : CordformNode() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun createTempConfigFile(): File {
|
||||
private fun createTempConfigFile(configObject: ConfigObject): File {
|
||||
val options = ConfigRenderOptions
|
||||
.defaults()
|
||||
.setOriginComments(false)
|
||||
.setComments(false)
|
||||
.setFormatted(true)
|
||||
.setJson(false)
|
||||
val configFileText = config.root().render(options).split("\n").toList()
|
||||
val configFileText = configObject.render(options).split("\n").toList()
|
||||
// Need to write a temporary file first to use the project.copy, which resolves directories correctly.
|
||||
val tmpDir = File(project.buildDir, "tmp")
|
||||
Files.createDirectories(tmpDir.toPath())
|
||||
@ -179,7 +200,27 @@ class Node(private val project: Project) : CordformNode() {
|
||||
*/
|
||||
internal fun installConfig() {
|
||||
configureProperties()
|
||||
val tmpConfFile = createTempConfigFile()
|
||||
val tmpConfFile = createTempConfigFile(config.root())
|
||||
appendOptionalConfig(tmpConfFile)
|
||||
project.copy {
|
||||
it.apply {
|
||||
from(tmpConfFile)
|
||||
into(rootDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs the Dockerized configuration file to the root directory and detokenises it.
|
||||
*/
|
||||
internal fun installDockerConfig() {
|
||||
configureProperties()
|
||||
val dockerConf = config
|
||||
.withValue("p2pAddress", ConfigValueFactory.fromAnyRef("$containerName:$p2pPort"))
|
||||
.withValue("rpcSettings.address", ConfigValueFactory.fromAnyRef("$containerName:${rpcSettings.port}"))
|
||||
.withValue("rpcSettings.adminAddress", ConfigValueFactory.fromAnyRef("$containerName:${rpcSettings.adminPort}"))
|
||||
.withValue("detectPublicIp", ConfigValueFactory.fromAnyRef(false))
|
||||
val tmpConfFile = createTempConfigFile(dockerConf.root())
|
||||
appendOptionalConfig(tmpConfFile)
|
||||
project.copy {
|
||||
it.apply {
|
||||
|
@ -1,59 +0,0 @@
|
||||
package net.corda.plugins
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import com.typesafe.config.ConfigValueFactory
|
||||
import groovy.lang.Closure
|
||||
import org.gradle.api.Project
|
||||
|
||||
class RpcSettings(private val project: Project) {
|
||||
private var config: Config = ConfigFactory.empty()
|
||||
|
||||
/**
|
||||
* RPC address for the node.
|
||||
*/
|
||||
fun address(value: String) {
|
||||
config += "address" to value
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC admin address for the node (necessary if [useSsl] is false or unset).
|
||||
*/
|
||||
fun adminAddress(value: String) {
|
||||
config += "adminAddress" to value
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies whether the node RPC layer will require SSL from clients.
|
||||
*/
|
||||
fun useSsl(value: Boolean) {
|
||||
config += "useSsl" to value
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies whether the RPC broker is separate from the node.
|
||||
*/
|
||||
fun standAloneBroker(value: Boolean) {
|
||||
config += "standAloneBroker" to value
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies SSL certificates options for the RPC layer.
|
||||
*/
|
||||
fun ssl(configureClosure: Closure<in SslOptions>) {
|
||||
val sslOptions = project.configure(SslOptions(), configureClosure) as SslOptions
|
||||
config = sslOptions.addTo("ssl", config)
|
||||
}
|
||||
|
||||
internal fun addTo(key: String, config: Config): Config {
|
||||
if (this.config.isEmpty) {
|
||||
return config
|
||||
}
|
||||
return config + (key to this.config.root())
|
||||
}
|
||||
}
|
||||
|
||||
internal operator fun Config.plus(entry: Pair<String, Any>): Config {
|
||||
|
||||
return withValue(entry.first, ConfigValueFactory.fromAnyRef(entry.second))
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
package net.corda.plugins
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
||||
|
||||
class SslOptions {
|
||||
private var config: Config = ConfigFactory.empty()
|
||||
|
||||
/**
|
||||
* Password for the keystore.
|
||||
*/
|
||||
fun keyStorePassword(value: String) {
|
||||
config += "keyStorePassword" to value
|
||||
}
|
||||
|
||||
/**
|
||||
* Password for the truststore.
|
||||
*/
|
||||
fun trustStorePassword(value: String) {
|
||||
config += "trustStorePassword" to value
|
||||
}
|
||||
|
||||
/**
|
||||
* Directory under which key stores are to be placed.
|
||||
*/
|
||||
fun certificatesDirectory(value: String) {
|
||||
config += "certificatesDirectory" to value
|
||||
}
|
||||
|
||||
/**
|
||||
* Absolute path to SSL keystore. Default: "[certificatesDirectory]/sslkeystore.jks"
|
||||
*/
|
||||
fun sslKeystore(value: String) {
|
||||
config += "sslKeystore" to value
|
||||
}
|
||||
|
||||
/**
|
||||
* Absolute path to SSL truststore. Default: "[certificatesDirectory]/truststore.jks"
|
||||
*/
|
||||
fun trustStoreFile(value: String) {
|
||||
config += "trustStoreFile" to value
|
||||
}
|
||||
|
||||
internal fun addTo(key: String, config: Config): Config {
|
||||
if (this.config.isEmpty) {
|
||||
return config
|
||||
}
|
||||
return config + (key to this.config.root())
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
# Base image from (http://phusion.github.io/baseimage-docker)
|
||||
FROM openjdk:8u151-jre-alpine
|
||||
|
||||
ENV CORDA_VERSION=${BUILDTIME_CORDA_VERSION}
|
||||
ENV JAVA_OPTIONS=${BUILDTIME_JAVA_OPTIONS}
|
||||
|
||||
# Set image labels
|
||||
LABEL net.corda.version = ${CORDA_VERSION} \
|
||||
maintainer = "<devops@r3.com>" \
|
||||
vendor = "R3"
|
||||
|
||||
RUN apk upgrade --update && \
|
||||
apk add --update --no-cache bash iputils && \
|
||||
rm -rf /var/cache/apk/* && \
|
||||
# Add user to run the app && \
|
||||
addgroup corda && \
|
||||
adduser -G corda -D -s /bin/bash corda && \
|
||||
# Create /opt/corda directory && \
|
||||
mkdir -p /opt/corda/plugins && \
|
||||
mkdir -p /opt/corda/logs
|
||||
|
||||
# Copy corda files
|
||||
ADD --chown=corda:corda corda.jar /opt/corda/corda.jar
|
||||
ADD --chown=corda:corda node.conf /opt/corda/node.conf
|
||||
ADD --chown=corda:corda network-parameters /opt/corda/
|
||||
ADD --chown=corda:corda cordapps/ /opt/corda/cordapps
|
||||
ADD --chown=corda:corda additional-node-infos/ /opt/corda/additional-node-infos
|
||||
ADD --chown=corda:corda certificates/ /opt/corda/certificates
|
||||
ADD --chown=corda:corda drivers/ /opt/corda/drivers
|
||||
ADD --chown=corda:corda persistence* /opt/corda/
|
||||
|
||||
COPY run-corda.sh /run-corda.sh
|
||||
|
||||
RUN chmod +x /run-corda.sh && \
|
||||
sync && \
|
||||
chown -R corda:corda /opt/corda
|
||||
|
||||
# Working directory for Corda
|
||||
WORKDIR /opt/corda
|
||||
ENV HOME=/opt/corda
|
||||
USER corda
|
||||
|
||||
# Start it
|
||||
CMD ["/run-corda.sh"]
|
@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
|
||||
# If variable not present use default values
|
||||
: ${CORDA_HOME:=/opt/corda}
|
||||
: ${JAVA_OPTIONS:=-Xmx512m}
|
||||
|
||||
export CORDA_HOME JAVA_OPTIONS
|
||||
|
||||
cd ${CORDA_HOME}
|
||||
java $JAVA_OPTIONS -jar ${CORDA_HOME}/corda.jar 2>&1
|
@ -30,6 +30,10 @@ class PublishTasks implements Plugin<Project> {
|
||||
createConfigurations()
|
||||
}
|
||||
|
||||
/**
|
||||
* This call must come at the end of any publish block because it configures the publishing and any
|
||||
* values set after this call in the DSL will not be configured properly (and will use the default value)
|
||||
*/
|
||||
void setPublishName(String publishName) {
|
||||
project.logger.info("Changing publishing name from ${project.name} to ${publishName}")
|
||||
this.publishName = publishName
|
||||
@ -51,12 +55,14 @@ class PublishTasks implements Plugin<Project> {
|
||||
}
|
||||
|
||||
void configureMavenPublish(BintrayConfigExtension bintrayConfig) {
|
||||
project.logger.info("Configuring maven publish for $publishName")
|
||||
project.apply([plugin: 'maven-publish'])
|
||||
project.publishing.publications.create(publishName, MavenPublication) {
|
||||
groupId project.group
|
||||
artifactId publishName
|
||||
|
||||
if (publishConfig.publishSources) {
|
||||
project.logger.info("Publishing sources for $publishName")
|
||||
artifact project.tasks.sourceJar
|
||||
}
|
||||
artifact project.tasks.javadocJar
|
||||
|
@ -12,6 +12,7 @@ import net.corda.core.internal.div
|
||||
import net.corda.core.internal.exists
|
||||
import net.corda.core.internal.list
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.seconds
|
||||
@ -19,7 +20,6 @@ import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.flows.CashIssueAndPaymentFlow
|
||||
import net.corda.nodeapi.internal.createDevNetworkMapCa
|
||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||
import net.corda.nodeapi.internal.network.NetworkParameters
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
|
@ -4,7 +4,7 @@ import com.r3.corda.networkmanage.common.utils.SignedNetworkMap
|
||||
import com.r3.corda.networkmanage.common.utils.SignedNetworkParameters
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.internal.DigitalSignatureWithCert
|
||||
import net.corda.nodeapi.internal.network.NetworkParameters
|
||||
import net.corda.core.node.NetworkParameters
|
||||
|
||||
/**
|
||||
* Data access object interface for NetworkMap persistence layer
|
||||
|
@ -6,8 +6,8 @@ import com.r3.corda.networkmanage.common.utils.SignedNetworkParameters
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.internal.DigitalSignatureWithCert
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.nodeapi.internal.network.NetworkParameters
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
|
||||
/**
|
||||
|
@ -3,10 +3,10 @@ package com.r3.corda.networkmanage.common.persistence.entity
|
||||
import com.r3.corda.networkmanage.common.utils.SignedNetworkParameters
|
||||
import net.corda.core.internal.DigitalSignatureWithCert
|
||||
import net.corda.core.internal.SignedDataWithCert
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||
import net.corda.nodeapi.internal.network.NetworkParameters
|
||||
import org.hibernate.annotations.CreationTimestamp
|
||||
import java.time.Instant
|
||||
import javax.persistence.*
|
||||
|
@ -3,10 +3,10 @@ package com.r3.corda.networkmanage.common.signer
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateStatus
|
||||
import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
|
||||
import net.corda.core.internal.SignedDataWithCert
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.nodeapi.internal.network.NetworkMap
|
||||
import net.corda.nodeapi.internal.network.NetworkParameters
|
||||
|
||||
class NetworkMapSigner(private val networkMapStorage: NetworkMapStorage, private val signer: Signer) {
|
||||
private companion object {
|
||||
@ -37,7 +37,7 @@ class NetworkMapSigner(private val networkMapStorage: NetworkMapStorage, private
|
||||
logger.debug("Fetching node info hashes with VALID certificates...")
|
||||
val nodeInfoHashes = networkMapStorage.getNodeInfoHashes(CertificateStatus.VALID)
|
||||
logger.debug("Retrieved node info hashes: $nodeInfoHashes")
|
||||
val newNetworkMap = NetworkMap(nodeInfoHashes, latestNetworkParameters.serialize().hash)
|
||||
val newNetworkMap = NetworkMap(nodeInfoHashes, latestNetworkParameters.serialize().hash, null)
|
||||
val serialisedNetworkMap = newNetworkMap.serialize()
|
||||
if (serialisedNetworkMap != currentSignedNetworkMap?.raw) {
|
||||
logger.info("Signing a new network map: $newNetworkMap")
|
||||
|
@ -7,12 +7,12 @@ import joptsimple.ArgumentAcceptingOptionSpec
|
||||
import joptsimple.OptionParser
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.internal.SignedDataWithCert
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.serialization.internal.SerializationEnvironmentImpl
|
||||
import net.corda.core.serialization.internal.nodeSerializationEnv
|
||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||
import net.corda.nodeapi.internal.crypto.X509KeyStore
|
||||
import net.corda.nodeapi.internal.network.NetworkMap
|
||||
import net.corda.nodeapi.internal.network.NetworkParameters
|
||||
import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT
|
||||
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
|
||||
import net.corda.nodeapi.internal.serialization.amqp.AMQPClientSerializationScheme
|
||||
|
@ -5,10 +5,10 @@ import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage
|
||||
import com.r3.corda.networkmanage.common.persistence.configureDatabase
|
||||
import com.r3.corda.networkmanage.common.utils.*
|
||||
import com.r3.corda.networkmanage.doorman.signer.LocalSigner
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.nodeapi.internal.crypto.X509KeyStore
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import net.corda.nodeapi.internal.network.NetworkParameters
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||
import java.time.Instant
|
||||
import kotlin.concurrent.thread
|
||||
|
@ -12,9 +12,9 @@ import com.r3.corda.networkmanage.doorman.signer.LocalSigner
|
||||
import com.r3.corda.networkmanage.doorman.webservice.MonitoringWebService
|
||||
import com.r3.corda.networkmanage.doorman.webservice.NetworkMapWebService
|
||||
import com.r3.corda.networkmanage.doorman.webservice.RegistrationWebService
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.nodeapi.internal.network.NetworkParameters
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import java.io.Closeable
|
||||
import java.net.URI
|
||||
|
@ -1,21 +1,15 @@
|
||||
package com.r3.corda.networkmanage.doorman
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import com.typesafe.config.ConfigParseOptions
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.exists
|
||||
import net.corda.core.internal.readAll
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.NotaryInfo
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.core.utilities.parsePublicKeyBase58
|
||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||
import net.corda.nodeapi.internal.config.parseAs
|
||||
import net.corda.nodeapi.internal.network.NetworkParameters
|
||||
import net.corda.nodeapi.internal.network.NotaryInfo
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
|
@ -10,13 +10,13 @@ import com.r3.corda.networkmanage.common.utils.SignedNetworkMap
|
||||
import com.r3.corda.networkmanage.doorman.NetworkMapConfig
|
||||
import com.r3.corda.networkmanage.doorman.webservice.NetworkMapWebService.Companion.NETWORK_MAP_PATH
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.trace
|
||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||
import net.corda.nodeapi.internal.network.NetworkParameters
|
||||
import java.io.InputStream
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.SignatureException
|
||||
|
@ -7,6 +7,8 @@ import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
|
||||
/**
|
||||
@ -44,3 +46,11 @@ class SignedNodeInfo(val raw: SerializedBytes<NodeInfo>, val signatures: List<Di
|
||||
return nodeInfo
|
||||
}
|
||||
}
|
||||
|
||||
inline fun NodeInfo.sign(signer: (PublicKey, SerializedBytes<NodeInfo>) -> DigitalSignature): SignedNodeInfo {
|
||||
// For now we exclude any composite identities, see [SignedNodeInfo]
|
||||
val owningKeys = legalIdentities.map { it.owningKey }.filter { it !is CompositeKey }
|
||||
val serialised = serialize()
|
||||
val signatures = owningKeys.map { signer(it, serialised) }
|
||||
return SignedNodeInfo(serialised, signatures)
|
||||
}
|
||||
|
@ -4,10 +4,7 @@ import net.corda.core.CordaOID
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SignatureScheme
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.internal.CertRole
|
||||
import net.corda.core.internal.reader
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.internal.writer
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.core.utilities.millis
|
||||
import org.bouncycastle.asn1.*
|
||||
|
@ -5,12 +5,14 @@ import net.corda.cordform.CordformNode
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.internal.concurrent.fork
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.NotaryInfo
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.nodeapi.internal.serialization.CordaSerializationMagic
|
||||
import net.corda.core.serialization.internal.SerializationEnvironmentImpl
|
||||
import net.corda.core.serialization.internal._contextSerializationEnv
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||
@ -18,7 +20,7 @@ import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT
|
||||
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
|
||||
import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme
|
||||
import net.corda.nodeapi.internal.serialization.kryo.AbstractKryoSerializationScheme
|
||||
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||
import net.corda.nodeapi.internal.serialization.kryo.kryoMagic
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
@ -167,7 +169,7 @@ class NetworkBootstrapper {
|
||||
epoch = 1
|
||||
), overwriteFile = true)
|
||||
|
||||
nodeDirs.forEach(copier::install)
|
||||
nodeDirs.forEach { copier.install(it) }
|
||||
}
|
||||
|
||||
private fun NotaryInfo.prettyPrint(): String = "${identity.name} (${if (validating) "" else "non-"}validating)"
|
||||
@ -196,8 +198,8 @@ class NetworkBootstrapper {
|
||||
}
|
||||
|
||||
private object KryoParametersSerializationScheme : AbstractKryoSerializationScheme() {
|
||||
override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean {
|
||||
return byteSequence == KryoHeaderV0_1 && target == SerializationContext.UseCase.P2P
|
||||
override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean {
|
||||
return magic == kryoMagic && target == SerializationContext.UseCase.P2P
|
||||
}
|
||||
|
||||
override fun rpcClientKryoPool(context: SerializationContext) = throw UnsupportedOperationException()
|
||||
|
@ -1,58 +1,48 @@
|
||||
package net.corda.nodeapi.internal.network
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.CertRole
|
||||
import net.corda.core.internal.SignedDataWithCert
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Instant
|
||||
|
||||
|
||||
const val NETWORK_PARAMS_FILE_NAME = "network-parameters"
|
||||
const val NETWORK_PARAMS_UPDATE_FILE_NAME = "network-parameters-update"
|
||||
|
||||
/**
|
||||
* Data class containing hash of [NetworkParameters] and network participant's [NodeInfo] hashes.
|
||||
* Data structure representing the network map available from the HTTP network map service as a serialised blob.
|
||||
* @property nodeInfoHashes list of network participant's [NodeInfo] hashes
|
||||
* @property networkParameterHash hash of the current active [NetworkParameters]
|
||||
* @property parametersUpdate if present means that network operator has scheduled an update of the network parameters
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class NetworkMap(val nodeInfoHashes: List<SecureHash>, val networkParameterHash: SecureHash)
|
||||
data class NetworkMap(
|
||||
val nodeInfoHashes: List<SecureHash>,
|
||||
val networkParameterHash: SecureHash,
|
||||
val parametersUpdate: ParametersUpdate?
|
||||
)
|
||||
|
||||
/**
|
||||
* @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
|
||||
* @property epoch Version number of the network parameters. Starting from 1, this will always increment on each new set
|
||||
* of parameters.
|
||||
* Data class representing scheduled network parameters update.
|
||||
* @property newParametersHash Hash of the new [NetworkParameters] which can be requested from the network map
|
||||
* @property description Short description of the update
|
||||
* @property updateDeadline deadline by which new network parameters need to be accepted, after this date network operator
|
||||
* can switch to new parameters which will result in getting nodes with old parameters out of the network
|
||||
*/
|
||||
// TODO Add eventHorizon - how many days a node can be offline before being automatically ejected from the network.
|
||||
// It needs separate design.
|
||||
// TODO Currently maxTransactionSize is not wired.
|
||||
@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" }
|
||||
}
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
data class NotaryInfo(val identity: Party, val validating: Boolean)
|
||||
data class ParametersUpdate(
|
||||
val newParametersHash: SecureHash,
|
||||
val description: String,
|
||||
val updateDeadline: Instant
|
||||
)
|
||||
|
||||
fun <T : Any> SignedDataWithCert<T>.verifiedNetworkMapCert(rootCert: X509Certificate): T {
|
||||
require(CertRole.extract(sig.by) == CertRole.NETWORK_MAP) { "Incorrect cert role: ${CertRole.extract(sig.by)}" }
|
||||
X509Utilities.validateCertificateChain(rootCert, sig.by, rootCert)
|
||||
return verified()
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
package net.corda.nodeapi.internal.network
|
||||
|
||||
import net.corda.core.internal.copyTo
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.signWithCert
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.nodeapi.internal.createDevNetworkMapCa
|
||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||
@ -13,7 +12,9 @@ import java.nio.file.StandardCopyOption
|
||||
class NetworkParametersCopier(
|
||||
networkParameters: NetworkParameters,
|
||||
networkMapCa: CertificateAndKeyPair = createDevNetworkMapCa(),
|
||||
overwriteFile: Boolean = false
|
||||
overwriteFile: Boolean = false,
|
||||
@VisibleForTesting
|
||||
val update: Boolean = false
|
||||
) {
|
||||
private val copyOptions = if (overwriteFile) arrayOf(StandardCopyOption.REPLACE_EXISTING) else emptyArray()
|
||||
private val serialisedSignedNetParams = networkParameters.signWithCert(
|
||||
@ -22,8 +23,9 @@ class NetworkParametersCopier(
|
||||
).serialize()
|
||||
|
||||
fun install(nodeDir: Path) {
|
||||
val fileName = if (update) NETWORK_PARAMS_UPDATE_FILE_NAME else NETWORK_PARAMS_FILE_NAME
|
||||
try {
|
||||
serialisedSignedNetParams.open().copyTo(nodeDir / NETWORK_PARAMS_FILE_NAME, *copyOptions)
|
||||
serialisedSignedNetParams.open().copyTo(nodeDir / fileName, *copyOptions)
|
||||
} catch (e: FileAlreadyExistsException) {
|
||||
// This is only thrown if the file already exists and we didn't specify to overwrite it. In that case we
|
||||
// ignore this exception as we're happy with the existing file.
|
||||
|
@ -4,8 +4,8 @@ package net.corda.nodeapi.internal.serialization
|
||||
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
import net.corda.nodeapi.internal.serialization.amqp.AmqpHeaderV1_0
|
||||
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||
import net.corda.nodeapi.internal.serialization.amqp.amqpMagic
|
||||
import net.corda.nodeapi.internal.serialization.kryo.kryoMagic
|
||||
|
||||
/*
|
||||
* Serialisation contexts for the client.
|
||||
@ -13,14 +13,13 @@ import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||
* servers from trying to instantiate any of them.
|
||||
*/
|
||||
|
||||
val KRYO_RPC_CLIENT_CONTEXT = SerializationContextImpl(KryoHeaderV0_1,
|
||||
val KRYO_RPC_CLIENT_CONTEXT = SerializationContextImpl(kryoMagic,
|
||||
SerializationDefaults.javaClass.classLoader,
|
||||
GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()),
|
||||
emptyMap(),
|
||||
true,
|
||||
SerializationContext.UseCase.RPCClient)
|
||||
|
||||
val AMQP_RPC_CLIENT_CONTEXT = SerializationContextImpl(AmqpHeaderV1_0,
|
||||
val AMQP_RPC_CLIENT_CONTEXT = SerializationContextImpl(amqpMagic,
|
||||
SerializationDefaults.javaClass.classLoader,
|
||||
GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()),
|
||||
emptyMap(),
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user