mirror of
https://github.com/corda/corda.git
synced 2025-06-16 06:08:13 +00:00
Merge branch 'os-merge-point' into os-merge-shams
# Conflicts: # build.gradle # core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt # core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt # docs/source/api-persistence.rst # node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlListener.kt # node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt # node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt # settings.gradle # testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt # testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TransactionDSLInterpreter.kt # testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt
This commit is contained in:
14
.ci/kill_corda_procs.cmd
Normal file
14
.ci/kill_corda_procs.cmd
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
@echo off
|
||||||
|
|
||||||
|
REM Setlocal EnableDelayedExpansion
|
||||||
|
FOR /F "tokens=1,2 delims= " %%G IN ('jps -l') DO (call :sub %%H %%G)
|
||||||
|
goto :eof
|
||||||
|
|
||||||
|
:sub
|
||||||
|
|
||||||
|
IF %1==net.corda.webserver.WebServer taskkill /F /PID %2
|
||||||
|
IF %1==net.corda.node.Corda taskkill /F /PID %2
|
||||||
|
IF %1==corda.jar taskkill /F /PID %2
|
||||||
|
IF %1==corda-webserver.jar taskkill /F /PID %2
|
||||||
|
IF %1==org.gradle.launcher.daemon.bootstrap.GradleDaemon taskkill /F /PID %2
|
||||||
|
goto :eof
|
@ -121,6 +121,7 @@ see changes to this list.
|
|||||||
* Lulu Ren (Monad-Labs)
|
* Lulu Ren (Monad-Labs)
|
||||||
* Maksymilian Pawlak (R3)
|
* Maksymilian Pawlak (R3)
|
||||||
* Manila Gauns (Persistent Systems Limited)
|
* Manila Gauns (Persistent Systems Limited)
|
||||||
|
* Manos Batsis
|
||||||
* Marek Scocovsky (ABSA)
|
* Marek Scocovsky (ABSA)
|
||||||
* marekdapps
|
* marekdapps
|
||||||
* Mark Lauer (Westpac)
|
* Mark Lauer (Westpac)
|
||||||
|
@ -412,6 +412,8 @@ bintrayConfig {
|
|||||||
'corda-notary-bft-smart',
|
'corda-notary-bft-smart',
|
||||||
'corda-notary-jpa',
|
'corda-notary-jpa',
|
||||||
'corda-notary-mysql'
|
'corda-notary-mysql'
|
||||||
|
'corda-common-configuration-parsing',
|
||||||
|
'corda-common-validation'
|
||||||
]
|
]
|
||||||
license {
|
license {
|
||||||
name = 'Apache-2.0'
|
name = 'Apache-2.0'
|
||||||
|
@ -351,7 +351,11 @@ object JacksonSupport {
|
|||||||
val nameMatches = mapper.partiesFromName(parser.text)
|
val nameMatches = mapper.partiesFromName(parser.text)
|
||||||
return when {
|
return when {
|
||||||
nameMatches.isEmpty() -> {
|
nameMatches.isEmpty() -> {
|
||||||
val publicKey = parser.readValueAs<PublicKey>()
|
val publicKey = try {
|
||||||
|
parser.readValueAs<PublicKey>()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw JsonParseException(parser, "No matching Party found, then tried to directly deserialise ${parser.text} as a PublicKey with no success", e)
|
||||||
|
}
|
||||||
mapper.partyFromKey(publicKey)
|
mapper.partyFromKey(publicKey)
|
||||||
?: throw JsonParseException(parser, "Could not find a Party with key ${publicKey.toStringShort()}")
|
?: throw JsonParseException(parser, "Could not find a Party with key ${publicKey.toStringShort()}")
|
||||||
}
|
}
|
||||||
|
@ -204,13 +204,13 @@ class NodeMonitorModel : AutoCloseable {
|
|||||||
val nodeInfo = _connection.proxy.nodeInfo()
|
val nodeInfo = _connection.proxy.nodeInfo()
|
||||||
require(nodeInfo.legalIdentitiesAndCerts.isNotEmpty())
|
require(nodeInfo.legalIdentitiesAndCerts.isNotEmpty())
|
||||||
_connection
|
_connection
|
||||||
} catch (throwable: Throwable) {
|
} catch (exception: Exception) {
|
||||||
if (shouldRetry) {
|
if (shouldRetry) {
|
||||||
// Deliberately not logging full stack trace as it will be full of internal stacktraces.
|
// Deliberately not logging full stack trace as it will be full of internal stacktraces.
|
||||||
logger.info("Exception upon establishing connection: {}", throwable.message)
|
logger.info("Exception upon establishing connection: {}", exception.message)
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
throw throwable
|
throw exception
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,7 +124,7 @@ class CordaRPCClientTest : NodeBasedTest(listOf("net.corda.finance")) {
|
|||||||
nodeIsShut.onCompleted()
|
nodeIsShut.onCompleted()
|
||||||
} catch (e: ActiveMQSecurityException) {
|
} catch (e: ActiveMQSecurityException) {
|
||||||
// nothing here - this happens if trying to connect before the node is started
|
// nothing here - this happens if trying to connect before the node is started
|
||||||
} catch (e: Throwable) {
|
} catch (e: Exception) {
|
||||||
nodeIsShut.onError(e)
|
nodeIsShut.onError(e)
|
||||||
}
|
}
|
||||||
}, 1, 1, TimeUnit.SECONDS)
|
}, 1, 1, TimeUnit.SECONDS)
|
||||||
|
@ -100,6 +100,8 @@ class RPCClientProxyHandler(
|
|||||||
private val log = contextLogger()
|
private val log = contextLogger()
|
||||||
// To check whether toString() is being invoked
|
// To check whether toString() is being invoked
|
||||||
val toStringMethod: Method = Object::toString.javaMethod!!
|
val toStringMethod: Method = Object::toString.javaMethod!!
|
||||||
|
val equalsMethod: Method = Object::equals.javaMethod!!
|
||||||
|
val hashCodeMethod: Method = Object::hashCode.javaMethod!!
|
||||||
|
|
||||||
private fun addRpcCallSiteToThrowable(throwable: Throwable, callSite: CallSite) {
|
private fun addRpcCallSiteToThrowable(throwable: Throwable, callSite: CallSite) {
|
||||||
var currentThrowable = throwable
|
var currentThrowable = throwable
|
||||||
@ -234,7 +236,13 @@ class RPCClientProxyHandler(
|
|||||||
lifeCycle.requireState { it == State.STARTED || it == State.SERVER_VERSION_NOT_SET }
|
lifeCycle.requireState { it == State.STARTED || it == State.SERVER_VERSION_NOT_SET }
|
||||||
checkProtocolVersion(method)
|
checkProtocolVersion(method)
|
||||||
if (method == toStringMethod) {
|
if (method == toStringMethod) {
|
||||||
return "Client RPC proxy for $rpcOpsClass"
|
return toString()
|
||||||
|
}
|
||||||
|
if (method == equalsMethod) {
|
||||||
|
return equals(arguments?.getOrNull(0))
|
||||||
|
}
|
||||||
|
if (method == hashCodeMethod) {
|
||||||
|
return hashCode()
|
||||||
}
|
}
|
||||||
if (consumerSession!!.isClosed) {
|
if (consumerSession!!.isClosed) {
|
||||||
throw RPCException("RPC Proxy is closed")
|
throw RPCException("RPC Proxy is closed")
|
||||||
@ -364,6 +372,8 @@ class RPCClientProxyHandler(
|
|||||||
close(false)
|
close(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Closes this handler and sends notifications to all observables, so it can immediately clean up resources.
|
* Closes this handler and sends notifications to all observables, so it can immediately clean up resources.
|
||||||
* Notifications sent to observables are to be acknowledged, therefore this call blocks until all acknowledgements are received.
|
* Notifications sent to observables are to be acknowledged, therefore this call blocks until all acknowledgements are received.
|
||||||
@ -554,8 +564,8 @@ class RPCClientProxyHandler(
|
|||||||
observationExecutorPool.run(k) {
|
observationExecutorPool.run(k) {
|
||||||
try {
|
try {
|
||||||
m[k]?.onError(ConnectionFailureException())
|
m[k]?.onError(ConnectionFailureException())
|
||||||
} catch (th: Throwable) {
|
} catch (e: Exception) {
|
||||||
log.error("Unexpected exception when RPC connection failure handling", th)
|
log.error("Unexpected exception when RPC connection failure handling", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -568,6 +578,32 @@ class RPCClientProxyHandler(
|
|||||||
rpcReplyMap.clear()
|
rpcReplyMap.clear()
|
||||||
callSiteMap?.clear()
|
callSiteMap?.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as RPCClientProxyHandler
|
||||||
|
|
||||||
|
if (rpcUsername != other.rpcUsername) return false
|
||||||
|
if (clientAddress != other.clientAddress) return false
|
||||||
|
if (sessionId != other.sessionId) return false
|
||||||
|
if (targetLegalIdentity != other.targetLegalIdentity) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = rpcUsername.hashCode()
|
||||||
|
result = 31 * result + clientAddress.hashCode()
|
||||||
|
result = 31 * result + sessionId.hashCode()
|
||||||
|
result = 31 * result + (targetLegalIdentity?.hashCode() ?: 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "{rpcUsername='$rpcUsername', clientAddress=$clientAddress, sessionId=$sessionId, targetLegalIdentity=$targetLegalIdentity}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private typealias RpcObservableMap = Cache<InvocationId, UnicastSubject<Notification<*>>>
|
private typealias RpcObservableMap = Cache<InvocationId, UnicastSubject<Notification<*>>>
|
||||||
|
@ -17,7 +17,7 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
jar {
|
jar {
|
||||||
baseName 'common-configuration-parsing'
|
baseName 'corda-common-configuration-parsing'
|
||||||
}
|
}
|
||||||
|
|
||||||
publish {
|
publish {
|
||||||
|
@ -117,7 +117,7 @@ private class OptionalPropertyWithDefault<TYPE : Any>(delegate: Configuration.Pr
|
|||||||
|
|
||||||
private class FunctionalProperty<TYPE, MAPPED : Any>(delegate: Configuration.Property.Definition.Standard<TYPE>, private val mappedTypeName: String, internal val extractListValue: (Config, String) -> List<TYPE>, private val convert: (TYPE) -> Valid<MAPPED>) : RequiredDelegatedProperty<MAPPED, Configuration.Property.Definition.Standard<TYPE>>(delegate), Configuration.Property.Definition.Standard<MAPPED> {
|
private class FunctionalProperty<TYPE, MAPPED : Any>(delegate: Configuration.Property.Definition.Standard<TYPE>, private val mappedTypeName: String, internal val extractListValue: (Config, String) -> List<TYPE>, private val convert: (TYPE) -> Valid<MAPPED>) : RequiredDelegatedProperty<MAPPED, Configuration.Property.Definition.Standard<TYPE>>(delegate), Configuration.Property.Definition.Standard<MAPPED> {
|
||||||
|
|
||||||
override fun valueIn(configuration: Config) = convert.invoke(delegate.valueIn(configuration)).orThrow()
|
override fun valueIn(configuration: Config) = convert.invoke(delegate.valueIn(configuration)).value()
|
||||||
|
|
||||||
override val typeName: String = if (super.typeName == "#$mappedTypeName") super.typeName else "$mappedTypeName(${super.typeName})"
|
override val typeName: String = if (super.typeName == "#$mappedTypeName") super.typeName else "$mappedTypeName(${super.typeName})"
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ class SpecificationTest {
|
|||||||
val rpcSettings = RpcSettingsSpec.parse(configuration)
|
val rpcSettings = RpcSettingsSpec.parse(configuration)
|
||||||
|
|
||||||
assertThat(rpcSettings.isValid).isTrue()
|
assertThat(rpcSettings.isValid).isTrue()
|
||||||
assertThat(rpcSettings.orThrow()).satisfies { value ->
|
assertThat(rpcSettings.value()).satisfies { value ->
|
||||||
|
|
||||||
assertThat(value.useSsl).isEqualTo(useSslValue)
|
assertThat(value.useSsl).isEqualTo(useSslValue)
|
||||||
assertThat(value.addresses).satisfies { addresses ->
|
assertThat(value.addresses).satisfies { addresses ->
|
||||||
|
@ -18,7 +18,7 @@ class VersionExtractorTest {
|
|||||||
val versionValue = Configuration.Version.Extractor.DEFAULT_VERSION_VALUE + 1
|
val versionValue = Configuration.Version.Extractor.DEFAULT_VERSION_VALUE + 1
|
||||||
val rawConfiguration = configObject("configuration" to configObject("metadata" to configObject("version" to versionValue), "node" to configObject("p2pAddress" to "localhost:8080"))).toConfig()
|
val rawConfiguration = configObject("configuration" to configObject("metadata" to configObject("version" to versionValue), "node" to configObject("p2pAddress" to "localhost:8080"))).toConfig()
|
||||||
|
|
||||||
val version = extractVersion.invoke(rawConfiguration).orThrow()
|
val version = extractVersion.invoke(rawConfiguration).value()
|
||||||
assertThat(version).isEqualTo(versionValue)
|
assertThat(version).isEqualTo(versionValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ class VersionExtractorTest {
|
|||||||
|
|
||||||
val rawConfiguration = configObject("configuration" to configObject("node" to configObject("p2pAddress" to "localhost:8080"))).toConfig()
|
val rawConfiguration = configObject("configuration" to configObject("node" to configObject("p2pAddress" to "localhost:8080"))).toConfig()
|
||||||
|
|
||||||
val version = extractVersion.invoke(rawConfiguration).orThrow()
|
val version = extractVersion.invoke(rawConfiguration).value()
|
||||||
assertThat(version).isEqualTo(Configuration.Version.Extractor.DEFAULT_VERSION_VALUE)
|
assertThat(version).isEqualTo(Configuration.Version.Extractor.DEFAULT_VERSION_VALUE)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,7 +36,7 @@ class VersionExtractorTest {
|
|||||||
|
|
||||||
val rawConfiguration = configObject("configuration" to configObject("metadata" to configObject(), "node" to configObject("p2pAddress" to "localhost:8080"))).toConfig()
|
val rawConfiguration = configObject("configuration" to configObject("metadata" to configObject(), "node" to configObject("p2pAddress" to "localhost:8080"))).toConfig()
|
||||||
|
|
||||||
val version = extractVersion.invoke(rawConfiguration).orThrow()
|
val version = extractVersion.invoke(rawConfiguration).value()
|
||||||
|
|
||||||
assertThat(version).isEqualTo(Configuration.Version.Extractor.DEFAULT_VERSION_VALUE)
|
assertThat(version).isEqualTo(Configuration.Version.Extractor.DEFAULT_VERSION_VALUE)
|
||||||
}
|
}
|
||||||
@ -46,7 +46,7 @@ class VersionExtractorTest {
|
|||||||
|
|
||||||
val rawConfiguration = configObject("configuration" to configObject("metadata" to configObject("version" to null), "node" to configObject("p2pAddress" to "localhost:8080"))).toConfig()
|
val rawConfiguration = configObject("configuration" to configObject("metadata" to configObject("version" to null), "node" to configObject("p2pAddress" to "localhost:8080"))).toConfig()
|
||||||
|
|
||||||
val version = extractVersion.invoke(rawConfiguration).orThrow()
|
val version = extractVersion.invoke(rawConfiguration).value()
|
||||||
|
|
||||||
assertThat(version).isEqualTo(Configuration.Version.Extractor.DEFAULT_VERSION_VALUE)
|
assertThat(version).isEqualTo(Configuration.Version.Extractor.DEFAULT_VERSION_VALUE)
|
||||||
}
|
}
|
||||||
@ -56,7 +56,7 @@ class VersionExtractorTest {
|
|||||||
|
|
||||||
val rawConfiguration = configObject().toConfig()
|
val rawConfiguration = configObject().toConfig()
|
||||||
|
|
||||||
val version = extractVersion.invoke(rawConfiguration).orThrow()
|
val version = extractVersion.invoke(rawConfiguration).value()
|
||||||
|
|
||||||
assertThat(version).isEqualTo(Configuration.Version.Extractor.DEFAULT_VERSION_VALUE)
|
assertThat(version).isEqualTo(Configuration.Version.Extractor.DEFAULT_VERSION_VALUE)
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,7 @@ class VersionedParsingExampleTest {
|
|||||||
private fun assertResult(result: Valid<RpcSettings>, principalAddressValue: Address, adminAddressValue: Address) {
|
private fun assertResult(result: Valid<RpcSettings>, principalAddressValue: Address, adminAddressValue: Address) {
|
||||||
|
|
||||||
assertThat(result.isValid).isTrue()
|
assertThat(result.isValid).isTrue()
|
||||||
assertThat(result.orThrow()).satisfies { value ->
|
assertThat(result.value()).satisfies { value ->
|
||||||
|
|
||||||
assertThat(value.principal).isEqualTo(principalAddressValue)
|
assertThat(value.principal).isEqualTo(principalAddressValue)
|
||||||
assertThat(value.admin).isEqualTo(adminAddressValue)
|
assertThat(value.admin).isEqualTo(adminAddressValue)
|
||||||
@ -94,7 +94,7 @@ class VersionedParsingExampleTest {
|
|||||||
val adminAddress = addressFor(adminHost, adminPort)
|
val adminAddress = addressFor(adminHost, adminPort)
|
||||||
|
|
||||||
return if (principalAddress.isValid && adminAddress.isValid) {
|
return if (principalAddress.isValid && adminAddress.isValid) {
|
||||||
return valid(RpcSettings(principalAddress.value, adminAddress.value))
|
return valid(RpcSettings(principalAddress.value(), adminAddress.value()))
|
||||||
} else {
|
} else {
|
||||||
invalid(principalAddress.errors + adminAddress.errors)
|
invalid(principalAddress.errors + adminAddress.errors)
|
||||||
}
|
}
|
||||||
@ -128,4 +128,4 @@ class VersionedParsingExampleTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Configuration.Version.Extractor.parseRequired(config: Config, options: Configuration.Validation.Options = Configuration.Validation.Options.defaults) = parse(config, options).map { it ?: throw IllegalStateException("Absent version value.") }
|
private fun Configuration.Version.Extractor.parseRequired(config: Config, options: Configuration.Validation.Options = Configuration.Validation.Options.defaults) = parse(config, options).map { it }
|
@ -11,7 +11,7 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
jar {
|
jar {
|
||||||
baseName 'common-validation'
|
baseName 'corda-common-validation'
|
||||||
}
|
}
|
||||||
|
|
||||||
publish {
|
publish {
|
||||||
|
@ -8,11 +8,11 @@ import java.util.Collections.emptySet
|
|||||||
*/
|
*/
|
||||||
interface Validated<TARGET, ERROR> {
|
interface Validated<TARGET, ERROR> {
|
||||||
/**
|
/**
|
||||||
* The valid [TARGET] value.
|
* Returns a valid [TARGET] if no validation errors are present. Otherwise, it throws the exception produced by [exceptionOnErrors], defaulting to [IllegalStateException].
|
||||||
*
|
*
|
||||||
* @throws IllegalStateException if accessed in presence of validation errors.
|
* @throws IllegalStateException or the result of [exceptionOnErrors] if there are errors.
|
||||||
*/
|
*/
|
||||||
val value: TARGET
|
fun value(exceptionOnErrors: (Set<ERROR>) -> Exception = { errors -> IllegalStateException(errors.joinToString(System.lineSeparator())) }): TARGET
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The errors produced during validation, if any.
|
* The errors produced during validation, if any.
|
||||||
@ -32,14 +32,7 @@ interface Validated<TARGET, ERROR> {
|
|||||||
/**
|
/**
|
||||||
* Returns the underlying value as optional, with a null result instead of an exception if validation rules were violated.
|
* Returns the underlying value as optional, with a null result instead of an exception if validation rules were violated.
|
||||||
*/
|
*/
|
||||||
val optional: TARGET? get() = if (isValid) value else null
|
val optional: TARGET? get() = if (isValid) value() else null
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a valid [TARGET] if no validation errors are present. Otherwise, it throws the exception produced by [exceptionOnErrors], defaulting to [IllegalStateException].
|
|
||||||
*
|
|
||||||
* @throws IllegalStateException or the result of [exceptionOnErrors] if there are errors.
|
|
||||||
*/
|
|
||||||
fun orThrow(exceptionOnErrors: (Set<ERROR>) -> Exception = { errors -> IllegalStateException(errors.joinToString(System.lineSeparator())) }): TARGET
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies the [convert] function to the [TARGET] value, if valid. Otherwise, returns a [Validated] monad with a [MAPPED] generic type and the current errors set.
|
* Applies the [convert] function to the [TARGET] value, if valid. Otherwise, returns a [Validated] monad with a [MAPPED] generic type and the current errors set.
|
||||||
@ -62,7 +55,7 @@ interface Validated<TARGET, ERROR> {
|
|||||||
*/
|
*/
|
||||||
fun doIfValid(action: (TARGET) -> Unit): Validated<TARGET, ERROR> {
|
fun doIfValid(action: (TARGET) -> Unit): Validated<TARGET, ERROR> {
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
action.invoke(value)
|
action.invoke(value())
|
||||||
}
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
@ -110,10 +103,10 @@ interface Validated<TARGET, ERROR> {
|
|||||||
/**
|
/**
|
||||||
* A successful validation result, containing a valid [TARGET] value and no [ERROR]s.
|
* A successful validation result, containing a valid [TARGET] value and no [ERROR]s.
|
||||||
*/
|
*/
|
||||||
class Successful<TARGET, ERROR>(override val value: TARGET) : Result<TARGET, ERROR>(), Validated<TARGET, ERROR> {
|
class Successful<TARGET, ERROR>(private val value: TARGET) : Result<TARGET, ERROR>(), Validated<TARGET, ERROR> {
|
||||||
override val errors: Set<ERROR> = emptySet<ERROR>()
|
override val errors: Set<ERROR> = emptySet<ERROR>()
|
||||||
|
|
||||||
override fun orThrow(exceptionOnErrors: (Set<ERROR>) -> Exception) = value
|
override fun value(exceptionOnErrors: (Set<ERROR>) -> Exception) = value
|
||||||
|
|
||||||
override fun <MAPPED> map(convert: (TARGET) -> MAPPED): Validated<MAPPED, ERROR> {
|
override fun <MAPPED> map(convert: (TARGET) -> MAPPED): Validated<MAPPED, ERROR> {
|
||||||
return valid(convert.invoke(value))
|
return valid(convert.invoke(value))
|
||||||
@ -136,9 +129,7 @@ interface Validated<TARGET, ERROR> {
|
|||||||
require(errors.isNotEmpty())
|
require(errors.isNotEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
override val value: TARGET get() = throw IllegalStateException("Invalid state.")
|
override fun value(exceptionOnErrors: (Set<ERROR>) -> Exception) = throw exceptionOnErrors.invoke(errors)
|
||||||
|
|
||||||
override fun orThrow(exceptionOnErrors: (Set<ERROR>) -> Exception) = throw exceptionOnErrors.invoke(errors)
|
|
||||||
|
|
||||||
override fun <MAPPED> map(convert: (TARGET) -> MAPPED): Validated<MAPPED, ERROR> {
|
override fun <MAPPED> map(convert: (TARGET) -> MAPPED): Validated<MAPPED, ERROR> {
|
||||||
return invalid(errors)
|
return invalid(errors)
|
||||||
|
@ -4,6 +4,7 @@ import net.corda.core.KeepForDJVM
|
|||||||
import net.corda.core.internal.extractFile
|
import net.corda.core.internal.extractFile
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
@ -36,10 +37,10 @@ interface Attachment : NamedByHash {
|
|||||||
@JvmDefault
|
@JvmDefault
|
||||||
fun openAsJAR(): JarInputStream {
|
fun openAsJAR(): JarInputStream {
|
||||||
val stream = open()
|
val stream = open()
|
||||||
try {
|
return try {
|
||||||
return JarInputStream(stream)
|
JarInputStream(stream)
|
||||||
} catch (t: Throwable) {
|
} catch (e: IOException) {
|
||||||
stream.use { throw t }
|
stream.use { throw e }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,6 +149,16 @@ abstract class TransactionVerificationException(val txId: SecureHash, message: S
|
|||||||
"is not satisfied. Encumbered states should also be referenced as an encumbrance of another state to form " +
|
"is not satisfied. Encumbered states should also be referenced as an encumbrance of another state to form " +
|
||||||
"a full cycle. Offending indices $nonMatching", null)
|
"a full cycle. Offending indices $nonMatching", null)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All encumbered states should be assigned to the same notary. This is due to the fact that multi-notary
|
||||||
|
* transactions are not supported and thus two encumbered states with different notaries cannot be consumed
|
||||||
|
* in the same transaction.
|
||||||
|
*/
|
||||||
|
@KeepForDJVM
|
||||||
|
class TransactionNotaryMismatchEncumbranceException(txId: SecureHash, encumberedIndex: Int, encumbranceIndex: Int, encumberedNotary: Party, encumbranceNotary: Party)
|
||||||
|
: TransactionVerificationException(txId, "Encumbered output states assigned to different notaries found. " +
|
||||||
|
"Output state with index $encumberedIndex is assigned to notary [$encumberedNotary], while its encumbrance with index $encumbranceIndex is assigned to notary [$encumbranceNotary]", null)
|
||||||
|
|
||||||
/** Whether the inputs or outputs list contains an encumbrance issue, see [TransactionMissingEncumbranceException]. */
|
/** Whether the inputs or outputs list contains an encumbrance issue, see [TransactionMissingEncumbranceException]. */
|
||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
@KeepForDJVM
|
@KeepForDJVM
|
||||||
|
@ -49,6 +49,7 @@ sealed class NotaryError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Occurs when the transaction sent for notarisation is assigned to a different notary identity. */
|
/** Occurs when the transaction sent for notarisation is assigned to a different notary identity. */
|
||||||
|
@Deprecated("Deprecated since platform version 4. This object is no longer used, [TransactionInvalid] will be reported in case of notary mismatch")
|
||||||
object WrongNotary : NotaryError()
|
object WrongNotary : NotaryError()
|
||||||
|
|
||||||
/** Occurs when the notarisation request signature does not verify for the provided transaction. */
|
/** Occurs when the notarisation request signature does not verify for the provided transaction. */
|
||||||
|
@ -72,9 +72,9 @@ object JarSignatureCollector {
|
|||||||
private fun Sequence<JarEntry>.toFileSignerSet(): Sequence<Pair<String, Set<CodeSigner>>> =
|
private fun Sequence<JarEntry>.toFileSignerSet(): Sequence<Pair<String, Set<CodeSigner>>> =
|
||||||
map { entry -> entry.name to (entry.codeSigners?.toSet() ?: emptySet()) }
|
map { entry -> entry.name to (entry.codeSigners?.toSet() ?: emptySet()) }
|
||||||
|
|
||||||
private fun Set<CodeSigner>.toOrderedPublicKeys(): List<PublicKey> = map {
|
private fun Set<CodeSigner>.toPartiesOrderedByName(): List<Party> = map {
|
||||||
(it.signerCertPath.certificates[0] as X509Certificate).publicKey
|
Party(it.signerCertPath.certificates[0] as X509Certificate)
|
||||||
}.sortedBy { it.hash} // Sorted for determinism.
|
}.sortedBy { it.name.toString() } // Sorted for determinism.
|
||||||
|
|
||||||
private fun Set<CodeSigner>.toPartiesOrderedByName(): List<Party> = map {
|
private fun Set<CodeSigner>.toPartiesOrderedByName(): List<Party> = map {
|
||||||
Party(it.signerCertPath.certificates[0] as X509Certificate)
|
Party(it.signerCertPath.certificates[0] as X509Certificate)
|
||||||
|
@ -71,8 +71,8 @@ fun <V, W> CordaFuture<out V>.flatMap(transform: (V) -> CordaFuture<out W>): Cor
|
|||||||
thenMatch(success@ {
|
thenMatch(success@ {
|
||||||
result.captureLater(try {
|
result.captureLater(try {
|
||||||
transform(it)
|
transform(it)
|
||||||
} catch (t: Throwable) {
|
} catch (e: Exception) {
|
||||||
result.setException(t)
|
result.setException(e)
|
||||||
return@success
|
return@success
|
||||||
})
|
})
|
||||||
}, {
|
}, {
|
||||||
@ -128,8 +128,8 @@ interface ValueOrException<in V> {
|
|||||||
fun capture(block: () -> V): Boolean {
|
fun capture(block: () -> V): Boolean {
|
||||||
return set(try {
|
return set(try {
|
||||||
block()
|
block()
|
||||||
} catch (t: Throwable) {
|
} catch (e: Exception) {
|
||||||
return setException(t)
|
return setException(e)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -153,8 +153,8 @@ internal class CordaFutureImpl<V>(private val impl: CompletableFuture<V> = Compl
|
|||||||
impl.whenComplete { _, _ ->
|
impl.whenComplete { _, _ ->
|
||||||
try {
|
try {
|
||||||
callback(this)
|
callback(this)
|
||||||
} catch (t: Throwable) {
|
} catch (e: Exception) {
|
||||||
log.error(listenerFailedMessage, t)
|
log.error(listenerFailedMessage, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,61 +24,84 @@ abstract class NotaryServiceFlow(val otherSideSession: FlowSession, val service:
|
|||||||
private const val maxAllowedInputsAndReferences = 10_000
|
private const val maxAllowedInputsAndReferences = 10_000
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var transactionId: SecureHash? = null
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call(): Void? {
|
override fun call(): Void? {
|
||||||
check(serviceHub.myInfo.legalIdentities.any { serviceHub.networkMapCache.isNotary(it) }) {
|
check(serviceHub.myInfo.legalIdentities.any { serviceHub.networkMapCache.isNotary(it) }) {
|
||||||
"We are not a notary on the network"
|
"We are not a notary on the network"
|
||||||
}
|
}
|
||||||
val requestPayload = otherSideSession.receive<NotarisationPayload>().unwrap { it }
|
val requestPayload = otherSideSession.receive<NotarisationPayload>().unwrap { it }
|
||||||
var txId: SecureHash? = null
|
|
||||||
try {
|
try {
|
||||||
val parts = validateRequest(requestPayload)
|
val tx: TransactionParts = validateRequest(requestPayload)
|
||||||
txId = parts.id
|
val request = NotarisationRequest(tx.inputs, tx.id)
|
||||||
checkNotary(parts.notary)
|
validateRequestSignature(request, requestPayload.requestSignature)
|
||||||
|
|
||||||
|
verifyTransaction(requestPayload)
|
||||||
|
|
||||||
service.commitInputStates(
|
service.commitInputStates(
|
||||||
parts.inputs,
|
tx.inputs,
|
||||||
txId,
|
tx.id,
|
||||||
otherSideSession.counterparty,
|
otherSideSession.counterparty,
|
||||||
requestPayload.requestSignature,
|
requestPayload.requestSignature,
|
||||||
parts.timestamp,
|
tx.timeWindow,
|
||||||
parts.references
|
tx.references)
|
||||||
)
|
|
||||||
signTransactionAndSendResponse(txId)
|
|
||||||
} catch (e: NotaryInternalException) {
|
} catch (e: NotaryInternalException) {
|
||||||
throw NotaryException(e.error, txId)
|
logError(e.error)
|
||||||
|
// Any exception that's not a NotaryInternalException is assumed to be an unexpected internal error
|
||||||
|
// that is not relayed back to the client.
|
||||||
|
throw NotaryException(e.error, transactionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signTransactionAndSendResponse(transactionId!!)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Checks whether the number of input states is too large. */
|
private fun validateRequest(requestPayload: NotarisationPayload): TransactionParts {
|
||||||
protected fun checkInputs(inputs: List<StateRef>) {
|
try {
|
||||||
if (inputs.size > maxAllowedInputsAndReferences) {
|
val transaction = extractParts(requestPayload)
|
||||||
val error = NotaryError.TransactionInvalid(
|
transactionId = transaction.id
|
||||||
IllegalArgumentException("A transaction cannot have more than $maxAllowedInputsAndReferences " +
|
checkNotary(transaction.notary)
|
||||||
"inputs or references, received: ${inputs.size}")
|
checkInputs(transaction.inputs + transaction.references)
|
||||||
)
|
return transaction
|
||||||
|
} catch (e: Exception) {
|
||||||
|
val error = NotaryError.TransactionInvalid(e)
|
||||||
throw NotaryInternalException(error)
|
throw NotaryInternalException(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Extract the common transaction components required for notarisation. */
|
||||||
* Implement custom logic to perform transaction verification based on validity and privacy requirements.
|
protected abstract fun extractParts(requestPayload: NotarisationPayload): TransactionParts
|
||||||
*/
|
|
||||||
|
/** Check if transaction is intended to be signed by this notary. */
|
||||||
@Suspendable
|
@Suspendable
|
||||||
protected abstract fun validateRequest(requestPayload: NotarisationPayload): TransactionParts
|
private fun checkNotary(notary: Party?) {
|
||||||
|
require(notary?.owningKey == service.notaryIdentityKey) {
|
||||||
|
"The notary specified on the transaction: [$notary] does not match the notary service's identity: [${service.notaryIdentityKey}] "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Checks whether the number of input states is too large. */
|
||||||
|
private fun checkInputs(inputs: List<StateRef>) {
|
||||||
|
require(inputs.size < maxAllowedInputsAndReferences) {
|
||||||
|
"A transaction cannot have more than $maxAllowedInputsAndReferences " +
|
||||||
|
"inputs or references, received: ${inputs.size}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Verifies that the correct notarisation request was signed by the counterparty. */
|
/** Verifies that the correct notarisation request was signed by the counterparty. */
|
||||||
protected fun validateRequestSignature(request: NotarisationRequest, signature: NotarisationRequestSignature) {
|
private fun validateRequestSignature(request: NotarisationRequest, signature: NotarisationRequestSignature) {
|
||||||
val requestingParty = otherSideSession.counterparty
|
val requestingParty = otherSideSession.counterparty
|
||||||
request.verifySignature(signature, requestingParty)
|
request.verifySignature(signature, requestingParty)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Check if transaction is intended to be signed by this notary. */
|
/**
|
||||||
|
* Override to implement custom logic to perform transaction verification based on validity and privacy requirements.
|
||||||
|
*/
|
||||||
@Suspendable
|
@Suspendable
|
||||||
protected fun checkNotary(notary: Party?) {
|
protected open fun verifyTransaction(requestPayload: NotarisationPayload) {
|
||||||
if (notary?.owningKey != service.notaryIdentityKey) {
|
|
||||||
throw NotaryInternalException(NotaryError.WrongNotary)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
@ -91,15 +114,23 @@ abstract class NotaryServiceFlow(val otherSideSession: FlowSession, val service:
|
|||||||
* The minimum amount of information needed to notarise a transaction. Note that this does not include
|
* The minimum amount of information needed to notarise a transaction. Note that this does not include
|
||||||
* any sensitive transaction details.
|
* any sensitive transaction details.
|
||||||
*/
|
*/
|
||||||
protected data class TransactionParts @JvmOverloads constructor(
|
protected data class TransactionParts(
|
||||||
val id: SecureHash,
|
val id: SecureHash,
|
||||||
val inputs: List<StateRef>,
|
val inputs: List<StateRef>,
|
||||||
val timestamp: TimeWindow?,
|
val timeWindow: TimeWindow?,
|
||||||
val notary: Party?,
|
val notary: Party?,
|
||||||
val references: List<StateRef> = emptyList()
|
val references: List<StateRef> = emptyList()
|
||||||
) {
|
)
|
||||||
fun copy(id: SecureHash, inputs: List<StateRef>, timestamp: TimeWindow?, notary: Party?): TransactionParts {
|
|
||||||
return TransactionParts(id, inputs, timestamp, notary, references)
|
private fun logError(error: NotaryError) {
|
||||||
|
val errorCause = when (error) {
|
||||||
|
is NotaryError.RequestSignatureInvalid -> error.cause
|
||||||
|
is NotaryError.TransactionInvalid -> error.cause
|
||||||
|
is NotaryError.General -> error.cause
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (errorCause != null) {
|
||||||
|
logger.error("Error notarising transaction $transactionId", errorCause)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,43 +2,32 @@ package net.corda.core.internal.notary
|
|||||||
|
|
||||||
import net.corda.core.contracts.StateRef
|
import net.corda.core.contracts.StateRef
|
||||||
import net.corda.core.contracts.TimeWindow
|
import net.corda.core.contracts.TimeWindow
|
||||||
import net.corda.core.crypto.DigitalSignature
|
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.isFulfilledBy
|
import net.corda.core.crypto.isFulfilledBy
|
||||||
import net.corda.core.flows.*
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.node.ServiceHub
|
import net.corda.core.node.ServiceHub
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import java.security.InvalidKeyException
|
import net.corda.core.utilities.toBase58String
|
||||||
import java.security.SignatureException
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
/** Verifies the signature against this notarisation request. Checks that the signature is issued by the right party. */
|
/** Verifies the signature against this notarisation request. Checks that the signature is issued by the right party. */
|
||||||
fun NotarisationRequest.verifySignature(requestSignature: NotarisationRequestSignature, intendedSigner: Party) {
|
fun NotarisationRequest.verifySignature(requestSignature: NotarisationRequestSignature, intendedSigner: Party) {
|
||||||
|
try {
|
||||||
val signature = requestSignature.digitalSignature
|
val signature = requestSignature.digitalSignature
|
||||||
if (intendedSigner.owningKey != signature.by) {
|
require(intendedSigner.owningKey == signature.by) {
|
||||||
val errorMessage = "Expected a signature by ${intendedSigner.owningKey}, but received by ${signature.by}}"
|
"Expected a signature by ${intendedSigner.owningKey.toBase58String()}, but received by ${signature.by.toBase58String()}}"
|
||||||
throw NotaryInternalException(NotaryError.RequestSignatureInvalid(IllegalArgumentException(errorMessage)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: if requestSignature was generated over an old version of NotarisationRequest, we need to be able to
|
// TODO: if requestSignature was generated over an old version of NotarisationRequest, we need to be able to
|
||||||
// reserialize it in that version to get the exact same bytes. Modify the serialization logic once that's
|
// reserialize it in that version to get the exact same bytes. Modify the serialization logic once that's
|
||||||
// available.
|
// available.
|
||||||
val expectedSignedBytes = this.serialize().bytes
|
val expectedSignedBytes = this.serialize().bytes
|
||||||
verifyCorrectBytesSigned(signature, expectedSignedBytes)
|
signature.verify(expectedSignedBytes)
|
||||||
}
|
|
||||||
|
|
||||||
private fun verifyCorrectBytesSigned(signature: DigitalSignature.WithKey, bytes: ByteArray) {
|
|
||||||
try {
|
|
||||||
signature.verify(bytes)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
when (e) {
|
|
||||||
is InvalidKeyException, is SignatureException -> {
|
|
||||||
val error = NotaryError.RequestSignatureInvalid(e)
|
val error = NotaryError.RequestSignatureInvalid(e)
|
||||||
throw NotaryInternalException(error)
|
throw NotaryInternalException(error)
|
||||||
}
|
}
|
||||||
else -> throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -53,6 +53,7 @@ abstract class SinglePartyNotaryService : NotaryService() {
|
|||||||
references
|
references
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (result is UniquenessProvider.Result.Failure) {
|
if (result is UniquenessProvider.Result.Failure) {
|
||||||
throw NotaryInternalException(result.error)
|
throw NotaryInternalException(result.error)
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,8 @@ import net.corda.core.transactions.TransactionBuilder
|
|||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.sql.Connection
|
import java.sql.Connection
|
||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
|
import java.util.function.Consumer
|
||||||
|
import javax.persistence.EntityManager
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subset of node services that are used for loading transactions from the wire into fully resolved, looked up
|
* Subset of node services that are used for loading transactions from the wire into fully resolved, looked up
|
||||||
@ -358,6 +360,28 @@ interface ServiceHub : ServicesForResolution {
|
|||||||
*/
|
*/
|
||||||
fun jdbcSession(): Connection
|
fun jdbcSession(): Connection
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposes the Java Persistence API (JPA) to flows via a restricted [EntityManager]. This method can be used to
|
||||||
|
* persist and query entities which inherit from [MappedSchema]. This is particularly useful if off-ledger data
|
||||||
|
* needs to be kept in conjunction with on-ledger state data.
|
||||||
|
*
|
||||||
|
* NOTE: Suspendable flow operations such as send, receive, subFlow and sleep, cannot be called within the lambda.
|
||||||
|
*
|
||||||
|
* @param block a lambda function with access to an [EntityManager].
|
||||||
|
*/
|
||||||
|
fun <T : Any> withEntityManager(block: EntityManager.() -> T): T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposes the Java Persistence API (JPA) to flows via a restricted [EntityManager]. This method can be used to
|
||||||
|
* persist and query entities which inherit from [MappedSchema]. This is particularly useful if off-ledger data
|
||||||
|
* needs to be kept in conjunction with on-ledger state data.
|
||||||
|
*
|
||||||
|
* NOTE: Suspendable flow operations such as send, receive, subFlow and sleep, cannot be called within the lambda.
|
||||||
|
*
|
||||||
|
* @param block a lambda function with access to an [EntityManager].
|
||||||
|
*/
|
||||||
|
fun withEntityManager(block: Consumer<EntityManager>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows the registration of a callback that may inform services when the app is shutting down.
|
* Allows the registration of a callback that may inform services when the app is shutting down.
|
||||||
*
|
*
|
||||||
|
@ -11,9 +11,11 @@ import net.corda.core.internal.checkMinimumPlatformVersion
|
|||||||
import net.corda.core.internal.uncheckedCast
|
import net.corda.core.internal.uncheckedCast
|
||||||
import net.corda.core.node.NetworkParameters
|
import net.corda.core.node.NetworkParameters
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
import net.corda.core.utilities.Try
|
||||||
import net.corda.core.utilities.loggerFor
|
import net.corda.core.utilities.loggerFor
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.function.Predicate
|
import java.util.function.Predicate
|
||||||
|
import kotlin.collections.HashSet
|
||||||
import net.corda.core.utilities.warnOnce
|
import net.corda.core.utilities.warnOnce
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -58,6 +60,17 @@ data class LedgerTransaction @JvmOverloads constructor(
|
|||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
val logger = loggerFor<LedgerTransaction>()
|
val logger = loggerFor<LedgerTransaction>()
|
||||||
|
private fun contractClassFor(className: ContractClassName, classLoader: ClassLoader?): Try<Class<out Contract>> {
|
||||||
|
return Try.on {
|
||||||
|
(classLoader ?: this::class.java.classLoader)
|
||||||
|
.loadClass(className)
|
||||||
|
.asSubclass(Contract::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stateToContractClass(state: TransactionState<ContractState>): Try<Class<out Contract>> {
|
||||||
|
return contractClassFor(state.contract, state.data::class.java.classLoader)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val inputStates: List<ContractState> get() = inputs.map { it.state.data }
|
val inputStates: List<ContractState> get() = inputs.map { it.state.data }
|
||||||
@ -91,25 +104,7 @@ data class LedgerTransaction @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For all input and output [TransactionState]s, validates that the wrapped [ContractState] matches up with the
|
* Verify that package ownership is respected.
|
||||||
* wrapped [Contract], as declared by the [BelongsToContract] annotation on the [ContractState]'s class.
|
|
||||||
*
|
|
||||||
* A warning will be written to the log if any mismatch is detected.
|
|
||||||
*/
|
|
||||||
private fun validateStatesAgainstContract() = allStates.forEach(::validateStateAgainstContract)
|
|
||||||
|
|
||||||
private fun validateStateAgainstContract(state: TransactionState<ContractState>) {
|
|
||||||
state.data.requiredContractClassName?.let { requiredContractClassName ->
|
|
||||||
if (state.contract != requiredContractClassName)
|
|
||||||
logger.warnOnce("""
|
|
||||||
State of class ${state.data::class.java.typeName} belongs to contract $requiredContractClassName, but
|
|
||||||
is bundled in TransactionState with ${state.contract}.
|
|
||||||
""".trimIndent().replace('\n', ' '))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify that for each contract the network wide package owner is respected.
|
|
||||||
*
|
*
|
||||||
* TODO - revisit once transaction contains network parameters.
|
* TODO - revisit once transaction contains network parameters.
|
||||||
*/
|
*/
|
||||||
@ -132,6 +127,24 @@ data class LedgerTransaction @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For all input and output [TransactionState]s, validates that the wrapped [ContractState] matches up with the
|
||||||
|
* wrapped [Contract], as declared by the [BelongsToContract] annotation on the [ContractState]'s class.
|
||||||
|
*
|
||||||
|
* A warning will be written to the log if any mismatch is detected.
|
||||||
|
*/
|
||||||
|
private fun validateStatesAgainstContract() = allStates.forEach(::validateStateAgainstContract)
|
||||||
|
|
||||||
|
private fun validateStateAgainstContract(state: TransactionState<ContractState>) {
|
||||||
|
state.data.requiredContractClassName?.let { requiredContractClassName ->
|
||||||
|
if (state.contract != requiredContractClassName)
|
||||||
|
logger.warnOnce("""
|
||||||
|
State of class ${state.data::class.java.typeName} belongs to contract $requiredContractClassName, but
|
||||||
|
is bundled in TransactionState with ${state.contract}.
|
||||||
|
""".trimIndent().replace('\n', ' '))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enforces the validity of the actual constraints.
|
* Enforces the validity of the actual constraints.
|
||||||
* * Constraints should be one of the valid supported ones.
|
* * Constraints should be one of the valid supported ones.
|
||||||
@ -181,6 +194,9 @@ data class LedgerTransaction @JvmOverloads constructor(
|
|||||||
val constraintAttachment = AttachmentWithContext(contractAttachment, state.contract,
|
val constraintAttachment = AttachmentWithContext(contractAttachment, state.contract,
|
||||||
networkParameters?.whitelistedContractImplementations)
|
networkParameters?.whitelistedContractImplementations)
|
||||||
|
|
||||||
|
if (state.constraint is SignatureAttachmentConstraint)
|
||||||
|
checkMinimumPlatformVersion(networkParameters?.minimumPlatformVersion ?: 1, 4, "Signature constraints")
|
||||||
|
|
||||||
if (!state.constraint.isSatisfiedBy(constraintAttachment)) {
|
if (!state.constraint.isSatisfiedBy(constraintAttachment)) {
|
||||||
throw TransactionVerificationException.ContractConstraintRejection(id, state.contract)
|
throw TransactionVerificationException.ContractConstraintRejection(id, state.contract)
|
||||||
}
|
}
|
||||||
@ -268,10 +284,39 @@ data class LedgerTransaction @JvmOverloads constructor(
|
|||||||
// Check that in the outputs,
|
// Check that in the outputs,
|
||||||
// a) an encumbered state does not refer to itself as the encumbrance
|
// a) an encumbered state does not refer to itself as the encumbrance
|
||||||
// b) the number of outputs can contain the encumbrance
|
// b) the number of outputs can contain the encumbrance
|
||||||
// c) the bi-directionality (full cycle) property is satisfied.
|
// c) the bi-directionality (full cycle) property is satisfied
|
||||||
|
// d) encumbered output states are assigned to the same notary.
|
||||||
val statesAndEncumbrance = outputs.withIndex().filter { it.value.encumbrance != null }.map { Pair(it.index, it.value.encumbrance!!) }
|
val statesAndEncumbrance = outputs.withIndex().filter { it.value.encumbrance != null }.map { Pair(it.index, it.value.encumbrance!!) }
|
||||||
if (!statesAndEncumbrance.isEmpty()) {
|
if (!statesAndEncumbrance.isEmpty()) {
|
||||||
checkOutputEncumbrances(statesAndEncumbrance)
|
checkBidirectionalOutputEncumbrances(statesAndEncumbrance)
|
||||||
|
checkNotariesOutputEncumbrance(statesAndEncumbrance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to check if all encumbered states are assigned to the same notary Party.
|
||||||
|
// This method should be invoked after [checkBidirectionalOutputEncumbrances], because it assumes that the
|
||||||
|
// bi-directionality property is already satisfied.
|
||||||
|
private fun checkNotariesOutputEncumbrance(statesAndEncumbrance: List<Pair<Int, Int>>) {
|
||||||
|
// We only check for transactions in which notary is null (i.e., issuing transactions).
|
||||||
|
// Note that if a notary is defined for a transaction, we already check if all outputs are assigned
|
||||||
|
// to the same notary (transaction's notary) in [checkNoNotaryChange()].
|
||||||
|
if (notary == null) {
|
||||||
|
// indicesAlreadyChecked is used to bypass already checked indices and to avoid cycles.
|
||||||
|
val indicesAlreadyChecked = HashSet<Int>()
|
||||||
|
statesAndEncumbrance.forEach {
|
||||||
|
checkNotary(it.first, indicesAlreadyChecked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private tailrec fun checkNotary(index: Int, indicesAlreadyChecked: HashSet<Int>) {
|
||||||
|
if (indicesAlreadyChecked.add(index)) {
|
||||||
|
val encumbranceIndex = outputs[index].encumbrance!!
|
||||||
|
if (outputs[index].notary != outputs[encumbranceIndex].notary) {
|
||||||
|
throw TransactionVerificationException.TransactionNotaryMismatchEncumbranceException(id, index, encumbranceIndex, outputs[index].notary, outputs[encumbranceIndex].notary)
|
||||||
|
} else {
|
||||||
|
checkNotary(encumbranceIndex, indicesAlreadyChecked)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -309,7 +354,7 @@ data class LedgerTransaction @JvmOverloads constructor(
|
|||||||
// b -> c and c -> b
|
// b -> c and c -> b
|
||||||
// c -> a b -> a
|
// c -> a b -> a
|
||||||
// and form a full cycle, meaning that the bi-directionality property is satisfied.
|
// and form a full cycle, meaning that the bi-directionality property is satisfied.
|
||||||
private fun checkOutputEncumbrances(statesAndEncumbrance: List<Pair<Int, Int>>) {
|
private fun checkBidirectionalOutputEncumbrances(statesAndEncumbrance: List<Pair<Int, Int>>) {
|
||||||
// [Set] of "from" (encumbered states).
|
// [Set] of "from" (encumbered states).
|
||||||
val encumberedSet = mutableSetOf<Int>()
|
val encumberedSet = mutableSetOf<Int>()
|
||||||
// [Set] of "to" (encumbrance states).
|
// [Set] of "to" (encumbrance states).
|
||||||
|
@ -265,8 +265,11 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
|
|||||||
override fun toString(): String = "${javaClass.simpleName}(id=$id)"
|
override fun toString(): String = "${javaClass.simpleName}(id=$id)"
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
private fun missingSignatureMsg(missing: Set<PublicKey>, descriptions: List<String>, id: SecureHash): String =
|
private fun missingSignatureMsg(missing: Set<PublicKey>, descriptions: List<String>, id: SecureHash): String {
|
||||||
"Missing signatures for $descriptions on transaction ${id.prefixChars()} for ${missing.joinToString()}"
|
return "Missing signatures on transaction ${id.prefixChars()} for " +
|
||||||
|
"keys: ${missing.joinToString { it.toStringShort() }}, " +
|
||||||
|
"by signers: ${descriptions.joinToString()} "
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@KeepForDJVM
|
@KeepForDJVM
|
||||||
|
@ -9,6 +9,7 @@ import net.corda.core.crypto.SecureHash
|
|||||||
import net.corda.core.crypto.SignableData
|
import net.corda.core.crypto.SignableData
|
||||||
import net.corda.core.crypto.SignatureMetadata
|
import net.corda.core.crypto.SignatureMetadata
|
||||||
import net.corda.core.crypto.keys
|
import net.corda.core.crypto.keys
|
||||||
|
import net.corda.core.crypto.*
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.AttachmentWithContext
|
import net.corda.core.internal.AttachmentWithContext
|
||||||
import net.corda.core.internal.FlowStateMachine
|
import net.corda.core.internal.FlowStateMachine
|
||||||
@ -24,6 +25,8 @@ import net.corda.core.node.services.KeyManagementService
|
|||||||
import net.corda.core.serialization.SerializationContext
|
import net.corda.core.serialization.SerializationContext
|
||||||
import net.corda.core.serialization.SerializationFactory
|
import net.corda.core.serialization.SerializationFactory
|
||||||
import net.corda.core.utilities.loggerFor
|
import net.corda.core.utilities.loggerFor
|
||||||
|
import net.corda.core.utilities.contextLogger
|
||||||
|
import net.corda.core.utilities.warnOnce
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@ -89,7 +92,7 @@ open class TransactionBuilder @JvmOverloads constructor(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
val logger = loggerFor<TransactionBuilder>()
|
private val log = contextLogger()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val inputsWithTransactionState = arrayListOf<TransactionState<ContractState>>()
|
private val inputsWithTransactionState = arrayListOf<TransactionState<ContractState>>()
|
||||||
@ -213,14 +216,16 @@ open class TransactionBuilder @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For each contract, resolve the AutomaticPlaceholderConstraint, and select the attachment.
|
// For each contract, resolve the AutomaticPlaceholderConstraint, and select the attachment.
|
||||||
val contractAttachmentsAndResolvedOutputStates: List<Pair<AttachmentId, List<TransactionState<ContractState>>?>> = allContracts.toSet().map { ctr ->
|
val contractAttachmentsAndResolvedOutputStates: List<Pair<AttachmentId, List<TransactionState<ContractState>>?>> = allContracts.toSet()
|
||||||
|
.map { ctr ->
|
||||||
handleContract(ctr, inputContractGroups[ctr], outputContractGroups[ctr], explicitAttachmentContractsMap[ctr], serializationContext, services)
|
handleContract(ctr, inputContractGroups[ctr], outputContractGroups[ctr], explicitAttachmentContractsMap[ctr], serializationContext, services)
|
||||||
}
|
}
|
||||||
|
|
||||||
val resolvedStates: List<TransactionState<ContractState>> = contractAttachmentsAndResolvedOutputStates.mapNotNull { it.second }.flatten()
|
val resolvedStates: List<TransactionState<ContractState>> = contractAttachmentsAndResolvedOutputStates.mapNotNull { it.second }
|
||||||
|
.flatten()
|
||||||
|
|
||||||
// The output states need to preserve the order in which they were added.
|
// The output states need to preserve the order in which they were added.
|
||||||
val resolvedOutputStatesInTheOriginalOrder: List<TransactionState<ContractState>> = outputStates().map { os -> resolvedStates.find { rs -> rs.data == os.data && rs.encumbrance == os.encumbrance}!! }
|
val resolvedOutputStatesInTheOriginalOrder: List<TransactionState<ContractState>> = outputStates().map { os -> resolvedStates.find { rs -> rs.data == os.data && rs.encumbrance == os.encumbrance }!! }
|
||||||
|
|
||||||
val attachments: Collection<AttachmentId> = contractAttachmentsAndResolvedOutputStates.map { it.first } + refStateContractAttachments
|
val attachments: Collection<AttachmentId> = contractAttachmentsAndResolvedOutputStates.map { it.first } + refStateContractAttachments
|
||||||
|
|
||||||
@ -342,8 +347,18 @@ open class TransactionBuilder @JvmOverloads constructor(
|
|||||||
attachmentToUse: ContractAttachment,
|
attachmentToUse: ContractAttachment,
|
||||||
services: ServicesForResolution): AttachmentConstraint = when {
|
services: ServicesForResolution): AttachmentConstraint = when {
|
||||||
inputStates != null -> attachmentConstraintsTransition(inputStates.groupBy { it.constraint }.keys, attachmentToUse)
|
inputStates != null -> attachmentConstraintsTransition(inputStates.groupBy { it.constraint }.keys, attachmentToUse)
|
||||||
useWhitelistedByZoneAttachmentConstraint(contractClassName, services.networkParameters) -> WhitelistedByZoneAttachmentConstraint
|
attachmentToUse.signers.isNotEmpty() && services.networkParameters.minimumPlatformVersion < 4 -> {
|
||||||
|
log.warnOnce("Signature constraints not available on network requiring a minimum platform version of 4. Current is: ${services.networkParameters.minimumPlatformVersion}.")
|
||||||
|
if (useWhitelistedByZoneAttachmentConstraint(contractClassName, services.networkParameters)) {
|
||||||
|
log.warnOnce("Reverting back to using whitelisted zone constraints for contract $contractClassName")
|
||||||
|
WhitelistedByZoneAttachmentConstraint
|
||||||
|
} else {
|
||||||
|
log.warnOnce("Reverting back to using hash constraints for contract $contractClassName")
|
||||||
|
HashAttachmentConstraint(attachmentToUse.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
attachmentToUse.signers.isNotEmpty() -> makeSignatureAttachmentConstraint(attachmentToUse.signers)
|
attachmentToUse.signers.isNotEmpty() -> makeSignatureAttachmentConstraint(attachmentToUse.signers)
|
||||||
|
useWhitelistedByZoneAttachmentConstraint(contractClassName, services.networkParameters) -> WhitelistedByZoneAttachmentConstraint
|
||||||
else -> HashAttachmentConstraint(attachmentToUse.id)
|
else -> HashAttachmentConstraint(attachmentToUse.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -469,7 +484,7 @@ open class TransactionBuilder @JvmOverloads constructor(
|
|||||||
addReferenceState(resolvedStateAndRef.referenced())
|
addReferenceState(resolvedStateAndRef.referenced())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.warn("WARNING: You must pass in a ServiceHub reference to TransactionBuilder to resolve " +
|
log.warn("WARNING: You must pass in a ServiceHub reference to TransactionBuilder to resolve " +
|
||||||
"state pointers outside of flows. If you are writing a unit test then pass in a " +
|
"state pointers outside of flows. If you are writing a unit test then pass in a " +
|
||||||
"MockServices instance.")
|
"MockServices instance.")
|
||||||
return
|
return
|
||||||
@ -539,7 +554,7 @@ open class TransactionBuilder @JvmOverloads constructor(
|
|||||||
state: ContractState,
|
state: ContractState,
|
||||||
contract: ContractClassName = requireNotNull(state.requiredContractClassName) {
|
contract: ContractClassName = requireNotNull(state.requiredContractClassName) {
|
||||||
//TODO: add link to docsite page, when there is one.
|
//TODO: add link to docsite page, when there is one.
|
||||||
"""
|
"""
|
||||||
Unable to infer Contract class name because state class ${state::class.java.name} is not annotated with
|
Unable to infer Contract class name because state class ${state::class.java.name} is not annotated with
|
||||||
@BelongsToContract, and does not have an enclosing class which implements Contract. Either annotate ${state::class.java.name}
|
@BelongsToContract, and does not have an enclosing class which implements Contract. Either annotate ${state::class.java.name}
|
||||||
with @BelongsToContract, or supply an explicit contract parameter to addOutputState().
|
with @BelongsToContract, or supply an explicit contract parameter to addOutputState().
|
||||||
@ -555,7 +570,7 @@ with @BelongsToContract, or supply an explicit contract parameter to addOutputSt
|
|||||||
state: ContractState,
|
state: ContractState,
|
||||||
contract: ContractClassName = requireNotNull(state.requiredContractClassName) {
|
contract: ContractClassName = requireNotNull(state.requiredContractClassName) {
|
||||||
//TODO: add link to docsite page, when there is one.
|
//TODO: add link to docsite page, when there is one.
|
||||||
"""
|
"""
|
||||||
Unable to infer Contract class name because state class ${state::class.java.name} is not annotated with
|
Unable to infer Contract class name because state class ${state::class.java.name} is not annotated with
|
||||||
@BelongsToContract, and does not have an enclosing class which implements Contract. Either annotate ${state::class.java.name}
|
@BelongsToContract, and does not have an enclosing class which implements Contract. Either annotate ${state::class.java.name}
|
||||||
with @BelongsToContract, or supply an explicit contract parameter to addOutputState().
|
with @BelongsToContract, or supply an explicit contract parameter to addOutputState().
|
||||||
|
@ -19,8 +19,8 @@ sealed class Try<out A> {
|
|||||||
inline fun <T> on(body: () -> T): Try<T> {
|
inline fun <T> on(body: () -> T): Try<T> {
|
||||||
return try {
|
return try {
|
||||||
Success(body())
|
Success(body())
|
||||||
} catch (t: Throwable) {
|
} catch (e: Exception) {
|
||||||
Failure(t)
|
Failure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,7 @@ package net.corda.core.transactions
|
|||||||
|
|
||||||
import com.nhaarman.mockito_kotlin.doReturn
|
import com.nhaarman.mockito_kotlin.doReturn
|
||||||
import com.nhaarman.mockito_kotlin.whenever
|
import com.nhaarman.mockito_kotlin.whenever
|
||||||
import net.corda.core.contracts.Contract
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.contracts.ContractState
|
|
||||||
import net.corda.core.contracts.TransactionVerificationException
|
|
||||||
import net.corda.core.contracts.requireThat
|
|
||||||
import net.corda.core.identity.AbstractParty
|
import net.corda.core.identity.AbstractParty
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.finance.DOLLARS
|
import net.corda.finance.DOLLARS
|
||||||
@ -18,6 +15,7 @@ import net.corda.testing.core.TestIdentity
|
|||||||
import net.corda.testing.internal.rigorousMock
|
import net.corda.testing.internal.rigorousMock
|
||||||
import net.corda.testing.node.MockServices
|
import net.corda.testing.node.MockServices
|
||||||
import net.corda.testing.node.ledger
|
import net.corda.testing.node.ledger
|
||||||
|
import org.assertj.core.api.AssertionsForClassTypes
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@ -33,6 +31,7 @@ class TransactionEncumbranceTests {
|
|||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
|
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
|
||||||
|
val DUMMY_NOTARY2 = TestIdentity(DUMMY_NOTARY_NAME.copy(organisation = "${DUMMY_NOTARY_NAME.organisation}2"), 30).party
|
||||||
val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
|
val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
|
||||||
val MINI_CORP = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")).party
|
val MINI_CORP = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")).party
|
||||||
val MEGA_CORP get() = megaCorp.party
|
val MEGA_CORP get() = megaCorp.party
|
||||||
@ -77,7 +76,7 @@ class TransactionEncumbranceTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `states can be bi-directionally encumbered`() {
|
fun `states must be bi-directionally encumbered`() {
|
||||||
// Basic encumbrance example for encumbrance index links 0 -> 1 and 1 -> 0
|
// Basic encumbrance example for encumbrance index links 0 -> 1 and 1 -> 0
|
||||||
ledgerServices.ledger(DUMMY_NOTARY) {
|
ledgerServices.ledger(DUMMY_NOTARY) {
|
||||||
transaction {
|
transaction {
|
||||||
@ -316,4 +315,42 @@ class TransactionEncumbranceTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `encumbered states cannot be assigned to different notaries`() {
|
||||||
|
// Single encumbrance with different notaries.
|
||||||
|
assertFailsWith<TransactionVerificationException.TransactionNotaryMismatchEncumbranceException> {
|
||||||
|
TransactionBuilder()
|
||||||
|
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 1, AutomaticHashConstraint)
|
||||||
|
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY2, 0, AutomaticHashConstraint)
|
||||||
|
.addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey)
|
||||||
|
.toLedgerTransaction(ledgerServices)
|
||||||
|
}
|
||||||
|
|
||||||
|
// More complex encumbrance (full cycle of size 4) where one of the encumbered states is assigned to a different notary.
|
||||||
|
// 0 -> 1, 1 -> 3, 3 -> 2, 2 -> 0
|
||||||
|
// We expect that state at index 3 cannot be encumbered with the state at index 2, due to mismatched notaries.
|
||||||
|
AssertionsForClassTypes.assertThatExceptionOfType(TransactionVerificationException.TransactionNotaryMismatchEncumbranceException::class.java).isThrownBy {
|
||||||
|
TransactionBuilder()
|
||||||
|
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 1, AutomaticHashConstraint)
|
||||||
|
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 3, AutomaticHashConstraint)
|
||||||
|
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY2, 0, AutomaticHashConstraint)
|
||||||
|
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 2, AutomaticHashConstraint)
|
||||||
|
.addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey)
|
||||||
|
.toLedgerTransaction(ledgerServices)
|
||||||
|
}.withMessageContaining("index 3 is assigned to notary [O=Notary Service, L=Zurich, C=CH], while its encumbrance with index 2 is assigned to notary [O=Notary Service2, L=Zurich, C=CH]")
|
||||||
|
|
||||||
|
// Two different encumbrance chains, where only one fails due to mismatched notary.
|
||||||
|
// 0 -> 1, 1 -> 0, 2 -> 3, 3 -> 2 where encumbered states with indices 2 and 3, respectively, are assigned
|
||||||
|
// to different notaries.
|
||||||
|
AssertionsForClassTypes.assertThatExceptionOfType(TransactionVerificationException.TransactionNotaryMismatchEncumbranceException::class.java).isThrownBy {
|
||||||
|
TransactionBuilder()
|
||||||
|
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 1, AutomaticHashConstraint)
|
||||||
|
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 0, AutomaticHashConstraint)
|
||||||
|
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 3, AutomaticHashConstraint)
|
||||||
|
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY2, 2, AutomaticHashConstraint)
|
||||||
|
.addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey)
|
||||||
|
.toLedgerTransaction(ledgerServices)
|
||||||
|
}.withMessageContaining("index 2 is assigned to notary [O=Notary Service, L=Zurich, C=CH], while its encumbrance with index 3 is assigned to notary [O=Notary Service2, L=Zurich, C=CH]")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -153,3 +153,102 @@ which is then referenced within a custom flow:
|
|||||||
:end-before: DOCEND TopupIssuer
|
:end-before: DOCEND TopupIssuer
|
||||||
|
|
||||||
For examples on testing ``@CordaService`` implementations, see the oracle example :doc:`here <oracles>`
|
For examples on testing ``@CordaService`` implementations, see the oracle example :doc:`here <oracles>`
|
||||||
|
|
||||||
|
JPA Support
|
||||||
|
-----------
|
||||||
|
In addition to ``jdbcSession``, ``ServiceHub`` also exposes the Java Persistence API to flows via the ``withEntityManager``
|
||||||
|
method. This method can be used to persist and query entities which inherit from ``MappedSchema``. This is particularly
|
||||||
|
useful if off-ledger data must be maintained in conjunction with on-ledger state data.
|
||||||
|
|
||||||
|
.. note:: Your entity must be included as a mappedType in as part of a MappedSchema for it to be added to Hibernate
|
||||||
|
as a custom schema. See Samples below.
|
||||||
|
|
||||||
|
The code snippet below defines a ``PersistentFoo`` type inside ``FooSchemaV1``. Note that ``PersistentFoo`` is added to
|
||||||
|
a list of mapped types which is passed to ``MappedSChema``. This is exactly how state schemas are defined, except that
|
||||||
|
the entity in this case should not subclass ``PersistentState`` (as it is not a state object). See examples:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. sourcecode:: java
|
||||||
|
|
||||||
|
public class FooSchema {}
|
||||||
|
|
||||||
|
@CordaSerializable
|
||||||
|
public class FooSchemaV1 extends MappedSchema {
|
||||||
|
FooSchemaV1() {
|
||||||
|
super(FooSchema.class, 1, ImmutableList.of(PersistentFoo.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "foos")
|
||||||
|
class PersistentFoo implements Serializable {
|
||||||
|
@Id
|
||||||
|
@Column(name = "foo_id")
|
||||||
|
String fooId;
|
||||||
|
|
||||||
|
@Column(name = "foo_data")
|
||||||
|
String fooData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
|
object FooSchema
|
||||||
|
|
||||||
|
object FooSchemaV1 : MappedSchema(schemaFamily = FooSchema.javaClass, version = 1, mappedTypes = listOf(PersistentFoo::class.java)) {
|
||||||
|
@Entity
|
||||||
|
@Table(name = "foos")
|
||||||
|
class PersistentFoo(@Id @Column(name = "foo_id") var fooId: String, @Column(name = "foo_data") var fooData: String) : Serializable
|
||||||
|
}
|
||||||
|
|
||||||
|
Instances of ``PersistentFoo`` can be persisted inside a flow as follows:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. sourcecode:: java
|
||||||
|
|
||||||
|
PersistentFoo foo = new PersistentFoo(new UniqueIdentifier().getId().toString(), "Bar");
|
||||||
|
node.getServices().withEntityManager(entityManager -> {
|
||||||
|
entityManager.persist(foo);
|
||||||
|
entityManager.flush();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
|
val foo = FooSchemaV1.PersistentFoo(UniqueIdentifier().id.toString(), "Bar")
|
||||||
|
serviceHub.withEntityManager {
|
||||||
|
persist(foo)
|
||||||
|
}
|
||||||
|
|
||||||
|
And retrieved via a query, as follows:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. sourcecode:: java
|
||||||
|
|
||||||
|
node.getServices().withEntityManager((EntityManager entityManager) -> {
|
||||||
|
CriteriaQuery<PersistentFoo> query = entityManager.getCriteriaBuilder().createQuery(PersistentFoo.class);
|
||||||
|
Root<PersistentFoo> type = query.from(PersistentFoo.class);
|
||||||
|
query.select(type);
|
||||||
|
return entityManager.createQuery(query).getResultList();
|
||||||
|
});
|
||||||
|
|
||||||
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
|
val result: MutableList<FooSchemaV1.PersistentFoo> = services.withEntityManager {
|
||||||
|
val query = criteriaBuilder.createQuery(FooSchemaV1.PersistentFoo::class.java)
|
||||||
|
val type = query.from(FooSchemaV1.PersistentFoo::class.java)
|
||||||
|
query.select(type)
|
||||||
|
createQuery(query).resultList
|
||||||
|
}
|
||||||
|
|
||||||
|
Please note that suspendable flow operations such as:
|
||||||
|
|
||||||
|
* ``FlowSession.send``
|
||||||
|
* ``FlowSession.receive``
|
||||||
|
* ``FlowLogic.receiveAll``
|
||||||
|
* ``FlowLogic.sleep``
|
||||||
|
* ``FlowLogic.subFlow``
|
||||||
|
|
||||||
|
Cannot be used within the lambda function passed to ``withEntityManager``.
|
||||||
|
@ -2,16 +2,19 @@ package net.corda.docs.java.tutorial.helloworld;
|
|||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable;
|
import co.paralleluniverse.fibers.Suspendable;
|
||||||
import com.template.TemplateContract;
|
import com.template.TemplateContract;
|
||||||
import net.corda.core.flows.*;
|
import net.corda.core.flows.FlowException;
|
||||||
|
import net.corda.core.flows.FlowLogic;
|
||||||
|
import net.corda.core.flows.InitiatingFlow;
|
||||||
|
import net.corda.core.flows.StartableByRPC;
|
||||||
|
import net.corda.core.utilities.ProgressTracker;
|
||||||
|
|
||||||
// DOCSTART 01
|
// DOCSTART 01
|
||||||
// Add these imports:
|
// Add these imports:
|
||||||
import net.corda.core.contracts.Command;
|
import net.corda.core.contracts.Command;
|
||||||
import net.corda.core.contracts.CommandData;
|
import net.corda.core.flows.FinalityFlow;
|
||||||
import net.corda.core.identity.Party;
|
import net.corda.core.identity.Party;
|
||||||
import net.corda.core.transactions.SignedTransaction;
|
import net.corda.core.transactions.SignedTransaction;
|
||||||
import net.corda.core.transactions.TransactionBuilder;
|
import net.corda.core.transactions.TransactionBuilder;
|
||||||
import net.corda.core.utilities.ProgressTracker;
|
|
||||||
|
|
||||||
// Replace Initiator's definition with:
|
// Replace Initiator's definition with:
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
@ -46,13 +49,12 @@ public class IOUFlow extends FlowLogic<Void> {
|
|||||||
|
|
||||||
// We create the transaction components.
|
// We create the transaction components.
|
||||||
IOUState outputState = new IOUState(iouValue, getOurIdentity(), otherParty);
|
IOUState outputState = new IOUState(iouValue, getOurIdentity(), otherParty);
|
||||||
CommandData cmdType = new TemplateContract.Commands.Action();
|
Command command = new Command<>(new TemplateContract.Commands.Action(), getOurIdentity().getOwningKey());
|
||||||
Command cmd = new Command<>(cmdType, getOurIdentity().getOwningKey());
|
|
||||||
|
|
||||||
// We create a transaction builder and add the components.
|
// We create a transaction builder and add the components.
|
||||||
TransactionBuilder txBuilder = new TransactionBuilder(notary)
|
TransactionBuilder txBuilder = new TransactionBuilder(notary)
|
||||||
.addOutputState(outputState, TemplateContract.ID)
|
.addOutputState(outputState, TemplateContract.ID)
|
||||||
.addCommand(cmd);
|
.addCommand(command);
|
||||||
|
|
||||||
// Signing the transaction.
|
// Signing the transaction.
|
||||||
SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder);
|
SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder);
|
||||||
|
@ -2,11 +2,11 @@ package net.corda.docs.java.tutorial.helloworld;
|
|||||||
|
|
||||||
import net.corda.core.contracts.ContractState;
|
import net.corda.core.contracts.ContractState;
|
||||||
import net.corda.core.identity.AbstractParty;
|
import net.corda.core.identity.AbstractParty;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
// DOCSTART 01
|
// DOCSTART 01
|
||||||
// Add these imports:
|
// Add this import:
|
||||||
import com.google.common.collect.ImmutableList;
|
|
||||||
import net.corda.core.identity.Party;
|
import net.corda.core.identity.Party;
|
||||||
|
|
||||||
// Replace TemplateState's definition with:
|
// Replace TemplateState's definition with:
|
||||||
@ -35,7 +35,7 @@ public class IOUState implements ContractState {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<AbstractParty> getParticipants() {
|
public List<AbstractParty> getParticipants() {
|
||||||
return ImmutableList.of(lender, borrower);
|
return Arrays.asList(lender, borrower);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// DOCEND 01
|
// DOCEND 01
|
@ -6,15 +6,14 @@ import net.corda.core.transactions.LedgerTransaction;
|
|||||||
|
|
||||||
// DOCSTART 01
|
// DOCSTART 01
|
||||||
// Add these imports:
|
// Add these imports:
|
||||||
import com.google.common.collect.ImmutableList;
|
|
||||||
import net.corda.core.contracts.CommandWithParties;
|
import net.corda.core.contracts.CommandWithParties;
|
||||||
import net.corda.core.identity.Party;
|
import net.corda.core.identity.Party;
|
||||||
|
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static net.corda.core.contracts.ContractsDSL.requireSingleCommand;
|
import static net.corda.core.contracts.ContractsDSL.requireSingleCommand;
|
||||||
import static net.corda.core.contracts.ContractsDSL.requireThat;
|
|
||||||
|
|
||||||
// Replace TemplateContract's definition with:
|
// Replace TemplateContract's definition with:
|
||||||
public class IOUContract implements Contract {
|
public class IOUContract implements Contract {
|
||||||
@ -28,26 +27,29 @@ public class IOUContract implements Contract {
|
|||||||
public void verify(LedgerTransaction tx) {
|
public void verify(LedgerTransaction tx) {
|
||||||
final CommandWithParties<IOUContract.Create> command = requireSingleCommand(tx.getCommands(), IOUContract.Create.class);
|
final CommandWithParties<IOUContract.Create> command = requireSingleCommand(tx.getCommands(), IOUContract.Create.class);
|
||||||
|
|
||||||
requireThat(check -> {
|
|
||||||
// Constraints on the shape of the transaction.
|
// Constraints on the shape of the transaction.
|
||||||
check.using("No inputs should be consumed when issuing an IOU.", tx.getInputs().isEmpty());
|
if (!tx.getInputs().isEmpty())
|
||||||
check.using("There should be one output state of type IOUState.", tx.getOutputs().size() == 1);
|
throw new IllegalArgumentException("No inputs should be consumed when issuing an IOU.");
|
||||||
|
if (!(tx.getOutputs().size() == 1))
|
||||||
|
throw new IllegalArgumentException("There should be one output state of type IOUState.");
|
||||||
|
|
||||||
// IOU-specific constraints.
|
// IOU-specific constraints.
|
||||||
final IOUState out = tx.outputsOfType(IOUState.class).get(0);
|
final IOUState output = tx.outputsOfType(IOUState.class).get(0);
|
||||||
final Party lender = out.getLender();
|
final Party lender = output.getLender();
|
||||||
final Party borrower = out.getBorrower();
|
final Party borrower = output.getBorrower();
|
||||||
check.using("The IOU's value must be non-negative.", out.getValue() > 0);
|
if (output.getValue() <= 0)
|
||||||
check.using("The lender and the borrower cannot be the same entity.", lender != borrower);
|
throw new IllegalArgumentException("The IOU's value must be non-negative.");
|
||||||
|
if (lender.equals(borrower))
|
||||||
|
throw new IllegalArgumentException("The lender and the borrower cannot be the same entity.");
|
||||||
|
|
||||||
// Constraints on the signers.
|
// Constraints on the signers.
|
||||||
final List<PublicKey> signers = command.getSigners();
|
final List<PublicKey> requiredSigners = command.getSigners();
|
||||||
check.using("There must be two signers.", signers.size() == 2);
|
final List<PublicKey> expectedSigners = Arrays.asList(borrower.getOwningKey(), lender.getOwningKey());
|
||||||
check.using("The borrower and lender must be signers.", signers.containsAll(
|
if (requiredSigners.size() != 2)
|
||||||
ImmutableList.of(borrower.getOwningKey(), lender.getOwningKey())));
|
throw new IllegalArgumentException("There must be two signers.");
|
||||||
|
if (!(requiredSigners.containsAll(expectedSigners)))
|
||||||
|
throw new IllegalArgumentException("The borrower and lender must be signers.");
|
||||||
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// DOCEND 01
|
// DOCEND 01
|
@ -2,9 +2,7 @@ package net.corda.docs.java.tutorial.twoparty;
|
|||||||
|
|
||||||
// DOCSTART 01
|
// DOCSTART 01
|
||||||
import co.paralleluniverse.fibers.Suspendable;
|
import co.paralleluniverse.fibers.Suspendable;
|
||||||
import com.google.common.collect.ImmutableList;
|
|
||||||
import net.corda.core.contracts.Command;
|
import net.corda.core.contracts.Command;
|
||||||
import net.corda.core.contracts.StateAndContract;
|
|
||||||
import net.corda.core.flows.*;
|
import net.corda.core.flows.*;
|
||||||
import net.corda.core.identity.Party;
|
import net.corda.core.identity.Party;
|
||||||
import net.corda.core.transactions.SignedTransaction;
|
import net.corda.core.transactions.SignedTransaction;
|
||||||
@ -12,6 +10,7 @@ import net.corda.core.transactions.TransactionBuilder;
|
|||||||
import net.corda.core.utilities.ProgressTracker;
|
import net.corda.core.utilities.ProgressTracker;
|
||||||
|
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
// DOCEND 01
|
// DOCEND 01
|
||||||
|
|
||||||
@ -42,22 +41,19 @@ public class IOUFlow extends FlowLogic<Void> {
|
|||||||
@Suspendable
|
@Suspendable
|
||||||
@Override
|
@Override
|
||||||
public Void call() throws FlowException {
|
public Void call() throws FlowException {
|
||||||
|
// DOCSTART 02
|
||||||
// We retrieve the notary identity from the network map.
|
// We retrieve the notary identity from the network map.
|
||||||
Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0);
|
Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0);
|
||||||
|
|
||||||
// DOCSTART 02
|
|
||||||
// We create a transaction builder.
|
|
||||||
TransactionBuilder txBuilder = new TransactionBuilder();
|
|
||||||
txBuilder.setNotary(notary);
|
|
||||||
|
|
||||||
// We create the transaction components.
|
// We create the transaction components.
|
||||||
IOUState outputState = new IOUState(iouValue, getOurIdentity(), otherParty);
|
IOUState outputState = new IOUState(iouValue, getOurIdentity(), otherParty);
|
||||||
StateAndContract outputContractAndState = new StateAndContract(outputState, IOUContract.ID);
|
List<PublicKey> requiredSigners = Arrays.asList(getOurIdentity().getOwningKey(), otherParty.getOwningKey());
|
||||||
List<PublicKey> requiredSigners = ImmutableList.of(getOurIdentity().getOwningKey(), otherParty.getOwningKey());
|
Command command = new Command<>(new IOUContract.Create(), requiredSigners);
|
||||||
Command cmd = new Command<>(new IOUContract.Create(), requiredSigners);
|
|
||||||
|
|
||||||
// We add the items to the builder.
|
// We create a transaction builder and add the components.
|
||||||
txBuilder.withItems(outputContractAndState, cmd);
|
TransactionBuilder txBuilder = new TransactionBuilder(notary)
|
||||||
|
.addOutputState(outputState, IOUContract.ID)
|
||||||
|
.addCommand(command);
|
||||||
|
|
||||||
// Verifying the transaction.
|
// Verifying the transaction.
|
||||||
txBuilder.verify(getServiceHub());
|
txBuilder.verify(getServiceHub());
|
||||||
@ -70,7 +66,7 @@ public class IOUFlow extends FlowLogic<Void> {
|
|||||||
|
|
||||||
// Obtaining the counterparty's signature.
|
// Obtaining the counterparty's signature.
|
||||||
SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(
|
SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(
|
||||||
signedTx, ImmutableList.of(otherPartySession), CollectSignaturesFlow.tracker()));
|
signedTx, Arrays.asList(otherPartySession), CollectSignaturesFlow.tracker()));
|
||||||
|
|
||||||
// Finalising the transaction.
|
// Finalising the transaction.
|
||||||
subFlow(new FinalityFlow(fullySignedTx));
|
subFlow(new FinalityFlow(fullySignedTx));
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
package net.corda.docs.java.tutorial.twoparty;
|
package net.corda.docs.java.tutorial.twoparty;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableList;
|
|
||||||
import net.corda.core.contracts.ContractState;
|
import net.corda.core.contracts.ContractState;
|
||||||
import net.corda.core.identity.AbstractParty;
|
import net.corda.core.identity.AbstractParty;
|
||||||
import net.corda.core.identity.Party;
|
import net.corda.core.identity.Party;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class IOUState implements ContractState {
|
public class IOUState implements ContractState {
|
||||||
@ -32,6 +32,6 @@ public class IOUState implements ContractState {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<AbstractParty> getParticipants() {
|
public List<AbstractParty> getParticipants() {
|
||||||
return ImmutableList.of(lender, borrower);
|
return Arrays.asList(lender, borrower);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -8,13 +8,13 @@ import net.corda.core.flows.FinalityFlow
|
|||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
import net.corda.core.flows.InitiatingFlow
|
import net.corda.core.flows.InitiatingFlow
|
||||||
import net.corda.core.flows.StartableByRPC
|
import net.corda.core.flows.StartableByRPC
|
||||||
|
import net.corda.core.utilities.ProgressTracker
|
||||||
|
|
||||||
// DOCSTART 01
|
// DOCSTART 01
|
||||||
// Add these imports:
|
// Add these imports:
|
||||||
import net.corda.core.contracts.Command
|
import net.corda.core.contracts.Command
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.core.utilities.ProgressTracker
|
|
||||||
|
|
||||||
// Replace Initiator's definition with:
|
// Replace Initiator's definition with:
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
@ -33,12 +33,12 @@ class IOUFlow(val iouValue: Int,
|
|||||||
|
|
||||||
// We create the transaction components.
|
// We create the transaction components.
|
||||||
val outputState = IOUState(iouValue, ourIdentity, otherParty)
|
val outputState = IOUState(iouValue, ourIdentity, otherParty)
|
||||||
val cmd = Command(TemplateContract.Commands.Action(), ourIdentity.owningKey)
|
val command = Command(TemplateContract.Commands.Action(), ourIdentity.owningKey)
|
||||||
|
|
||||||
// We create a transaction builder and add the components.
|
// We create a transaction builder and add the components.
|
||||||
val txBuilder = TransactionBuilder(notary = notary)
|
val txBuilder = TransactionBuilder(notary = notary)
|
||||||
.addOutputState(outputState, TemplateContract.ID)
|
.addOutputState(outputState, TemplateContract.ID)
|
||||||
.addCommand(cmd)
|
.addCommand(command)
|
||||||
|
|
||||||
// We sign the transaction.
|
// We sign the transaction.
|
||||||
val signedTx = serviceHub.signInitialTransaction(txBuilder)
|
val signedTx = serviceHub.signInitialTransaction(txBuilder)
|
||||||
|
@ -5,7 +5,7 @@ package net.corda.docs.kotlin.tutorial.helloworld
|
|||||||
import net.corda.core.contracts.ContractState
|
import net.corda.core.contracts.ContractState
|
||||||
|
|
||||||
// DOCSTART 01
|
// DOCSTART 01
|
||||||
// Add these imports:
|
// Add this import:
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
|
|
||||||
// Replace TemplateState's definition with:
|
// Replace TemplateState's definition with:
|
||||||
|
@ -5,7 +5,7 @@ import net.corda.core.contracts.Contract
|
|||||||
import net.corda.core.transactions.LedgerTransaction
|
import net.corda.core.transactions.LedgerTransaction
|
||||||
|
|
||||||
// DOCSTART 01
|
// DOCSTART 01
|
||||||
// Add these imports:
|
// Add this import:
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
|
|
||||||
class IOUContract : Contract {
|
class IOUContract : Contract {
|
||||||
@ -25,14 +25,14 @@ class IOUContract : Contract {
|
|||||||
"There should be one output state of type IOUState." using (tx.outputs.size == 1)
|
"There should be one output state of type IOUState." using (tx.outputs.size == 1)
|
||||||
|
|
||||||
// IOU-specific constraints.
|
// IOU-specific constraints.
|
||||||
val out = tx.outputsOfType<IOUState>().single()
|
val output = tx.outputsOfType<IOUState>().single()
|
||||||
"The IOU's value must be non-negative." using (out.value > 0)
|
"The IOU's value must be non-negative." using (output.value > 0)
|
||||||
"The lender and the borrower cannot be the same entity." using (out.lender != out.borrower)
|
"The lender and the borrower cannot be the same entity." using (output.lender != output.borrower)
|
||||||
|
|
||||||
// Constraints on the signers.
|
// Constraints on the signers.
|
||||||
|
val expectedSigners = listOf(output.borrower.owningKey, output.lender.owningKey)
|
||||||
"There must be two signers." using (command.signers.toSet().size == 2)
|
"There must be two signers." using (command.signers.toSet().size == 2)
|
||||||
"The borrower and lender must be signers." using (command.signers.containsAll(listOf(
|
"The borrower and lender must be signers." using (command.signers.containsAll(expectedSigners))
|
||||||
out.borrower.owningKey, out.lender.owningKey)))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ package net.corda.docs.kotlin.tutorial.twoparty
|
|||||||
// DOCSTART 01
|
// DOCSTART 01
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.contracts.Command
|
import net.corda.core.contracts.Command
|
||||||
import net.corda.core.contracts.StateAndContract
|
|
||||||
import net.corda.core.flows.CollectSignaturesFlow
|
import net.corda.core.flows.CollectSignaturesFlow
|
||||||
import net.corda.core.flows.FinalityFlow
|
import net.corda.core.flows.FinalityFlow
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
@ -27,20 +26,18 @@ class IOUFlow(val iouValue: Int,
|
|||||||
/** The flow logic is encapsulated within the call() method. */
|
/** The flow logic is encapsulated within the call() method. */
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call() {
|
override fun call() {
|
||||||
|
// DOCSTART 02
|
||||||
// We retrieve the notary identity from the network map.
|
// We retrieve the notary identity from the network map.
|
||||||
val notary = serviceHub.networkMapCache.notaryIdentities[0]
|
val notary = serviceHub.networkMapCache.notaryIdentities[0]
|
||||||
|
|
||||||
// DOCSTART 02
|
|
||||||
// We create a transaction builder.
|
|
||||||
val txBuilder = TransactionBuilder(notary = notary)
|
|
||||||
|
|
||||||
// We create the transaction components.
|
// We create the transaction components.
|
||||||
val outputState = IOUState(iouValue, ourIdentity, otherParty)
|
val outputState = IOUState(iouValue, ourIdentity, otherParty)
|
||||||
val outputContractAndState = StateAndContract(outputState, IOUContract.ID)
|
val command = Command(IOUContract.Create(), listOf(ourIdentity.owningKey, otherParty.owningKey))
|
||||||
val cmd = Command(IOUContract.Create(), listOf(ourIdentity.owningKey, otherParty.owningKey))
|
|
||||||
|
|
||||||
// We add the items to the builder.
|
// We create a transaction builder and add the components.
|
||||||
txBuilder.withItems(outputContractAndState, cmd)
|
val txBuilder = TransactionBuilder(notary = notary)
|
||||||
|
.addOutputState(outputState, IOUContract.ID)
|
||||||
|
.addCommand(command)
|
||||||
|
|
||||||
// Verifying the transaction.
|
// Verifying the transaction.
|
||||||
txBuilder.verify(serviceHub)
|
txBuilder.verify(serviceHub)
|
||||||
|
@ -3,10 +3,7 @@
|
|||||||
package net.corda.docs.kotlin.tutorial.twoparty
|
package net.corda.docs.kotlin.tutorial.twoparty
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.flows.FlowSession
|
|
||||||
import net.corda.core.flows.InitiatedBy
|
|
||||||
import net.corda.core.flows.SignTransactionFlow
|
|
||||||
import net.corda.docs.kotlin.tutorial.helloworld.IOUFlow
|
import net.corda.docs.kotlin.tutorial.helloworld.IOUFlow
|
||||||
import net.corda.docs.kotlin.tutorial.helloworld.IOUState
|
import net.corda.docs.kotlin.tutorial.helloworld.IOUState
|
||||||
|
|
||||||
|
@ -40,8 +40,7 @@ FlowLogic
|
|||||||
---------
|
---------
|
||||||
All flows must subclass ``FlowLogic``. You then define the steps taken by the flow by overriding ``FlowLogic.call``.
|
All flows must subclass ``FlowLogic``. You then define the steps taken by the flow by overriding ``FlowLogic.call``.
|
||||||
|
|
||||||
Let's define our ``IOUFlow`` in either ``Initiator.java`` or ``Flows.kt``. Delete the two existing flows in the
|
Let's define our ``IOUFlow``. Delete the existing ``Responder`` flow. Then replace the definition of ``Initiator`` with the following:
|
||||||
template (``Initiator`` and ``Responder``), and replace them with the following:
|
|
||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
|
@ -13,29 +13,38 @@ By this point, :doc:`your dev environment should be set up <getting-set-up>`, yo
|
|||||||
:doc:`your first CorDapp <tutorial-cordapp>`, and you're familiar with Corda's :doc:`key concepts <key-concepts>`. What
|
:doc:`your first CorDapp <tutorial-cordapp>`, and you're familiar with Corda's :doc:`key concepts <key-concepts>`. What
|
||||||
comes next?
|
comes next?
|
||||||
|
|
||||||
If you're a developer, the next step is to write your own CorDapp. CorDapps are plugins that are installed on one or
|
If you're a developer, the next step is to write your own CorDapp. CorDapps are applications that are installed on one or
|
||||||
more Corda nodes, and give the nodes' owners the ability to make their node conduct some new process - anything from
|
more Corda nodes, and that allow the node's operator to instruct their node to perform some new process - anything from
|
||||||
issuing a debt instrument to making a restaurant booking.
|
issuing a debt instrument to making a restaurant booking.
|
||||||
|
|
||||||
Our use-case
|
Our use-case
|
||||||
------------
|
------------
|
||||||
Our CorDapp will model IOUs on-ledger. An IOU – short for “I O(we) (yo)U” – records the fact that one person owes
|
We will write a CorDapp to model IOUs on the blockchain. Each IOU – short for “I O(we) (yo)U” – will record the fact that one node owes
|
||||||
another person a given amount of money. Clearly this is sensitive information that we'd only want to communicate on
|
another node a certain amount. This simple CorDapp will showcase several key benefits of Corda as a blockchain platform:
|
||||||
a need-to-know basis between the lender and the borrower. Fortunately, this is one of the areas where Corda excels.
|
|
||||||
Corda makes it easy to allow a small set of parties to agree on a shared fact without needing to share this fact with
|
|
||||||
everyone else on the network, as is the norm in blockchain platforms.
|
|
||||||
|
|
||||||
To serve any useful function, our CorDapp will need at least two things:
|
* **Privacy** - Since IOUs represent sensitive information, we will be taking advantage of Corda's ability to only share
|
||||||
|
ledger updates with other nodes on a need-to-know basis, instead of using a gossip protocol to share this information with every node on
|
||||||
|
the network as you would with a traditional blockchain platform
|
||||||
|
|
||||||
* **States**, the shared facts that Corda nodes reach consensus over and are then stored on the ledger
|
* **Well-known identities** - Each Corda node has a well-known identity on the network. This allows us to write code in terms of real
|
||||||
* **Flows**, which encapsulate the procedure for carrying out a specific ledger update
|
identities, rather than anonymous public keys
|
||||||
|
|
||||||
Our IOU CorDapp is no exception. It will define both a state and a flow:
|
* **Re-use of existing, proven technologies** - We will be writing our CorDapp using standard Java. It will run on a Corda node, which is
|
||||||
|
simply a Java process and runs on a regular Java machine (e.g. on your local machine or in the cloud). The nodes will store their data in
|
||||||
|
a standard SQL database
|
||||||
|
|
||||||
|
CorDapps usually define at least three things:
|
||||||
|
|
||||||
|
* **States** - the (possibly shared) facts that are written to the ledger
|
||||||
|
* **Flows** - the procedures for carrying out specific ledger updates
|
||||||
|
* **Contracts** - the constraints governing how states of a given type can evolve over time
|
||||||
|
|
||||||
|
Our IOU CorDapp is no exception. It will define the following components:
|
||||||
|
|
||||||
The IOUState
|
The IOUState
|
||||||
^^^^^^^^^^^^
|
^^^^^^^^^^^^
|
||||||
Our state will be the ``IOUState``. It will store the value of the IOU, as well as the identities of the lender and the
|
Our state will be the ``IOUState``, representing an IOU. It will contain the IOU's value, its lender and its borrower. We can visualize
|
||||||
borrower. We can visualize ``IOUState`` as follows:
|
``IOUState`` as follows:
|
||||||
|
|
||||||
.. image:: resources/tutorial-state.png
|
.. image:: resources/tutorial-state.png
|
||||||
:scale: 25%
|
:scale: 25%
|
||||||
@ -43,20 +52,20 @@ borrower. We can visualize ``IOUState`` as follows:
|
|||||||
|
|
||||||
The IOUFlow
|
The IOUFlow
|
||||||
^^^^^^^^^^^
|
^^^^^^^^^^^
|
||||||
Our flow will be the ``IOUFlow``. This flow will completely automate the process of issuing a new IOU onto a ledger. It
|
Our flow will be the ``IOUFlow``. This flow will completely automate the process of issuing a new IOU onto a ledger. It has the following
|
||||||
is composed of the following steps:
|
steps:
|
||||||
|
|
||||||
.. image:: resources/simple-tutorial-flow.png
|
.. image:: resources/simple-tutorial-flow.png
|
||||||
:scale: 25%
|
:scale: 25%
|
||||||
:align: center
|
:align: center
|
||||||
|
|
||||||
In traditional distributed ledger systems, where all data is broadcast to every network participant, you don’t need to
|
The IOUContract
|
||||||
think about data flows – you simply package up your ledger update and send it to everyone else on the network. But in
|
^^^^^^^^^^^^^^^
|
||||||
Corda, where privacy is a core focus, flows allow us to carefully control who sees what during the process of
|
For this tutorial, we will use the default ``TemplateContract``. We will update it to create a fully-fledged ``IOUContract`` in the next
|
||||||
agreeing a ledger update.
|
tutorial.
|
||||||
|
|
||||||
Progress so far
|
Progress so far
|
||||||
---------------
|
---------------
|
||||||
We've sketched out a simple CorDapp that will allow nodes to confidentially issue new IOUs onto a ledger.
|
We've designed a simple CorDapp that will allow nodes to agree new IOUs on the blockchain.
|
||||||
|
|
||||||
Next, we'll be taking a look at the template project we'll be using as the basis for our CorDapp.
|
Next, we'll take a look at the template project we'll be using as the basis for our CorDapp.
|
||||||
|
@ -107,9 +107,7 @@ commands.
|
|||||||
|
|
||||||
We want to create an IOU of 99 with PartyB. We start the ``IOUFlow`` by typing:
|
We want to create an IOU of 99 with PartyB. We start the ``IOUFlow`` by typing:
|
||||||
|
|
||||||
.. container:: codeset
|
.. code-block:: bash
|
||||||
|
|
||||||
.. code-block:: kotlin
|
|
||||||
|
|
||||||
start IOUFlow iouValue: 99, otherParty: "O=PartyB,L=New York,C=US"
|
start IOUFlow iouValue: 99, otherParty: "O=PartyB,L=New York,C=US"
|
||||||
|
|
||||||
@ -162,12 +160,27 @@ The vaults of PartyA and PartyB should both display the following output:
|
|||||||
|
|
||||||
This is the transaction issuing our ``IOUState`` onto a ledger.
|
This is the transaction issuing our ``IOUState`` onto a ledger.
|
||||||
|
|
||||||
|
However, if we run the same command on the other node (the notary), we will see the following:
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
{
|
||||||
|
"states" : [ ],
|
||||||
|
"statesMetadata" : [ ],
|
||||||
|
"totalStatesAvailable" : -1,
|
||||||
|
"stateTypes" : "UNCONSUMED",
|
||||||
|
"otherResults" : [ ]
|
||||||
|
}
|
||||||
|
|
||||||
|
This is the result of Corda's privacy model. Because the notary was not involved in the transaction and had no need to see the data, the
|
||||||
|
transaction was not distributed to them.
|
||||||
|
|
||||||
Conclusion
|
Conclusion
|
||||||
----------
|
----------
|
||||||
We have written a simple CorDapp that allows IOUs to be issued onto the ledger. Our CorDapp is made up of two key
|
We have written a simple CorDapp that allows IOUs to be issued onto the ledger. Our CorDapp is made up of two key
|
||||||
parts:
|
parts:
|
||||||
|
|
||||||
* The ``IOUState``, representing IOUs on the ledger
|
* The ``IOUState``, representing IOUs on the blockchain
|
||||||
* The ``IOUFlow``, orchestrating the process of agreeing the creation of an IOU on-ledger
|
* The ``IOUFlow``, orchestrating the process of agreeing the creation of an IOU on-ledger
|
||||||
|
|
||||||
After completing this tutorial, your CorDapp should look like this:
|
After completing this tutorial, your CorDapp should look like this:
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
Writing the state
|
Writing the state
|
||||||
=================
|
=================
|
||||||
|
|
||||||
In Corda, shared facts on the ledger are represented as states. Our first task will be to define a new state type to
|
In Corda, shared facts on the blockchain are represented as states. Our first task will be to define a new state type to
|
||||||
represent an IOU.
|
represent an IOU.
|
||||||
|
|
||||||
The ContractState interface
|
The ContractState interface
|
||||||
@ -28,7 +28,7 @@ We can see that the ``ContractState`` interface has a single field, ``participan
|
|||||||
entities for which this state is relevant.
|
entities for which this state is relevant.
|
||||||
|
|
||||||
Beyond this, our state is free to define any fields, methods, helpers or inner classes it requires to accurately
|
Beyond this, our state is free to define any fields, methods, helpers or inner classes it requires to accurately
|
||||||
represent a given type of shared fact on the ledger.
|
represent a given type of shared fact on the blockchain.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
@ -46,7 +46,7 @@ represent a given type of shared fact on the ledger.
|
|||||||
|
|
||||||
Modelling IOUs
|
Modelling IOUs
|
||||||
--------------
|
--------------
|
||||||
How should we define the ``IOUState`` representing IOUs on the ledger? Beyond implementing the ``ContractState``
|
How should we define the ``IOUState`` representing IOUs on the blockchain? Beyond implementing the ``ContractState``
|
||||||
interface, our ``IOUState`` will also need properties to track the relevant features of the IOU:
|
interface, our ``IOUState`` will also need properties to track the relevant features of the IOU:
|
||||||
|
|
||||||
* The value of the IOU
|
* The value of the IOU
|
||||||
@ -99,7 +99,7 @@ Corda are simply classes that implement the ``ContractState`` interface. They ca
|
|||||||
methods you like.
|
methods you like.
|
||||||
|
|
||||||
All that's left to do is write the ``IOUFlow`` that will allow a node to orchestrate the creation of a new ``IOUState``
|
All that's left to do is write the ``IOUFlow`` that will allow a node to orchestrate the creation of a new ``IOUState``
|
||||||
on the ledger, while only sharing information on a need-to-know basis.
|
on the blockchain, while only sharing information on a need-to-know basis.
|
||||||
|
|
||||||
What about the contract?
|
What about the contract?
|
||||||
------------------------
|
------------------------
|
||||||
|
@ -7,41 +7,39 @@
|
|||||||
The CorDapp Template
|
The CorDapp Template
|
||||||
====================
|
====================
|
||||||
|
|
||||||
When writing a new CorDapp, you’ll generally want to base it on the standard templates:
|
When writing a new CorDapp, you’ll generally want to start from one of the standard templates:
|
||||||
|
|
||||||
* The `Java Cordapp Template <https://github.com/corda/cordapp-template-java>`_
|
* The `Java Cordapp Template <https://github.com/corda/cordapp-template-java>`_
|
||||||
* The `Kotlin Cordapp Template <https://github.com/corda/cordapp-template-kotlin>`_
|
* The `Kotlin Cordapp Template <https://github.com/corda/cordapp-template-kotlin>`_
|
||||||
|
|
||||||
The Cordapp templates provide the required boilerplate for developing a CorDapp, and allow you to quickly deploy your
|
The Cordapp templates provide the boilerplate for developing a new CorDapp. CorDapps can be written in either Java or Kotlin. We will be
|
||||||
CorDapp onto a local test network of dummy nodes to test its functionality.
|
providing the code in both languages throughout this tutorial.
|
||||||
|
|
||||||
CorDapps can be written in both Java and Kotlin, and will be providing the code in both languages in this tutorial.
|
Note that there's no need to download and install Corda itself. The required libraries are automatically downloaded from an online Maven
|
||||||
|
repository and cached locally.
|
||||||
Note that there's no need to download and install Corda itself. Corda's required libraries will be downloaded
|
|
||||||
automatically from an online Maven repository.
|
|
||||||
|
|
||||||
Downloading the template
|
Downloading the template
|
||||||
------------------------
|
------------------------
|
||||||
To download the template, open a terminal window in the directory where you want to download the CorDapp template, and
|
Open a terminal window in the directory where you want to download the CorDapp template, and run the following command:
|
||||||
run the following command:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. code-block:: java
|
||||||
|
|
||||||
git clone https://github.com/corda/cordapp-template-java.git ; cd cordapp-template-java
|
git clone https://github.com/corda/cordapp-template-java.git ; cd cordapp-template-java
|
||||||
|
|
||||||
*or*
|
.. code-block:: kotlin
|
||||||
|
|
||||||
git clone https://github.com/corda/cordapp-template-kotlin.git ; cd cordapp-template-kotlin
|
git clone https://github.com/corda/cordapp-template-kotlin.git ; cd cordapp-template-kotlin
|
||||||
|
|
||||||
Opening the template in IntelliJ
|
Opening the template in IntelliJ
|
||||||
--------------------------------
|
--------------------------------
|
||||||
|
|
||||||
Once the template is download, open it in IntelliJ by following the instructions here:
|
Once the template is download, open it in IntelliJ by following the instructions here:
|
||||||
https://docs.corda.net/tutorial-cordapp.html#opening-the-example-cordapp-in-intellij.
|
https://docs.corda.net/tutorial-cordapp.html#opening-the-example-cordapp-in-intellij.
|
||||||
|
|
||||||
Template structure
|
Template structure
|
||||||
------------------
|
------------------
|
||||||
The template has a number of files, but we can ignore most of them. We will only be modifying the following files:
|
For this tutorial, we will only be modifying the following files:
|
||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
|
@ -154,7 +154,7 @@ Transaction constraints
|
|||||||
~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
We also want our transaction to have no inputs and only a single output - an issuance transaction.
|
We also want our transaction to have no inputs and only a single output - an issuance transaction.
|
||||||
|
|
||||||
To impose this and the subsequent constraints, we are using Corda's built-in ``requireThat`` block. ``requireThat``
|
In Kotlin, we impose these and the subsequent constraints using Corda's built-in ``requireThat`` block. ``requireThat``
|
||||||
provides a terse way to write the following:
|
provides a terse way to write the following:
|
||||||
|
|
||||||
* If the condition on the right-hand side doesn't evaluate to true...
|
* If the condition on the right-hand side doesn't evaluate to true...
|
||||||
@ -162,6 +162,8 @@ provides a terse way to write the following:
|
|||||||
|
|
||||||
As before, the act of throwing this exception causes the transaction to be considered invalid.
|
As before, the act of throwing this exception causes the transaction to be considered invalid.
|
||||||
|
|
||||||
|
In Java, we simply throw an ``IllegalArgumentException`` manually instead.
|
||||||
|
|
||||||
IOU constraints
|
IOU constraints
|
||||||
~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~
|
||||||
We want to impose two constraints on the ``IOUState`` itself:
|
We want to impose two constraints on the ``IOUState`` itself:
|
||||||
@ -169,9 +171,7 @@ We want to impose two constraints on the ``IOUState`` itself:
|
|||||||
* Its value must be non-negative
|
* Its value must be non-negative
|
||||||
* The lender and the borrower cannot be the same entity
|
* The lender and the borrower cannot be the same entity
|
||||||
|
|
||||||
We impose these constraints in the same ``requireThat`` block as before.
|
You can see that we're not restricted to only writing constraints inside ``verify``. We can also write
|
||||||
|
|
||||||
You can see that we're not restricted to only writing constraints in the ``requireThat`` block. We can also write
|
|
||||||
other statements - in this case, extracting the transaction's single ``IOUState`` and assigning it to a variable.
|
other statements - in this case, extracting the transaction's single ``IOUState`` and assigning it to a variable.
|
||||||
|
|
||||||
Signer constraints
|
Signer constraints
|
||||||
@ -180,7 +180,7 @@ Finally, we require both the lender and the borrower to be required signers on t
|
|||||||
required signers is equal to the union of all the signers listed on the commands. We therefore extract the signers from
|
required signers is equal to the union of all the signers listed on the commands. We therefore extract the signers from
|
||||||
the ``Create`` command we retrieved earlier.
|
the ``Create`` command we retrieved earlier.
|
||||||
|
|
||||||
This is an absolutely essential constraint - it ensures that no ``IOUState`` can ever be created on the ledger without
|
This is an absolutely essential constraint - it ensures that no ``IOUState`` can ever be created on the blockchain without
|
||||||
the express agreement of both the lender and borrower nodes.
|
the express agreement of both the lender and borrower nodes.
|
||||||
|
|
||||||
Progress so far
|
Progress so far
|
||||||
|
@ -31,8 +31,7 @@ In ``IOUFlow.java``/``Flows.kt``, change the imports block to the following:
|
|||||||
:start-after: DOCSTART 01
|
:start-after: DOCSTART 01
|
||||||
:end-before: DOCEND 01
|
:end-before: DOCEND 01
|
||||||
|
|
||||||
And update ``IOUFlow.call`` by changing the code following the retrieval of the notary's identity from the network as
|
And update ``IOUFlow.call`` to the following:
|
||||||
follows:
|
|
||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
@ -136,7 +135,7 @@ defined in ``IOUContract``. We can now re-run our updated CorDapp, using the
|
|||||||
:doc:`same instructions as before <hello-world-running>`.
|
:doc:`same instructions as before <hello-world-running>`.
|
||||||
|
|
||||||
Our CorDapp now imposes restrictions on the issuance of IOUs. Most importantly, IOU issuance now requires agreement
|
Our CorDapp now imposes restrictions on the issuance of IOUs. Most importantly, IOU issuance now requires agreement
|
||||||
from both the lender and the borrower before an IOU can be created on the ledger. This prevents either the lender or
|
from both the lender and the borrower before an IOU can be created on the blockchain. This prevents either the lender or
|
||||||
the borrower from unilaterally updating the ledger in a way that only benefits themselves.
|
the borrower from unilaterally updating the ledger in a way that only benefits themselves.
|
||||||
|
|
||||||
After completing this tutorial, your CorDapp should look like this:
|
After completing this tutorial, your CorDapp should look like this:
|
||||||
|
@ -6,10 +6,10 @@ Hello, World! Pt.2 - Contract constraints
|
|||||||
In the Hello, World tutorial, we built a CorDapp allowing us to model IOUs on ledger. Our CorDapp was made up of two
|
In the Hello, World tutorial, we built a CorDapp allowing us to model IOUs on ledger. Our CorDapp was made up of two
|
||||||
elements:
|
elements:
|
||||||
|
|
||||||
* An ``IOUState``, representing IOUs on the ledger
|
* An ``IOUState``, representing IOUs on the blockchain
|
||||||
* An ``IOUFlow``, orchestrating the process of agreeing the creation of an IOU on-ledger
|
* An ``IOUFlow``, orchestrating the process of agreeing the creation of an IOU on-ledger
|
||||||
|
|
||||||
However, our CorDapp did not impose any constraints on the evolution of IOUs on the ledger over time. Anyone was free
|
However, our CorDapp did not impose any constraints on the evolution of IOUs on the blockchain over time. Anyone was free
|
||||||
to create IOUs of any value, between any party.
|
to create IOUs of any value, between any party.
|
||||||
|
|
||||||
In this tutorial, we'll write a contract to imposes rules on how an ``IOUState`` can change over time. In turn, this
|
In this tutorial, we'll write a contract to imposes rules on how an ``IOUState`` can change over time. In turn, this
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
ext {
|
|
||||||
javaassist_version = "3.12.1.GA"
|
|
||||||
}
|
|
||||||
|
|
||||||
apply plugin: 'kotlin'
|
|
||||||
apply plugin: 'idea'
|
|
||||||
|
|
||||||
description 'A javaagent to allow hooking into Kryo'
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
|
||||||
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
|
||||||
compile "javassist:javassist:$javaassist_version"
|
|
||||||
compile "com.esotericsoftware:kryo:4.0.0"
|
|
||||||
compile "$quasar_group:quasar-core:$quasar_version:jdk8"
|
|
||||||
}
|
|
||||||
|
|
||||||
jar {
|
|
||||||
archiveName = "${project.name}.jar"
|
|
||||||
manifest {
|
|
||||||
attributes(
|
|
||||||
'Premain-Class': 'net.corda.kryohook.KryoHookAgent',
|
|
||||||
'Can-Redefine-Classes': 'true',
|
|
||||||
'Can-Retransform-Classes': 'true',
|
|
||||||
'Can-Set-Native-Method-Prefix': 'true',
|
|
||||||
'Implementation-Title': "KryoHook",
|
|
||||||
'Implementation-Version': rootProject.version
|
|
||||||
)
|
|
||||||
}
|
|
||||||
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
|
|
||||||
}
|
|
@ -1,164 +0,0 @@
|
|||||||
package net.corda.kryohook
|
|
||||||
|
|
||||||
import co.paralleluniverse.strands.Strand
|
|
||||||
import com.esotericsoftware.kryo.Kryo
|
|
||||||
import com.esotericsoftware.kryo.io.Output
|
|
||||||
import javassist.ClassPool
|
|
||||||
import javassist.CtClass
|
|
||||||
import java.lang.instrument.ClassFileTransformer
|
|
||||||
import java.lang.instrument.Instrumentation
|
|
||||||
import java.security.ProtectionDomain
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
|
|
||||||
class KryoHookAgent {
|
|
||||||
companion object {
|
|
||||||
@JvmStatic
|
|
||||||
fun premain(@Suppress("UNUSED_PARAMETER") argumentsString: String?, instrumentation: Instrumentation) {
|
|
||||||
Runtime.getRuntime().addShutdownHook(Thread {
|
|
||||||
val statsTrees = KryoHook.events.values.flatMap {
|
|
||||||
readTrees(it, 0).second
|
|
||||||
}
|
|
||||||
val builder = StringBuilder()
|
|
||||||
statsTrees.forEach {
|
|
||||||
prettyStatsTree(0, it, builder)
|
|
||||||
}
|
|
||||||
print(builder.toString())
|
|
||||||
})
|
|
||||||
instrumentation.addTransformer(KryoHook)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun prettyStatsTree(indent: Int, statsTree: StatsTree, builder: StringBuilder) {
|
|
||||||
when (statsTree) {
|
|
||||||
is StatsTree.Object -> {
|
|
||||||
builder.append(kotlin.CharArray(indent) { ' ' })
|
|
||||||
builder.append(statsTree.className)
|
|
||||||
builder.append(" ")
|
|
||||||
builder.append(statsTree.size)
|
|
||||||
builder.append("\n")
|
|
||||||
for (child in statsTree.children) {
|
|
||||||
prettyStatsTree(indent + 2, child, builder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The hook simply records the write() entries and exits together with the output offset at the time of the call.
|
|
||||||
* This is recorded in a StrandID -> List<StatsEvent> map.
|
|
||||||
*
|
|
||||||
* Later we "parse" these lists into a tree.
|
|
||||||
*/
|
|
||||||
object KryoHook : ClassFileTransformer {
|
|
||||||
val classPool = ClassPool.getDefault()!!
|
|
||||||
|
|
||||||
val hookClassName = javaClass.name!!
|
|
||||||
|
|
||||||
override fun transform(
|
|
||||||
loader: ClassLoader?,
|
|
||||||
className: String,
|
|
||||||
classBeingRedefined: Class<*>?,
|
|
||||||
protectionDomain: ProtectionDomain?,
|
|
||||||
classfileBuffer: ByteArray
|
|
||||||
): ByteArray? {
|
|
||||||
if (className.startsWith("java") || className.startsWith("javassist") || className.startsWith("kotlin")) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return try {
|
|
||||||
val clazz = classPool.makeClass(classfileBuffer.inputStream())
|
|
||||||
instrumentClass(clazz)?.toBytecode()
|
|
||||||
} catch (throwable: Throwable) {
|
|
||||||
println("SOMETHING WENT WRONG")
|
|
||||||
throwable.printStackTrace(System.out)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun instrumentClass(clazz: CtClass): CtClass? {
|
|
||||||
for (method in clazz.declaredBehaviors) {
|
|
||||||
if (method.name == "write") {
|
|
||||||
val parameterTypeNames = method.parameterTypes.map { it.name }
|
|
||||||
if (parameterTypeNames == listOf("com.esotericsoftware.kryo.Kryo", "com.esotericsoftware.kryo.io.Output", "java.lang.Object")) {
|
|
||||||
if (method.isEmpty) continue
|
|
||||||
println("Instrumenting ${clazz.name}")
|
|
||||||
method.insertBefore("$hookClassName.${this::writeEnter.name}($1, $2, $3);")
|
|
||||||
method.insertAfter("$hookClassName.${this::writeExit.name}($1, $2, $3);")
|
|
||||||
return clazz
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// StrandID -> StatsEvent map
|
|
||||||
val events = ConcurrentHashMap<Long, ArrayList<StatsEvent>>()
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun writeEnter(@Suppress("UNUSED_PARAMETER") kryo: Kryo, output: Output, obj: Any) {
|
|
||||||
events.getOrPut(Strand.currentStrand().id) { ArrayList() }.add(
|
|
||||||
StatsEvent.Enter(obj.javaClass.name, output.total())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@JvmStatic
|
|
||||||
fun writeExit(@Suppress("UNUSED_PARAMETER") kryo: Kryo, output: Output, obj: Any) {
|
|
||||||
events[Strand.currentStrand().id]!!.add(
|
|
||||||
StatsEvent.Exit(obj.javaClass.name, output.total())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO we could add events on entries/exits to field serializers to get more info on what's being serialised.
|
|
||||||
*/
|
|
||||||
sealed class StatsEvent {
|
|
||||||
data class Enter(val className: String, val offset: Long) : StatsEvent()
|
|
||||||
data class Exit(val className: String, val offset: Long) : StatsEvent()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO add Field constructor.
|
|
||||||
*/
|
|
||||||
sealed class StatsTree {
|
|
||||||
data class Object(
|
|
||||||
val className: String,
|
|
||||||
val size: Long,
|
|
||||||
val children: List<StatsTree>
|
|
||||||
) : StatsTree()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun readTree(events: List<StatsEvent>, index: Int): Pair<Int, StatsTree> {
|
|
||||||
val event = events[index]
|
|
||||||
when (event) {
|
|
||||||
is StatsEvent.Enter -> {
|
|
||||||
val (nextIndex, children) = readTrees(events, index + 1)
|
|
||||||
val exit = events[nextIndex] as StatsEvent.Exit
|
|
||||||
require(event.className == exit.className)
|
|
||||||
return Pair(nextIndex + 1, StatsTree.Object(event.className, exit.offset - event.offset, children))
|
|
||||||
}
|
|
||||||
is StatsEvent.Exit -> {
|
|
||||||
throw IllegalStateException("Wasn't expecting Exit")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun readTrees(events: List<StatsEvent>, index: Int): Pair<Int, List<StatsTree>> {
|
|
||||||
val trees = ArrayList<StatsTree>()
|
|
||||||
var i = index
|
|
||||||
while (true) {
|
|
||||||
val event = events.getOrNull(i)
|
|
||||||
when (event) {
|
|
||||||
is StatsEvent.Enter -> {
|
|
||||||
val (nextIndex, tree) = readTree(events, i)
|
|
||||||
trees.add(tree)
|
|
||||||
i = nextIndex
|
|
||||||
}
|
|
||||||
is StatsEvent.Exit -> {
|
|
||||||
return Pair(i, trees)
|
|
||||||
}
|
|
||||||
null -> {
|
|
||||||
return Pair(i, trees)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
What is this
|
|
||||||
------------
|
|
||||||
|
|
||||||
This is a javaagent that hooks into Kryo serializers to record a breakdown of how many bytes objects take in the output.
|
|
||||||
|
|
||||||
The dump is quite ugly now, but the in-memory representation is a simple tree so we could put some nice visualisation on
|
|
||||||
top if we want.
|
|
||||||
|
|
||||||
How do I run it
|
|
||||||
---------------
|
|
||||||
|
|
||||||
Build the agent:
|
|
||||||
```
|
|
||||||
./gradlew experimental:kryo-hook:jar
|
|
||||||
```
|
|
||||||
|
|
||||||
Add this JVM flag to what you're running:
|
|
||||||
|
|
||||||
```
|
|
||||||
-javaagent:<PROJECT>/experimental/kryo-hook/build/libs/kryo-hook.jar
|
|
||||||
```
|
|
||||||
|
|
||||||
The agent will dump the output when the JVM shuts down.
|
|
@ -191,6 +191,7 @@ object QuasarInstrumentationHook : ClassFileTransformer {
|
|||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
println("SOMETHING WENT WRONG")
|
println("SOMETHING WENT WRONG")
|
||||||
throwable.printStackTrace(System.out)
|
throwable.printStackTrace(System.out)
|
||||||
|
if (throwable is VirtualMachineError) throw throwable
|
||||||
classfileBuffer
|
classfileBuffer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -220,8 +220,8 @@ object RPCApi {
|
|||||||
companion object {
|
companion object {
|
||||||
private fun Any.safeSerialize(context: SerializationContext, wrap: (Throwable) -> Any) = try {
|
private fun Any.safeSerialize(context: SerializationContext, wrap: (Throwable) -> Any) = try {
|
||||||
serialize(context = context)
|
serialize(context = context)
|
||||||
} catch (t: Throwable) {
|
} catch (e: Exception) {
|
||||||
wrap(t).serialize(context = context)
|
wrap(e).serialize(context = context)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fromClientMessage(context: SerializationContext, message: ClientMessage): ServerToClient {
|
fun fromClientMessage(context: SerializationContext, message: ClientMessage): ServerToClient {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package net.corda.nodeapi.internal.crypto
|
package net.corda.nodeapi.internal.crypto
|
||||||
|
|
||||||
|
import net.corda.core.crypto.Crypto.SPHINCS256_SHA256
|
||||||
import net.corda.core.crypto.SignatureScheme
|
import net.corda.core.crypto.SignatureScheme
|
||||||
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
|
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
|
||||||
import org.bouncycastle.operator.ContentSigner
|
import org.bouncycastle.operator.ContentSigner
|
||||||
@ -17,7 +18,9 @@ object ContentSignerBuilder {
|
|||||||
fun build(signatureScheme: SignatureScheme, privateKey: PrivateKey, provider: Provider, random: SecureRandom? = null): ContentSigner {
|
fun build(signatureScheme: SignatureScheme, privateKey: PrivateKey, provider: Provider, random: SecureRandom? = null): ContentSigner {
|
||||||
val sigAlgId = signatureScheme.signatureOID
|
val sigAlgId = signatureScheme.signatureOID
|
||||||
val sig = Signature.getInstance(signatureScheme.signatureName, provider).apply {
|
val sig = Signature.getInstance(signatureScheme.signatureName, provider).apply {
|
||||||
if (random != null) {
|
// TODO special handling for Sphincs due to a known BouncyCastle's Sphincs bug we reported.
|
||||||
|
// It is fixed in BC 161b12, so consider updating the below if-statement after updating BouncyCastle.
|
||||||
|
if (random != null && signatureScheme != SPHINCS256_SHA256) {
|
||||||
initSign(privateKey, random)
|
initSign(privateKey, random)
|
||||||
} else {
|
} else {
|
||||||
initSign(privateKey)
|
initSign(privateKey)
|
||||||
|
@ -38,3 +38,5 @@ interface CryptoService {
|
|||||||
*/
|
*/
|
||||||
fun getSigner(alias: String): ContentSigner
|
fun getSigner(alias: String): ContentSigner
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open class CryptoServiceException(message: String?, cause: Throwable? = null) : Exception(message, cause)
|
||||||
|
@ -169,8 +169,8 @@ class CordaPersistence(
|
|||||||
var recoverableFailureCount = 0
|
var recoverableFailureCount = 0
|
||||||
fun <T> quietly(task: () -> T) = try {
|
fun <T> quietly(task: () -> T) = try {
|
||||||
task()
|
task()
|
||||||
} catch (t: Throwable) {
|
} catch (e: Exception) {
|
||||||
log.warn("Cleanup task failed:", t)
|
log.warn("Cleanup task failed:", e)
|
||||||
}
|
}
|
||||||
while (true) {
|
while (true) {
|
||||||
val transaction = contextDatabase.currentOrNew(isolationLevel) // XXX: Does this code really support statement changing the contextDatabase?
|
val transaction = contextDatabase.currentOrNew(isolationLevel) // XXX: Does this code really support statement changing the contextDatabase?
|
||||||
@ -178,7 +178,7 @@ class CordaPersistence(
|
|||||||
val answer = transaction.statement()
|
val answer = transaction.statement()
|
||||||
transaction.commit()
|
transaction.commit()
|
||||||
return answer
|
return answer
|
||||||
} catch (e: Throwable) {
|
} catch (e: Exception) {
|
||||||
quietly(transaction::rollback)
|
quietly(transaction::rollback)
|
||||||
if (e is SQLException || (recoverAnyNestedSQLException && e.hasSQLExceptionCause())) {
|
if (e is SQLException || (recoverAnyNestedSQLException && e.hasSQLExceptionCause())) {
|
||||||
if (++recoverableFailureCount > recoverableFailureTolerance) throw e
|
if (++recoverableFailureCount > recoverableFailureTolerance) throw e
|
||||||
|
@ -7,6 +7,7 @@ import org.hibernate.Transaction
|
|||||||
import rx.subjects.PublishSubject
|
import rx.subjects.PublishSubject
|
||||||
import java.sql.Connection
|
import java.sql.Connection
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import javax.persistence.EntityManager
|
||||||
|
|
||||||
fun currentDBSession(): Session = contextTransaction.session
|
fun currentDBSession(): Session = contextTransaction.session
|
||||||
private val _contextTransaction = ThreadLocal<DatabaseTransaction>()
|
private val _contextTransaction = ThreadLocal<DatabaseTransaction>()
|
||||||
@ -65,6 +66,12 @@ class DatabaseTransaction(
|
|||||||
session
|
session
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns a delegate which overrides certain operations that we do not want CorDapp developers to call.
|
||||||
|
val restrictedEntityManager: RestrictedEntityManager by lazy {
|
||||||
|
val entityManager = session as EntityManager
|
||||||
|
RestrictedEntityManager(entityManager)
|
||||||
|
}
|
||||||
|
|
||||||
val session: Session by sessionDelegate
|
val session: Session by sessionDelegate
|
||||||
private lateinit var hibernateTransaction: Transaction
|
private lateinit var hibernateTransaction: Transaction
|
||||||
|
|
||||||
@ -111,3 +118,4 @@ class DatabaseTransaction(
|
|||||||
boundary.filter { !it.success }.subscribe { callback() }
|
boundary.filter { !it.success }.subscribe { callback() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
package net.corda.nodeapi.internal.persistence
|
||||||
|
|
||||||
|
import javax.persistence.EntityManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A delegate of [EntityManager] which disallows some operations.
|
||||||
|
*/
|
||||||
|
class RestrictedEntityManager(private val delegate: EntityManager) : EntityManager by delegate {
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
throw UnsupportedOperationException("This method cannot be called via ServiceHub.withEntityManager.")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clear() {
|
||||||
|
throw UnsupportedOperationException("This method cannot be called via ServiceHub.withEntityManager.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Figure out which other methods on EntityManager need to be blocked?
|
||||||
|
}
|
@ -330,7 +330,7 @@ class X509UtilitiesTest {
|
|||||||
lock.notifyAll()
|
lock.notifyAll()
|
||||||
}
|
}
|
||||||
sslServerSocket.close()
|
sslServerSocket.close()
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Exception) {
|
||||||
serverError = true
|
serverError = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -150,6 +150,7 @@ dependencies {
|
|||||||
// Capsule is a library for building independently executable fat JARs.
|
// Capsule is a library for building independently executable fat JARs.
|
||||||
// We only need this dependency to compile our Caplet against.
|
// We only need this dependency to compile our Caplet against.
|
||||||
compileOnly "co.paralleluniverse:capsule:$capsule_version"
|
compileOnly "co.paralleluniverse:capsule:$capsule_version"
|
||||||
|
testCompile "co.paralleluniverse:capsule:$capsule_version"
|
||||||
|
|
||||||
// OkHTTP: Simple HTTP library.
|
// OkHTTP: Simple HTTP library.
|
||||||
compile "com.squareup.okhttp3:okhttp:$okhttp_version"
|
compile "com.squareup.okhttp3:okhttp:$okhttp_version"
|
||||||
|
@ -35,7 +35,7 @@ class ErrorCodeLoggingTests : IntegrationTest() {
|
|||||||
node.rpc.startFlow(::MyFlow).waitForCompletion()
|
node.rpc.startFlow(::MyFlow).waitForCompletion()
|
||||||
val logFile = node.logFile()
|
val logFile = node.logFile()
|
||||||
|
|
||||||
val linesWithErrorCode = logFile.useLines { lines -> lines.filter { line -> line.contains("[errorCode=") }.toList() }
|
val linesWithErrorCode = logFile.useLines { lines -> lines.filter { line -> line.contains("[errorCode=") }.filter { line -> line.contains("moreInformationAt=https://errors.corda.net/") }.toList() }
|
||||||
|
|
||||||
assertThat(linesWithErrorCode).isNotEmpty
|
assertThat(linesWithErrorCode).isNotEmpty
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
package net.corda.node.services.rpc
|
||||||
|
|
||||||
|
import net.corda.core.utilities.getOrThrow
|
||||||
|
import net.corda.testing.driver.DriverParameters
|
||||||
|
import net.corda.testing.driver.driver
|
||||||
|
import org.assertj.core.api.Assertions.assertThatCode
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class NodeHandleTests {
|
||||||
|
@Test
|
||||||
|
fun object_defined_functions_are_static_for_node_rpc_ops() {
|
||||||
|
driver(DriverParameters(startNodesInProcess = true)) {
|
||||||
|
val rpcClient = startNode().getOrThrow().rpc
|
||||||
|
|
||||||
|
assertThatCode { rpcClient.hashCode() }.doesNotThrowAnyException()
|
||||||
|
@Suppress("UnusedEquals")
|
||||||
|
assertThatCode { rpcClient == rpcClient }.doesNotThrowAnyException()
|
||||||
|
assertThatCode { rpcClient.toString() }.doesNotThrowAnyException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -25,8 +25,7 @@ public class CordaCaplet extends Capsule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Config parseConfigFile(List<String> args) {
|
private Config parseConfigFile(List<String> args) {
|
||||||
String baseDirOption = getOption(args, "--base-directory");
|
this.baseDir = getBaseDirectory(args);
|
||||||
this.baseDir = Paths.get((baseDirOption == null) ? "." : baseDirOption).toAbsolutePath().normalize().toString();
|
|
||||||
String config = getOption(args, "--config-file");
|
String config = getOption(args, "--config-file");
|
||||||
File configFile = (config == null) ? new File(baseDir, "node.conf") : new File(config);
|
File configFile = (config == null) ? new File(baseDir, "node.conf") : new File(config);
|
||||||
try {
|
try {
|
||||||
@ -41,17 +40,44 @@ public class CordaCaplet extends Capsule {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
File getConfigFile(List<String> args, String baseDir) {
|
||||||
|
String config = getOptionMultiple(args, Arrays.asList("--config-file", "-f"));
|
||||||
|
return (config == null || config.equals("")) ? new File(baseDir, "node.conf") : new File(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
String getBaseDirectory(List<String> args) {
|
||||||
|
String baseDir = getOptionMultiple(args, Arrays.asList("--base-directory", "-b"));
|
||||||
|
return Paths.get((baseDir == null) ? "." : baseDir).toAbsolutePath().normalize().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getOptionMultiple(List<String> args, List<String> possibleOptions) {
|
||||||
|
String result = null;
|
||||||
|
for(String option: possibleOptions) {
|
||||||
|
result = getOption(args, option);
|
||||||
|
if (result != null) break;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private String getOption(List<String> args, String option) {
|
private String getOption(List<String> args, String option) {
|
||||||
final String lowerCaseOption = option.toLowerCase();
|
final String lowerCaseOption = option.toLowerCase();
|
||||||
int index = 0;
|
int index = 0;
|
||||||
for (String arg : args) {
|
for (String arg : args) {
|
||||||
if (arg.toLowerCase().equals(lowerCaseOption)) {
|
if (arg.toLowerCase().equals(lowerCaseOption)) {
|
||||||
if (index < args.size() - 1) {
|
if (index < args.size() - 1 && !args.get(index + 1).startsWith("-")) {
|
||||||
return args.get(index + 1);
|
return args.get(index + 1);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (arg.toLowerCase().startsWith(lowerCaseOption)) {
|
||||||
|
if (arg.length() > option.length() && arg.substring(option.length(), option.length() + 1).equals("=")) {
|
||||||
|
return arg.substring(option.length() + 1);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -96,7 +122,10 @@ public class CordaCaplet extends Capsule {
|
|||||||
|
|
||||||
File cordappsDir = new File(baseDir, "cordapps");
|
File cordappsDir = new File(baseDir, "cordapps");
|
||||||
// Create cordapps directory if it doesn't exist.
|
// Create cordapps directory if it doesn't exist.
|
||||||
requireCordappsDirExists(cordappsDir);
|
if (!checkIfCordappDirExists(cordappsDir)) {
|
||||||
|
// If it fails, just return the existing class path. The main Corda jar will detect the error and fail gracefully.
|
||||||
|
return cp;
|
||||||
|
}
|
||||||
// Add additional directories of JARs to the classpath (at the end), e.g., for JDBC drivers.
|
// Add additional directories of JARs to the classpath (at the end), e.g., for JDBC drivers.
|
||||||
augmentClasspath(cp, new File(baseDir, "drivers"));
|
augmentClasspath(cp, new File(baseDir, "drivers"));
|
||||||
augmentClasspath(cp, cordappsDir);
|
augmentClasspath(cp, cordappsDir);
|
||||||
@ -173,17 +202,18 @@ public class CordaCaplet extends Capsule {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void requireCordappsDirExists(File dir) {
|
private Boolean checkIfCordappDirExists(File dir) {
|
||||||
try {
|
try {
|
||||||
if (!dir.mkdir() && !dir.exists()) { // It is unlikely to enter this if-branch, but just in case.
|
if (!dir.mkdir() && !dir.exists()) { // It is unlikely to enter this if-branch, but just in case.
|
||||||
logOnFailedCordappDir();
|
logOnFailedCordappDir();
|
||||||
throw new RuntimeException("Cordapps dir could not be created"); // Let Capsule handle the error (log error, clean up, die).
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (SecurityException | NullPointerException e) {
|
catch (SecurityException | NullPointerException e) {
|
||||||
logOnFailedCordappDir();
|
logOnFailedCordappDir();
|
||||||
throw e; // Let Capsule handle the error (log error, clean up, die).
|
return false;
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void logOnFailedCordappDir() {
|
private void logOnFailedCordappDir() {
|
||||||
|
@ -9,10 +9,10 @@ import net.corda.confidential.SwapIdentitiesHandler
|
|||||||
import net.corda.core.CordaException
|
import net.corda.core.CordaException
|
||||||
import net.corda.core.concurrent.CordaFuture
|
import net.corda.core.concurrent.CordaFuture
|
||||||
import net.corda.core.context.InvocationContext
|
import net.corda.core.context.InvocationContext
|
||||||
import net.corda.core.crypto.internal.AliasPrivateKey
|
|
||||||
import net.corda.core.crypto.DigitalSignature
|
import net.corda.core.crypto.DigitalSignature
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.isCRLDistributionPointBlacklisted
|
import net.corda.core.crypto.isCRLDistributionPointBlacklisted
|
||||||
|
import net.corda.core.crypto.internal.AliasPrivateKey
|
||||||
import net.corda.core.crypto.newSecureRandom
|
import net.corda.core.crypto.newSecureRandom
|
||||||
import net.corda.core.flows.*
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.identity.AbstractParty
|
import net.corda.core.identity.AbstractParty
|
||||||
@ -105,6 +105,8 @@ import java.util.concurrent.ExecutorService
|
|||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import java.util.concurrent.TimeUnit.MINUTES
|
import java.util.concurrent.TimeUnit.MINUTES
|
||||||
import java.util.concurrent.TimeUnit.SECONDS
|
import java.util.concurrent.TimeUnit.SECONDS
|
||||||
|
import java.util.function.Consumer
|
||||||
|
import javax.persistence.EntityManager
|
||||||
import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair
|
import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -505,8 +507,8 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
|||||||
val republishInterval = try {
|
val republishInterval = try {
|
||||||
networkMapClient.publish(signedNodeInfo)
|
networkMapClient.publish(signedNodeInfo)
|
||||||
heartbeatInterval
|
heartbeatInterval
|
||||||
} catch (t: Throwable) {
|
} catch (e: Exception) {
|
||||||
log.warn("Error encountered while publishing node info, will retry again", t)
|
log.warn("Error encountered while publishing node info, will retry again", e)
|
||||||
// TODO: Exponential backoff? It should reach max interval of eventHorizon/2.
|
// TODO: Exponential backoff? It should reach max interval of eventHorizon/2.
|
||||||
1.minutes
|
1.minutes
|
||||||
}
|
}
|
||||||
@ -802,7 +804,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
|||||||
// Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because
|
// Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because
|
||||||
// the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with
|
// the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with
|
||||||
// the identity key. But the infrastructure to make that easy isn't here yet.
|
// the identity key. But the infrastructure to make that easy isn't here yet.
|
||||||
return BasicHSMKeyManagementService(cacheFactory,identityService, database, cryptoService)
|
return BasicHSMKeyManagementService(cacheFactory, identityService, database, cryptoService)
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun stop() {
|
open fun stop() {
|
||||||
@ -988,6 +990,14 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
|||||||
|
|
||||||
override fun jdbcSession(): Connection = database.createSession()
|
override fun jdbcSession(): Connection = database.createSession()
|
||||||
|
|
||||||
|
override fun <T : Any> withEntityManager(block: EntityManager.() -> T): T {
|
||||||
|
return block(contextTransaction.restrictedEntityManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun withEntityManager(block: Consumer<EntityManager>) {
|
||||||
|
block.accept(contextTransaction.restrictedEntityManager)
|
||||||
|
}
|
||||||
|
|
||||||
// allows services to register handlers to be informed when the node stop method is called
|
// allows services to register handlers to be informed when the node stop method is called
|
||||||
override fun registerUnloadHandler(runOnStop: () -> Unit) {
|
override fun registerUnloadHandler(runOnStop: () -> Unit) {
|
||||||
this@AbstractNode.runOnStop += runOnStop
|
this@AbstractNode.runOnStop += runOnStop
|
||||||
@ -1032,7 +1042,6 @@ class FlowStarterImpl(private val smm: StateMachineManager, private val flowLogi
|
|||||||
private val _future = openFuture<FlowStateMachine<T>>()
|
private val _future = openFuture<FlowStateMachine<T>>()
|
||||||
override val future: CordaFuture<FlowStateMachine<T>>
|
override val future: CordaFuture<FlowStateMachine<T>>
|
||||||
get() = _future
|
get() = _future
|
||||||
|
|
||||||
}
|
}
|
||||||
return startFlow(startFlowEvent)
|
return startFlow(startFlowEvent)
|
||||||
}
|
}
|
||||||
|
@ -362,7 +362,7 @@ open class Node(configuration: NodeConfiguration,
|
|||||||
log.info("Retrieved public IP from Network Map Service: $this. This will be used instead of the provided \"$host\" as the advertised address.")
|
log.info("Retrieved public IP from Network Map Service: $this. This will be used instead of the provided \"$host\" as the advertised address.")
|
||||||
}
|
}
|
||||||
retrievedHostName
|
retrievedHostName
|
||||||
} catch (ignore: Throwable) {
|
} catch (ignore: Exception) {
|
||||||
// Cannot reach the network map service, ignore the exception and use provided P2P address instead.
|
// Cannot reach the network map service, ignore the exception and use provided P2P address instead.
|
||||||
log.warn("Cannot connect to the network map service for public IP detection.")
|
log.warn("Cannot connect to the network map service for public IP detection.")
|
||||||
null
|
null
|
||||||
|
@ -191,7 +191,7 @@ open class NodeStartup : NodeStartupLogging {
|
|||||||
node.startupComplete.then {
|
node.startupComplete.then {
|
||||||
try {
|
try {
|
||||||
InteractiveShell.runLocalShell(node::stop)
|
InteractiveShell.runLocalShell(node::stop)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Exception) {
|
||||||
logger.error("Shell failed to start", e)
|
logger.error("Shell failed to start", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package net.corda.node.services.api
|
package net.corda.node.services.api
|
||||||
|
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.identity.Party
|
|
||||||
import net.corda.core.identity.PartyAndCertificate
|
import net.corda.core.identity.PartyAndCertificate
|
||||||
import net.corda.core.internal.CertRole
|
import net.corda.core.internal.CertRole
|
||||||
import net.corda.core.node.services.IdentityService
|
import net.corda.core.node.services.IdentityService
|
||||||
@ -36,22 +35,17 @@ interface IdentityServiceInternal : IdentityService {
|
|||||||
@Throws(CertificateExpiredException::class, CertificateNotYetValidException::class, InvalidAlgorithmParameterException::class)
|
@Throws(CertificateExpiredException::class, CertificateNotYetValidException::class, InvalidAlgorithmParameterException::class)
|
||||||
fun verifyAndRegisterIdentity(identity: PartyAndCertificate, isNewRandomIdentity: Boolean): PartyAndCertificate?
|
fun verifyAndRegisterIdentity(identity: PartyAndCertificate, isNewRandomIdentity: Boolean): PartyAndCertificate?
|
||||||
|
|
||||||
fun partiesFromName(query: String, exactMatch: Boolean, x500name: CordaX500Name, results: LinkedHashSet<Party>, party: Party) {
|
|
||||||
val components = listOfNotNull(x500name.commonName, x500name.organisationUnit, x500name.organisation, x500name.locality, x500name.state, x500name.country)
|
|
||||||
components.forEach { component ->
|
|
||||||
if (exactMatch && component == query) {
|
|
||||||
results += party
|
|
||||||
} else if (!exactMatch) {
|
|
||||||
// We can imagine this being a query over a lucene index in future.
|
// We can imagine this being a query over a lucene index in future.
|
||||||
//
|
//
|
||||||
// Kostas says: We can easily use the Jaro-Winkler distance metric as it is best suited for short
|
// Kostas says: When exactMatch = false, we can easily use the Jaro-Winkler distance metric as it is best suited for short
|
||||||
// strings such as entity/company names, and to detect small typos. We can also apply it for city
|
// strings such as entity/company names, and to detect small typos. We can also apply it for city
|
||||||
// or any keyword related search in lists of records (not raw text - for raw text we need indexing)
|
// or any keyword related search in lists of records (not raw text - for raw text we need indexing)
|
||||||
// and we can return results in hierarchical order (based on normalised String similarity 0.0-1.0).
|
// and we can return results in hierarchical order (based on normalised String similarity 0.0-1.0).
|
||||||
if (component.contains(query, ignoreCase = true))
|
/** Check if [x500name] matches the [query]. */
|
||||||
results += party
|
fun x500Matches(query: String, exactMatch: Boolean, x500name: CordaX500Name): Boolean {
|
||||||
}
|
val components = listOfNotNull(x500name.commonName, x500name.organisationUnit, x500name.organisation, x500name.locality, x500name.state, x500name.country)
|
||||||
}
|
return components.any { (exactMatch && it == query)
|
||||||
|
|| (!exactMatch && it.contains(query, ignoreCase = true)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -113,8 +113,8 @@ interface NodeConfiguration {
|
|||||||
|
|
||||||
fun makeCryptoService(): CryptoService {
|
fun makeCryptoService(): CryptoService {
|
||||||
return when(cryptoServiceName) {
|
return when(cryptoServiceName) {
|
||||||
SupportedCryptoServices.BC_SIMPLE -> BCCryptoService(this)
|
// Pick default BCCryptoService when null.
|
||||||
null -> BCCryptoService(this) // Pick default BCCryptoService when null.
|
SupportedCryptoServices.BC_SIMPLE, null -> BCCryptoService(this.myLegalName.x500Principal, this.signingCertificateStore)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -123,7 +123,7 @@ data class FlowOverrideConfig(val overrides: List<FlowOverride> = listOf())
|
|||||||
data class FlowOverride(val initiator: String, val responder: String)
|
data class FlowOverride(val initiator: String, val responder: String)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Currently registered JMX Reporters
|
* Currently registered JMX Reporters.
|
||||||
*/
|
*/
|
||||||
enum class JmxReporterType {
|
enum class JmxReporterType {
|
||||||
JOLOKIA, NEW_RELIC
|
JOLOKIA, NEW_RELIC
|
||||||
|
@ -63,8 +63,10 @@ class InMemoryIdentityService(identities: List<PartyAndCertificate> = emptyList(
|
|||||||
|
|
||||||
override fun partiesFromName(query: String, exactMatch: Boolean): Set<Party> {
|
override fun partiesFromName(query: String, exactMatch: Boolean): Set<Party> {
|
||||||
val results = LinkedHashSet<Party>()
|
val results = LinkedHashSet<Party>()
|
||||||
for ((x500name, partyAndCertificate) in principalToParties) {
|
principalToParties.forEach { (x500name, partyAndCertificate) ->
|
||||||
partiesFromName(query, exactMatch, x500name, results, partyAndCertificate.party)
|
if (x500Matches(query, exactMatch, x500name)) {
|
||||||
|
results += partyAndCertificate.party
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
@ -143,7 +143,7 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri
|
|||||||
val key = mapToKey(identity)
|
val key = mapToKey(identity)
|
||||||
if (isNewRandomIdentity) {
|
if (isNewRandomIdentity) {
|
||||||
// Because this is supposed to be new and random, there's no way we have it in the database already, so skip the pessimistic check.
|
// Because this is supposed to be new and random, there's no way we have it in the database already, so skip the pessimistic check.
|
||||||
keyToParties.set(key, identity)
|
keyToParties[key] = identity
|
||||||
} else {
|
} else {
|
||||||
keyToParties.addWithDuplicatesAllowed(key, identity)
|
keyToParties.addWithDuplicatesAllowed(key, identity)
|
||||||
}
|
}
|
||||||
@ -174,8 +174,10 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri
|
|||||||
override fun partiesFromName(query: String, exactMatch: Boolean): Set<Party> {
|
override fun partiesFromName(query: String, exactMatch: Boolean): Set<Party> {
|
||||||
return database.transaction {
|
return database.transaction {
|
||||||
val results = LinkedHashSet<Party>()
|
val results = LinkedHashSet<Party>()
|
||||||
for ((x500name, partyId) in principalToParties.allPersisted()) {
|
principalToParties.allPersisted().forEach { (x500name, partyId) ->
|
||||||
partiesFromName(query, exactMatch, x500name, results, keyToParties[partyId]!!.party)
|
if (x500Matches(query, exactMatch, x500name)) {
|
||||||
|
results += keyToParties[partyId]!!.party
|
||||||
|
}
|
||||||
}
|
}
|
||||||
results
|
results
|
||||||
}
|
}
|
||||||
|
@ -2,31 +2,39 @@ package net.corda.node.services.keys.cryptoservice
|
|||||||
|
|
||||||
import net.corda.core.crypto.Crypto
|
import net.corda.core.crypto.Crypto
|
||||||
import net.corda.core.crypto.newSecureRandom
|
import net.corda.core.crypto.newSecureRandom
|
||||||
|
import net.corda.core.crypto.sha256
|
||||||
import net.corda.node.services.config.NodeConfiguration
|
import net.corda.node.services.config.NodeConfiguration
|
||||||
import net.corda.nodeapi.internal.config.CertificateStore
|
import net.corda.nodeapi.internal.config.CertificateStore
|
||||||
|
import net.corda.nodeapi.internal.config.CertificateStoreSupplier
|
||||||
import net.corda.nodeapi.internal.crypto.ContentSignerBuilder
|
import net.corda.nodeapi.internal.crypto.ContentSignerBuilder
|
||||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||||
import net.corda.nodeapi.internal.cryptoservice.CryptoService
|
import net.corda.nodeapi.internal.cryptoservice.CryptoService
|
||||||
|
import net.corda.nodeapi.internal.cryptoservice.CryptoServiceException
|
||||||
import org.bouncycastle.operator.ContentSigner
|
import org.bouncycastle.operator.ContentSigner
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
import java.security.KeyStore
|
import java.security.KeyStore
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
import javax.security.auth.x500.X500Principal
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Basic implementation of a [CryptoService] that uses BouncyCastle for cryptographic operations
|
* Basic implementation of a [CryptoService] that uses BouncyCastle for cryptographic operations
|
||||||
* and a Java KeyStore in the form of [CertificateStore] to store private keys.
|
* and a Java KeyStore in the form of [CertificateStore] to store private keys.
|
||||||
* This service reuses the [NodeConfiguration.signingCertificateStore] to store keys.
|
* This service reuses the [NodeConfiguration.signingCertificateStore] to store keys.
|
||||||
*/
|
*/
|
||||||
class BCCryptoService(private val nodeConf: NodeConfiguration) : CryptoService {
|
class BCCryptoService(private val legalName: X500Principal, private val certificateStoreSupplier: CertificateStoreSupplier) : CryptoService {
|
||||||
|
|
||||||
// TODO check if keyStore exists.
|
// TODO check if keyStore exists.
|
||||||
// TODO make it private when E2ETestKeyManagementService does not require direct access to the private key.
|
// TODO make it private when E2ETestKeyManagementService does not require direct access to the private key.
|
||||||
internal var certificateStore: CertificateStore = nodeConf.signingCertificateStore.get(true)
|
internal var certificateStore: CertificateStore = certificateStoreSupplier.get(true)
|
||||||
|
|
||||||
override fun generateKeyPair(alias: String, schemeNumberID: Int): PublicKey {
|
override fun generateKeyPair(alias: String, schemeNumberID: Int): PublicKey {
|
||||||
|
try {
|
||||||
val keyPair = Crypto.generateKeyPair(Crypto.findSignatureScheme(schemeNumberID))
|
val keyPair = Crypto.generateKeyPair(Crypto.findSignatureScheme(schemeNumberID))
|
||||||
importKey(alias, keyPair)
|
importKey(alias, keyPair)
|
||||||
return keyPair.public
|
return keyPair.public
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw CryptoServiceException("Cannot generate key for alias $alias and signature scheme with id $schemeNumberID", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun containsKey(alias: String): Boolean {
|
override fun containsKey(alias: String): Boolean {
|
||||||
@ -34,17 +42,29 @@ class BCCryptoService(private val nodeConf: NodeConfiguration) : CryptoService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getPublicKey(alias: String): PublicKey {
|
override fun getPublicKey(alias: String): PublicKey {
|
||||||
|
try {
|
||||||
return certificateStore.query { getPublicKey(alias) }
|
return certificateStore.query { getPublicKey(alias) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw CryptoServiceException("Cannot get public key for alias $alias", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun sign(alias: String, data: ByteArray): ByteArray {
|
override fun sign(alias: String, data: ByteArray): ByteArray {
|
||||||
return Crypto.doSign(certificateStore.query { getPrivateKey(alias, certificateStore.entryPassword) } , data)
|
try {
|
||||||
|
return Crypto.doSign(certificateStore.query { getPrivateKey(alias, certificateStore.entryPassword) }, data)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw CryptoServiceException("Cannot sign using the key with alias $alias. SHA256 of data to be signed: ${data.sha256()}", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getSigner(alias: String): ContentSigner {
|
override fun getSigner(alias: String): ContentSigner {
|
||||||
|
try {
|
||||||
val privateKey = certificateStore.query { getPrivateKey(alias, certificateStore.entryPassword) }
|
val privateKey = certificateStore.query { getPrivateKey(alias, certificateStore.entryPassword) }
|
||||||
val signatureScheme = Crypto.findSignatureScheme(privateKey)
|
val signatureScheme = Crypto.findSignatureScheme(privateKey)
|
||||||
return ContentSignerBuilder.build(signatureScheme, privateKey, Crypto.findProvider(signatureScheme.providerName), newSecureRandom())
|
return ContentSignerBuilder.build(signatureScheme, privateKey, Crypto.findProvider(signatureScheme.providerName), newSecureRandom())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw CryptoServiceException("Cannot get Signer for key with alias $alias", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -53,14 +73,18 @@ class BCCryptoService(private val nodeConf: NodeConfiguration) : CryptoService {
|
|||||||
* loaded [certificateStore] in memory with the contents of the corresponding [KeyStore] file.
|
* loaded [certificateStore] in memory with the contents of the corresponding [KeyStore] file.
|
||||||
*/
|
*/
|
||||||
fun resyncKeystore() {
|
fun resyncKeystore() {
|
||||||
certificateStore = nodeConf.signingCertificateStore.get(true)
|
certificateStore = certificateStoreSupplier.get(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Import an already existing [KeyPair] to this [CryptoService]. */
|
/** Import an already existing [KeyPair] to this [CryptoService]. */
|
||||||
fun importKey(alias: String, keyPair: KeyPair) {
|
fun importKey(alias: String, keyPair: KeyPair) {
|
||||||
|
try {
|
||||||
// Store a self-signed certificate, as Keystore requires to store certificates instead of public keys.
|
// Store a self-signed certificate, as Keystore requires to store certificates instead of public keys.
|
||||||
// We could probably add a null cert, but we store a self-signed cert that will be used to retrieve the public key.
|
// We could probably add a null cert, but we store a self-signed cert that will be used to retrieve the public key.
|
||||||
val cert = X509Utilities.createSelfSignedCACertificate(nodeConf.myLegalName.x500Principal, keyPair)
|
val cert = X509Utilities.createSelfSignedCACertificate(legalName, keyPair)
|
||||||
certificateStore.query { setPrivateKey(alias, keyPair.private, listOf(cert), certificateStore.entryPassword) }
|
certificateStore.query { setPrivateKey(alias, keyPair.private, listOf(cert), certificateStore.entryPassword) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw CryptoServiceException("Cannot import key with alias $alias", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -268,6 +268,7 @@ class RPCServer<OPS : RPCOps>(
|
|||||||
log.error("Failed to send message, kicking client. Message was ${job.message}", throwable)
|
log.error("Failed to send message, kicking client. Message was ${job.message}", throwable)
|
||||||
serverControl!!.closeConsumerConnectionsForAddress(job.clientAddress.toString())
|
serverControl!!.closeConsumerConnectionsForAddress(job.clientAddress.toString())
|
||||||
invalidateClient(job.clientAddress)
|
invalidateClient(job.clientAddress)
|
||||||
|
if (throwable is VirtualMachineError) throw throwable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,8 +94,8 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
|
|||||||
override fun run() {
|
override fun run() {
|
||||||
val nextScheduleDelay = try {
|
val nextScheduleDelay = try {
|
||||||
updateNetworkMapCache()
|
updateNetworkMapCache()
|
||||||
} catch (t: Throwable) {
|
} catch (e: Exception) {
|
||||||
logger.warn("Error encountered while updating network map, will retry in $defaultRetryInterval", t)
|
logger.warn("Error encountered while updating network map, will retry in $defaultRetryInterval", e)
|
||||||
defaultRetryInterval
|
defaultRetryInterval
|
||||||
}
|
}
|
||||||
// Schedule the next update.
|
// Schedule the next update.
|
||||||
|
@ -223,9 +223,13 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
|||||||
val result = logic.call()
|
val result = logic.call()
|
||||||
suspend(FlowIORequest.WaitForSessionConfirmations, maySkipCheckpoint = true)
|
suspend(FlowIORequest.WaitForSessionConfirmations, maySkipCheckpoint = true)
|
||||||
Try.Success(result)
|
Try.Success(result)
|
||||||
} catch (throwable: Throwable) {
|
} catch (t: Throwable) {
|
||||||
logger.info("Flow threw exception... sending it to flow hospital", throwable)
|
if(t is VirtualMachineError) {
|
||||||
Try.Failure<R>(throwable)
|
logger.error("Caught unrecoverable error from flow. Forcibly terminating the JVM, this might leave resources open, and most likely will.", t)
|
||||||
|
Runtime.getRuntime().halt(1)
|
||||||
|
}
|
||||||
|
logger.info("Flow raised an error... sending it to flow hospital", t)
|
||||||
|
Try.Failure<R>(t)
|
||||||
}
|
}
|
||||||
val softLocksId = if (hasSoftLockedStates) logic.runId.uuid else null
|
val softLocksId = if (hasSoftLockedStates) logic.runId.uuid else null
|
||||||
val finalEvent = when (resultOrError) {
|
val finalEvent = when (resultOrError) {
|
||||||
@ -377,8 +381,8 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
|||||||
maySkipCheckpoint = skipPersistingCheckpoint,
|
maySkipCheckpoint = skipPersistingCheckpoint,
|
||||||
fiber = this.checkpointSerialize(context = serializationContext.value)
|
fiber = this.checkpointSerialize(context = serializationContext.value)
|
||||||
)
|
)
|
||||||
} catch (throwable: Throwable) {
|
} catch (exception: Exception) {
|
||||||
Event.Error(throwable)
|
Event.Error(exception)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We must commit the database transaction before returning from this closure otherwise Quasar may schedule
|
// We must commit the database transaction before returning from this closure otherwise Quasar may schedule
|
||||||
|
@ -43,6 +43,7 @@ import net.corda.nodeapi.internal.persistence.wrapWithDatabaseTransaction
|
|||||||
import net.corda.serialization.internal.CheckpointSerializeAsTokenContextImpl
|
import net.corda.serialization.internal.CheckpointSerializeAsTokenContextImpl
|
||||||
import net.corda.serialization.internal.withTokenContext
|
import net.corda.serialization.internal.withTokenContext
|
||||||
import org.apache.activemq.artemis.utils.ReusableLatch
|
import org.apache.activemq.artemis.utils.ReusableLatch
|
||||||
|
import org.apache.logging.log4j.LogManager
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.subjects.PublishSubject
|
import rx.subjects.PublishSubject
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
@ -135,8 +136,14 @@ class SingleThreadedStateMachineManager(
|
|||||||
val fibers = restoreFlowsFromCheckpoints()
|
val fibers = restoreFlowsFromCheckpoints()
|
||||||
metrics.register("Flows.InFlight", Gauge<Int> { mutex.content.flows.size })
|
metrics.register("Flows.InFlight", Gauge<Int> { mutex.content.flows.size })
|
||||||
Fiber.setDefaultUncaughtExceptionHandler { fiber, throwable ->
|
Fiber.setDefaultUncaughtExceptionHandler { fiber, throwable ->
|
||||||
|
if (throwable is VirtualMachineError) {
|
||||||
|
(fiber as FlowStateMachineImpl<*>).logger.error("Caught unrecoverable error from flow. Forcibly terminating the JVM, this might leave resources open, and most likely will.", throwable)
|
||||||
|
LogManager.shutdown(true)
|
||||||
|
Runtime.getRuntime().halt(1)
|
||||||
|
} else {
|
||||||
(fiber as FlowStateMachineImpl<*>).logger.warn("Caught exception from flow", throwable)
|
(fiber as FlowStateMachineImpl<*>).logger.warn("Caught exception from flow", throwable)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
serviceHub.networkMapCache.nodeReady.then {
|
serviceHub.networkMapCache.nodeReady.then {
|
||||||
logger.info("Node ready, info: ${serviceHub.myInfo}")
|
logger.info("Node ready, info: ${serviceHub.myInfo}")
|
||||||
resumeRestoredFlows(fibers)
|
resumeRestoredFlows(fibers)
|
||||||
@ -606,7 +613,7 @@ class SingleThreadedStateMachineManager(
|
|||||||
private fun deserializeCheckpoint(serializedCheckpoint: SerializedBytes<Checkpoint>): Checkpoint? {
|
private fun deserializeCheckpoint(serializedCheckpoint: SerializedBytes<Checkpoint>): Checkpoint? {
|
||||||
return try {
|
return try {
|
||||||
serializedCheckpoint.checkpointDeserialize(context = checkpointSerializationContext!!)
|
serializedCheckpoint.checkpointDeserialize(context = checkpointSerializationContext!!)
|
||||||
} catch (exception: Throwable) {
|
} catch (exception: Exception) {
|
||||||
logger.error("Encountered unrestorable checkpoint!", exception)
|
logger.error("Encountered unrestorable checkpoint!", exception)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,7 @@ class TransitionExecutorImpl(
|
|||||||
for (action in transition.actions) {
|
for (action in transition.actions) {
|
||||||
try {
|
try {
|
||||||
actionExecutor.executeAction(fiber, action)
|
actionExecutor.executeAction(fiber, action)
|
||||||
} catch (exception: Throwable) {
|
} catch (exception: Exception) {
|
||||||
contextTransactionOrNull?.close()
|
contextTransactionOrNull?.close()
|
||||||
if (transition.newState.checkpoint.errorState is ErrorState.Errored) {
|
if (transition.newState.checkpoint.errorState is ErrorState.Errored) {
|
||||||
// If we errored while transitioning to an error state then we cannot record the additional
|
// If we errored while transitioning to an error state then we cannot record the additional
|
||||||
|
@ -77,8 +77,8 @@ class FiberDeserializationChecker {
|
|||||||
is Job.Check -> {
|
is Job.Check -> {
|
||||||
try {
|
try {
|
||||||
job.serializedFiber.checkpointDeserialize(context = checkpointSerializationContext)
|
job.serializedFiber.checkpointDeserialize(context = checkpointSerializationContext)
|
||||||
} catch (throwable: Throwable) {
|
} catch (exception: Exception) {
|
||||||
log.error("Encountered unrestorable checkpoint!", throwable)
|
log.error("Encountered unrestorable checkpoint!", exception)
|
||||||
foundUnrestorableFibers = true
|
foundUnrestorableFibers = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,15 @@
|
|||||||
package net.corda.node.services.transactions
|
package net.corda.node.services.transactions
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
|
||||||
import net.corda.core.contracts.ComponentGroupEnum
|
import net.corda.core.contracts.ComponentGroupEnum
|
||||||
import net.corda.core.flows.FlowSession
|
import net.corda.core.flows.FlowSession
|
||||||
import net.corda.core.flows.NotarisationPayload
|
import net.corda.core.flows.NotarisationPayload
|
||||||
import net.corda.core.flows.NotarisationRequest
|
|
||||||
import net.corda.core.internal.notary.SinglePartyNotaryService
|
|
||||||
import net.corda.core.internal.notary.NotaryServiceFlow
|
import net.corda.core.internal.notary.NotaryServiceFlow
|
||||||
|
import net.corda.core.internal.notary.SinglePartyNotaryService
|
||||||
import net.corda.core.transactions.ContractUpgradeFilteredTransaction
|
import net.corda.core.transactions.ContractUpgradeFilteredTransaction
|
||||||
import net.corda.core.transactions.CoreTransaction
|
|
||||||
import net.corda.core.transactions.FilteredTransaction
|
import net.corda.core.transactions.FilteredTransaction
|
||||||
import net.corda.core.transactions.NotaryChangeWireTransaction
|
import net.corda.core.transactions.NotaryChangeWireTransaction
|
||||||
|
|
||||||
class NonValidatingNotaryFlow(otherSideSession: FlowSession, service: SinglePartyNotaryService) : NotaryServiceFlow(otherSideSession, service) {
|
/**
|
||||||
/**
|
|
||||||
* The received transaction is not checked for contract-validity, as that would require fully
|
* The received transaction is not checked for contract-validity, as that would require fully
|
||||||
* resolving it into a [TransactionForVerification], for which the caller would have to reveal the whole transaction
|
* resolving it into a [TransactionForVerification], for which the caller would have to reveal the whole transaction
|
||||||
* history chain.
|
* history chain.
|
||||||
@ -21,18 +17,9 @@ class NonValidatingNotaryFlow(otherSideSession: FlowSession, service: SinglePart
|
|||||||
* the caller, it is possible to raise a dispute and verify the validity of the transaction and subsequently
|
* the caller, it is possible to raise a dispute and verify the validity of the transaction and subsequently
|
||||||
* undo the commit of the input states (the exact mechanism still needs to be worked out).
|
* undo the commit of the input states (the exact mechanism still needs to be worked out).
|
||||||
*/
|
*/
|
||||||
@Suspendable
|
class NonValidatingNotaryFlow(otherSideSession: FlowSession, service: SinglePartyNotaryService) : NotaryServiceFlow(otherSideSession, service) {
|
||||||
override fun validateRequest(requestPayload: NotarisationPayload): TransactionParts {
|
override fun extractParts(requestPayload: NotarisationPayload): TransactionParts {
|
||||||
val transaction = requestPayload.coreTransaction
|
val tx = requestPayload.coreTransaction
|
||||||
checkInputs(transaction.inputs + transaction.references)
|
|
||||||
val request = NotarisationRequest(transaction.inputs, transaction.id)
|
|
||||||
validateRequestSignature(request, requestPayload.requestSignature)
|
|
||||||
val parts = extractParts(transaction)
|
|
||||||
checkNotary(parts.notary)
|
|
||||||
return parts
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun extractParts(tx: CoreTransaction): TransactionParts {
|
|
||||||
return when (tx) {
|
return when (tx) {
|
||||||
is FilteredTransaction -> {
|
is FilteredTransaction -> {
|
||||||
tx.apply {
|
tx.apply {
|
||||||
@ -43,7 +30,7 @@ class NonValidatingNotaryFlow(otherSideSession: FlowSession, service: SinglePart
|
|||||||
}
|
}
|
||||||
TransactionParts(tx.id, tx.inputs, tx.timeWindow, tx.notary, tx.references)
|
TransactionParts(tx.id, tx.inputs, tx.timeWindow, tx.notary, tx.references)
|
||||||
}
|
}
|
||||||
is ContractUpgradeFilteredTransaction -> TransactionParts(tx.id, tx.inputs, null, tx.notary)
|
is ContractUpgradeFilteredTransaction,
|
||||||
is NotaryChangeWireTransaction -> TransactionParts(tx.id, tx.inputs, null, tx.notary)
|
is NotaryChangeWireTransaction -> TransactionParts(tx.id, tx.inputs, null, tx.notary)
|
||||||
else -> {
|
else -> {
|
||||||
throw IllegalArgumentException("Received unexpected transaction type: ${tx::class.java.simpleName}," +
|
throw IllegalArgumentException("Received unexpected transaction type: ${tx::class.java.simpleName}," +
|
||||||
|
@ -2,19 +2,16 @@ package net.corda.node.services.transactions
|
|||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.contracts.TimeWindow
|
import net.corda.core.contracts.TimeWindow
|
||||||
import net.corda.core.contracts.TransactionVerificationException
|
|
||||||
import net.corda.core.flows.FlowSession
|
import net.corda.core.flows.FlowSession
|
||||||
import net.corda.core.flows.NotarisationPayload
|
import net.corda.core.flows.NotarisationPayload
|
||||||
import net.corda.core.flows.NotarisationRequest
|
|
||||||
import net.corda.core.flows.NotaryError
|
import net.corda.core.flows.NotaryError
|
||||||
import net.corda.core.internal.ResolveTransactionsFlow
|
import net.corda.core.internal.ResolveTransactionsFlow
|
||||||
import net.corda.core.internal.notary.SinglePartyNotaryService
|
|
||||||
import net.corda.core.internal.notary.NotaryInternalException
|
import net.corda.core.internal.notary.NotaryInternalException
|
||||||
import net.corda.core.internal.notary.NotaryServiceFlow
|
import net.corda.core.internal.notary.NotaryServiceFlow
|
||||||
|
import net.corda.core.internal.notary.SinglePartyNotaryService
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.transactions.TransactionWithSignatures
|
import net.corda.core.transactions.TransactionWithSignatures
|
||||||
import net.corda.core.transactions.WireTransaction
|
import net.corda.core.transactions.WireTransaction
|
||||||
import java.security.SignatureException
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A notary commit flow that makes sure a given transaction is valid before committing it. This does mean that the calling
|
* A notary commit flow that makes sure a given transaction is valid before committing it. This does mean that the calling
|
||||||
@ -22,29 +19,25 @@ import java.security.SignatureException
|
|||||||
* has its input states "blocked" by a transaction from another party, and needs to establish whether that transaction was
|
* has its input states "blocked" by a transaction from another party, and needs to establish whether that transaction was
|
||||||
* indeed valid.
|
* indeed valid.
|
||||||
*/
|
*/
|
||||||
class ValidatingNotaryFlow(otherSideSession: FlowSession, service: SinglePartyNotaryService) : NotaryServiceFlow(otherSideSession, service) {
|
open class ValidatingNotaryFlow(otherSideSession: FlowSession, service: SinglePartyNotaryService) : NotaryServiceFlow(otherSideSession, service) {
|
||||||
|
override fun extractParts(requestPayload: NotarisationPayload): TransactionParts {
|
||||||
|
val stx = requestPayload.signedTransaction
|
||||||
|
val timeWindow: TimeWindow? = if (stx.coreTransaction is WireTransaction) stx.tx.timeWindow else null
|
||||||
|
return TransactionParts(stx.id, stx.inputs, timeWindow, stx.notary, stx.references)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fully resolves the received transaction and its dependencies, runs contract verification logic and checks that
|
* Fully resolves the received transaction and its dependencies, runs contract verification logic and checks that
|
||||||
* the transaction in question has all required signatures apart from the notary's.
|
* the transaction in question has all required signatures apart from the notary's.
|
||||||
*/
|
*/
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun validateRequest(requestPayload: NotarisationPayload): TransactionParts {
|
override fun verifyTransaction(requestPayload: NotarisationPayload) {
|
||||||
try {
|
try {
|
||||||
val stx = requestPayload.signedTransaction
|
val stx = requestPayload.signedTransaction
|
||||||
checkInputs(stx.inputs + stx.references)
|
|
||||||
validateRequestSignature(NotarisationRequest(stx.inputs, stx.id), requestPayload.requestSignature)
|
|
||||||
val notary = stx.notary
|
|
||||||
checkNotary(notary)
|
|
||||||
resolveAndContractVerify(stx)
|
resolveAndContractVerify(stx)
|
||||||
verifySignatures(stx)
|
verifySignatures(stx)
|
||||||
val timeWindow: TimeWindow? = if (stx.coreTransaction is WireTransaction) stx.tx.timeWindow else null
|
|
||||||
return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!, stx.references)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw when (e) {
|
throw NotaryInternalException(NotaryError.TransactionInvalid(e))
|
||||||
is TransactionVerificationException,
|
|
||||||
is SignatureException -> NotaryInternalException(NotaryError.TransactionInvalid(e))
|
|
||||||
else -> e
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,10 +53,6 @@ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: SinglePartyNo
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun checkSignatures(tx: TransactionWithSignatures) {
|
private fun checkSignatures(tx: TransactionWithSignatures) {
|
||||||
try {
|
|
||||||
tx.verifySignaturesExcept(service.notaryIdentityKey)
|
tx.verifySignaturesExcept(service.notaryIdentityKey)
|
||||||
} catch (e: SignatureException) {
|
|
||||||
throw NotaryInternalException(NotaryError.TransactionInvalid(e))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.Parameterized;
|
||||||
|
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
@RunWith(Parameterized.class)
|
||||||
|
public class CordaCapletBaseDirectoryParsingFailureTest {
|
||||||
|
@Parameterized.Parameters
|
||||||
|
public static Collection<Object[]> CombinationsToTest() {
|
||||||
|
return Arrays.asList(
|
||||||
|
new Object[][]{
|
||||||
|
{new String[]{"--base-directory", "--another-option"}},
|
||||||
|
{new String[]{"--base-directory=", "-a"}},
|
||||||
|
{new String[]{"-b", "--another-option"}},
|
||||||
|
{new String[]{"-b=", "-a"}}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String[] cmdLineArguments;
|
||||||
|
|
||||||
|
public CordaCapletBaseDirectoryParsingFailureTest(String[] baseOption) {
|
||||||
|
this.cmdLineArguments = baseOption;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testThatBaseDirectoryFallsBackToCurrentWhenBaseDirectoryIsNotSupplied() {
|
||||||
|
final CordaCaplet caplet = CordaCapletTestUtils.getCaplet();
|
||||||
|
final String returnPath = caplet.getBaseDirectory(Arrays.asList(cmdLineArguments));
|
||||||
|
final String expected = Paths.get(".").toAbsolutePath().normalize().toString();
|
||||||
|
assertEquals(expected, returnPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
39
node/src/test/java/CordaCapletBaseDirectoryParsingTest.java
Normal file
39
node/src/test/java/CordaCapletBaseDirectoryParsingTest.java
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.Parameterized;
|
||||||
|
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
@RunWith(Parameterized.class)
|
||||||
|
public class CordaCapletBaseDirectoryParsingTest {
|
||||||
|
@Parameterized.Parameters
|
||||||
|
public static Collection<Object[]> CombinationsToTest() {
|
||||||
|
return Arrays.asList(
|
||||||
|
new Object[][]{
|
||||||
|
{new String[]{"--base-directory", "blah"}},
|
||||||
|
{new String[]{"--base-directory=blah"}},
|
||||||
|
{new String[]{"-b", "blah"}},
|
||||||
|
{new String[]{"-b=blah"}}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private String[] cmdLineArguments;
|
||||||
|
|
||||||
|
public CordaCapletBaseDirectoryParsingTest(String[] arr) {
|
||||||
|
this.cmdLineArguments = arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testThatBaseDirectoryParameterIsRecognised() {
|
||||||
|
final CordaCaplet caplet = CordaCapletTestUtils.getCaplet();
|
||||||
|
final String returnPath = caplet.getBaseDirectory(Arrays.asList(cmdLineArguments));
|
||||||
|
final String expected = Paths.get(".").resolve("blah").toAbsolutePath().normalize().toString();
|
||||||
|
assertEquals(expected, returnPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
|||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.Parameterized;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
@RunWith(Parameterized.class)
|
||||||
|
public class CordaCapletConfigFileParsingFailureTest {
|
||||||
|
@Parameterized.Parameters
|
||||||
|
public static Collection<Object[]> CombinationsToTest() {
|
||||||
|
return Arrays.asList(
|
||||||
|
new Object[][]{
|
||||||
|
{new String[]{"--config-file", "--another-option"}},
|
||||||
|
{new String[]{"--config-file=", "-a"}},
|
||||||
|
{new String[]{"-f", "--another-option"}},
|
||||||
|
{new String[]{"-f=", "-a"}}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String[] cmdLineArguments;
|
||||||
|
|
||||||
|
public CordaCapletConfigFileParsingFailureTest(String[] baseOption) {
|
||||||
|
this.cmdLineArguments = baseOption;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testThatBaseDirectoryFallsBackToDefaultWhenConfigFileIsNotSupplied() {
|
||||||
|
final CordaCaplet caplet = CordaCapletTestUtils.getCaplet();
|
||||||
|
final File returnPath = caplet.getConfigFile(Arrays.asList(cmdLineArguments), CordaCapletTestUtils.getBaseDir());
|
||||||
|
final File expected = Paths.get(".").resolve("node.conf").toAbsolutePath().normalize().toFile();
|
||||||
|
assertEquals(expected, returnPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
40
node/src/test/java/CordaCapletConfigFileParsingTest.java
Normal file
40
node/src/test/java/CordaCapletConfigFileParsingTest.java
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.Parameterized;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
@RunWith(Parameterized.class)
|
||||||
|
public class CordaCapletConfigFileParsingTest {
|
||||||
|
@Parameterized.Parameters
|
||||||
|
public static Collection<Object[]> CombinationsToTest() {
|
||||||
|
return Arrays.asList(
|
||||||
|
new Object[][]{
|
||||||
|
{new String[]{"--config-file", "blah.conf"}},
|
||||||
|
{new String[]{"--config-file=blah.conf"}},
|
||||||
|
{new String[]{"-f", "blah.conf"}},
|
||||||
|
{new String[]{"-f=blah.conf"}}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private String[] cmdLineArguments;
|
||||||
|
|
||||||
|
public CordaCapletConfigFileParsingTest(String[] arr) {
|
||||||
|
this.cmdLineArguments = arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testThatConfigFileParameterIsRecognised() {
|
||||||
|
final CordaCaplet caplet = CordaCapletTestUtils.getCaplet();
|
||||||
|
final File returnPath = caplet.getConfigFile(Arrays.asList(cmdLineArguments), CordaCapletTestUtils.getBaseDir());
|
||||||
|
final File expected = Paths.get(".").resolve("blah.conf").toAbsolutePath().normalize().toFile();
|
||||||
|
assertEquals(expected, returnPath.getAbsoluteFile());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
16
node/src/test/java/CordaCapletTestUtils.java
Normal file
16
node/src/test/java/CordaCapletTestUtils.java
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
class CordaCapletTestUtils {
|
||||||
|
static CordaCaplet getCaplet() {
|
||||||
|
final String path = System.getProperty("user.dir") + File.separator + "build" + File.separator + "libs" + File.separator;
|
||||||
|
final File jar = Arrays.stream(Objects.requireNonNull(new File(path).listFiles())).filter(x -> x.getName().startsWith("corda-node") && x.getName().endsWith(".jar")).findFirst().get();
|
||||||
|
return new CordaCaplet(new Capsule(jar.toPath()));
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getBaseDir() {
|
||||||
|
return Paths.get(".").toAbsolutePath().normalize().toString();
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,6 @@ import net.corda.core.internal.FlowIORequest
|
|||||||
import net.corda.core.internal.ResolveTransactionsFlow
|
import net.corda.core.internal.ResolveTransactionsFlow
|
||||||
import net.corda.core.internal.bufferUntilSubscribed
|
import net.corda.core.internal.bufferUntilSubscribed
|
||||||
import net.corda.core.internal.concurrent.openFuture
|
import net.corda.core.internal.concurrent.openFuture
|
||||||
import net.corda.core.internal.notary.NotaryServiceFlow
|
|
||||||
import net.corda.core.internal.notary.SinglePartyNotaryService
|
import net.corda.core.internal.notary.SinglePartyNotaryService
|
||||||
import net.corda.core.internal.notary.UniquenessProvider
|
import net.corda.core.internal.notary.UniquenessProvider
|
||||||
import net.corda.core.node.NotaryInfo
|
import net.corda.core.node.NotaryInfo
|
||||||
@ -22,6 +21,7 @@ import net.corda.core.transactions.TransactionBuilder
|
|||||||
import net.corda.core.utilities.ProgressTracker
|
import net.corda.core.utilities.ProgressTracker
|
||||||
import net.corda.core.utilities.seconds
|
import net.corda.core.utilities.seconds
|
||||||
import net.corda.node.services.api.ServiceHubInternal
|
import net.corda.node.services.api.ServiceHubInternal
|
||||||
|
import net.corda.node.services.transactions.ValidatingNotaryFlow
|
||||||
import net.corda.nodeapi.internal.DevIdentityGenerator
|
import net.corda.nodeapi.internal.DevIdentityGenerator
|
||||||
import net.corda.nodeapi.internal.network.NetworkParametersCopier
|
import net.corda.nodeapi.internal.network.NetworkParametersCopier
|
||||||
import net.corda.testing.common.internal.testNetworkParameters
|
import net.corda.testing.common.internal.testNetworkParameters
|
||||||
@ -199,9 +199,9 @@ class TimedFlowTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** A notary flow that will yield without returning a response on the very first received request. */
|
/** A notary flow that will yield without returning a response on the very first received request. */
|
||||||
private class TestNotaryFlow(otherSide: FlowSession, service: TestNotaryService) : NotaryServiceFlow(otherSide, service) {
|
private class TestNotaryFlow(otherSide: FlowSession, service: TestNotaryService) : ValidatingNotaryFlow(otherSide, service) {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun validateRequest(requestPayload: NotarisationPayload): TransactionParts {
|
override fun verifyTransaction(requestPayload: NotarisationPayload) {
|
||||||
val myIdentity = serviceHub.myInfo.legalIdentities.first()
|
val myIdentity = serviceHub.myInfo.legalIdentities.first()
|
||||||
MDC.put("name", myIdentity.name.toString())
|
MDC.put("name", myIdentity.name.toString())
|
||||||
logger.info("Received a request from ${otherSideSession.counterparty.name}")
|
logger.info("Received a request from ${otherSideSession.counterparty.name}")
|
||||||
@ -215,7 +215,6 @@ class TimedFlowTests {
|
|||||||
} else {
|
} else {
|
||||||
logger.info("Processing")
|
logger.info("Processing")
|
||||||
}
|
}
|
||||||
return TransactionParts(stx.id, stx.inputs, stx.tx.timeWindow, stx.notary)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -276,7 +276,7 @@ class NodeConfigurationImplTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `jmxReporterType is null and defaults to Jokolia`() {
|
fun `jmxReporterType is null and defaults to Jokolia`() {
|
||||||
val rawConfig = getConfig("working-config.conf", ConfigFactory.parseMap(mapOf("devMode" to true)))
|
val rawConfig = getConfig("working-config.conf", ConfigFactory.parseMap(mapOf("devMode" to true)))
|
||||||
val nodeConfig = rawConfig.parseAsNodeConfiguration().orThrow()
|
val nodeConfig = rawConfig.parseAsNodeConfiguration().value()
|
||||||
assertTrue(JmxReporterType.JOLOKIA.toString() == nodeConfig.jmxReporterType.toString())
|
assertTrue(JmxReporterType.JOLOKIA.toString() == nodeConfig.jmxReporterType.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -284,7 +284,7 @@ class NodeConfigurationImplTest {
|
|||||||
fun `jmxReporterType is not null and is set to New Relic`() {
|
fun `jmxReporterType is not null and is set to New Relic`() {
|
||||||
var rawConfig = getConfig("working-config.conf", ConfigFactory.parseMap(mapOf("devMode" to true)))
|
var rawConfig = getConfig("working-config.conf", ConfigFactory.parseMap(mapOf("devMode" to true)))
|
||||||
rawConfig = rawConfig.withValue("jmxReporterType", ConfigValueFactory.fromAnyRef("NEW_RELIC"))
|
rawConfig = rawConfig.withValue("jmxReporterType", ConfigValueFactory.fromAnyRef("NEW_RELIC"))
|
||||||
val nodeConfig = rawConfig.parseAsNodeConfiguration().orThrow()
|
val nodeConfig = rawConfig.parseAsNodeConfiguration().value()
|
||||||
assertTrue(JmxReporterType.NEW_RELIC.toString() == nodeConfig.jmxReporterType.toString())
|
assertTrue(JmxReporterType.NEW_RELIC.toString() == nodeConfig.jmxReporterType.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -292,7 +292,7 @@ class NodeConfigurationImplTest {
|
|||||||
fun `jmxReporterType is not null and set to Jokolia`() {
|
fun `jmxReporterType is not null and set to Jokolia`() {
|
||||||
var rawConfig = getConfig("working-config.conf", ConfigFactory.parseMap(mapOf("devMode" to true)))
|
var rawConfig = getConfig("working-config.conf", ConfigFactory.parseMap(mapOf("devMode" to true)))
|
||||||
rawConfig = rawConfig.withValue("jmxReporterType", ConfigValueFactory.fromAnyRef("JOLOKIA"))
|
rawConfig = rawConfig.withValue("jmxReporterType", ConfigValueFactory.fromAnyRef("JOLOKIA"))
|
||||||
val nodeConfig = rawConfig.parseAsNodeConfiguration().orThrow()
|
val nodeConfig = rawConfig.parseAsNodeConfiguration().value()
|
||||||
assertTrue(JmxReporterType.JOLOKIA.toString() == nodeConfig.jmxReporterType.toString())
|
assertTrue(JmxReporterType.JOLOKIA.toString() == nodeConfig.jmxReporterType.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,83 @@
|
|||||||
|
package net.corda.node.services.keys.cryptoservice
|
||||||
|
|
||||||
|
import net.corda.core.crypto.Crypto
|
||||||
|
import net.corda.core.crypto.SignatureScheme
|
||||||
|
import net.corda.core.internal.div
|
||||||
|
import net.corda.core.utilities.days
|
||||||
|
import net.corda.nodeapi.internal.config.CertificateStoreSupplier
|
||||||
|
import net.corda.nodeapi.internal.crypto.CertificateType
|
||||||
|
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||||
|
import net.corda.nodeapi.internal.cryptoservice.CryptoServiceException
|
||||||
|
import net.corda.testing.core.ALICE_NAME
|
||||||
|
import net.corda.testing.internal.stubs.CertificateStoreStubs
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TemporaryFolder
|
||||||
|
import java.time.Duration
|
||||||
|
import javax.security.auth.x500.X500Principal
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class BCCryptoServiceTests {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val clearData = "data".toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
@JvmField
|
||||||
|
val temporaryFolder = TemporaryFolder()
|
||||||
|
private lateinit var signingCertificateStore: CertificateStoreSupplier
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
val baseDirectory = temporaryFolder.root.toPath()
|
||||||
|
val certificatesDirectory = baseDirectory / "certificates"
|
||||||
|
signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `BCCryptoService generate key pair and sign both data and cert`() {
|
||||||
|
val cryptoService = BCCryptoService(ALICE_NAME.x500Principal, signingCertificateStore)
|
||||||
|
// Testing every supported scheme.
|
||||||
|
Crypto.supportedSignatureSchemes().filter { it != Crypto.COMPOSITE_KEY }.forEach { generateKeyAndSignForScheme(cryptoService, it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateKeyAndSignForScheme(cryptoService: BCCryptoService, signatureScheme: SignatureScheme) {
|
||||||
|
val schemeNumberID = signatureScheme.schemeNumberID
|
||||||
|
val alias = "signature$schemeNumberID"
|
||||||
|
val pubKey = cryptoService.generateKeyPair(alias, schemeNumberID)
|
||||||
|
assertTrue { cryptoService.containsKey(alias) }
|
||||||
|
|
||||||
|
val signatureData = cryptoService.sign(alias, clearData)
|
||||||
|
assertTrue(Crypto.doVerify(pubKey, signatureData, clearData))
|
||||||
|
|
||||||
|
// Test that getSigner can indeed sign a certificate.
|
||||||
|
val signer = cryptoService.getSigner(alias)
|
||||||
|
val x500Principal = X500Principal("CN=Test")
|
||||||
|
val window = X509Utilities.getCertificateValidityWindow(Duration.ZERO, 365.days)
|
||||||
|
val certificate = X509Utilities.createCertificate(
|
||||||
|
CertificateType.CONFIDENTIAL_LEGAL_IDENTITY,
|
||||||
|
x500Principal,
|
||||||
|
pubKey,
|
||||||
|
signer,
|
||||||
|
x500Principal,
|
||||||
|
pubKey,
|
||||||
|
window)
|
||||||
|
|
||||||
|
certificate.checkValidity()
|
||||||
|
certificate.verify(pubKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `When key does not exist getPublicKey, sign and getSigner should throw`() {
|
||||||
|
val nonExistingAlias = "nonExistingAlias"
|
||||||
|
val cryptoService = BCCryptoService(ALICE_NAME.x500Principal, signingCertificateStore)
|
||||||
|
assertFalse { cryptoService.containsKey(nonExistingAlias) }
|
||||||
|
assertFailsWith<CryptoServiceException> { cryptoService.getPublicKey(nonExistingAlias) }
|
||||||
|
assertFailsWith<CryptoServiceException> { cryptoService.sign(nonExistingAlias, clearData) }
|
||||||
|
assertFailsWith<CryptoServiceException> { cryptoService.getSigner(nonExistingAlias) }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
package net.corda.node.services.persistence
|
||||||
|
|
||||||
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import com.esotericsoftware.kryo.KryoException
|
||||||
|
import net.corda.core.contracts.UniqueIdentifier
|
||||||
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.flows.FlowLogic.Companion.sleep
|
||||||
|
import net.corda.core.identity.CordaX500Name
|
||||||
|
import net.corda.core.schemas.MappedSchema
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||||
|
import net.corda.testing.common.internal.testNetworkParameters
|
||||||
|
import net.corda.testing.core.TestIdentity
|
||||||
|
import net.corda.testing.node.MockNetwork
|
||||||
|
import net.corda.testing.node.MockServices
|
||||||
|
import net.corda.testing.node.makeTestIdentityService
|
||||||
|
import org.junit.BeforeClass
|
||||||
|
import org.junit.ClassRule
|
||||||
|
import org.junit.Test
|
||||||
|
import java.io.Serializable
|
||||||
|
import java.time.Duration
|
||||||
|
import javax.persistence.Column
|
||||||
|
import javax.persistence.Entity
|
||||||
|
import javax.persistence.Id
|
||||||
|
import javax.persistence.Table
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
|
||||||
|
|
||||||
|
class ExposeJpaToFlowsTests {
|
||||||
|
|
||||||
|
object FooSchema
|
||||||
|
|
||||||
|
object FooSchemaV1 : MappedSchema(schemaFamily = FooSchema.javaClass, version = 1, mappedTypes = listOf(PersistentFoo::class.java)) {
|
||||||
|
@Entity
|
||||||
|
@Table(name = "foos")
|
||||||
|
class PersistentFoo(@Id @Column(name = "foo_id") var fooId: String, @Column(name = "foo_data") var fooData: String) : Serializable
|
||||||
|
}
|
||||||
|
|
||||||
|
val myself = TestIdentity(CordaX500Name("Me", "London", "GB"))
|
||||||
|
val cordapps = listOf("net.corda.node.services.persistence")
|
||||||
|
val databaseAndServices = MockServices.makeTestDatabaseAndMockServices(
|
||||||
|
cordappPackages = cordapps,
|
||||||
|
identityService = makeTestIdentityService(myself.identity),
|
||||||
|
initialIdentity = myself,
|
||||||
|
networkParameters = testNetworkParameters(minimumPlatformVersion = 4)
|
||||||
|
)
|
||||||
|
|
||||||
|
val services: MockServices = databaseAndServices.second
|
||||||
|
val database: CordaPersistence = databaseAndServices.first
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `can persist and query custom entities`() {
|
||||||
|
val foo = FooSchemaV1.PersistentFoo(UniqueIdentifier().id.toString(), "Bar")
|
||||||
|
|
||||||
|
// Persist the foo.
|
||||||
|
val result: MutableList<FooSchemaV1.PersistentFoo> = database.transaction {
|
||||||
|
services.withEntityManager {
|
||||||
|
// Persist.
|
||||||
|
persist(foo)
|
||||||
|
// Query.
|
||||||
|
val query = criteriaBuilder.createQuery(FooSchemaV1.PersistentFoo::class.java)
|
||||||
|
val type = query.from(FooSchemaV1.PersistentFoo::class.java)
|
||||||
|
query.select(type)
|
||||||
|
createQuery(query).resultList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("Bar", result.single().fooData)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `can't perform suspendable operations inside withEntityManager`() {
|
||||||
|
val mockNet = MockNetwork(cordapps)
|
||||||
|
val mockNode = mockNet.createNode()
|
||||||
|
assertFailsWith(KryoException::class) {
|
||||||
|
mockNode.startFlow(object : FlowLogic<Unit>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
serviceHub.withEntityManager {
|
||||||
|
val session = initiateFlow(myself.party)
|
||||||
|
session.send("Ooohhh eee oooh ah ah ting tang walla walla bing bang!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
mockNet.stopNodes()
|
||||||
|
}
|
||||||
|
}
|
@ -276,7 +276,7 @@ class NodeVaultServiceTest {
|
|||||||
assertThat(vaultService.queryBy<Cash.State>(criteriaByLockId1).states).hasSize(3)
|
assertThat(vaultService.queryBy<Cash.State>(criteriaByLockId1).states).hasSize(3)
|
||||||
}
|
}
|
||||||
println("SOFT LOCK STATES #1 succeeded")
|
println("SOFT LOCK STATES #1 succeeded")
|
||||||
} catch (e: Throwable) {
|
} catch (e: Exception) {
|
||||||
println("SOFT LOCK STATES #1 failed")
|
println("SOFT LOCK STATES #1 failed")
|
||||||
} finally {
|
} finally {
|
||||||
countDown.countDown()
|
countDown.countDown()
|
||||||
@ -292,7 +292,7 @@ class NodeVaultServiceTest {
|
|||||||
assertThat(vaultService.queryBy<Cash.State>(criteriaByLockId2).states).hasSize(3)
|
assertThat(vaultService.queryBy<Cash.State>(criteriaByLockId2).states).hasSize(3)
|
||||||
}
|
}
|
||||||
println("SOFT LOCK STATES #2 succeeded")
|
println("SOFT LOCK STATES #2 succeeded")
|
||||||
} catch (e: Throwable) {
|
} catch (e: Exception) {
|
||||||
println("SOFT LOCK STATES #2 failed")
|
println("SOFT LOCK STATES #2 failed")
|
||||||
} finally {
|
} finally {
|
||||||
countDown.countDown()
|
countDown.countDown()
|
||||||
|
@ -327,7 +327,7 @@ class TLSAuthenticationTests {
|
|||||||
lock.notifyAll()
|
lock.notifyAll()
|
||||||
}
|
}
|
||||||
sslServerSocket.close()
|
sslServerSocket.close()
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Exception) {
|
||||||
serverError = true
|
serverError = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,7 @@ class InterestRateSwapAPI {
|
|||||||
return try {
|
return try {
|
||||||
rpc.startFlow(AutoOfferFlow::Requester, newDeal).returnValue.getOrThrow()
|
rpc.startFlow(AutoOfferFlow::Requester, newDeal).returnValue.getOrThrow()
|
||||||
ResponseEntity.created(URI.create(generateDealLink(newDeal))).build()
|
ResponseEntity.created(URI.create(generateDealLink(newDeal))).build()
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Exception) {
|
||||||
logger.info("Exception when creating deal: $ex", ex)
|
logger.info("Exception when creating deal: $ex", ex)
|
||||||
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ex.toString())
|
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ex.toString())
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,19 @@
|
|||||||
package net.corda.notarydemo
|
package net.corda.notarydemo
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.contracts.TimeWindow
|
import net.corda.core.flows.FlowLogic
|
||||||
import net.corda.core.contracts.TransactionVerificationException
|
import net.corda.core.flows.FlowSession
|
||||||
import net.corda.core.flows.*
|
import net.corda.core.flows.NotarisationPayload
|
||||||
|
import net.corda.core.flows.NotaryError
|
||||||
import net.corda.core.internal.ResolveTransactionsFlow
|
import net.corda.core.internal.ResolveTransactionsFlow
|
||||||
import net.corda.core.internal.notary.NotaryInternalException
|
import net.corda.core.internal.notary.NotaryInternalException
|
||||||
import net.corda.core.internal.notary.NotaryServiceFlow
|
|
||||||
import net.corda.core.internal.notary.SinglePartyNotaryService
|
import net.corda.core.internal.notary.SinglePartyNotaryService
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.transactions.TransactionWithSignatures
|
import net.corda.core.transactions.TransactionWithSignatures
|
||||||
import net.corda.core.transactions.WireTransaction
|
|
||||||
import net.corda.node.services.api.ServiceHubInternal
|
import net.corda.node.services.api.ServiceHubInternal
|
||||||
import net.corda.node.services.transactions.PersistentUniquenessProvider
|
import net.corda.node.services.transactions.PersistentUniquenessProvider
|
||||||
|
import net.corda.node.services.transactions.ValidatingNotaryFlow
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.security.SignatureException
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A custom notary service should provide a constructor that accepts two parameters of types [ServiceHubInternal] and [PublicKey].
|
* A custom notary service should provide a constructor that accepts two parameters of types [ServiceHubInternal] and [PublicKey].
|
||||||
@ -35,28 +34,15 @@ class MyCustomValidatingNotaryService(override val services: ServiceHubInternal,
|
|||||||
|
|
||||||
@Suppress("UNUSED_PARAMETER")
|
@Suppress("UNUSED_PARAMETER")
|
||||||
// START 2
|
// START 2
|
||||||
class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidatingNotaryService) : NotaryServiceFlow(otherSide, service) {
|
class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidatingNotaryService) : ValidatingNotaryFlow(otherSide, service) {
|
||||||
/**
|
override fun verifyTransaction(requestPayload: NotarisationPayload) {
|
||||||
* The received transaction is checked for contract-validity, for which the caller also has to to reveal the whole
|
|
||||||
* transaction dependency chain.
|
|
||||||
*/
|
|
||||||
@Suspendable
|
|
||||||
override fun validateRequest(requestPayload: NotarisationPayload): TransactionParts {
|
|
||||||
try {
|
try {
|
||||||
val stx = requestPayload.signedTransaction
|
val stx = requestPayload.signedTransaction
|
||||||
validateRequestSignature(NotarisationRequest(stx.inputs, stx.id), requestPayload.requestSignature)
|
|
||||||
val notary = stx.notary
|
|
||||||
checkNotary(notary)
|
|
||||||
verifySignatures(stx)
|
|
||||||
resolveAndContractVerify(stx)
|
resolveAndContractVerify(stx)
|
||||||
val timeWindow: TimeWindow? = if (stx.coreTransaction is WireTransaction) stx.tx.timeWindow else null
|
verifySignatures(stx)
|
||||||
return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!, stx.references)
|
customVerify(stx)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw when (e) {
|
throw NotaryInternalException(NotaryError.TransactionInvalid(e))
|
||||||
is TransactionVerificationException,
|
|
||||||
is SignatureException -> NotaryInternalException(NotaryError.TransactionInvalid(e))
|
|
||||||
else -> e
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,7 +50,6 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating
|
|||||||
private fun resolveAndContractVerify(stx: SignedTransaction) {
|
private fun resolveAndContractVerify(stx: SignedTransaction) {
|
||||||
subFlow(ResolveTransactionsFlow(stx, otherSideSession))
|
subFlow(ResolveTransactionsFlow(stx, otherSideSession))
|
||||||
stx.verify(serviceHub, false)
|
stx.verify(serviceHub, false)
|
||||||
customVerify(stx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun verifySignatures(stx: SignedTransaction) {
|
private fun verifySignatures(stx: SignedTransaction) {
|
||||||
@ -73,11 +58,7 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun checkSignatures(tx: TransactionWithSignatures) {
|
private fun checkSignatures(tx: TransactionWithSignatures) {
|
||||||
try {
|
|
||||||
tx.verifySignaturesExcept(service.notaryIdentityKey)
|
tx.verifySignaturesExcept(service.notaryIdentityKey)
|
||||||
} catch (e: SignatureException) {
|
|
||||||
throw NotaryInternalException(NotaryError.TransactionInvalid(e))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun customVerify(stx: SignedTransaction) {
|
private fun customVerify(stx: SignedTransaction) {
|
||||||
|
@ -14,6 +14,7 @@ import org.apache.qpid.proton.amqp.UnsignedInteger
|
|||||||
import org.apache.qpid.proton.codec.Data
|
import org.apache.qpid.proton.codec.Data
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.NotSerializableException
|
import java.io.NotSerializableException
|
||||||
|
import java.lang.Exception
|
||||||
import java.lang.reflect.ParameterizedType
|
import java.lang.reflect.ParameterizedType
|
||||||
import java.lang.reflect.Type
|
import java.lang.reflect.Type
|
||||||
import java.lang.reflect.TypeVariable
|
import java.lang.reflect.TypeVariable
|
||||||
@ -100,8 +101,8 @@ class DeserializationInput constructor(
|
|||||||
throw NotSerializableException(amqp.mitigation)
|
throw NotSerializableException(amqp.mitigation)
|
||||||
} catch (nse: NotSerializableException) {
|
} catch (nse: NotSerializableException) {
|
||||||
throw nse
|
throw nse
|
||||||
} catch (t: Throwable) {
|
} catch (e: Exception) {
|
||||||
throw NotSerializableException("Internal deserialization failure: ${t.javaClass.name}: ${t.message}").apply { initCause(t) }
|
throw NotSerializableException("Internal deserialization failure: ${e.javaClass.name}: ${e.message}").apply { initCause(e) }
|
||||||
} finally {
|
} finally {
|
||||||
objectHistory.clear()
|
objectHistory.clear()
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,6 @@ include 'experimental'
|
|||||||
include 'experimental:avalanche'
|
include 'experimental:avalanche'
|
||||||
include 'experimental:behave'
|
include 'experimental:behave'
|
||||||
include 'experimental:quasar-hook'
|
include 'experimental:quasar-hook'
|
||||||
include 'experimental:kryo-hook'
|
|
||||||
// include 'experimental:intellij-plugin'
|
// include 'experimental:intellij-plugin'
|
||||||
include 'experimental:flow-hook'
|
include 'experimental:flow-hook'
|
||||||
include 'experimental:flow-worker'
|
include 'experimental:flow-worker'
|
||||||
|
@ -179,8 +179,8 @@ class DriverTests : IntegrationTest() {
|
|||||||
fun `driver waits for in-process nodes to finish`() {
|
fun `driver waits for in-process nodes to finish`() {
|
||||||
fun NodeHandle.stopQuietly() = try {
|
fun NodeHandle.stopQuietly() = try {
|
||||||
stop()
|
stop()
|
||||||
} catch (t: Throwable) {
|
} catch (e: Exception) {
|
||||||
t.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|
||||||
val handlesFuture = openFuture<List<NodeHandle>>()
|
val handlesFuture = openFuture<List<NodeHandle>>()
|
||||||
|
@ -28,6 +28,8 @@ import net.corda.node.services.schema.NodeSchemaService
|
|||||||
import net.corda.node.services.transactions.InMemoryTransactionVerifierService
|
import net.corda.node.services.transactions.InMemoryTransactionVerifierService
|
||||||
import net.corda.node.services.vault.NodeVaultService
|
import net.corda.node.services.vault.NodeVaultService
|
||||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||||
|
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||||
|
import net.corda.nodeapi.internal.persistence.contextTransaction
|
||||||
import net.corda.testing.common.internal.testNetworkParameters
|
import net.corda.testing.common.internal.testNetworkParameters
|
||||||
import net.corda.testing.core.TestIdentity
|
import net.corda.testing.core.TestIdentity
|
||||||
import net.corda.testing.internal.DEV_ROOT_CA
|
import net.corda.testing.internal.DEV_ROOT_CA
|
||||||
@ -39,6 +41,8 @@ import java.security.KeyPair
|
|||||||
import java.sql.Connection
|
import java.sql.Connection
|
||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.function.Consumer
|
||||||
|
import javax.persistence.EntityManager
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a simple [InMemoryIdentityService] containing the supplied [identities].
|
* Returns a simple [InMemoryIdentityService] containing the supplied [identities].
|
||||||
@ -117,6 +121,14 @@ open class MockServices private constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun jdbcSession(): Connection = database.createSession()
|
override fun jdbcSession(): Connection = database.createSession()
|
||||||
|
|
||||||
|
override fun <T : Any> withEntityManager(block: EntityManager.() -> T): T {
|
||||||
|
return block(contextTransaction.restrictedEntityManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun withEntityManager(block: Consumer<EntityManager>) {
|
||||||
|
return block.accept(contextTransaction.restrictedEntityManager)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Pair(database, mockService)
|
return Pair(database, mockService)
|
||||||
@ -264,6 +276,14 @@ open class MockServices private constructor(
|
|||||||
|
|
||||||
override fun jdbcSession(): Connection = throw UnsupportedOperationException()
|
override fun jdbcSession(): Connection = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun <T : Any> withEntityManager(block: EntityManager.() -> T): T {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun withEntityManager(block: Consumer<EntityManager>) {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
override fun registerUnloadHandler(runOnStop: () -> Unit) = throw UnsupportedOperationException()
|
override fun registerUnloadHandler(runOnStop: () -> Unit) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
/** Add the given package name to the list of packages which will be scanned for cordapp contract verification code */
|
/** Add the given package name to the list of packages which will be scanned for cordapp contract verification code */
|
||||||
|
@ -737,7 +737,7 @@ class DriverDSLImpl(
|
|||||||
* Keeping [Config] around is needed as the user may specify extra config options not specified in [NodeConfiguration].
|
* Keeping [Config] around is needed as the user may specify extra config options not specified in [NodeConfiguration].
|
||||||
*/
|
*/
|
||||||
private class NodeConfig(val typesafe: Config) {
|
private class NodeConfig(val typesafe: Config) {
|
||||||
val corda: NodeConfiguration = typesafe.parseAsNodeConfiguration().orThrow()
|
val corda: NodeConfiguration = typesafe.parseAsNodeConfiguration().value()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -106,8 +106,8 @@ fun <A> poll(
|
|||||||
} else {
|
} else {
|
||||||
executorService.schedule(this, pollInterval.toMillis(), TimeUnit.MILLISECONDS)
|
executorService.schedule(this, pollInterval.toMillis(), TimeUnit.MILLISECONDS)
|
||||||
}
|
}
|
||||||
} catch (t: Throwable) {
|
} catch (e: Exception) {
|
||||||
resultFuture.setException(t)
|
resultFuture.setException(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -118,7 +118,7 @@ abstract class NodeBasedTest(private val cordappPackages: List<String> = emptyLi
|
|||||||
|
|
||||||
val specificConfig = config.withValue(NodeConfiguration.cordappDirectoriesKey, ConfigValueFactory.fromIterable(cordappDirectories.toSet()))
|
val specificConfig = config.withValue(NodeConfiguration.cordappDirectoriesKey, ConfigValueFactory.fromIterable(cordappDirectories.toSet()))
|
||||||
|
|
||||||
val parsedConfig = specificConfig.parseAsNodeConfiguration().orThrow()
|
val parsedConfig = specificConfig.parseAsNodeConfiguration().value()
|
||||||
|
|
||||||
defaultNetworkParameters.install(baseDirectory)
|
defaultNetworkParameters.install(baseDirectory)
|
||||||
return InProcessNode(parsedConfig, MOCK_VERSION_INFO.copy(platformVersion = platformVersion), flowManager)
|
return InProcessNode(parsedConfig, MOCK_VERSION_INFO.copy(platformVersion = platformVersion), flowManager)
|
||||||
|
@ -65,6 +65,9 @@ class ShutdownManager(private val executorService: ExecutorService) {
|
|||||||
it.value()
|
it.value()
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
log.warn("Exception while calling a shutdown action, this might create resource leaks", t)
|
log.warn("Exception while calling a shutdown action, this might create resource leaks", t)
|
||||||
|
if (t is VirtualMachineError) {
|
||||||
|
throw t
|
||||||
|
}
|
||||||
}
|
}
|
||||||
is Try.Failure -> log.warn("Exception while getting shutdown method, disregarding", it.exception)
|
is Try.Failure -> log.warn("Exception while getting shutdown method, disregarding", it.exception)
|
||||||
}
|
}
|
||||||
|
@ -91,8 +91,8 @@ fun startPublishingFixedRateInjector(
|
|||||||
}
|
}
|
||||||
workBoundSemaphore.release()
|
workBoundSemaphore.release()
|
||||||
}
|
}
|
||||||
} catch (throwable: Throwable) {
|
} catch (e: Exception) {
|
||||||
throwable.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user