Merge branch 'os-merge-point' into os-merge-shams

# Conflicts:
#	build.gradle
#	core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt
#	core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt
#	docs/source/api-persistence.rst
#	node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlListener.kt
#	node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt
#	node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt
#	settings.gradle
#	testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt
#	testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TransactionDSLInterpreter.kt
#	testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt
This commit is contained in:
Shams Asari 2018-11-14 18:13:01 +00:00
commit 4984e92e31
115 changed files with 1262 additions and 687 deletions

14
.ci/kill_corda_procs.cmd Normal file
View 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

View File

@ -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)

View File

@ -412,6 +412,8 @@ bintrayConfig {
'corda-notary-bft-smart',
'corda-notary-jpa',
'corda-notary-mysql'
'corda-common-configuration-parsing',
'corda-common-validation'
]
license {
name = 'Apache-2.0'

View File

@ -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()}")
}

View File

@ -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
}
}

View File

@ -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)

View File

@ -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<*>>>

View File

@ -17,7 +17,7 @@ dependencies {
}
jar {
baseName 'common-configuration-parsing'
baseName 'corda-common-configuration-parsing'
}
publish {

View File

@ -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})"

View File

@ -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 ->

View File

@ -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)
}

View File

@ -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 }

View File

@ -11,7 +11,7 @@ dependencies {
}
jar {
baseName 'common-validation'
baseName 'corda-common-validation'
}
publish {

View File

@ -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)

View File

@ -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 }
}
}

View File

@ -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

View File

@ -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. */

View File

@ -72,9 +72,9 @@ 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>.toPartiesOrderedByName(): List<Party> = map {
Party(it.signerCertPath.certificates[0] as X509Certificate)

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -53,6 +53,7 @@ abstract class SinglePartyNotaryService : NotaryService() {
references
)
)
if (result is UniquenessProvider.Result.Failure) {
throw NotaryInternalException(result.error)
}

View File

@ -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.
*

View File

@ -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).

View File

@ -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

View File

@ -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().

View File

@ -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)
}
}
}

View File

@ -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]")
}
}

View File

@ -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``.

View File

@ -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);

View File

@ -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

View File

@ -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

View File

@ -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));

View File

@ -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);
}
}

View File

@ -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)

View File

@ -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:

View File

@ -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))
}
}
}

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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 dont 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.

View File

@ -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:

View File

@ -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?
------------------------

View File

@ -7,41 +7,39 @@
The CorDapp Template
====================
When writing a new CorDapp, youll generally want to base it on the standard templates:
When writing a new CorDapp, youll 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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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) } }
}

View File

@ -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)
}
}
}
}

View File

@ -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.

View File

@ -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
}
}

View File

@ -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 {

View File

@ -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)

View File

@ -38,3 +38,5 @@ interface CryptoService {
*/
fun getSigner(alias: String): ContentSigner
}
open class CryptoServiceException(message: String?, cause: Throwable? = null) : Exception(message, cause)

View File

@ -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

View File

@ -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() }
}
}

View File

@ -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?
}

View File

@ -330,7 +330,7 @@ class X509UtilitiesTest {
lock.notifyAll()
}
sslServerSocket.close()
} catch (ex: Throwable) {
} catch (ex: Exception) {
serverError = true
}
}

View File

@ -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"

View File

@ -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
}

View File

@ -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()
}
}
}

View File

@ -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 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() {

View File

@ -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)
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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)) }
}
/**

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -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
}
}

View File

@ -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.

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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}," +

View File

@ -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)
}
}

View 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 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);
}
}

View 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);
}
}

View 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 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);
}
}

View 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());
}
}

View 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();
}
}

View File

@ -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)
}
}
}

View File

@ -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())
}

View File

@ -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) }
}
}

View File

@ -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()
}
}

View File

@ -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()

View File

@ -327,7 +327,7 @@ class TLSAuthenticationTests {
lock.notifyAll()
}
sslServerSocket.close()
} catch (ex: Throwable) {
} catch (ex: Exception) {
serverError = true
}
}

View File

@ -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())
}

View File

@ -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) {

View File

@ -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()
}

View File

@ -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'

View File

@ -179,8 +179,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>>()

View File

@ -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 */

View File

@ -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 {

View File

@ -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)
}
}
}

View File

@ -118,7 +118,7 @@ abstract class NodeBasedTest(private val cordappPackages: List<String> = emptyLi
val specificConfig = config.withValue(NodeConfiguration.cordappDirectoriesKey, ConfigValueFactory.fromIterable(cordappDirectories.toSet()))
val parsedConfig = specificConfig.parseAsNodeConfiguration().orThrow()
val parsedConfig = specificConfig.parseAsNodeConfiguration().value()
defaultNetworkParameters.install(baseDirectory)
return InProcessNode(parsedConfig, MOCK_VERSION_INFO.copy(platformVersion = platformVersion), flowManager)

View File

@ -65,6 +65,9 @@ class ShutdownManager(private val executorService: ExecutorService) {
it.value()
} catch (t: Throwable) {
log.warn("Exception while calling a shutdown action, this might create resource leaks", t)
if (t is VirtualMachineError) {
throw t
}
}
is Try.Failure -> log.warn("Exception while getting shutdown method, disregarding", it.exception)
}

View File

@ -91,8 +91,8 @@ fun startPublishingFixedRateInjector(
}
workBoundSemaphore.release()
}
} catch (throwable: Throwable) {
throwable.printStackTrace()
} catch (e: Exception) {
e.printStackTrace()
}
}
}

Some files were not shown because too many files have changed in this diff Show More