mirror of
https://github.com/corda/corda.git
synced 2025-04-19 08:36:39 +00:00
commit
105adc9e8d
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
|
2
.idea/compiler.xml
generated
2
.idea/compiler.xml
generated
@ -165,6 +165,8 @@
|
||||
<module name="ha-testing_integrationTest" target="1.8" />
|
||||
<module name="ha-testing_main" target="1.8" />
|
||||
<module name="ha-testing_test" target="1.8" />
|
||||
<module name="health-survey_main" target="1.8" />
|
||||
<module name="health-survey_test" target="1.8" />
|
||||
<module name="hsm-tool_main" target="1.8" />
|
||||
<module name="hsm-tool_test" target="1.8" />
|
||||
<module name="intellij-plugin_main" target="1.8" />
|
||||
|
@ -121,6 +121,7 @@ see changes to this list.
|
||||
* Lulu Ren (Monad-Labs)
|
||||
* Maksymilian Pawlak (R3)
|
||||
* Manila Gauns (Persistent Systems Limited)
|
||||
* Manos Batsis
|
||||
* Marek Scocovsky (ABSA)
|
||||
* marekdapps
|
||||
* Mark Lauer (Westpac)
|
||||
|
@ -411,7 +411,9 @@ bintrayConfig {
|
||||
'corda-notary-raft',
|
||||
'corda-notary-bft-smart',
|
||||
'corda-notary-jpa',
|
||||
'corda-notary-mysql'
|
||||
'corda-notary-mysql',
|
||||
'corda-common-configuration-parsing',
|
||||
'corda-common-validation'
|
||||
]
|
||||
license {
|
||||
name = 'Apache-2.0'
|
||||
|
@ -351,7 +351,11 @@ object JacksonSupport {
|
||||
val nameMatches = mapper.partiesFromName(parser.text)
|
||||
return when {
|
||||
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)
|
||||
?: throw JsonParseException(parser, "Could not find a Party with key ${publicKey.toStringShort()}")
|
||||
}
|
||||
|
@ -204,13 +204,13 @@ class NodeMonitorModel : AutoCloseable {
|
||||
val nodeInfo = _connection.proxy.nodeInfo()
|
||||
require(nodeInfo.legalIdentitiesAndCerts.isNotEmpty())
|
||||
_connection
|
||||
} catch (throwable: Throwable) {
|
||||
} catch (exception: Exception) {
|
||||
if (shouldRetry) {
|
||||
// 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
|
||||
} else {
|
||||
throw throwable
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -124,7 +124,7 @@ class CordaRPCClientTest : NodeBasedTest(listOf("net.corda.finance")) {
|
||||
nodeIsShut.onCompleted()
|
||||
} catch (e: ActiveMQSecurityException) {
|
||||
// nothing here - this happens if trying to connect before the node is started
|
||||
} catch (e: Throwable) {
|
||||
} catch (e: Exception) {
|
||||
nodeIsShut.onError(e)
|
||||
}
|
||||
}, 1, 1, TimeUnit.SECONDS)
|
||||
|
@ -100,6 +100,8 @@ class RPCClientProxyHandler(
|
||||
private val log = contextLogger()
|
||||
// To check whether toString() is being invoked
|
||||
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) {
|
||||
var currentThrowable = throwable
|
||||
@ -234,7 +236,13 @@ class RPCClientProxyHandler(
|
||||
lifeCycle.requireState { it == State.STARTED || it == State.SERVER_VERSION_NOT_SET }
|
||||
checkProtocolVersion(method)
|
||||
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) {
|
||||
throw RPCException("RPC Proxy is closed")
|
||||
@ -364,6 +372,8 @@ class RPCClientProxyHandler(
|
||||
close(false)
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@ -554,8 +564,8 @@ class RPCClientProxyHandler(
|
||||
observationExecutorPool.run(k) {
|
||||
try {
|
||||
m[k]?.onError(ConnectionFailureException())
|
||||
} catch (th: Throwable) {
|
||||
log.error("Unexpected exception when RPC connection failure handling", th)
|
||||
} catch (e: Exception) {
|
||||
log.error("Unexpected exception when RPC connection failure handling", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -568,6 +578,32 @@ class RPCClientProxyHandler(
|
||||
rpcReplyMap.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<*>>>
|
||||
|
@ -17,7 +17,7 @@ dependencies {
|
||||
}
|
||||
|
||||
jar {
|
||||
baseName 'common-configuration-parsing'
|
||||
baseName 'corda-common-configuration-parsing'
|
||||
}
|
||||
|
||||
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> {
|
||||
|
||||
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})"
|
||||
|
||||
|
@ -39,7 +39,7 @@ class SpecificationTest {
|
||||
val rpcSettings = RpcSettingsSpec.parse(configuration)
|
||||
|
||||
assertThat(rpcSettings.isValid).isTrue()
|
||||
assertThat(rpcSettings.orThrow()).satisfies { value ->
|
||||
assertThat(rpcSettings.value()).satisfies { value ->
|
||||
|
||||
assertThat(value.useSsl).isEqualTo(useSslValue)
|
||||
assertThat(value.addresses).satisfies { addresses ->
|
||||
|
@ -18,7 +18,7 @@ class VersionExtractorTest {
|
||||
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 version = extractVersion.invoke(rawConfiguration).orThrow()
|
||||
val version = extractVersion.invoke(rawConfiguration).value()
|
||||
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 version = extractVersion.invoke(rawConfiguration).orThrow()
|
||||
val version = extractVersion.invoke(rawConfiguration).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 version = extractVersion.invoke(rawConfiguration).orThrow()
|
||||
val version = extractVersion.invoke(rawConfiguration).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 version = extractVersion.invoke(rawConfiguration).orThrow()
|
||||
val version = extractVersion.invoke(rawConfiguration).value()
|
||||
|
||||
assertThat(version).isEqualTo(Configuration.Version.Extractor.DEFAULT_VERSION_VALUE)
|
||||
}
|
||||
@ -56,7 +56,7 @@ class VersionExtractorTest {
|
||||
|
||||
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)
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ class VersionedParsingExampleTest {
|
||||
private fun assertResult(result: Valid<RpcSettings>, principalAddressValue: Address, adminAddressValue: Address) {
|
||||
|
||||
assertThat(result.isValid).isTrue()
|
||||
assertThat(result.orThrow()).satisfies { value ->
|
||||
assertThat(result.value()).satisfies { value ->
|
||||
|
||||
assertThat(value.principal).isEqualTo(principalAddressValue)
|
||||
assertThat(value.admin).isEqualTo(adminAddressValue)
|
||||
@ -94,7 +94,7 @@ class VersionedParsingExampleTest {
|
||||
val adminAddress = addressFor(adminHost, adminPort)
|
||||
|
||||
return if (principalAddress.isValid && adminAddress.isValid) {
|
||||
return valid(RpcSettings(principalAddress.value, adminAddress.value))
|
||||
return valid(RpcSettings(principalAddress.value(), adminAddress.value()))
|
||||
} else {
|
||||
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 {
|
||||
baseName 'common-validation'
|
||||
baseName 'corda-common-validation'
|
||||
}
|
||||
|
||||
publish {
|
||||
|
@ -8,11 +8,11 @@ import java.util.Collections.emptySet
|
||||
*/
|
||||
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.
|
||||
@ -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.
|
||||
*/
|
||||
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
|
||||
val optional: TARGET? get() = if (isValid) value() else null
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
if (isValid) {
|
||||
action.invoke(value)
|
||||
action.invoke(value())
|
||||
}
|
||||
return this
|
||||
}
|
||||
@ -110,10 +103,10 @@ interface Validated<TARGET, ERROR> {
|
||||
/**
|
||||
* 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 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> {
|
||||
return valid(convert.invoke(value))
|
||||
@ -136,9 +129,7 @@ interface Validated<TARGET, ERROR> {
|
||||
require(errors.isNotEmpty())
|
||||
}
|
||||
|
||||
override val value: TARGET get() = throw IllegalStateException("Invalid state.")
|
||||
|
||||
override fun orThrow(exceptionOnErrors: (Set<ERROR>) -> Exception) = throw exceptionOnErrors.invoke(errors)
|
||||
override fun value(exceptionOnErrors: (Set<ERROR>) -> Exception) = throw exceptionOnErrors.invoke(errors)
|
||||
|
||||
override fun <MAPPED> map(convert: (TARGET) -> MAPPED): Validated<MAPPED, ERROR> {
|
||||
return invalid(errors)
|
||||
|
@ -4,6 +4,7 @@ import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.internal.extractFile
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.security.PublicKey
|
||||
@ -36,10 +37,10 @@ interface Attachment : NamedByHash {
|
||||
@JvmDefault
|
||||
fun openAsJAR(): JarInputStream {
|
||||
val stream = open()
|
||||
try {
|
||||
return JarInputStream(stream)
|
||||
} catch (t: Throwable) {
|
||||
stream.use { throw t }
|
||||
return try {
|
||||
JarInputStream(stream)
|
||||
} catch (e: IOException) {
|
||||
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 " +
|
||||
"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]. */
|
||||
@CordaSerializable
|
||||
@KeepForDJVM
|
||||
|
@ -49,6 +49,7 @@ sealed class NotaryError {
|
||||
}
|
||||
|
||||
/** 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()
|
||||
|
||||
/** Occurs when the notarisation request signature does not verify for the provided transaction. */
|
||||
|
@ -72,14 +72,14 @@ object JarSignatureCollector {
|
||||
private fun Sequence<JarEntry>.toFileSignerSet(): Sequence<Pair<String, Set<CodeSigner>>> =
|
||||
map { entry -> entry.name to (entry.codeSigners?.toSet() ?: emptySet()) }
|
||||
|
||||
private fun Set<CodeSigner>.toOrderedPublicKeys(): List<PublicKey> = map {
|
||||
(it.signerCertPath.certificates[0] as X509Certificate).publicKey
|
||||
}.sortedBy { it.hash} // Sorted for determinism.
|
||||
|
||||
private fun Set<CodeSigner>.toPartiesOrderedByName(): List<Party> = map {
|
||||
Party(it.signerCertPath.certificates[0] as X509Certificate)
|
||||
}.sortedBy { it.name.toString() } // Sorted for determinism.
|
||||
|
||||
private fun Set<CodeSigner>.toOrderedPublicKeys(): List<PublicKey> = map {
|
||||
(it.signerCertPath.certificates[0] as X509Certificate).publicKey
|
||||
}.sortedBy { it.hash} // Sorted for determinism.
|
||||
|
||||
private fun Set<CodeSigner>.toCertificates(): List<X509Certificate> = map {
|
||||
it.signerCertPath.certificates[0] as X509Certificate
|
||||
}.sortedBy { it.toString() } // Sorted for determinism.
|
||||
|
@ -71,8 +71,8 @@ fun <V, W> CordaFuture<out V>.flatMap(transform: (V) -> CordaFuture<out W>): Cor
|
||||
thenMatch(success@ {
|
||||
result.captureLater(try {
|
||||
transform(it)
|
||||
} catch (t: Throwable) {
|
||||
result.setException(t)
|
||||
} catch (e: Exception) {
|
||||
result.setException(e)
|
||||
return@success
|
||||
})
|
||||
}, {
|
||||
@ -128,8 +128,8 @@ interface ValueOrException<in V> {
|
||||
fun capture(block: () -> V): Boolean {
|
||||
return set(try {
|
||||
block()
|
||||
} catch (t: Throwable) {
|
||||
return setException(t)
|
||||
} catch (e: Exception) {
|
||||
return setException(e)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -153,8 +153,8 @@ internal class CordaFutureImpl<V>(private val impl: CompletableFuture<V> = Compl
|
||||
impl.whenComplete { _, _ ->
|
||||
try {
|
||||
callback(this)
|
||||
} catch (t: Throwable) {
|
||||
log.error(listenerFailedMessage, t)
|
||||
} catch (e: Exception) {
|
||||
log.error(listenerFailedMessage, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,61 +24,84 @@ abstract class NotaryServiceFlow(val otherSideSession: FlowSession, val service:
|
||||
private const val maxAllowedInputsAndReferences = 10_000
|
||||
}
|
||||
|
||||
private var transactionId: SecureHash? = null
|
||||
|
||||
@Suspendable
|
||||
override fun call(): Void? {
|
||||
check(serviceHub.myInfo.legalIdentities.any { serviceHub.networkMapCache.isNotary(it) }) {
|
||||
"We are not a notary on the network"
|
||||
}
|
||||
val requestPayload = otherSideSession.receive<NotarisationPayload>().unwrap { it }
|
||||
var txId: SecureHash? = null
|
||||
|
||||
try {
|
||||
val parts = validateRequest(requestPayload)
|
||||
txId = parts.id
|
||||
checkNotary(parts.notary)
|
||||
val tx: TransactionParts = validateRequest(requestPayload)
|
||||
val request = NotarisationRequest(tx.inputs, tx.id)
|
||||
validateRequestSignature(request, requestPayload.requestSignature)
|
||||
|
||||
verifyTransaction(requestPayload)
|
||||
|
||||
service.commitInputStates(
|
||||
parts.inputs,
|
||||
txId,
|
||||
tx.inputs,
|
||||
tx.id,
|
||||
otherSideSession.counterparty,
|
||||
requestPayload.requestSignature,
|
||||
parts.timestamp,
|
||||
parts.references
|
||||
)
|
||||
signTransactionAndSendResponse(txId)
|
||||
tx.timeWindow,
|
||||
tx.references)
|
||||
|
||||
} 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
|
||||
}
|
||||
|
||||
/** Checks whether the number of input states is too large. */
|
||||
protected fun checkInputs(inputs: List<StateRef>) {
|
||||
if (inputs.size > maxAllowedInputsAndReferences) {
|
||||
val error = NotaryError.TransactionInvalid(
|
||||
IllegalArgumentException("A transaction cannot have more than $maxAllowedInputsAndReferences " +
|
||||
"inputs or references, received: ${inputs.size}")
|
||||
)
|
||||
private fun validateRequest(requestPayload: NotarisationPayload): TransactionParts {
|
||||
try {
|
||||
val transaction = extractParts(requestPayload)
|
||||
transactionId = transaction.id
|
||||
checkNotary(transaction.notary)
|
||||
checkInputs(transaction.inputs + transaction.references)
|
||||
return transaction
|
||||
} catch (e: Exception) {
|
||||
val error = NotaryError.TransactionInvalid(e)
|
||||
throw NotaryInternalException(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement custom logic to perform transaction verification based on validity and privacy requirements.
|
||||
*/
|
||||
/** Extract the common transaction components required for notarisation. */
|
||||
protected abstract fun extractParts(requestPayload: NotarisationPayload): TransactionParts
|
||||
|
||||
/** Check if transaction is intended to be signed by this notary. */
|
||||
@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. */
|
||||
protected fun validateRequestSignature(request: NotarisationRequest, signature: NotarisationRequestSignature) {
|
||||
private fun validateRequestSignature(request: NotarisationRequest, signature: NotarisationRequestSignature) {
|
||||
val requestingParty = otherSideSession.counterparty
|
||||
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
|
||||
protected fun checkNotary(notary: Party?) {
|
||||
if (notary?.owningKey != service.notaryIdentityKey) {
|
||||
throw NotaryInternalException(NotaryError.WrongNotary)
|
||||
}
|
||||
protected open fun verifyTransaction(requestPayload: NotarisationPayload) {
|
||||
}
|
||||
|
||||
@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
|
||||
* any sensitive transaction details.
|
||||
*/
|
||||
protected data class TransactionParts @JvmOverloads constructor(
|
||||
protected data class TransactionParts(
|
||||
val id: SecureHash,
|
||||
val inputs: List<StateRef>,
|
||||
val timestamp: TimeWindow?,
|
||||
val timeWindow: TimeWindow?,
|
||||
val notary: Party?,
|
||||
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,42 +2,31 @@ package net.corda.core.internal.notary
|
||||
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.serialization.serialize
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.SignatureException
|
||||
import net.corda.core.utilities.toBase58String
|
||||
import java.time.Instant
|
||||
|
||||
/** Verifies the signature against this notarisation request. Checks that the signature is issued by the right party. */
|
||||
fun NotarisationRequest.verifySignature(requestSignature: NotarisationRequestSignature, intendedSigner: Party) {
|
||||
val signature = requestSignature.digitalSignature
|
||||
if (intendedSigner.owningKey != signature.by) {
|
||||
val errorMessage = "Expected a signature by ${intendedSigner.owningKey}, but received by ${signature.by}}"
|
||||
throw NotaryInternalException(NotaryError.RequestSignatureInvalid(IllegalArgumentException(errorMessage)))
|
||||
}
|
||||
// TODO: if requestSignature was generated over an old version of NotarisationRequest, we need to be able to
|
||||
// reserialize it in that version to get the exact same bytes. Modify the serialization logic once that's
|
||||
// available.
|
||||
val expectedSignedBytes = this.serialize().bytes
|
||||
verifyCorrectBytesSigned(signature, expectedSignedBytes)
|
||||
}
|
||||
|
||||
private fun verifyCorrectBytesSigned(signature: DigitalSignature.WithKey, bytes: ByteArray) {
|
||||
try {
|
||||
signature.verify(bytes)
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is InvalidKeyException, is SignatureException -> {
|
||||
val error = NotaryError.RequestSignatureInvalid(e)
|
||||
throw NotaryInternalException(error)
|
||||
}
|
||||
else -> throw e
|
||||
val signature = requestSignature.digitalSignature
|
||||
require(intendedSigner.owningKey == signature.by) {
|
||||
"Expected a signature by ${intendedSigner.owningKey.toBase58String()}, but received by ${signature.by.toBase58String()}}"
|
||||
}
|
||||
|
||||
// TODO: if requestSignature was generated over an old version of NotarisationRequest, we need to be able to
|
||||
// reserialize it in that version to get the exact same bytes. Modify the serialization logic once that's
|
||||
// available.
|
||||
val expectedSignedBytes = this.serialize().bytes
|
||||
signature.verify(expectedSignedBytes)
|
||||
} catch (e: Exception) {
|
||||
val error = NotaryError.RequestSignatureInvalid(e)
|
||||
throw NotaryInternalException(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -53,6 +53,7 @@ abstract class SinglePartyNotaryService : NotaryService() {
|
||||
references
|
||||
)
|
||||
)
|
||||
|
||||
if (result is UniquenessProvider.Result.Failure) {
|
||||
throw NotaryInternalException(result.error)
|
||||
}
|
||||
|
@ -18,6 +18,8 @@ import net.corda.core.transactions.TransactionBuilder
|
||||
import java.security.PublicKey
|
||||
import java.sql.Connection
|
||||
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
|
||||
@ -358,6 +360,28 @@ interface ServiceHub : ServicesForResolution {
|
||||
*/
|
||||
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.
|
||||
*
|
||||
|
@ -11,9 +11,11 @@ import net.corda.core.internal.checkMinimumPlatformVersion
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.Try
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import java.util.*
|
||||
import java.util.function.Predicate
|
||||
import kotlin.collections.HashSet
|
||||
import net.corda.core.utilities.warnOnce
|
||||
|
||||
/**
|
||||
@ -58,6 +60,17 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
|
||||
private companion object {
|
||||
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 }
|
||||
@ -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
|
||||
* 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.
|
||||
* Verify that package ownership is respected.
|
||||
*
|
||||
* 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.
|
||||
* * Constraints should be one of the valid supported ones.
|
||||
@ -181,6 +194,9 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
val constraintAttachment = AttachmentWithContext(contractAttachment, state.contract,
|
||||
networkParameters?.whitelistedContractImplementations)
|
||||
|
||||
if (state.constraint is SignatureAttachmentConstraint)
|
||||
checkMinimumPlatformVersion(networkParameters?.minimumPlatformVersion ?: 1, 4, "Signature constraints")
|
||||
|
||||
if (!state.constraint.isSatisfiedBy(constraintAttachment)) {
|
||||
throw TransactionVerificationException.ContractConstraintRejection(id, state.contract)
|
||||
}
|
||||
@ -268,10 +284,39 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
// Check that in the outputs,
|
||||
// a) an encumbered state does not refer to itself as 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!!) }
|
||||
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
|
||||
// c -> a b -> a
|
||||
// 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).
|
||||
val encumberedSet = mutableSetOf<Int>()
|
||||
// [Set] of "to" (encumbrance states).
|
||||
|
@ -265,8 +265,11 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
|
||||
override fun toString(): String = "${javaClass.simpleName}(id=$id)"
|
||||
|
||||
private companion object {
|
||||
private fun missingSignatureMsg(missing: Set<PublicKey>, descriptions: List<String>, id: SecureHash): String =
|
||||
"Missing signatures for $descriptions on transaction ${id.prefixChars()} for ${missing.joinToString()}"
|
||||
private fun missingSignatureMsg(missing: Set<PublicKey>, descriptions: List<String>, id: SecureHash): String {
|
||||
return "Missing signatures on transaction ${id.prefixChars()} for " +
|
||||
"keys: ${missing.joinToString { it.toStringShort() }}, " +
|
||||
"by signers: ${descriptions.joinToString()} "
|
||||
}
|
||||
}
|
||||
|
||||
@KeepForDJVM
|
||||
|
@ -9,6 +9,7 @@ import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.SignableData
|
||||
import net.corda.core.crypto.SignatureMetadata
|
||||
import net.corda.core.crypto.keys
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.AttachmentWithContext
|
||||
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.SerializationFactory
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.warnOnce
|
||||
import java.security.PublicKey
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
@ -89,7 +92,7 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
) {
|
||||
|
||||
private companion object {
|
||||
val logger = loggerFor<TransactionBuilder>()
|
||||
private val log = contextLogger()
|
||||
}
|
||||
|
||||
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.
|
||||
val contractAttachmentsAndResolvedOutputStates: List<Pair<AttachmentId, List<TransactionState<ContractState>>?>> = allContracts.toSet().map { ctr ->
|
||||
handleContract(ctr, inputContractGroups[ctr], outputContractGroups[ctr], explicitAttachmentContractsMap[ctr], serializationContext, services)
|
||||
}
|
||||
val contractAttachmentsAndResolvedOutputStates: List<Pair<AttachmentId, List<TransactionState<ContractState>>?>> = allContracts.toSet()
|
||||
.map { ctr ->
|
||||
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.
|
||||
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
|
||||
|
||||
@ -342,8 +347,18 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
attachmentToUse: ContractAttachment,
|
||||
services: ServicesForResolution): AttachmentConstraint = when {
|
||||
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)
|
||||
useWhitelistedByZoneAttachmentConstraint(contractClassName, services.networkParameters) -> WhitelistedByZoneAttachmentConstraint
|
||||
else -> HashAttachmentConstraint(attachmentToUse.id)
|
||||
}
|
||||
|
||||
@ -469,7 +484,7 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
addReferenceState(resolvedStateAndRef.referenced())
|
||||
}
|
||||
} 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 " +
|
||||
"MockServices instance.")
|
||||
return
|
||||
@ -539,7 +554,7 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
state: ContractState,
|
||||
contract: ContractClassName = requireNotNull(state.requiredContractClassName) {
|
||||
//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
|
||||
@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().
|
||||
@ -555,7 +570,7 @@ with @BelongsToContract, or supply an explicit contract parameter to addOutputSt
|
||||
state: ContractState,
|
||||
contract: ContractClassName = requireNotNull(state.requiredContractClassName) {
|
||||
//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
|
||||
@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().
|
||||
|
@ -19,8 +19,8 @@ sealed class Try<out A> {
|
||||
inline fun <T> on(body: () -> T): Try<T> {
|
||||
return try {
|
||||
Success(body())
|
||||
} catch (t: Throwable) {
|
||||
Failure(t)
|
||||
} catch (e: Exception) {
|
||||
Failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,10 +2,7 @@ package net.corda.core.transactions
|
||||
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.contracts.Contract
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.TransactionVerificationException
|
||||
import net.corda.core.contracts.requireThat
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
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.node.MockServices
|
||||
import net.corda.testing.node.ledger
|
||||
import org.assertj.core.api.AssertionsForClassTypes
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.time.Instant
|
||||
@ -33,6 +31,7 @@ class TransactionEncumbranceTests {
|
||||
|
||||
private companion object {
|
||||
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 MINI_CORP = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")).party
|
||||
val MEGA_CORP get() = megaCorp.party
|
||||
@ -77,7 +76,7 @@ class TransactionEncumbranceTests {
|
||||
}
|
||||
|
||||
@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
|
||||
ledgerServices.ledger(DUMMY_NOTARY) {
|
||||
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
|
||||
|
||||
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 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
|
||||
// Add these imports:
|
||||
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.transactions.SignedTransaction;
|
||||
import net.corda.core.transactions.TransactionBuilder;
|
||||
import net.corda.core.utilities.ProgressTracker;
|
||||
|
||||
// Replace Initiator's definition with:
|
||||
@InitiatingFlow
|
||||
@ -46,13 +49,12 @@ public class IOUFlow extends FlowLogic<Void> {
|
||||
|
||||
// We create the transaction components.
|
||||
IOUState outputState = new IOUState(iouValue, getOurIdentity(), otherParty);
|
||||
CommandData cmdType = new TemplateContract.Commands.Action();
|
||||
Command cmd = new Command<>(cmdType, getOurIdentity().getOwningKey());
|
||||
Command command = new Command<>(new TemplateContract.Commands.Action(), getOurIdentity().getOwningKey());
|
||||
|
||||
// We create a transaction builder and add the components.
|
||||
TransactionBuilder txBuilder = new TransactionBuilder(notary)
|
||||
.addOutputState(outputState, TemplateContract.ID)
|
||||
.addCommand(cmd);
|
||||
.addCommand(command);
|
||||
|
||||
// Signing the transaction.
|
||||
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.identity.AbstractParty;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
// DOCSTART 01
|
||||
// Add these imports:
|
||||
import com.google.common.collect.ImmutableList;
|
||||
// Add this import:
|
||||
import net.corda.core.identity.Party;
|
||||
|
||||
// Replace TemplateState's definition with:
|
||||
@ -35,7 +35,7 @@ public class IOUState implements ContractState {
|
||||
|
||||
@Override
|
||||
public List<AbstractParty> getParticipants() {
|
||||
return ImmutableList.of(lender, borrower);
|
||||
return Arrays.asList(lender, borrower);
|
||||
}
|
||||
}
|
||||
// DOCEND 01
|
@ -6,15 +6,14 @@ import net.corda.core.transactions.LedgerTransaction;
|
||||
|
||||
// DOCSTART 01
|
||||
// Add these imports:
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import net.corda.core.contracts.CommandWithParties;
|
||||
import net.corda.core.identity.Party;
|
||||
|
||||
import java.security.PublicKey;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static net.corda.core.contracts.ContractsDSL.requireSingleCommand;
|
||||
import static net.corda.core.contracts.ContractsDSL.requireThat;
|
||||
|
||||
// Replace TemplateContract's definition with:
|
||||
public class IOUContract implements Contract {
|
||||
@ -28,26 +27,29 @@ public class IOUContract implements Contract {
|
||||
public void verify(LedgerTransaction tx) {
|
||||
final CommandWithParties<IOUContract.Create> command = requireSingleCommand(tx.getCommands(), IOUContract.Create.class);
|
||||
|
||||
requireThat(check -> {
|
||||
// Constraints on the shape of the transaction.
|
||||
check.using("No inputs should be consumed when issuing an IOU.", tx.getInputs().isEmpty());
|
||||
check.using("There should be one output state of type IOUState.", tx.getOutputs().size() == 1);
|
||||
// Constraints on the shape of the transaction.
|
||||
if (!tx.getInputs().isEmpty())
|
||||
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.
|
||||
final IOUState out = tx.outputsOfType(IOUState.class).get(0);
|
||||
final Party lender = out.getLender();
|
||||
final Party borrower = out.getBorrower();
|
||||
check.using("The IOU's value must be non-negative.", out.getValue() > 0);
|
||||
check.using("The lender and the borrower cannot be the same entity.", lender != borrower);
|
||||
// IOU-specific constraints.
|
||||
final IOUState output = tx.outputsOfType(IOUState.class).get(0);
|
||||
final Party lender = output.getLender();
|
||||
final Party borrower = output.getBorrower();
|
||||
if (output.getValue() <= 0)
|
||||
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.
|
||||
final List<PublicKey> signers = command.getSigners();
|
||||
check.using("There must be two signers.", signers.size() == 2);
|
||||
check.using("The borrower and lender must be signers.", signers.containsAll(
|
||||
ImmutableList.of(borrower.getOwningKey(), lender.getOwningKey())));
|
||||
// Constraints on the signers.
|
||||
final List<PublicKey> requiredSigners = command.getSigners();
|
||||
final List<PublicKey> expectedSigners = Arrays.asList(borrower.getOwningKey(), lender.getOwningKey());
|
||||
if (requiredSigners.size() != 2)
|
||||
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
|
@ -2,9 +2,7 @@ package net.corda.docs.java.tutorial.twoparty;
|
||||
|
||||
// DOCSTART 01
|
||||
import co.paralleluniverse.fibers.Suspendable;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import net.corda.core.contracts.Command;
|
||||
import net.corda.core.contracts.StateAndContract;
|
||||
import net.corda.core.flows.*;
|
||||
import net.corda.core.identity.Party;
|
||||
import net.corda.core.transactions.SignedTransaction;
|
||||
@ -12,6 +10,7 @@ import net.corda.core.transactions.TransactionBuilder;
|
||||
import net.corda.core.utilities.ProgressTracker;
|
||||
|
||||
import java.security.PublicKey;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
// DOCEND 01
|
||||
|
||||
@ -42,22 +41,19 @@ public class IOUFlow extends FlowLogic<Void> {
|
||||
@Suspendable
|
||||
@Override
|
||||
public Void call() throws FlowException {
|
||||
// DOCSTART 02
|
||||
// We retrieve the notary identity from the network map.
|
||||
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.
|
||||
IOUState outputState = new IOUState(iouValue, getOurIdentity(), otherParty);
|
||||
StateAndContract outputContractAndState = new StateAndContract(outputState, IOUContract.ID);
|
||||
List<PublicKey> requiredSigners = ImmutableList.of(getOurIdentity().getOwningKey(), otherParty.getOwningKey());
|
||||
Command cmd = new Command<>(new IOUContract.Create(), requiredSigners);
|
||||
List<PublicKey> requiredSigners = Arrays.asList(getOurIdentity().getOwningKey(), otherParty.getOwningKey());
|
||||
Command command = new Command<>(new IOUContract.Create(), requiredSigners);
|
||||
|
||||
// We add the items to the builder.
|
||||
txBuilder.withItems(outputContractAndState, cmd);
|
||||
// We create a transaction builder and add the components.
|
||||
TransactionBuilder txBuilder = new TransactionBuilder(notary)
|
||||
.addOutputState(outputState, IOUContract.ID)
|
||||
.addCommand(command);
|
||||
|
||||
// Verifying the transaction.
|
||||
txBuilder.verify(getServiceHub());
|
||||
@ -70,7 +66,7 @@ public class IOUFlow extends FlowLogic<Void> {
|
||||
|
||||
// Obtaining the counterparty's signature.
|
||||
SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(
|
||||
signedTx, ImmutableList.of(otherPartySession), CollectSignaturesFlow.tracker()));
|
||||
signedTx, Arrays.asList(otherPartySession), CollectSignaturesFlow.tracker()));
|
||||
|
||||
// Finalising the transaction.
|
||||
subFlow(new FinalityFlow(fullySignedTx));
|
||||
|
@ -1,10 +1,10 @@
|
||||
package net.corda.docs.java.tutorial.twoparty;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import net.corda.core.contracts.ContractState;
|
||||
import net.corda.core.identity.AbstractParty;
|
||||
import net.corda.core.identity.Party;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class IOUState implements ContractState {
|
||||
@ -32,6 +32,6 @@ public class IOUState implements ContractState {
|
||||
|
||||
@Override
|
||||
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.InitiatingFlow
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
|
||||
// DOCSTART 01
|
||||
// Add these imports:
|
||||
import net.corda.core.contracts.Command
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
|
||||
// Replace Initiator's definition with:
|
||||
@InitiatingFlow
|
||||
@ -33,12 +33,12 @@ class IOUFlow(val iouValue: Int,
|
||||
|
||||
// We create the transaction components.
|
||||
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.
|
||||
val txBuilder = TransactionBuilder(notary = notary)
|
||||
.addOutputState(outputState, TemplateContract.ID)
|
||||
.addCommand(cmd)
|
||||
.addCommand(command)
|
||||
|
||||
// We sign the transaction.
|
||||
val signedTx = serviceHub.signInitialTransaction(txBuilder)
|
||||
|
@ -5,7 +5,7 @@ package net.corda.docs.kotlin.tutorial.helloworld
|
||||
import net.corda.core.contracts.ContractState
|
||||
|
||||
// DOCSTART 01
|
||||
// Add these imports:
|
||||
// Add this import:
|
||||
import net.corda.core.identity.Party
|
||||
|
||||
// Replace TemplateState's definition with:
|
||||
|
@ -5,7 +5,7 @@ import net.corda.core.contracts.Contract
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
|
||||
// DOCSTART 01
|
||||
// Add these imports:
|
||||
// Add this import:
|
||||
import net.corda.core.contracts.*
|
||||
|
||||
class IOUContract : Contract {
|
||||
@ -25,14 +25,14 @@ class IOUContract : Contract {
|
||||
"There should be one output state of type IOUState." using (tx.outputs.size == 1)
|
||||
|
||||
// IOU-specific constraints.
|
||||
val out = tx.outputsOfType<IOUState>().single()
|
||||
"The IOU's value must be non-negative." using (out.value > 0)
|
||||
"The lender and the borrower cannot be the same entity." using (out.lender != out.borrower)
|
||||
val output = tx.outputsOfType<IOUState>().single()
|
||||
"The IOU's value must be non-negative." using (output.value > 0)
|
||||
"The lender and the borrower cannot be the same entity." using (output.lender != output.borrower)
|
||||
|
||||
// Constraints on the signers.
|
||||
val expectedSigners = listOf(output.borrower.owningKey, output.lender.owningKey)
|
||||
"There must be two signers." using (command.signers.toSet().size == 2)
|
||||
"The borrower and lender must be signers." using (command.signers.containsAll(listOf(
|
||||
out.borrower.owningKey, out.lender.owningKey)))
|
||||
"The borrower and lender must be signers." using (command.signers.containsAll(expectedSigners))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ package net.corda.docs.kotlin.tutorial.twoparty
|
||||
// DOCSTART 01
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.Command
|
||||
import net.corda.core.contracts.StateAndContract
|
||||
import net.corda.core.flows.CollectSignaturesFlow
|
||||
import net.corda.core.flows.FinalityFlow
|
||||
import net.corda.core.flows.FlowLogic
|
||||
@ -27,20 +26,18 @@ class IOUFlow(val iouValue: Int,
|
||||
/** The flow logic is encapsulated within the call() method. */
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
// DOCSTART 02
|
||||
// We retrieve the notary identity from the network map.
|
||||
val notary = serviceHub.networkMapCache.notaryIdentities[0]
|
||||
|
||||
// DOCSTART 02
|
||||
// We create a transaction builder.
|
||||
val txBuilder = TransactionBuilder(notary = notary)
|
||||
|
||||
// We create the transaction components.
|
||||
val outputState = IOUState(iouValue, ourIdentity, otherParty)
|
||||
val outputContractAndState = StateAndContract(outputState, IOUContract.ID)
|
||||
val cmd = Command(IOUContract.Create(), listOf(ourIdentity.owningKey, otherParty.owningKey))
|
||||
val command = Command(IOUContract.Create(), listOf(ourIdentity.owningKey, otherParty.owningKey))
|
||||
|
||||
// We add the items to the builder.
|
||||
txBuilder.withItems(outputContractAndState, cmd)
|
||||
// We create a transaction builder and add the components.
|
||||
val txBuilder = TransactionBuilder(notary = notary)
|
||||
.addOutputState(outputState, IOUContract.ID)
|
||||
.addCommand(command)
|
||||
|
||||
// Verifying the transaction.
|
||||
txBuilder.verify(serviceHub)
|
||||
|
@ -3,10 +3,7 @@
|
||||
package net.corda.docs.kotlin.tutorial.twoparty
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.flows.SignTransactionFlow
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.docs.kotlin.tutorial.helloworld.IOUFlow
|
||||
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``.
|
||||
|
||||
Let's define our ``IOUFlow`` in either ``Initiator.java`` or ``Flows.kt``. Delete the two existing flows in the
|
||||
template (``Initiator`` and ``Responder``), and replace them with the following:
|
||||
Let's define our ``IOUFlow``. Delete the existing ``Responder`` flow. Then replace the definition of ``Initiator`` with the following:
|
||||
|
||||
.. 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
|
||||
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
|
||||
more Corda nodes, and give the nodes' owners the ability to make their node conduct some new process - anything from
|
||||
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 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.
|
||||
|
||||
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
|
||||
another person a given amount of money. Clearly this is sensitive information that we'd only want to communicate on
|
||||
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.
|
||||
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 node a certain amount. This simple CorDapp will showcase several key benefits of Corda as a blockchain platform:
|
||||
|
||||
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
|
||||
* **Flows**, which encapsulate the procedure for carrying out a specific ledger update
|
||||
* **Well-known identities** - Each Corda node has a well-known identity on the network. This allows us to write code in terms of real
|
||||
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
|
||||
^^^^^^^^^^^^
|
||||
Our state will be the ``IOUState``. It will store the value of the IOU, as well as the identities of the lender and the
|
||||
borrower. We can visualize ``IOUState`` as follows:
|
||||
Our state will be the ``IOUState``, representing an IOU. It will contain the IOU's value, its lender and its borrower. We can visualize
|
||||
``IOUState`` as follows:
|
||||
|
||||
.. image:: resources/tutorial-state.png
|
||||
:scale: 25%
|
||||
@ -43,20 +52,20 @@ borrower. We can visualize ``IOUState`` as follows:
|
||||
|
||||
The IOUFlow
|
||||
^^^^^^^^^^^
|
||||
Our flow will be the ``IOUFlow``. This flow will completely automate the process of issuing a new IOU onto a ledger. It
|
||||
is composed of the following steps:
|
||||
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
|
||||
steps:
|
||||
|
||||
.. image:: resources/simple-tutorial-flow.png
|
||||
:scale: 25%
|
||||
:align: center
|
||||
|
||||
In traditional distributed ledger systems, where all data is broadcast to every network participant, you don’t need to
|
||||
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
|
||||
agreeing a ledger update.
|
||||
The IOUContract
|
||||
^^^^^^^^^^^^^^^
|
||||
For this tutorial, we will use the default ``TemplateContract``. We will update it to create a fully-fledged ``IOUContract`` in the next
|
||||
tutorial.
|
||||
|
||||
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,11 +107,9 @@ commands.
|
||||
|
||||
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"
|
||||
|
||||
This single command will cause PartyA and PartyB to automatically agree an IOU. This is one of the great advantages of
|
||||
the flow framework - it allows you to reduce complex negotiation and update processes into a single function call.
|
||||
@ -122,7 +120,7 @@ We can check the contents of each node's vault by running:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
run vaultQuery contractStateType: com.template.IOUState
|
||||
run vaultQuery contractStateType: com.template.IOUState
|
||||
|
||||
The vaults of PartyA and PartyB should both display the following output:
|
||||
|
||||
@ -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.
|
||||
|
||||
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
|
||||
----------
|
||||
We have written a simple CorDapp that allows IOUs to be issued onto the ledger. Our CorDapp is made up of two key
|
||||
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
|
||||
|
||||
After completing this tutorial, your CorDapp should look like this:
|
||||
|
@ -7,7 +7,7 @@
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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::
|
||||
|
||||
@ -46,7 +46,7 @@ represent a given type of shared fact on the ledger.
|
||||
|
||||
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:
|
||||
|
||||
* The value of the IOU
|
||||
@ -99,7 +99,7 @@ Corda are simply classes that implement the ``ContractState`` interface. They ca
|
||||
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``
|
||||
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?
|
||||
------------------------
|
||||
|
@ -7,41 +7,39 @@
|
||||
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 `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
|
||||
CorDapp onto a local test network of dummy nodes to test its functionality.
|
||||
The Cordapp templates provide the boilerplate for developing a new CorDapp. CorDapps can be written in either Java or Kotlin. We will be
|
||||
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. Corda's required libraries will be downloaded
|
||||
automatically from an online Maven repository.
|
||||
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.
|
||||
|
||||
Downloading the template
|
||||
------------------------
|
||||
To download the template, open a terminal window in the directory where you want to download the CorDapp template, and
|
||||
run the following command:
|
||||
Open a terminal window in the directory where you want to download the CorDapp template, and run the following command:
|
||||
|
||||
.. code-block:: bash
|
||||
.. container:: codeset
|
||||
|
||||
git clone https://github.com/corda/cordapp-template-java.git ; cd cordapp-template-java
|
||||
.. code-block:: java
|
||||
|
||||
*or*
|
||||
git clone https://github.com/corda/cordapp-template-java.git ; cd cordapp-template-java
|
||||
|
||||
git clone https://github.com/corda/cordapp-template-kotlin.git ; cd cordapp-template-kotlin
|
||||
.. code-block:: kotlin
|
||||
|
||||
git clone https://github.com/corda/cordapp-template-kotlin.git ; cd cordapp-template-kotlin
|
||||
|
||||
Opening the template in IntelliJ
|
||||
--------------------------------
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
|
@ -154,7 +154,7 @@ Transaction constraints
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
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:
|
||||
|
||||
* 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.
|
||||
|
||||
In Java, we simply throw an ``IllegalArgumentException`` manually instead.
|
||||
|
||||
IOU constraints
|
||||
~~~~~~~~~~~~~~~
|
||||
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
|
||||
* 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 in the ``requireThat`` block. We can also write
|
||||
You can see that we're not restricted to only writing constraints inside ``verify``. We can also write
|
||||
other statements - in this case, extracting the transaction's single ``IOUState`` and assigning it to a variable.
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
Progress so far
|
||||
|
@ -31,8 +31,7 @@ In ``IOUFlow.java``/``Flows.kt``, change the imports block to the following:
|
||||
:start-after: DOCSTART 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
|
||||
follows:
|
||||
And update ``IOUFlow.call`` to the following:
|
||||
|
||||
.. 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>`.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
In this tutorial, we'll write a contract to imposes rules on how an ``IOUState`` can change over time. In turn, this
|
||||
|
@ -22,7 +22,9 @@ import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.services.CordaService
|
||||
import net.corda.core.serialization.SerializeAsToken
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.serialization.internal.*
|
||||
import net.corda.core.serialization.internal.SerializationEnvironment
|
||||
import net.corda.core.serialization.internal.effectiveSerializationEnv
|
||||
import net.corda.core.serialization.internal.nodeSerializationEnv
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.node.CordaClock
|
||||
@ -62,6 +64,7 @@ import net.corda.node.utilities.EnterpriseNamedCacheFactory
|
||||
import net.corda.node.utilities.profiling.getTracingConfig
|
||||
import net.corda.nodeapi.internal.NodeInfoAndSigned
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.contextTransaction
|
||||
import net.corda.nodeapi.internal.persistence.isH2Database
|
||||
import net.corda.serialization.internal.*
|
||||
import org.apache.activemq.artemis.utils.ReusableLatch
|
||||
@ -78,10 +81,15 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.function.Consumer
|
||||
import javax.persistence.EntityManager
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class FlowWorkerServiceHub(override val configuration: NodeConfiguration, override val myInfo: NodeInfo,
|
||||
private val ourKeyPair: KeyPair, private val trustRoot: X509Certificate, private val nodeCa: X509Certificate,
|
||||
class FlowWorkerServiceHub(override val configuration: NodeConfiguration,
|
||||
override val myInfo: NodeInfo,
|
||||
private val ourKeyPair: KeyPair,
|
||||
private val trustRoot: X509Certificate,
|
||||
private val nodeCa: X509Certificate,
|
||||
private val signedNetworkParameters: NetworkParametersReader.NetworkParametersAndSigned) : ServiceHubInternal, SingletonSerializeAsToken() {
|
||||
|
||||
override val networkParameters: NetworkParameters = signedNetworkParameters.networkParameters
|
||||
@ -236,6 +244,14 @@ class FlowWorkerServiceHub(override val configuration: NodeConfiguration, overri
|
||||
|
||||
override fun jdbcSession(): Connection = database.createSession()
|
||||
|
||||
override fun <T : Any> withEntityManager(block: EntityManager.() -> T): T {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun withEntityManager(block: Consumer<EntityManager>) {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun registerUnloadHandler(runOnStop: () -> Unit) {
|
||||
this.runOnStop += runOnStop
|
||||
}
|
||||
|
@ -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) {
|
||||
println("SOMETHING WENT WRONG")
|
||||
throwable.printStackTrace(System.out)
|
||||
if (throwable is VirtualMachineError) throw throwable
|
||||
classfileBuffer
|
||||
}
|
||||
}
|
||||
|
@ -49,6 +49,7 @@ import net.corda.node.utilities.EnterpriseNamedCacheFactory
|
||||
import net.corda.node.utilities.profiling.getTracingConfig
|
||||
import net.corda.nodeapi.internal.NodeInfoAndSigned
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.contextTransaction
|
||||
import net.corda.nodeapi.internal.persistence.isH2Database
|
||||
import net.corda.serialization.internal.*
|
||||
import org.slf4j.Logger
|
||||
@ -59,8 +60,15 @@ import java.sql.Connection
|
||||
import java.time.Clock
|
||||
import java.time.Duration
|
||||
import java.util.*
|
||||
import java.util.function.Consumer
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
class RpcWorkerServiceHub(override val configuration: NodeConfiguration, override val myInfo: NodeInfo, private val signedNetworkParameters: NetworkParametersReader.NetworkParametersAndSigned, private val ourKeyPair: KeyPair, private val trustRoot: X509Certificate, private val nodeCa: X509Certificate) : ServiceHubInternal, SingletonSerializeAsToken() {
|
||||
class RpcWorkerServiceHub(override val configuration: NodeConfiguration,
|
||||
override val myInfo: NodeInfo,
|
||||
private val signedNetworkParameters: NetworkParametersReader.NetworkParametersAndSigned,
|
||||
private val ourKeyPair: KeyPair,
|
||||
private val trustRoot: X509Certificate,
|
||||
private val nodeCa: X509Certificate) : ServiceHubInternal, SingletonSerializeAsToken() {
|
||||
|
||||
override val clock: CordaClock = SimpleClock(Clock.systemUTC())
|
||||
private val versionInfo = getVersionInfo()
|
||||
@ -169,6 +177,14 @@ class RpcWorkerServiceHub(override val configuration: NodeConfiguration, overrid
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun <T : Any> withEntityManager(block: EntityManager.() -> T): T {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun withEntityManager(block: Consumer<EntityManager>) {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun registerUnloadHandler(runOnStop: () -> Unit) {
|
||||
this.runOnStop += runOnStop
|
||||
}
|
||||
|
@ -220,8 +220,8 @@ object RPCApi {
|
||||
companion object {
|
||||
private fun Any.safeSerialize(context: SerializationContext, wrap: (Throwable) -> Any) = try {
|
||||
serialize(context = context)
|
||||
} catch (t: Throwable) {
|
||||
wrap(t).serialize(context = context)
|
||||
} catch (e: Exception) {
|
||||
wrap(e).serialize(context = context)
|
||||
}
|
||||
|
||||
fun fromClientMessage(context: SerializationContext, message: ClientMessage): ServerToClient {
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.nodeapi.internal.crypto
|
||||
|
||||
import net.corda.core.crypto.Crypto.SPHINCS256_SHA256
|
||||
import net.corda.core.crypto.SignatureScheme
|
||||
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
|
||||
import org.bouncycastle.operator.ContentSigner
|
||||
@ -17,7 +18,9 @@ object ContentSignerBuilder {
|
||||
fun build(signatureScheme: SignatureScheme, privateKey: PrivateKey, provider: Provider, random: SecureRandom? = null): ContentSigner {
|
||||
val sigAlgId = signatureScheme.signatureOID
|
||||
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)
|
||||
} else {
|
||||
initSign(privateKey)
|
||||
|
@ -38,3 +38,5 @@ interface CryptoService {
|
||||
*/
|
||||
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
|
||||
fun <T> quietly(task: () -> T) = try {
|
||||
task()
|
||||
} catch (t: Throwable) {
|
||||
log.warn("Cleanup task failed:", t)
|
||||
} catch (e: Exception) {
|
||||
log.warn("Cleanup task failed:", e)
|
||||
}
|
||||
while (true) {
|
||||
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()
|
||||
transaction.commit()
|
||||
return answer
|
||||
} catch (e: Throwable) {
|
||||
} catch (e: Exception) {
|
||||
quietly(transaction::rollback)
|
||||
if (e is SQLException || (recoverAnyNestedSQLException && e.hasSQLExceptionCause())) {
|
||||
if (++recoverableFailureCount > recoverableFailureTolerance) throw e
|
||||
|
@ -7,6 +7,7 @@ import org.hibernate.Transaction
|
||||
import rx.subjects.PublishSubject
|
||||
import java.sql.Connection
|
||||
import java.util.*
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
fun currentDBSession(): Session = contextTransaction.session
|
||||
private val _contextTransaction = ThreadLocal<DatabaseTransaction>()
|
||||
@ -65,6 +66,12 @@ class DatabaseTransaction(
|
||||
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
|
||||
private lateinit var hibernateTransaction: Transaction
|
||||
|
||||
@ -111,3 +118,4 @@ class DatabaseTransaction(
|
||||
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()
|
||||
}
|
||||
sslServerSocket.close()
|
||||
} catch (ex: Throwable) {
|
||||
} catch (ex: Exception) {
|
||||
serverError = true
|
||||
}
|
||||
}
|
||||
|
@ -150,6 +150,7 @@ dependencies {
|
||||
// Capsule is a library for building independently executable fat JARs.
|
||||
// We only need this dependency to compile our Caplet against.
|
||||
compileOnly "co.paralleluniverse:capsule:$capsule_version"
|
||||
testCompile "co.paralleluniverse:capsule:$capsule_version"
|
||||
|
||||
// OkHTTP: Simple HTTP library.
|
||||
compile "com.squareup.okhttp3:okhttp:$okhttp_version"
|
||||
|
@ -35,7 +35,7 @@ class ErrorCodeLoggingTests : IntegrationTest() {
|
||||
node.rpc.startFlow(::MyFlow).waitForCompletion()
|
||||
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
|
||||
}
|
||||
|
@ -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) {
|
||||
String baseDirOption = getOption(args, "--base-directory");
|
||||
this.baseDir = Paths.get((baseDirOption == null) ? "." : baseDirOption).toAbsolutePath().normalize().toString();
|
||||
this.baseDir = getBaseDirectory(args);
|
||||
String config = getOption(args, "--config-file");
|
||||
File configFile = (config == null) ? new File(baseDir, "node.conf") : new File(config);
|
||||
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) {
|
||||
final String lowerCaseOption = option.toLowerCase();
|
||||
int index = 0;
|
||||
for (String arg : args) {
|
||||
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);
|
||||
} else {
|
||||
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++;
|
||||
}
|
||||
return null;
|
||||
@ -96,7 +122,10 @@ public class CordaCaplet extends Capsule {
|
||||
|
||||
File cordappsDir = new File(baseDir, "cordapps");
|
||||
// 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 (T) cp;
|
||||
}
|
||||
// Add additional directories of JARs to the classpath (at the end), e.g., for JDBC drivers.
|
||||
augmentClasspath(cp, new File(baseDir, "drivers"));
|
||||
augmentClasspath(cp, cordappsDir);
|
||||
@ -173,17 +202,18 @@ public class CordaCaplet extends Capsule {
|
||||
}
|
||||
}
|
||||
|
||||
private void requireCordappsDirExists(File dir) {
|
||||
private Boolean checkIfCordappDirExists(File dir) {
|
||||
try {
|
||||
if (!dir.mkdir() && !dir.exists()) { // It is unlikely to enter this if-branch, but just in case.
|
||||
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) {
|
||||
logOnFailedCordappDir();
|
||||
throw e; // Let Capsule handle the error (log error, clean up, die).
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void logOnFailedCordappDir() {
|
||||
|
@ -9,10 +9,10 @@ import net.corda.confidential.SwapIdentitiesHandler
|
||||
import net.corda.core.CordaException
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.context.InvocationContext
|
||||
import net.corda.core.crypto.internal.AliasPrivateKey
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.isCRLDistributionPointBlacklisted
|
||||
import net.corda.core.crypto.internal.AliasPrivateKey
|
||||
import net.corda.core.crypto.newSecureRandom
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.AbstractParty
|
||||
@ -105,6 +105,8 @@ import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit.MINUTES
|
||||
import java.util.concurrent.TimeUnit.SECONDS
|
||||
import java.util.function.Consumer
|
||||
import javax.persistence.EntityManager
|
||||
import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair
|
||||
|
||||
/**
|
||||
@ -505,8 +507,8 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
val republishInterval = try {
|
||||
networkMapClient.publish(signedNodeInfo)
|
||||
heartbeatInterval
|
||||
} catch (t: Throwable) {
|
||||
log.warn("Error encountered while publishing node info, will retry again", t)
|
||||
} catch (e: Exception) {
|
||||
log.warn("Error encountered while publishing node info, will retry again", e)
|
||||
// TODO: Exponential backoff? It should reach max interval of eventHorizon/2.
|
||||
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
|
||||
// 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.
|
||||
return BasicHSMKeyManagementService(cacheFactory,identityService, database, cryptoService)
|
||||
return BasicHSMKeyManagementService(cacheFactory, identityService, database, cryptoService)
|
||||
}
|
||||
|
||||
open fun stop() {
|
||||
@ -988,6 +990,14 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
|
||||
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
|
||||
override fun registerUnloadHandler(runOnStop: () -> Unit) {
|
||||
this@AbstractNode.runOnStop += runOnStop
|
||||
@ -1032,7 +1042,6 @@ class FlowStarterImpl(private val smm: StateMachineManager, private val flowLogi
|
||||
private val _future = openFuture<FlowStateMachine<T>>()
|
||||
override val future: CordaFuture<FlowStateMachine<T>>
|
||||
get() = _future
|
||||
|
||||
}
|
||||
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.")
|
||||
}
|
||||
retrievedHostName
|
||||
} catch (ignore: Throwable) {
|
||||
} catch (ignore: Exception) {
|
||||
// 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.")
|
||||
null
|
||||
|
@ -191,7 +191,7 @@ open class NodeStartup : NodeStartupLogging {
|
||||
node.startupComplete.then {
|
||||
try {
|
||||
InteractiveShell.runLocalShell(node::stop)
|
||||
} catch (e: Throwable) {
|
||||
} catch (e: Exception) {
|
||||
logger.error("Shell failed to start", e)
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package net.corda.node.services.api
|
||||
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.internal.CertRole
|
||||
import net.corda.core.node.services.IdentityService
|
||||
@ -36,22 +35,17 @@ interface IdentityServiceInternal : IdentityService {
|
||||
@Throws(CertificateExpiredException::class, CertificateNotYetValidException::class, InvalidAlgorithmParameterException::class)
|
||||
fun verifyAndRegisterIdentity(identity: PartyAndCertificate, isNewRandomIdentity: Boolean): PartyAndCertificate?
|
||||
|
||||
fun partiesFromName(query: String, exactMatch: Boolean, x500name: CordaX500Name, results: LinkedHashSet<Party>, party: Party) {
|
||||
// We can imagine this being a query over a lucene index in future.
|
||||
//
|
||||
// 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
|
||||
// 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).
|
||||
/** Check if [x500name] matches the [query]. */
|
||||
fun x500Matches(query: String, exactMatch: Boolean, x500name: CordaX500Name): Boolean {
|
||||
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.
|
||||
//
|
||||
// Kostas says: 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
|
||||
// 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).
|
||||
if (component.contains(query, ignoreCase = true))
|
||||
results += party
|
||||
}
|
||||
}
|
||||
return components.any { (exactMatch && it == query)
|
||||
|| (!exactMatch && it.contains(query, ignoreCase = true)) }
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -113,8 +113,8 @@ interface NodeConfiguration {
|
||||
|
||||
fun makeCryptoService(): CryptoService {
|
||||
return when(cryptoServiceName) {
|
||||
SupportedCryptoServices.BC_SIMPLE -> BCCryptoService(this)
|
||||
null -> BCCryptoService(this) // Pick default BCCryptoService when null.
|
||||
// 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)
|
||||
|
||||
/**
|
||||
* Currently registered JMX Reporters
|
||||
* Currently registered JMX Reporters.
|
||||
*/
|
||||
enum class JmxReporterType {
|
||||
JOLOKIA, NEW_RELIC
|
||||
|
@ -63,8 +63,10 @@ class InMemoryIdentityService(identities: List<PartyAndCertificate> = emptyList(
|
||||
|
||||
override fun partiesFromName(query: String, exactMatch: Boolean): Set<Party> {
|
||||
val results = LinkedHashSet<Party>()
|
||||
for ((x500name, partyAndCertificate) in principalToParties) {
|
||||
partiesFromName(query, exactMatch, x500name, results, partyAndCertificate.party)
|
||||
principalToParties.forEach { (x500name, partyAndCertificate) ->
|
||||
if (x500Matches(query, exactMatch, x500name)) {
|
||||
results += partyAndCertificate.party
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
@ -143,7 +143,7 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri
|
||||
val key = mapToKey(identity)
|
||||
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.
|
||||
keyToParties.set(key, identity)
|
||||
keyToParties[key] = identity
|
||||
} else {
|
||||
keyToParties.addWithDuplicatesAllowed(key, identity)
|
||||
}
|
||||
@ -174,8 +174,10 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri
|
||||
override fun partiesFromName(query: String, exactMatch: Boolean): Set<Party> {
|
||||
return database.transaction {
|
||||
val results = LinkedHashSet<Party>()
|
||||
for ((x500name, partyId) in principalToParties.allPersisted()) {
|
||||
partiesFromName(query, exactMatch, x500name, results, keyToParties[partyId]!!.party)
|
||||
principalToParties.allPersisted().forEach { (x500name, partyId) ->
|
||||
if (x500Matches(query, exactMatch, x500name)) {
|
||||
results += keyToParties[partyId]!!.party
|
||||
}
|
||||
}
|
||||
results
|
||||
}
|
||||
|
@ -2,31 +2,39 @@ package net.corda.node.services.keys.cryptoservice
|
||||
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.newSecureRandom
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
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.X509Utilities
|
||||
import net.corda.nodeapi.internal.cryptoservice.CryptoService
|
||||
import net.corda.nodeapi.internal.cryptoservice.CryptoServiceException
|
||||
import org.bouncycastle.operator.ContentSigner
|
||||
import java.security.KeyPair
|
||||
import java.security.KeyStore
|
||||
import java.security.PublicKey
|
||||
import javax.security.auth.x500.X500Principal
|
||||
|
||||
/**
|
||||
* Basic implementation of a [CryptoService] that uses BouncyCastle for cryptographic operations
|
||||
* and a Java KeyStore in the form of [CertificateStore] to store private 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 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 {
|
||||
val keyPair = Crypto.generateKeyPair(Crypto.findSignatureScheme(schemeNumberID))
|
||||
importKey(alias, keyPair)
|
||||
return keyPair.public
|
||||
try {
|
||||
val keyPair = Crypto.generateKeyPair(Crypto.findSignatureScheme(schemeNumberID))
|
||||
importKey(alias, keyPair)
|
||||
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 {
|
||||
@ -34,17 +42,29 @@ class BCCryptoService(private val nodeConf: NodeConfiguration) : CryptoService {
|
||||
}
|
||||
|
||||
override fun getPublicKey(alias: String): PublicKey {
|
||||
return certificateStore.query { getPublicKey(alias) }
|
||||
try {
|
||||
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 {
|
||||
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 {
|
||||
val privateKey = certificateStore.query { getPrivateKey(alias, certificateStore.entryPassword) }
|
||||
val signatureScheme = Crypto.findSignatureScheme(privateKey)
|
||||
return ContentSignerBuilder.build(signatureScheme, privateKey, Crypto.findProvider(signatureScheme.providerName), newSecureRandom())
|
||||
try {
|
||||
val privateKey = certificateStore.query { getPrivateKey(alias, certificateStore.entryPassword) }
|
||||
val signatureScheme = Crypto.findSignatureScheme(privateKey)
|
||||
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.
|
||||
*/
|
||||
fun resyncKeystore() {
|
||||
certificateStore = nodeConf.signingCertificateStore.get(true)
|
||||
certificateStore = certificateStoreSupplier.get(true)
|
||||
}
|
||||
|
||||
/** Import an already existing [KeyPair] to this [CryptoService]. */
|
||||
fun importKey(alias: String, keyPair: KeyPair) {
|
||||
// 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.
|
||||
val cert = X509Utilities.createSelfSignedCACertificate(nodeConf.myLegalName.x500Principal, keyPair)
|
||||
certificateStore.query { setPrivateKey(alias, keyPair.private, listOf(cert), certificateStore.entryPassword) }
|
||||
try {
|
||||
// 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.
|
||||
val cert = X509Utilities.createSelfSignedCACertificate(legalName, keyPair)
|
||||
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)
|
||||
serverControl!!.closeConsumerConnectionsForAddress(job.clientAddress.toString())
|
||||
invalidateClient(job.clientAddress)
|
||||
if (throwable is VirtualMachineError) throw throwable
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -94,8 +94,8 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
|
||||
override fun run() {
|
||||
val nextScheduleDelay = try {
|
||||
updateNetworkMapCache()
|
||||
} catch (t: Throwable) {
|
||||
logger.warn("Error encountered while updating network map, will retry in $defaultRetryInterval", t)
|
||||
} catch (e: Exception) {
|
||||
logger.warn("Error encountered while updating network map, will retry in $defaultRetryInterval", e)
|
||||
defaultRetryInterval
|
||||
}
|
||||
// Schedule the next update.
|
||||
|
@ -223,9 +223,13 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
val result = logic.call()
|
||||
suspend(FlowIORequest.WaitForSessionConfirmations, maySkipCheckpoint = true)
|
||||
Try.Success(result)
|
||||
} catch (throwable: Throwable) {
|
||||
logger.info("Flow threw exception... sending it to flow hospital", throwable)
|
||||
Try.Failure<R>(throwable)
|
||||
} catch (t: Throwable) {
|
||||
if(t is VirtualMachineError) {
|
||||
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 finalEvent = when (resultOrError) {
|
||||
@ -377,8 +381,8 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
maySkipCheckpoint = skipPersistingCheckpoint,
|
||||
fiber = this.checkpointSerialize(context = serializationContext.value)
|
||||
)
|
||||
} catch (throwable: Throwable) {
|
||||
Event.Error(throwable)
|
||||
} catch (exception: Exception) {
|
||||
Event.Error(exception)
|
||||
}
|
||||
|
||||
// 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.withTokenContext
|
||||
import org.apache.activemq.artemis.utils.ReusableLatch
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import java.security.SecureRandom
|
||||
@ -135,7 +136,13 @@ class SingleThreadedStateMachineManager(
|
||||
val fibers = restoreFlowsFromCheckpoints()
|
||||
metrics.register("Flows.InFlight", Gauge<Int> { mutex.content.flows.size })
|
||||
Fiber.setDefaultUncaughtExceptionHandler { fiber, throwable ->
|
||||
(fiber as FlowStateMachineImpl<*>).logger.warn("Caught exception from flow", 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)
|
||||
}
|
||||
}
|
||||
serviceHub.networkMapCache.nodeReady.then {
|
||||
logger.info("Node ready, info: ${serviceHub.myInfo}")
|
||||
@ -606,7 +613,7 @@ class SingleThreadedStateMachineManager(
|
||||
private fun deserializeCheckpoint(serializedCheckpoint: SerializedBytes<Checkpoint>): Checkpoint? {
|
||||
return try {
|
||||
serializedCheckpoint.checkpointDeserialize(context = checkpointSerializationContext!!)
|
||||
} catch (exception: Throwable) {
|
||||
} catch (exception: Exception) {
|
||||
logger.error("Encountered unrestorable checkpoint!", exception)
|
||||
null
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ class TransitionExecutorImpl(
|
||||
for (action in transition.actions) {
|
||||
try {
|
||||
actionExecutor.executeAction(fiber, action)
|
||||
} catch (exception: Throwable) {
|
||||
} catch (exception: Exception) {
|
||||
contextTransactionOrNull?.close()
|
||||
if (transition.newState.checkpoint.errorState is ErrorState.Errored) {
|
||||
// If we errored while transitioning to an error state then we cannot record the additional
|
||||
|
@ -77,8 +77,8 @@ class FiberDeserializationChecker {
|
||||
is Job.Check -> {
|
||||
try {
|
||||
job.serializedFiber.checkpointDeserialize(context = checkpointSerializationContext)
|
||||
} catch (throwable: Throwable) {
|
||||
log.error("Encountered unrestorable checkpoint!", throwable)
|
||||
} catch (exception: Exception) {
|
||||
log.error("Encountered unrestorable checkpoint!", exception)
|
||||
foundUnrestorableFibers = true
|
||||
}
|
||||
}
|
||||
|
@ -1,38 +1,25 @@
|
||||
package net.corda.node.services.transactions
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.ComponentGroupEnum
|
||||
import net.corda.core.flows.FlowSession
|
||||
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.SinglePartyNotaryService
|
||||
import net.corda.core.transactions.ContractUpgradeFilteredTransaction
|
||||
import net.corda.core.transactions.CoreTransaction
|
||||
import net.corda.core.transactions.FilteredTransaction
|
||||
import net.corda.core.transactions.NotaryChangeWireTransaction
|
||||
|
||||
/**
|
||||
* 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
|
||||
* history chain.
|
||||
* As a result, the Notary _will commit invalid transactions_ as well, but as it also records the identity of
|
||||
* 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).
|
||||
*/
|
||||
class NonValidatingNotaryFlow(otherSideSession: FlowSession, service: SinglePartyNotaryService) : NotaryServiceFlow(otherSideSession, service) {
|
||||
/**
|
||||
* 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
|
||||
* history chain.
|
||||
* As a result, the Notary _will commit invalid transactions_ as well, but as it also records the identity of
|
||||
* 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).
|
||||
*/
|
||||
@Suspendable
|
||||
override fun validateRequest(requestPayload: NotarisationPayload): TransactionParts {
|
||||
val transaction = 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 {
|
||||
override fun extractParts(requestPayload: NotarisationPayload): TransactionParts {
|
||||
val tx = requestPayload.coreTransaction
|
||||
return when (tx) {
|
||||
is FilteredTransaction -> {
|
||||
tx.apply {
|
||||
@ -43,7 +30,7 @@ class NonValidatingNotaryFlow(otherSideSession: FlowSession, service: SinglePart
|
||||
}
|
||||
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)
|
||||
else -> {
|
||||
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 net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.contracts.TransactionVerificationException
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.NotarisationPayload
|
||||
import net.corda.core.flows.NotarisationRequest
|
||||
import net.corda.core.flows.NotaryError
|
||||
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.NotaryServiceFlow
|
||||
import net.corda.core.internal.notary.SinglePartyNotaryService
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionWithSignatures
|
||||
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
|
||||
@ -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
|
||||
* 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
|
||||
* the transaction in question has all required signatures apart from the notary's.
|
||||
*/
|
||||
@Suspendable
|
||||
override fun validateRequest(requestPayload: NotarisationPayload): TransactionParts {
|
||||
override fun verifyTransaction(requestPayload: NotarisationPayload) {
|
||||
try {
|
||||
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)
|
||||
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) {
|
||||
throw when (e) {
|
||||
is TransactionVerificationException,
|
||||
is SignatureException -> NotaryInternalException(NotaryError.TransactionInvalid(e))
|
||||
else -> e
|
||||
}
|
||||
throw NotaryInternalException(NotaryError.TransactionInvalid(e))
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,10 +53,6 @@ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: SinglePartyNo
|
||||
}
|
||||
|
||||
private fun checkSignatures(tx: TransactionWithSignatures) {
|
||||
try {
|
||||
tx.verifySignaturesExcept(service.notaryIdentityKey)
|
||||
} catch (e: SignatureException) {
|
||||
throw NotaryInternalException(NotaryError.TransactionInvalid(e))
|
||||
}
|
||||
tx.verifySignaturesExcept(service.notaryIdentityKey)
|
||||
}
|
||||
}
|
||||
|
@ -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.bufferUntilSubscribed
|
||||
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.UniquenessProvider
|
||||
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.seconds
|
||||
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.network.NetworkParametersCopier
|
||||
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. */
|
||||
private class TestNotaryFlow(otherSide: FlowSession, service: TestNotaryService) : NotaryServiceFlow(otherSide, service) {
|
||||
private class TestNotaryFlow(otherSide: FlowSession, service: TestNotaryService) : ValidatingNotaryFlow(otherSide, service) {
|
||||
@Suspendable
|
||||
override fun validateRequest(requestPayload: NotarisationPayload): TransactionParts {
|
||||
override fun verifyTransaction(requestPayload: NotarisationPayload) {
|
||||
val myIdentity = serviceHub.myInfo.legalIdentities.first()
|
||||
MDC.put("name", myIdentity.name.toString())
|
||||
logger.info("Received a request from ${otherSideSession.counterparty.name}")
|
||||
@ -215,7 +215,6 @@ class TimedFlowTests {
|
||||
} else {
|
||||
logger.info("Processing")
|
||||
}
|
||||
return TransactionParts(stx.id, stx.inputs, stx.tx.timeWindow, stx.notary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -178,7 +178,7 @@ class NodeConfigurationImplTest {
|
||||
|
||||
@Test
|
||||
fun `mutual exclusion machineName set to default if not explicitly set`() {
|
||||
val config = getConfig("test-config-mutualExclusion-noMachineName.conf").parseAsNodeConfiguration(options = Configuration.Validation.Options(strict = false)).orThrow()
|
||||
val config = getConfig("test-config-mutualExclusion-noMachineName.conf").parseAsNodeConfiguration(options = Configuration.Validation.Options(strict = false)).value()
|
||||
assertEquals(InetAddress.getLocalHost().hostName, config.enterpriseConfiguration.mutualExclusionConfiguration.machineName)
|
||||
}
|
||||
|
||||
@ -276,7 +276,7 @@ class NodeConfigurationImplTest {
|
||||
@Test
|
||||
fun `jmxReporterType is null and defaults to Jokolia`() {
|
||||
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())
|
||||
}
|
||||
|
||||
@ -284,7 +284,7 @@ class NodeConfigurationImplTest {
|
||||
fun `jmxReporterType is not null and is set to New Relic`() {
|
||||
var rawConfig = getConfig("working-config.conf", ConfigFactory.parseMap(mapOf("devMode" to true)))
|
||||
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())
|
||||
}
|
||||
|
||||
@ -292,7 +292,7 @@ class NodeConfigurationImplTest {
|
||||
fun `jmxReporterType is not null and set to Jokolia`() {
|
||||
var rawConfig = getConfig("working-config.conf", ConfigFactory.parseMap(mapOf("devMode" to true)))
|
||||
rawConfig = rawConfig.withValue("jmxReporterType", ConfigValueFactory.fromAnyRef("JOLOKIA"))
|
||||
val nodeConfig = rawConfig.parseAsNodeConfiguration().orThrow()
|
||||
val nodeConfig = rawConfig.parseAsNodeConfiguration().value()
|
||||
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)
|
||||
}
|
||||
println("SOFT LOCK STATES #1 succeeded")
|
||||
} catch (e: Throwable) {
|
||||
} catch (e: Exception) {
|
||||
println("SOFT LOCK STATES #1 failed")
|
||||
} finally {
|
||||
countDown.countDown()
|
||||
@ -292,7 +292,7 @@ class NodeVaultServiceTest {
|
||||
assertThat(vaultService.queryBy<Cash.State>(criteriaByLockId2).states).hasSize(3)
|
||||
}
|
||||
println("SOFT LOCK STATES #2 succeeded")
|
||||
} catch (e: Throwable) {
|
||||
} catch (e: Exception) {
|
||||
println("SOFT LOCK STATES #2 failed")
|
||||
} finally {
|
||||
countDown.countDown()
|
||||
|
@ -327,7 +327,7 @@ class TLSAuthenticationTests {
|
||||
lock.notifyAll()
|
||||
}
|
||||
sslServerSocket.close()
|
||||
} catch (ex: Throwable) {
|
||||
} catch (ex: Exception) {
|
||||
serverError = true
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ class InterestRateSwapAPI {
|
||||
return try {
|
||||
rpc.startFlow(AutoOfferFlow::Requester, newDeal).returnValue.getOrThrow()
|
||||
ResponseEntity.created(URI.create(generateDealLink(newDeal))).build()
|
||||
} catch (ex: Throwable) {
|
||||
} catch (ex: Exception) {
|
||||
logger.info("Exception when creating deal: $ex", ex)
|
||||
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ex.toString())
|
||||
}
|
||||
|
@ -1,20 +1,19 @@
|
||||
package net.corda.notarydemo
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.contracts.TransactionVerificationException
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.NotarisationPayload
|
||||
import net.corda.core.flows.NotaryError
|
||||
import net.corda.core.internal.ResolveTransactionsFlow
|
||||
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.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionWithSignatures
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.node.services.api.ServiceHubInternal
|
||||
import net.corda.node.services.transactions.PersistentUniquenessProvider
|
||||
import net.corda.node.services.transactions.ValidatingNotaryFlow
|
||||
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].
|
||||
@ -35,28 +34,15 @@ class MyCustomValidatingNotaryService(override val services: ServiceHubInternal,
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
// START 2
|
||||
class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidatingNotaryService) : NotaryServiceFlow(otherSide, service) {
|
||||
/**
|
||||
* 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 {
|
||||
class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidatingNotaryService) : ValidatingNotaryFlow(otherSide, service) {
|
||||
override fun verifyTransaction(requestPayload: NotarisationPayload) {
|
||||
try {
|
||||
val stx = requestPayload.signedTransaction
|
||||
validateRequestSignature(NotarisationRequest(stx.inputs, stx.id), requestPayload.requestSignature)
|
||||
val notary = stx.notary
|
||||
checkNotary(notary)
|
||||
verifySignatures(stx)
|
||||
resolveAndContractVerify(stx)
|
||||
val timeWindow: TimeWindow? = if (stx.coreTransaction is WireTransaction) stx.tx.timeWindow else null
|
||||
return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!, stx.references)
|
||||
verifySignatures(stx)
|
||||
customVerify(stx)
|
||||
} catch (e: Exception) {
|
||||
throw when (e) {
|
||||
is TransactionVerificationException,
|
||||
is SignatureException -> NotaryInternalException(NotaryError.TransactionInvalid(e))
|
||||
else -> e
|
||||
}
|
||||
throw NotaryInternalException(NotaryError.TransactionInvalid(e))
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,7 +50,6 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating
|
||||
private fun resolveAndContractVerify(stx: SignedTransaction) {
|
||||
subFlow(ResolveTransactionsFlow(stx, otherSideSession))
|
||||
stx.verify(serviceHub, false)
|
||||
customVerify(stx)
|
||||
}
|
||||
|
||||
private fun verifySignatures(stx: SignedTransaction) {
|
||||
@ -73,11 +58,7 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating
|
||||
}
|
||||
|
||||
private fun checkSignatures(tx: TransactionWithSignatures) {
|
||||
try {
|
||||
tx.verifySignaturesExcept(service.notaryIdentityKey)
|
||||
} catch (e: SignatureException) {
|
||||
throw NotaryInternalException(NotaryError.TransactionInvalid(e))
|
||||
}
|
||||
tx.verifySignaturesExcept(service.notaryIdentityKey)
|
||||
}
|
||||
|
||||
private fun customVerify(stx: SignedTransaction) {
|
||||
|
@ -14,6 +14,7 @@ import org.apache.qpid.proton.amqp.UnsignedInteger
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.io.InputStream
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.Exception
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import java.lang.reflect.Type
|
||||
import java.lang.reflect.TypeVariable
|
||||
@ -100,8 +101,8 @@ class DeserializationInput constructor(
|
||||
throw NotSerializableException(amqp.mitigation)
|
||||
} catch (nse: NotSerializableException) {
|
||||
throw nse
|
||||
} catch (t: Throwable) {
|
||||
throw NotSerializableException("Internal deserialization failure: ${t.javaClass.name}: ${t.message}").apply { initCause(t) }
|
||||
} catch (e: Exception) {
|
||||
throw NotSerializableException("Internal deserialization failure: ${e.javaClass.name}: ${e.message}").apply { initCause(e) }
|
||||
} finally {
|
||||
objectHistory.clear()
|
||||
}
|
||||
|
@ -23,7 +23,6 @@ include 'experimental'
|
||||
include 'experimental:avalanche'
|
||||
include 'experimental:behave'
|
||||
include 'experimental:quasar-hook'
|
||||
include 'experimental:kryo-hook'
|
||||
// include 'experimental:intellij-plugin'
|
||||
include 'experimental:flow-hook'
|
||||
include 'experimental:flow-worker'
|
||||
|
@ -27,7 +27,6 @@ import net.corda.testing.node.internal.addressMustNotBeBound
|
||||
import net.corda.testing.node.internal.internalDriver
|
||||
import org.assertj.core.api.Assertions.*
|
||||
import org.json.simple.JSONObject
|
||||
import org.junit.Assume.assumeTrue
|
||||
import org.junit.ClassRule
|
||||
import org.junit.Test
|
||||
import java.util.*
|
||||
@ -179,8 +178,8 @@ class DriverTests : IntegrationTest() {
|
||||
fun `driver waits for in-process nodes to finish`() {
|
||||
fun NodeHandle.stopQuietly() = try {
|
||||
stop()
|
||||
} catch (t: Throwable) {
|
||||
t.printStackTrace()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
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.vault.NodeVaultService
|
||||
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.core.TestIdentity
|
||||
import net.corda.testing.internal.DEV_ROOT_CA
|
||||
@ -39,6 +41,8 @@ import java.security.KeyPair
|
||||
import java.sql.Connection
|
||||
import java.time.Clock
|
||||
import java.util.*
|
||||
import java.util.function.Consumer
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
/**
|
||||
* 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 <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)
|
||||
@ -264,6 +276,14 @@ open class MockServices private constructor(
|
||||
|
||||
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()
|
||||
|
||||
/** 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].
|
||||
*/
|
||||
private class NodeConfig(val typesafe: Config) {
|
||||
val corda: NodeConfiguration = typesafe.parseAsNodeConfiguration().orThrow()
|
||||
val corda: NodeConfiguration = typesafe.parseAsNodeConfiguration().value()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -106,8 +106,8 @@ fun <A> poll(
|
||||
} else {
|
||||
executorService.schedule(this, pollInterval.toMillis(), TimeUnit.MILLISECONDS)
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
resultFuture.setException(t)
|
||||
} catch (e: Exception) {
|
||||
resultFuture.setException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user