diff --git a/.ci/api-current.txt b/.ci/api-current.txt
index 8d1fc55cd4..bb614a9022 100644
--- a/.ci/api-current.txt
+++ b/.ci/api-current.txt
@@ -1384,12 +1384,10 @@ public static final class net.corda.core.flows.NotarisationRequest$Companion ext
@net.corda.core.serialization.CordaSerializable public abstract class net.corda.core.flows.NotaryError extends java.lang.Object
##
@net.corda.core.serialization.CordaSerializable public static final class net.corda.core.flows.NotaryError$Conflict extends net.corda.core.flows.NotaryError
- public (net.corda.core.crypto.SecureHash, net.corda.core.crypto.SignedData)
@org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component1()
- @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SignedData component2()
- @org.jetbrains.annotations.NotNull public final net.corda.core.flows.NotaryError$Conflict copy(net.corda.core.crypto.SecureHash, net.corda.core.crypto.SignedData)
+ @org.jetbrains.annotations.NotNull public final Map component2()
+ @org.jetbrains.annotations.NotNull public final net.corda.core.flows.NotaryError$Conflict copy(net.corda.core.crypto.SecureHash, Map)
public boolean equals(Object)
- @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SignedData getConflict()
@org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getTxId()
public int hashCode()
@org.jetbrains.annotations.NotNull public String toString()
@@ -1440,13 +1438,13 @@ public static final class net.corda.core.flows.NotaryError$TimeWindowInvalid$Com
public static final net.corda.core.flows.NotaryError$WrongNotary INSTANCE
##
@net.corda.core.serialization.CordaSerializable public final class net.corda.core.flows.NotaryException extends net.corda.core.flows.FlowException
- public (net.corda.core.flows.NotaryError)
+ public (net.corda.core.flows.NotaryError, net.corda.core.crypto.SecureHash)
@org.jetbrains.annotations.NotNull public final net.corda.core.flows.NotaryError getError()
##
public final class net.corda.core.flows.NotaryFlow extends java.lang.Object
public ()
##
-@net.corda.core.flows.InitiatingFlow public static class net.corda.core.flows.NotaryFlow$Client extends net.corda.core.flows.FlowLogic
+@net.corda.core.DoNotImplement @net.corda.core.flows.InitiatingFlow public static class net.corda.core.flows.NotaryFlow$Client extends net.corda.core.flows.FlowLogic
public (net.corda.core.transactions.SignedTransaction)
public (net.corda.core.transactions.SignedTransaction, net.corda.core.utilities.ProgressTracker)
@co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public List call()
@@ -3015,17 +3013,15 @@ public static final class net.corda.core.serialization.SingletonSerializationTok
@org.jetbrains.annotations.NotNull public final String getReason()
##
@net.corda.core.DoNotImplement @net.corda.core.serialization.CordaSerializable public final class net.corda.core.transactions.ContractUpgradeFilteredTransaction extends net.corda.core.transactions.CoreTransaction
- public (List, net.corda.core.identity.Party, net.corda.core.crypto.SecureHash)
- @org.jetbrains.annotations.NotNull public final List component1()
- @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party component2()
- @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component3()
- @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.ContractUpgradeFilteredTransaction copy(List, net.corda.core.identity.Party, net.corda.core.crypto.SecureHash)
+ public (Map, Map)
+ @org.jetbrains.annotations.NotNull public final Map component1()
+ @org.jetbrains.annotations.NotNull public final Map component2()
+ @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.ContractUpgradeFilteredTransaction copy(Map, Map)
public boolean equals(Object)
@org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash getId()
@org.jetbrains.annotations.NotNull public List getInputs()
@org.jetbrains.annotations.NotNull public net.corda.core.identity.Party getNotary()
@org.jetbrains.annotations.NotNull public List getOutputs()
- @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getRest()
public int hashCode()
public String toString()
##
@@ -3052,8 +3048,8 @@ public static final class net.corda.core.serialization.SingletonSerializationTok
@org.jetbrains.annotations.NotNull public final net.corda.core.contracts.PrivacySalt getPrivacySalt()
@org.jetbrains.annotations.NotNull public Set getRequiredSigningKeys()
@org.jetbrains.annotations.NotNull public List getSigs()
- @org.jetbrains.annotations.NotNull public final String getUpgradeContractClassName()
@org.jetbrains.annotations.NotNull public final net.corda.core.contracts.Attachment getUpgradedContractAttachment()
+ @org.jetbrains.annotations.NotNull public final String getUpgradedContractClassName()
public int hashCode()
public String toString()
public void verifyRequiredSignatures()
@@ -3061,15 +3057,11 @@ public static final class net.corda.core.serialization.SingletonSerializationTok
public void verifySignaturesExcept(java.security.PublicKey...)
##
@net.corda.core.DoNotImplement @net.corda.core.serialization.CordaSerializable public final class net.corda.core.transactions.ContractUpgradeWireTransaction extends net.corda.core.transactions.CoreTransaction
- public (List, net.corda.core.identity.Party, net.corda.core.crypto.SecureHash, String, net.corda.core.crypto.SecureHash, net.corda.core.contracts.PrivacySalt)
+ public (List, net.corda.core.contracts.PrivacySalt)
@org.jetbrains.annotations.NotNull public final net.corda.core.transactions.ContractUpgradeFilteredTransaction buildFilteredTransaction()
@org.jetbrains.annotations.NotNull public final List component1()
- @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party component2()
- @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component3()
- @org.jetbrains.annotations.NotNull public final String component4()
- @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component5()
- @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.PrivacySalt component6()
- @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.ContractUpgradeWireTransaction copy(List, net.corda.core.identity.Party, net.corda.core.crypto.SecureHash, String, net.corda.core.crypto.SecureHash, net.corda.core.contracts.PrivacySalt)
+ @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.PrivacySalt component2()
+ @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.ContractUpgradeWireTransaction copy(List, net.corda.core.contracts.PrivacySalt)
public boolean equals(Object)
@org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash getId()
@org.jetbrains.annotations.NotNull public List getInputs()
@@ -3077,8 +3069,8 @@ public static final class net.corda.core.serialization.SingletonSerializationTok
@org.jetbrains.annotations.NotNull public net.corda.core.identity.Party getNotary()
@org.jetbrains.annotations.NotNull public List getOutputs()
@org.jetbrains.annotations.NotNull public final net.corda.core.contracts.PrivacySalt getPrivacySalt()
- @org.jetbrains.annotations.NotNull public final String getUpgradeContractClassName()
@org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getUpgradedContractAttachmentId()
+ @org.jetbrains.annotations.NotNull public final String getUpgradedContractClassName()
public int hashCode()
@org.jetbrains.annotations.NotNull public final net.corda.core.transactions.ContractUpgradeLedgerTransaction resolve(net.corda.core.node.ServicesForResolution, List)
public String toString()
@@ -3213,11 +3205,9 @@ public static final class net.corda.core.transactions.LedgerTransaction$InOutGro
public void verifySignaturesExcept(java.security.PublicKey...)
##
@net.corda.core.DoNotImplement @net.corda.core.serialization.CordaSerializable public final class net.corda.core.transactions.NotaryChangeWireTransaction extends net.corda.core.transactions.CoreTransaction
- public (List, net.corda.core.identity.Party, net.corda.core.identity.Party)
+ public (List)
@org.jetbrains.annotations.NotNull public final List component1()
- @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party component2()
- @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party component3()
- @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.NotaryChangeWireTransaction copy(List, net.corda.core.identity.Party, net.corda.core.identity.Party)
+ @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.NotaryChangeWireTransaction copy(List)
public boolean equals(Object)
@org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash getId()
@org.jetbrains.annotations.NotNull public List getInputs()
diff --git a/confidential-identities/src/main/kotlin/net/corda/confidential/SwapIdentitiesFlow.kt b/confidential-identities/src/main/kotlin/net/corda/confidential/SwapIdentitiesFlow.kt
index e934afb9c8..d5ce6b4d09 100644
--- a/confidential-identities/src/main/kotlin/net/corda/confidential/SwapIdentitiesFlow.kt
+++ b/confidential-identities/src/main/kotlin/net/corda/confidential/SwapIdentitiesFlow.kt
@@ -86,7 +86,8 @@ class SwapIdentitiesFlow(private val otherParty: Party,
val legalIdentityAnonymous = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, revocationEnabled)
val serializedIdentity = SerializedBytes(legalIdentityAnonymous.serialize().bytes)
- // Special case that if we're both parties, a single identity is generated
+ // Special case that if we're both parties, a single identity is generated.
+ // TODO: for increased privacy, we should create one anonymous key per output state.
val identities = LinkedHashMap()
if (serviceHub.myInfo.isLegalIdentity(otherParty)) {
identities.put(otherParty, legalIdentityAnonymous.party.anonymise())
diff --git a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt
index 8d1d3f52f9..763d585e1e 100644
--- a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt
+++ b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt
@@ -179,7 +179,7 @@ fun KeyPair.verify(signatureData: ByteArray, clearData: ByteArray): Boolean = Cr
* which should never happen and suggests an unusual JVM or non-standard Java library.
*/
@Throws(NoSuchAlgorithmException::class)
-fun secureRandomBytes(numOfBytes: Int): ByteArray = newSecureRandom().generateSeed(numOfBytes)
+fun secureRandomBytes(numOfBytes: Int): ByteArray = ByteArray(numOfBytes).apply { newSecureRandom().nextBytes(this) }
/**
* This is a hack added because during deserialisation when no-param constructors are called sometimes default values
@@ -257,7 +257,11 @@ fun componentHash(opaqueBytes: OpaqueBytes, privacySalt: PrivacySalt, componentG
/** Return the SHA256(SHA256(nonce || serializedComponent)). */
fun componentHash(nonce: SecureHash, opaqueBytes: OpaqueBytes): SecureHash = SecureHash.sha256Twice(nonce.bytes + opaqueBytes.bytes)
-/** Serialise the object and return the hash of the serialized bytes. */
+/**
+ * Serialise the object and return the hash of the serialized bytes. Note that the resulting hash may not be deterministic
+ * across platform versions: serialization can produce different values if any of the types being serialized have changed,
+ * or if the version of serialization specified by the context changes.
+ */
fun serializedHash(x: T): SecureHash = x.serialize(context = SerializationDefaults.P2P_CONTEXT.withoutReferences()).bytes.sha256()
/**
diff --git a/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt b/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt
index 555c555c17..3fe8acdce3 100644
--- a/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt
+++ b/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt
@@ -88,7 +88,7 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
* Generates a random SHA-256 value.
*/
@JvmStatic
- fun randomSHA256() = sha256(newSecureRandom().generateSeed(32))
+ fun randomSHA256() = sha256(secureRandomBytes(32))
/**
* A SHA-256 hash value consisting of 32 0x00 bytes.
diff --git a/core/src/main/kotlin/net/corda/core/flows/NotarisationRequest.kt b/core/src/main/kotlin/net/corda/core/flows/NotarisationRequest.kt
index 21e7ce1e64..998849b7c9 100644
--- a/core/src/main/kotlin/net/corda/core/flows/NotarisationRequest.kt
+++ b/core/src/main/kotlin/net/corda/core/flows/NotarisationRequest.kt
@@ -11,8 +11,7 @@
package net.corda.core.flows
import net.corda.core.contracts.StateRef
-import net.corda.core.crypto.DigitalSignature
-import net.corda.core.crypto.SecureHash
+import net.corda.core.crypto.*
import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.serialize
@@ -53,7 +52,7 @@ class NotarisationRequest(statesToConsume: List, val transactionId: Se
val signature = requestSignature.digitalSignature
if (intendedSigner.owningKey != signature.by) {
val errorMessage = "Expected a signature by ${intendedSigner.owningKey}, but received by ${signature.by}}"
- throw NotaryException(NotaryError.RequestSignatureInvalid(IllegalArgumentException(errorMessage)))
+ 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
@@ -69,7 +68,7 @@ class NotarisationRequest(statesToConsume: List, val transactionId: Se
when (e) {
is InvalidKeyException, is SignatureException -> {
val error = NotaryError.RequestSignatureInvalid(e)
- throw NotaryException(error)
+ throw NotaryInternalException(error)
}
else -> throw e
}
@@ -108,4 +107,8 @@ data class NotarisationPayload(val transaction: Any, val requestSignature: Notar
* Should only be used by non-validating notaries.
*/
val coreTransaction get() = transaction as CoreTransaction
-}
\ No newline at end of file
+}
+
+/** Payload returned by the notary service flow to the client. */
+@CordaSerializable
+data class NotarisationResponse(val signatures: List)
\ No newline at end of file
diff --git a/core/src/main/kotlin/net/corda/core/flows/NotaryChangeFlow.kt b/core/src/main/kotlin/net/corda/core/flows/NotaryChangeFlow.kt
index 7c1867a2be..2a50e7361e 100644
--- a/core/src/main/kotlin/net/corda/core/flows/NotaryChangeFlow.kt
+++ b/core/src/main/kotlin/net/corda/core/flows/NotaryChangeFlow.kt
@@ -17,7 +17,7 @@ import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SignableData
import net.corda.core.crypto.SignatureMetadata
import net.corda.core.identity.Party
-import net.corda.core.transactions.NotaryChangeWireTransaction
+import net.corda.core.internal.NotaryChangeTransactionBuilder
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.ProgressTracker
@@ -40,11 +40,11 @@ class NotaryChangeFlow(
override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx {
val inputs = resolveEncumbrances(originalState)
- val tx = NotaryChangeWireTransaction(
+ val tx = NotaryChangeTransactionBuilder(
inputs.map { it.ref },
originalState.state.notary,
modification
- )
+ ).build()
val participantKeys = inputs.flatMap { it.state.data.participants }.map { it.owningKey }.toSet()
// TODO: We need a much faster way of finding our key in the transaction
diff --git a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt
index 62ce288c3e..2301f2be49 100644
--- a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt
+++ b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt
@@ -11,27 +11,24 @@
package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable
+import net.corda.core.DoNotImplement
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.TimeWindow
import net.corda.core.crypto.SecureHash
-import net.corda.core.crypto.SignedData
import net.corda.core.crypto.TransactionSignature
-import net.corda.core.crypto.keys
import net.corda.core.identity.Party
import net.corda.core.internal.FetchDataFlow
import net.corda.core.internal.generateSignature
+import net.corda.core.internal.validateSignatures
import net.corda.core.node.services.NotaryService
import net.corda.core.node.services.TrustedAuthorityNotaryService
-import net.corda.core.node.services.UniquenessProvider
import net.corda.core.serialization.CordaSerializable
-import net.corda.core.transactions.CoreTransaction
import net.corda.core.transactions.ContractUpgradeWireTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.UntrustworthyData
import net.corda.core.utilities.unwrap
-import java.security.SignatureException
import java.time.Instant
import java.util.function.Predicate
@@ -46,6 +43,7 @@ class NotaryFlow {
* @throws NotaryException in case the any of the inputs to the transaction have been consumed
* by another transaction or the time-window is invalid.
*/
+ @DoNotImplement
@InitiatingFlow
open class Client(private val stx: SignedTransaction,
override val progressTracker: ProgressTracker) : FlowLogic>() {
@@ -78,44 +76,32 @@ class NotaryFlow {
check(serviceHub.loadStates(stx.inputs.toSet()).all { it.state.notary == notaryParty }) {
"Input states must have the same Notary"
}
-
- try {
- stx.resolveTransactionWithSignatures(serviceHub).verifySignaturesExcept(notaryParty.owningKey)
- } catch (ex: SignatureException) {
- throw NotaryException(NotaryError.TransactionInvalid(ex))
- }
+ stx.resolveTransactionWithSignatures(serviceHub).verifySignaturesExcept(notaryParty.owningKey)
return notaryParty
}
/** Notarises the transaction with the [notaryParty], obtains the notary's signature(s). */
@Throws(NotaryException::class)
@Suspendable
- protected fun notarise(notaryParty: Party): UntrustworthyData> {
- return try {
- val session = initiateFlow(notaryParty)
- val requestSignature = NotarisationRequest(stx.inputs, stx.id).generateSignature(serviceHub)
- if (serviceHub.networkMapCache.isValidatingNotary(notaryParty)) {
- sendAndReceiveValidating(session, requestSignature)
- } else {
- sendAndReceiveNonValidating(notaryParty, session, requestSignature)
- }
- } catch (e: NotaryException) {
- if (e.error is NotaryError.Conflict) {
- e.error.conflict.verified()
- }
- throw e
+ protected fun notarise(notaryParty: Party): UntrustworthyData {
+ val session = initiateFlow(notaryParty)
+ val requestSignature = NotarisationRequest(stx.inputs, stx.id).generateSignature(serviceHub)
+ return if (serviceHub.networkMapCache.isValidatingNotary(notaryParty)) {
+ sendAndReceiveValidating(session, requestSignature)
+ } else {
+ sendAndReceiveNonValidating(notaryParty, session, requestSignature)
}
}
@Suspendable
- private fun sendAndReceiveValidating(session: FlowSession, signature: NotarisationRequestSignature): UntrustworthyData> {
+ private fun sendAndReceiveValidating(session: FlowSession, signature: NotarisationRequestSignature): UntrustworthyData {
val payload = NotarisationPayload(stx, signature)
subFlow(NotarySendTransactionFlow(session, payload))
return session.receive()
}
@Suspendable
- private fun sendAndReceiveNonValidating(notaryParty: Party, session: FlowSession, signature: NotarisationRequestSignature): UntrustworthyData> {
+ private fun sendAndReceiveNonValidating(notaryParty: Party, session: FlowSession, signature: NotarisationRequestSignature): UntrustworthyData {
val ctx = stx.coreTransaction
val tx = when (ctx) {
is ContractUpgradeWireTransaction -> ctx.buildFilteredTransaction()
@@ -126,18 +112,13 @@ class NotaryFlow {
}
/** Checks that the notary's signature(s) is/are valid. */
- protected fun validateResponse(response: UntrustworthyData>, notaryParty: Party): List {
- return response.unwrap { signatures ->
- signatures.forEach { validateSignature(it, stx.id, notaryParty) }
- signatures
+ protected fun validateResponse(response: UntrustworthyData, notaryParty: Party): List {
+ return response.unwrap {
+ it.validateSignatures(stx.id, notaryParty)
+ it.signatures
}
}
- private fun validateSignature(sig: TransactionSignature, txId: SecureHash, notaryParty: Party) {
- check(sig.by in notaryParty.owningKey.keys) { "Invalid signer for the notary result" }
- sig.verify(txId)
- }
-
/**
* The [NotarySendTransactionFlow] flow is similar to [SendTransactionFlow], but uses [NotarisationPayload] as the
* initial message, and retries message delivery.
@@ -166,11 +147,17 @@ class NotaryFlow {
check(serviceHub.myInfo.legalIdentities.any { serviceHub.networkMapCache.isNotary(it) }) {
"We are not a notary on the network"
}
- val (id, inputs, timeWindow, notary) = receiveAndVerifyTx()
- checkNotary(notary)
- service.validateTimeWindow(timeWindow)
- service.commitInputStates(inputs, id, otherSideSession.counterparty)
- signAndSendResponse(id)
+ var txId: SecureHash? = null
+ try {
+ val parts = receiveAndVerifyTx()
+ txId = parts.id
+ checkNotary(parts.notary)
+ service.validateTimeWindow(parts.timestamp)
+ service.commitInputStates(parts.inputs, txId, otherSideSession.counterparty)
+ signTransactionAndSendResponse(txId)
+ } catch (e: NotaryInternalException) {
+ throw NotaryException(e.error, txId)
+ }
return null
}
@@ -185,14 +172,14 @@ class NotaryFlow {
@Suspendable
protected fun checkNotary(notary: Party?) {
if (notary?.owningKey != service.notaryIdentityKey) {
- throw NotaryException(NotaryError.WrongNotary)
+ throw NotaryInternalException(NotaryError.WrongNotary)
}
}
@Suspendable
- private fun signAndSendResponse(txId: SecureHash) {
+ private fun signTransactionAndSendResponse(txId: SecureHash) {
val signature = service.sign(txId)
- otherSideSession.send(listOf(signature))
+ otherSideSession.send(NotarisationResponse(listOf(signature)))
}
}
}
@@ -207,14 +194,27 @@ data class TransactionParts(val id: SecureHash, val inputs: List, val
* Exception thrown by the notary service if any issues are encountered while trying to commit a transaction. The
* underlying [error] specifies the cause of failure.
*/
-class NotaryException(val error: NotaryError) : FlowException("Unable to notarise: $error")
+class NotaryException(
+ /** Cause of notarisation failure. */
+ val error: NotaryError,
+ /** Id of the transaction to be notarised. Can be _null_ if an error occurred before the id could be resolved. */
+ val txId: SecureHash? = null
+) : FlowException("Unable to notarise transaction${txId ?: " "}: $error")
+
+/** Exception internal to the notary service. Does not get exposed to CorDapps and flows calling [NotaryFlow.Client]. */
+class NotaryInternalException(val error: NotaryError) : FlowException("Unable to notarise: $error")
/** Specifies the cause for notarisation request failure. */
@CordaSerializable
sealed class NotaryError {
- /** Occurs when one or more input states of transaction with [txId] have already been consumed by another transaction. */
- data class Conflict(val txId: SecureHash, val conflict: SignedData) : NotaryError() {
- override fun toString() = "One or more input states for transaction $txId have been used in another transaction"
+ /** Occurs when one or more input states have already been consumed by another transaction. */
+ data class Conflict(
+ /** Id of the transaction that was attempted to be notarised. */
+ val txId: SecureHash,
+ /** Specifies which states have already been consumed in another transaction. */
+ val consumedStates: Map
+ ) : NotaryError() {
+ override fun toString() = "One or more input states have been used in another transaction"
}
/** Occurs when time specified in the [TimeWindow] command is outside the allowed tolerance. */
@@ -246,3 +246,15 @@ sealed class NotaryError {
override fun toString() = cause.toString()
}
}
+
+/** Contains information about the consuming transaction for a particular state. */
+// TODO: include notary timestamp?
+@CordaSerializable
+data class StateConsumptionDetails(
+ /**
+ * Hash of the consuming transaction id.
+ *
+ * Note that this is NOT the transaction id itself – revealing it could lead to privacy leaks.
+ */
+ val hashOfTransactionId: SecureHash
+)
\ No newline at end of file
diff --git a/core/src/main/kotlin/net/corda/core/internal/CertRole.kt b/core/src/main/kotlin/net/corda/core/internal/CertRole.kt
index b8f84b4c03..e437170592 100644
--- a/core/src/main/kotlin/net/corda/core/internal/CertRole.kt
+++ b/core/src/main/kotlin/net/corda/core/internal/CertRole.kt
@@ -34,9 +34,7 @@ import java.security.cert.X509Certificate
// also note that IDs are numbered from 1 upwards, matching numbering of other enum types in ASN.1 specifications.
// TODO: Link to the specification once it has a permanent URL
enum class CertRole(val validParents: NonEmptySet, val isIdentity: Boolean, val isWellKnown: Boolean) : ASN1Encodable {
- /**
- * Intermediate CA (Doorman service).
- */
+ /** Intermediate CA (Doorman service). */
INTERMEDIATE_CA(NonEmptySet.of(null), false, false),
/** Signing certificate for the network map. */
NETWORK_MAP(NonEmptySet.of(null), false, false),
@@ -47,6 +45,10 @@ enum class CertRole(val validParents: NonEmptySet, val isIdentity: Bo
/** Transport layer security certificate for a node. */
TLS(NonEmptySet.of(NODE_CA), false, false),
/** Well known (publicly visible) identity of a legal entity. */
+ // TODO: at the moment, Legal Identity certs are issued by Node CA only. However, [INTERMEDIATE_CA] is also added
+ // as a valid parent of [LEGAL_IDENTITY] for backwards compatibility purposes (eg. if we decide TLS has its
+ // own Root CA and Intermediate CA directly issues Legal Identities; thus, there won't be a requirement for
+ // Node CA). Consider removing [INTERMEDIATE_CA] from [validParents] when the model is finalised.
LEGAL_IDENTITY(NonEmptySet.of(INTERMEDIATE_CA, NODE_CA), true, true),
/** Confidential (limited visibility) identity of a legal entity. */
CONFIDENTIAL_LEGAL_IDENTITY(NonEmptySet.of(LEGAL_IDENTITY), true, false);
diff --git a/core/src/main/kotlin/net/corda/core/internal/ContractUpgradeUtils.kt b/core/src/main/kotlin/net/corda/core/internal/ContractUpgradeUtils.kt
index 2a048e51c5..f1cf6ba89e 100644
--- a/core/src/main/kotlin/net/corda/core/internal/ContractUpgradeUtils.kt
+++ b/core/src/main/kotlin/net/corda/core/internal/ContractUpgradeUtils.kt
@@ -31,18 +31,18 @@ object ContractUpgradeUtils {
val upgradedContractAttachmentId = getContractAttachmentId(upgradedContractClass.name, services)
val inputs = listOf(stateAndRef.ref)
- return ContractUpgradeWireTransaction(
+ return ContractUpgradeTransactionBuilder(
inputs,
stateAndRef.state.notary,
legacyContractAttachmentId,
upgradedContractClass.name,
upgradedContractAttachmentId,
privacySalt
- )
+ ).build()
}
private fun getContractAttachmentId(name: ContractClassName, services: ServicesForResolution): AttachmentId {
return services.cordappProvider.getContractAttachmentID(name)
?: throw IllegalStateException("Attachment not found for contract: $name")
}
-}
+}
\ No newline at end of file
diff --git a/core/src/main/kotlin/net/corda/core/internal/NotaryUtils.kt b/core/src/main/kotlin/net/corda/core/internal/NotaryUtils.kt
new file mode 100644
index 0000000000..a014e8ba17
--- /dev/null
+++ b/core/src/main/kotlin/net/corda/core/internal/NotaryUtils.kt
@@ -0,0 +1,16 @@
+package net.corda.core.internal
+
+import net.corda.core.crypto.SecureHash
+import net.corda.core.crypto.isFulfilledBy
+import net.corda.core.flows.NotarisationResponse
+import net.corda.core.identity.Party
+
+/**
+ * Checks that there are sufficient signatures to satisfy the notary signing requirement and validates the signatures
+ * against the given transaction id.
+ */
+fun NotarisationResponse.validateSignatures(txId: SecureHash, notary: Party) {
+ val signingKeys = signatures.map { it.by }
+ require(notary.owningKey.isFulfilledBy(signingKeys)) { "Insufficient signatures to fulfill the notary signing requirement for $notary" }
+ signatures.forEach { it.verify(txId) }
+}
\ No newline at end of file
diff --git a/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt b/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt
new file mode 100644
index 0000000000..3e774b4b6e
--- /dev/null
+++ b/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt
@@ -0,0 +1,45 @@
+package net.corda.core.internal
+
+import net.corda.core.contracts.ContractClassName
+import net.corda.core.contracts.PrivacySalt
+import net.corda.core.contracts.StateRef
+import net.corda.core.crypto.SecureHash
+import net.corda.core.crypto.sha256
+import net.corda.core.identity.Party
+import net.corda.core.serialization.serialize
+import net.corda.core.transactions.ContractUpgradeWireTransaction
+import net.corda.core.transactions.NotaryChangeWireTransaction
+import java.io.ByteArrayOutputStream
+
+/** Constructs a [NotaryChangeWireTransaction]. */
+class NotaryChangeTransactionBuilder(val inputs: List,
+ val notary: Party,
+ val newNotary: Party) {
+ fun build(): NotaryChangeWireTransaction {
+ val components = listOf(inputs, notary, newNotary).map { it.serialize() }
+ return NotaryChangeWireTransaction(components)
+ }
+}
+
+/** Constructs a [ContractUpgradeWireTransaction]. */
+class ContractUpgradeTransactionBuilder(
+ val inputs: List,
+ val notary: Party,
+ val legacyContractAttachmentId: SecureHash,
+ val upgradedContractClassName: ContractClassName,
+ val upgradedContractAttachmentId: SecureHash,
+ val privacySalt: PrivacySalt = PrivacySalt()) {
+ fun build(): ContractUpgradeWireTransaction {
+ val components = listOf(inputs, notary, legacyContractAttachmentId, upgradedContractClassName, upgradedContractAttachmentId).map { it.serialize() }
+ return ContractUpgradeWireTransaction(components, privacySalt)
+ }
+}
+
+/** Concatenates the hash components into a single [ByteArray] and returns its hash. */
+fun combinedHash(components: Iterable): SecureHash {
+ val stream = ByteArrayOutputStream()
+ components.forEach {
+ stream.write(it.bytes)
+ }
+ return stream.toByteArray().sha256()
+}
\ No newline at end of file
diff --git a/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt b/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt
index 23c2abdd6e..019dc31028 100644
--- a/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt
+++ b/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt
@@ -18,7 +18,6 @@ import net.corda.core.flows.*
import net.corda.core.identity.Party
import net.corda.core.node.ServiceHub
import net.corda.core.serialization.SingletonSerializeAsToken
-import net.corda.core.serialization.serialize
import net.corda.core.utilities.contextLogger
import org.slf4j.Logger
import java.security.PublicKey
@@ -40,18 +39,19 @@ abstract class NotaryService : SingletonSerializeAsToken() {
}
/**
- * Checks if the current instant provided by the clock falls within the specified time window.
+ * Checks if the current instant provided by the clock falls within the specified time window. Should only be
+ * used by a notary service flow.
*
- * @throws NotaryException if current time is outside the specified time window. The exception contains
+ * @throws NotaryInternalException if current time is outside the specified time window. The exception contains
* the [NotaryError.TimeWindowInvalid] error.
*/
@JvmStatic
- @Throws(NotaryException::class)
+ @Throws(NotaryInternalException::class)
fun validateTimeWindow(clock: Clock, timeWindow: TimeWindow?) {
if (timeWindow == null) return
val currentTime = clock.instant()
if (currentTime !in timeWindow) {
- throw NotaryException(
+ throw NotaryInternalException(
NotaryError.TimeWindowInvalid(currentTime, timeWindow)
)
}
@@ -92,28 +92,24 @@ abstract class TrustedAuthorityNotaryService : NotaryService() {
fun commitInputStates(inputs: List, txId: SecureHash, caller: Party) {
try {
uniquenessProvider.commit(inputs, txId, caller)
- } catch (e: UniquenessException) {
- val conflicts = inputs.filterIndexed { i, stateRef ->
- val consumingTx = e.error.stateHistory[stateRef]
- consumingTx != null && consumingTx != UniquenessProvider.ConsumingTx(txId, i, caller)
- }
- if (conflicts.isNotEmpty()) {
- // TODO: Create a new UniquenessException that only contains the conflicts filtered above.
- log.warn("Notary conflicts for $txId: $conflicts")
- throw notaryException(txId, e)
- }
+ } catch (e: NotaryInternalException) {
+ if (e.error is NotaryError.Conflict) {
+ val conflicts = inputs.filterIndexed { _, stateRef ->
+ val cause = e.error.consumedStates[stateRef]
+ cause != null && cause.hashOfTransactionId != txId.sha256()
+ }
+ if (conflicts.isNotEmpty()) {
+ // TODO: Create a new UniquenessException that only contains the conflicts filtered above.
+ log.info("Notary conflicts for $txId: $conflicts")
+ throw e
+ }
+ } else throw e
} catch (e: Exception) {
log.error("Internal error", e)
- throw NotaryException(NotaryError.General(Exception("Service unavailable, please try again later")))
+ throw NotaryInternalException(NotaryError.General(Exception("Service unavailable, please try again later")))
}
}
- private fun notaryException(txId: SecureHash, e: UniquenessException): NotaryException {
- val conflictData = e.error.serialize()
- val signedConflict = SignedData(conflictData, sign(conflictData.bytes))
- return NotaryException(NotaryError.Conflict(txId, signedConflict))
- }
-
/** Sign a [ByteArray] input. */
fun sign(bits: ByteArray): DigitalSignature.WithKey {
return services.keyManagementService.sign(bits, notaryIdentityKey)
@@ -127,6 +123,8 @@ abstract class TrustedAuthorityNotaryService : NotaryService() {
// TODO: Sign multiple transactions at once by building their Merkle tree and then signing over its root.
- @Deprecated("This property is no longer used") @Suppress("DEPRECATION")
- protected open val timeWindowChecker: TimeWindowChecker get() = throw UnsupportedOperationException("No default implementation, need to override")
+ @Deprecated("This property is no longer used")
+ @Suppress("DEPRECATION")
+ protected open val timeWindowChecker: TimeWindowChecker
+ get() = throw UnsupportedOperationException("No default implementation, need to override")
}
\ No newline at end of file
diff --git a/core/src/main/kotlin/net/corda/core/node/services/UniquenessProvider.kt b/core/src/main/kotlin/net/corda/core/node/services/UniquenessProvider.kt
index 5a3abe4a2c..1df509f61a 100644
--- a/core/src/main/kotlin/net/corda/core/node/services/UniquenessProvider.kt
+++ b/core/src/main/kotlin/net/corda/core/node/services/UniquenessProvider.kt
@@ -23,24 +23,22 @@ import net.corda.core.serialization.CordaSerializable
* A uniqueness provider is expected to be used from within the context of a flow.
*/
interface UniquenessProvider {
- /** Commits all input states of the given transaction */
+ /** Commits all input states of the given transaction. */
fun commit(states: List, txId: SecureHash, callerIdentity: Party)
- /** Specifies the consuming transaction for every conflicting state */
+ /** Specifies the consuming transaction for every conflicting state. */
@CordaSerializable
+ @Deprecated("No longer used due to potential privacy leak")
data class Conflict(val stateHistory: Map)
/**
* Specifies the transaction id, the position of the consumed state in the inputs, and
* the caller identity requesting the commit.
- *
- * TODO: need to do more design work to prevent privacy problems: knowing the id of a
- * transaction, by the rules of our system the party can obtain it and see its contents.
- * This allows a party to just submit invalid transactions with outputs it was aware of and
- * find out where exactly they were spent.
*/
@CordaSerializable
data class ConsumingTx(val id: SecureHash, val inputIndex: Int, val requestingParty: Party)
}
+@Deprecated("No longer used due to potential privacy leak")
+@Suppress("DEPRECATION")
class UniquenessException(val error: UniquenessProvider.Conflict) : CordaException(UniquenessException::class.java.name)
\ No newline at end of file
diff --git a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt
index 6665536df1..abb5ddfe1f 100644
--- a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt
+++ b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt
@@ -16,13 +16,14 @@ import net.corda.core.concurrent.CordaFuture
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowException
+import net.corda.core.flows.FlowLogic
import net.corda.core.identity.AbstractParty
import net.corda.core.messaging.DataFeed
-import net.corda.core.node.services.vault.PageSpecification
-import net.corda.core.node.services.vault.QueryCriteria
-import net.corda.core.node.services.vault.Sort
+import net.corda.core.node.services.Vault.StateStatus
+import net.corda.core.node.services.vault.*
import net.corda.core.serialization.CordaSerializable
import net.corda.core.toFuture
+import net.corda.core.transactions.LedgerTransaction
import net.corda.core.utilities.NonEmptySet
import rx.Observable
import java.time.Instant
@@ -120,15 +121,15 @@ class Vault(val states: Iterable>) {
/**
* Returned in queries [VaultService.queryBy] and [VaultService.trackBy].
* A Page contains:
- * 1) a [List] of actual [StateAndRef] requested by the specified [QueryCriteria] to a maximum of [MAX_PAGE_SIZE]
- * 2) a [List] of associated [Vault.StateMetadata], one per [StateAndRef] result
- * 3) a total number of states that met the given [QueryCriteria] if a [PageSpecification] was provided
- * (otherwise defaults to -1)
- * 4) Status types used in this query: UNCONSUMED, CONSUMED, ALL
- * 5) Other results as a [List] of any type (eg. aggregate function results with/without group by)
+ * 1) a [List] of actual [StateAndRef] requested by the specified [QueryCriteria] to a maximum of [MAX_PAGE_SIZE].
+ * 2) a [List] of associated [Vault.StateMetadata], one per [StateAndRef] result.
+ * 3) a total number of states that met the given [QueryCriteria] if a [PageSpecification] was provided,
+ * otherwise it defaults to -1.
+ * 4) Status types used in this query: [StateStatus.UNCONSUMED], [StateStatus.CONSUMED], [StateStatus.ALL].
+ * 5) Other results as a [List] of any type (eg. aggregate function results with/without group by).
*
* Note: currently otherResults are used only for Aggregate Functions (in which case, the states and statesMetadata
- * results will be empty)
+ * results will be empty).
*/
@CordaSerializable
data class Page(val states: List>,
@@ -168,17 +169,18 @@ interface VaultService {
/**
* Prefer the use of [updates] unless you know why you want to use this instead.
*
- * Get a synchronous Observable of updates. When observations are pushed to the Observer, the Vault will already incorporate
- * the update, and the database transaction associated with the update will still be open and current. If for some
- * reason the processing crosses outside of the database transaction (for example, the update is pushed outside the current
- * JVM or across to another [Thread] which is executing in a different database transaction) then the Vault may
- * not incorporate the update due to racing with committing the current database transaction.
+ * Get a synchronous [Observable] of updates. When observations are pushed to the Observer, the [Vault] will already
+ * incorporate the update, and the database transaction associated with the update will still be open and current.
+ * If for some reason the processing crosses outside of the database transaction (for example, the update is pushed
+ * outside the current JVM or across to another [Thread], which is executing in a different database transaction),
+ * then the [Vault] may not incorporate the update due to racing with committing the current database transaction.
*/
val rawUpdates: Observable>
/**
- * Get a synchronous Observable of updates. When observations are pushed to the Observer, the Vault will already incorporate
- * the update, and the database transaction associated with the update will have been committed and closed.
+ * Get a synchronous [Observable] of updates. When observations are pushed to the Observer, the [Vault] will
+ * already incorporate the update and the database transaction associated with the update will have been committed
+ * and closed.
*/
val updates: Observable>
@@ -190,10 +192,10 @@ interface VaultService {
}
/**
- * Add a note to an existing [LedgerTransaction] given by its unique [SecureHash] id
+ * Add a note to an existing [LedgerTransaction] given by its unique [SecureHash] id.
* Multiple notes may be attached to the same [LedgerTransaction].
- * These are additively and immutably persisted within the node local vault database in a single textual field
- * using a semi-colon separator
+ * These are additively and immutably persisted within the node local vault database in a single textual field.
+ * using a semi-colon separator.
*/
fun addNoteToTransaction(txnId: SecureHash, noteText: String)
@@ -202,7 +204,7 @@ interface VaultService {
// DOCEND VaultStatesQuery
/**
- * Soft locking is used to prevent multiple transactions trying to use the same output simultaneously.
+ * Soft locking is used to prevent multiple transactions trying to use the same states simultaneously.
* Violation of a soft lock would result in a double spend being created and rejected by the notary.
*/
@@ -210,35 +212,35 @@ interface VaultService {
/**
* Reserve a set of [StateRef] for a given [UUID] unique identifier.
- * Typically, the unique identifier will refer to a [FlowLogic.runId.uuid] associated with an in-flight flow.
+ * Typically, the unique identifier will refer to a [FlowLogic.runId]'s [UUID] associated with an in-flight flow.
* In this case if the flow terminates the locks will automatically be freed, even if there is an error.
- * However, the user can specify their own [UUID] and manage this manually, possibly across the lifetime of multiple flows,
- * or from other thread contexts e.g. [CordaService] instances.
+ * However, the user can specify their own [UUID] and manage this manually, possibly across the lifetime of multiple
+ * flows, or from other thread contexts e.g. [CordaService] instances.
* In the case of coin selection, soft locks are automatically taken upon gathering relevant unconsumed input refs.
*
- * @throws [StatesNotAvailableException] when not possible to softLock all of requested [StateRef]
+ * @throws [StatesNotAvailableException] when not possible to soft-lock all of requested [StateRef].
*/
@Throws(StatesNotAvailableException::class)
fun softLockReserve(lockId: UUID, stateRefs: NonEmptySet)
/**
* Release all or an explicitly specified set of [StateRef] for a given [UUID] unique identifier.
- * A vault soft lock manager is automatically notified of a Flows that are terminated, such that any soft locked states
- * may be released.
- * In the case of coin selection, softLock are automatically released once previously gathered unconsumed input refs
- * are consumed as part of cash spending.
+ * A [Vault] soft-lock manager is automatically notified from flows that are terminated, such that any soft locked
+ * states may be released.
+ * In the case of coin selection, soft-locks are automatically released once previously gathered unconsumed
+ * input refs are consumed as part of cash spending.
*/
fun softLockRelease(lockId: UUID, stateRefs: NonEmptySet? = null)
// DOCEND SoftLockAPI
/**
* Helper function to determine spendable states and soft locking them.
- * Currently performance will be worse than for the hand optimised version in `Cash.unconsumedCashStatesForSpending`
+ * Currently performance will be worse than for the hand optimised version in `Cash.unconsumedCashStatesForSpending`.
* However, this is fully generic and can operate with custom [FungibleAsset] states.
- * @param lockId The [FlowLogic.runId.uuid] of the current flow used to soft lock the states.
+ * @param lockId The [FlowLogic.runId]'s [UUID] of the current flow used to soft lock the states.
* @param eligibleStatesQuery A custom query object that selects down to the appropriate subset of all states of the
- * [contractStateType]. e.g. by selecting on account, issuer, etc. The query is internally augmented with the UNCONSUMED,
- * soft lock and contract type requirements.
+ * [contractStateType]. e.g. by selecting on account, issuer, etc. The query is internally augmented with the
+ * [StateStatus.UNCONSUMED], soft lock and contract type requirements.
* @param amount The required amount of the asset, but with the issuer stripped off.
* It is assumed that compatible issuer states will be filtered out by the [eligibleStatesQuery].
* @param contractStateType class type of the result set.
@@ -259,12 +261,12 @@ interface VaultService {
* and returns a [Vault.Page] object containing the following:
* 1. states as a List of (page number and size defined by [PageSpecification])
* 2. states metadata as a List of [Vault.StateMetadata] held in the Vault States table.
- * 3. total number of results available if [PageSpecification] supplied (otherwise returns -1)
- * 4. status types used in this query: UNCONSUMED, CONSUMED, ALL
- * 5. other results (aggregate functions with/without using value groups)
+ * 3. total number of results available if [PageSpecification] supplied (otherwise returns -1).
+ * 4. status types used in this query: [StateStatus.UNCONSUMED], [StateStatus.CONSUMED], [StateStatus.ALL].
+ * 5. other results (aggregate functions with/without using value groups).
*
* @throws VaultQueryException if the query cannot be executed for any reason
- * (missing criteria or parsing error, paging errors, unsupported query, underlying database error)
+ * (missing criteria or parsing error, paging errors, unsupported query, underlying database error).
*
* Notes
* If no [PageSpecification] is provided, a maximum of [DEFAULT_PAGE_SIZE] results will be returned.
@@ -281,11 +283,11 @@ interface VaultService {
/**
* Generic vault query function which takes a [QueryCriteria] object to define filters,
* optional [PageSpecification] and optional [Sort] modification criteria (default unsorted),
- * and returns a [Vault.PageAndUpdates] object containing
- * 1) a snapshot as a [Vault.Page] (described previously in [queryBy])
- * 2) an [Observable] of [Vault.Update]
+ * and returns a [DataFeed] object containing:
+ * 1) a snapshot as a [Vault.Page] (described previously in [queryBy]).
+ * 2) an [Observable] of [Vault.Update].
*
- * @throws VaultQueryException if the query cannot be executed for any reason
+ * @throws VaultQueryException if the query cannot be executed for any reason.
*
* Notes: the snapshot part of the query adheres to the same behaviour as the [queryBy] function.
* the [QueryCriteria] applies to both snapshot and deltas (streaming updates).
@@ -297,8 +299,8 @@ interface VaultService {
contractStateType: Class): DataFeed, Vault.Update>
// DOCEND VaultQueryAPI
- // Note: cannot apply @JvmOverloads to interfaces nor interface implementations
- // Java Helpers
+ // Note: cannot apply @JvmOverloads to interfaces nor interface implementations.
+ // Java Helpers.
fun queryBy(contractStateType: Class): Vault.Page {
return _queryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractStateType)
}
diff --git a/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt
index cd575391e4..5312ece067 100644
--- a/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt
+++ b/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt
@@ -13,12 +13,18 @@ package net.corda.core.transactions
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.TransactionSignature
-import net.corda.core.crypto.serializedHash
+import net.corda.core.crypto.componentHash
+import net.corda.core.crypto.computeNonce
import net.corda.core.identity.Party
import net.corda.core.internal.AttachmentWithContext
+import net.corda.core.internal.combinedHash
import net.corda.core.node.NetworkParameters
import net.corda.core.node.ServicesForResolution
import net.corda.core.serialization.CordaSerializable
+import net.corda.core.serialization.deserialize
+import net.corda.core.transactions.ContractUpgradeFilteredTransaction.FilteredComponent
+import net.corda.core.transactions.ContractUpgradeWireTransaction.Component.*
+import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.toBase58String
import java.security.PublicKey
@@ -28,13 +34,20 @@ import java.security.PublicKey
/** A special transaction for upgrading the contract of a state. */
@CordaSerializable
data class ContractUpgradeWireTransaction(
- override val inputs: List,
- override val notary: Party,
- val legacyContractAttachmentId: SecureHash,
- val upgradeContractClassName: ContractClassName,
- val upgradedContractAttachmentId: SecureHash,
+ /**
+ * Contains all of the transaction components in serialized form.
+ * This is used for calculating the transaction id in a deterministic fashion, since re-serializing properties
+ * may result in a different byte sequence depending on the serialization context.
+ */
+ val serializedComponents: List,
+ /** Required for hiding components in [ContractUpgradeFilteredTransaction]. */
val privacySalt: PrivacySalt = PrivacySalt()
) : CoreTransaction() {
+ override val inputs: List = serializedComponents[INPUTS.ordinal].deserialize()
+ override val notary: Party by lazy { serializedComponents[NOTARY.ordinal].deserialize() }
+ val legacyContractAttachmentId: SecureHash by lazy { serializedComponents[LEGACY_ATTACHMENT.ordinal].deserialize() }
+ val upgradedContractClassName: ContractClassName by lazy { serializedComponents[UPGRADED_CONTRACT.ordinal].deserialize() }
+ val upgradedContractAttachmentId: SecureHash by lazy { serializedComponents[UPGRADED_ATTACHMENT.ordinal].deserialize() }
init {
check(inputs.isNotEmpty()) { "A contract upgrade transaction must have inputs" }
@@ -49,11 +62,17 @@ data class ContractUpgradeWireTransaction(
get() = throw UnsupportedOperationException("ContractUpgradeWireTransaction does not contain output states, " +
"outputs can only be obtained from a resolved ContractUpgradeLedgerTransaction")
- /** Hash of the list of components that are hidden in the [ContractUpgradeFilteredTransaction]. */
- private val hiddenComponentHash: SecureHash
- get() = serializedHash(listOf(legacyContractAttachmentId, upgradeContractClassName, privacySalt))
+ override val id: SecureHash by lazy {
+ val componentHashes =serializedComponents.mapIndexed { index, component ->
+ componentHash(nonces[index], component)
+ }
+ combinedHash(componentHashes)
+ }
- override val id: SecureHash by lazy { serializedHash(inputs + notary).hashConcat(hiddenComponentHash) }
+ /** Required for filtering transaction components. */
+ private val nonces = (0 until serializedComponents.size).map {
+ computeNonce(privacySalt, it, 0)
+ }
/** Resolves input states and contract attachments, and builds a ContractUpgradeLedgerTransaction. */
fun resolve(services: ServicesForResolution, sigs: List): ContractUpgradeLedgerTransaction {
@@ -66,7 +85,7 @@ data class ContractUpgradeWireTransaction(
resolvedInputs,
notary,
legacyContractAttachment,
- upgradeContractClassName,
+ upgradedContractClassName,
upgradedContractAttachment,
id,
privacySalt,
@@ -75,8 +94,23 @@ data class ContractUpgradeWireTransaction(
)
}
+ /** Constructs a filtered transaction: the inputs and the notary party are always visible, while the rest are hidden. */
fun buildFilteredTransaction(): ContractUpgradeFilteredTransaction {
- return ContractUpgradeFilteredTransaction(inputs, notary, hiddenComponentHash)
+ val totalComponents = (0 until serializedComponents.size).toSet()
+ val visibleComponents = mapOf(
+ INPUTS.ordinal to FilteredComponent(serializedComponents[INPUTS.ordinal], nonces[INPUTS.ordinal]),
+ NOTARY.ordinal to FilteredComponent(serializedComponents[NOTARY.ordinal], nonces[NOTARY.ordinal])
+ )
+ val hiddenComponents = (totalComponents - visibleComponents.keys).map { index ->
+ val hash = componentHash(nonces[index], serializedComponents[index])
+ index to hash
+ }.toMap()
+
+ return ContractUpgradeFilteredTransaction(visibleComponents, hiddenComponents)
+ }
+
+ enum class Component {
+ INPUTS, NOTARY, LEGACY_ATTACHMENT, UPGRADED_CONTRACT, UPGRADED_ATTACHMENT
}
}
@@ -84,19 +118,43 @@ data class ContractUpgradeWireTransaction(
* A filtered version of the [ContractUpgradeWireTransaction]. In comparison with a regular [FilteredTransaction], there
* is no flexibility on what parts of the transaction to reveal – the inputs and notary field are always visible and the
* rest of the transaction is always hidden. Its only purpose is to hide transaction data when using a non-validating notary.
- *
- * @property inputs The inputs of this transaction.
- * @property notary The notary for this transaction.
- * @property rest Hash of the hidden components of the [ContractUpgradeWireTransaction].
*/
@CordaSerializable
data class ContractUpgradeFilteredTransaction(
- override val inputs: List,
- override val notary: Party,
- val rest: SecureHash
+ /** Transaction components that are exposed. */
+ val visibleComponents: Map,
+ /**
+ * Hashes of the transaction components that are not revealed in this transaction.
+ * Required for computing the transaction id.
+ */
+ val hiddenComponents: Map
) : CoreTransaction() {
- override val id: SecureHash get() = serializedHash(inputs + notary).hashConcat(rest)
+ override val inputs: List by lazy {
+ visibleComponents[INPUTS.ordinal]?.component?.deserialize>()
+ ?: throw IllegalArgumentException("Inputs not specified")
+ }
+ override val notary: Party by lazy {
+ visibleComponents[NOTARY.ordinal]?.component?.deserialize()
+ ?: throw IllegalArgumentException("Notary not specified")
+ }
+ override val id: SecureHash by lazy {
+ val totalComponents = visibleComponents.size + hiddenComponents.size
+ val hashList = (0 until totalComponents).map { i ->
+ when {
+ visibleComponents.containsKey(i) -> {
+ componentHash(visibleComponents[i]!!.nonce, visibleComponents[i]!!.component)
+ }
+ hiddenComponents.containsKey(i) -> hiddenComponents[i]!!
+ else -> throw IllegalStateException("Missing component hashes")
+ }
+ }
+ combinedHash(hashList)
+ }
override val outputs: List> get() = emptyList()
+
+ /** Contains the serialized component and the associated nonce for computing the transaction id. */
+ @CordaSerializable
+ class FilteredComponent(val component: OpaqueBytes, val nonce: SecureHash)
}
/**
@@ -113,7 +171,7 @@ data class ContractUpgradeLedgerTransaction(
override val inputs: List>,
override val notary: Party,
val legacyContractAttachment: Attachment,
- val upgradeContractClassName: ContractClassName,
+ val upgradedContractClassName: ContractClassName,
val upgradedContractAttachment: Attachment,
override val id: SecureHash,
val privacySalt: PrivacySalt,
@@ -175,7 +233,7 @@ data class ContractUpgradeLedgerTransaction(
// TODO: re-map encumbrance pointers
input.state.copy(
data = upgradedState,
- contract = upgradeContractClassName,
+ contract = upgradedContractClassName,
constraint = outputConstraint
)
}
@@ -192,7 +250,7 @@ data class ContractUpgradeLedgerTransaction(
private fun loadUpgradedContract(): UpgradedContract {
@Suppress("UNCHECKED_CAST")
return this::class.java.classLoader
- .loadClass(upgradeContractClassName)
+ .loadClass(upgradedContractClassName)
.asSubclass(Contract::class.java)
.getConstructor()
.newInstance() as UpgradedContract
diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt
index 4bad1c0c70..00a844f0b8 100644
--- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt
+++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt
@@ -410,6 +410,6 @@ data class LedgerTransaction @JvmOverloads constructor(
notary: Party?,
timeWindow: TimeWindow?,
privacySalt: PrivacySalt
- ) = copy(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, null)
+ ) = copy(inputs = inputs, outputs = outputs, commands = commands, attachments = attachments, id = id, notary = notary, timeWindow = timeWindow, privacySalt = privacySalt, networkParameters = null)
}
diff --git a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt
index 5e3f3e5c6f..581601b901 100644
--- a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt
+++ b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt
@@ -95,18 +95,18 @@ abstract class TraversableTransaction(open val componentGroups: List>(it).deserialize() })
val commandDataList = deserialiseComponentGroup(ComponentGroupEnum.COMMANDS_GROUP, { SerializedBytes(it).deserialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentsClassLoader(attachments)) })
val group = componentGroups.firstOrNull { it.groupIndex == ComponentGroupEnum.COMMANDS_GROUP.ordinal }
- if (group is FilteredComponentGroup) {
+ return if (group is FilteredComponentGroup) {
check(commandDataList.size <= signersList.size) { "Invalid Transaction. Less Signers (${signersList.size}) than CommandData (${commandDataList.size}) objects" }
val componentHashes = group.components.mapIndexed { index, component -> componentHash(group.nonces[index], component) }
val leafIndices = componentHashes.map { group.partialMerkleTree.leafIndex(it) }
if (leafIndices.isNotEmpty())
check(leafIndices.max()!! < signersList.size) { "Invalid Transaction. A command with no corresponding signer detected" }
- return commandDataList.mapIndexed { index, commandData -> Command(commandData, signersList[leafIndices[index]]) }
+ commandDataList.mapIndexed { index, commandData -> Command(commandData, signersList[leafIndices[index]]) }
} else {
// It is a WireTransaction
// or a FilteredTransaction with no Commands (in which case group is null).
check(commandDataList.size == signersList.size) { "Invalid Transaction. Sizes of CommandData (${commandDataList.size}) and Signers (${signersList.size}) do not match" }
- return commandDataList.mapIndexed { index, commandData -> Command(commandData, signersList[index]) }
+ commandDataList.mapIndexed { index, commandData -> Command(commandData, signersList[index]) }
}
}
}
@@ -158,9 +158,9 @@ class FilteredTransaction internal constructor(
// As all of the helper Map structures, like availableComponentNonces, availableComponentHashes
// and groupsMerkleRoots, are computed lazily via componentGroups.forEach, there should always be
// a match on Map.get ensuring it will never return null.
- filteredSerialisedComponents.put(componentGroupIndex, mutableListOf(serialisedComponent))
- filteredComponentNonces.put(componentGroupIndex, mutableListOf(wtx.availableComponentNonces[componentGroupIndex]!![internalIndex]))
- filteredComponentHashes.put(componentGroupIndex, mutableListOf(wtx.availableComponentHashes[componentGroupIndex]!![internalIndex]))
+ filteredSerialisedComponents[componentGroupIndex] = mutableListOf(serialisedComponent)
+ filteredComponentNonces[componentGroupIndex] = mutableListOf(wtx.availableComponentNonces[componentGroupIndex]!![internalIndex])
+ filteredComponentHashes[componentGroupIndex] = mutableListOf(wtx.availableComponentHashes[componentGroupIndex]!![internalIndex])
} else {
group.add(serialisedComponent)
// If the group[componentGroupIndex] existed, then we guarantee that
@@ -175,9 +175,9 @@ class FilteredTransaction internal constructor(
val signersGroupIndex = ComponentGroupEnum.SIGNERS_GROUP.ordinal
// There exist commands, thus the signers group is not empty.
val signersGroupComponents = wtx.componentGroups.first { it.groupIndex == signersGroupIndex }
- filteredSerialisedComponents.put(signersGroupIndex, signersGroupComponents.components.toMutableList())
- filteredComponentNonces.put(signersGroupIndex, wtx.availableComponentNonces[signersGroupIndex]!!.toMutableList())
- filteredComponentHashes.put(signersGroupIndex, wtx.availableComponentHashes[signersGroupIndex]!!.toMutableList())
+ filteredSerialisedComponents[signersGroupIndex] = signersGroupComponents.components.toMutableList()
+ filteredComponentNonces[signersGroupIndex] = wtx.availableComponentNonces[signersGroupIndex]!!.toMutableList()
+ filteredComponentHashes[signersGroupIndex] = wtx.availableComponentHashes[signersGroupIndex]!!.toMutableList()
}
}
}
@@ -322,14 +322,14 @@ class FilteredTransaction internal constructor(
.filter { signers -> publicKey in signers }.size
}
- inline private fun verificationCheck(value: Boolean, lazyMessage: () -> Any) {
+ private inline fun verificationCheck(value: Boolean, lazyMessage: () -> Any) {
if (!value) {
val message = lazyMessage()
throw FilteredTransactionVerificationException(id, message.toString())
}
}
- inline private fun visibilityCheck(value: Boolean, lazyMessage: () -> Any) {
+ private inline fun visibilityCheck(value: Boolean, lazyMessage: () -> Any) {
if (!value) {
val message = lazyMessage()
throw ComponentVisibilityException(id, message.toString())
diff --git a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt
index 8964127f1d..685c1385c8 100644
--- a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt
+++ b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt
@@ -13,11 +13,15 @@ package net.corda.core.transactions
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.TransactionSignature
-import net.corda.core.crypto.serializedHash
+import net.corda.core.crypto.sha256
import net.corda.core.identity.Party
import net.corda.core.node.ServiceHub
import net.corda.core.node.ServicesForResolution
import net.corda.core.serialization.CordaSerializable
+import net.corda.core.serialization.deserialize
+import net.corda.core.serialization.serialize
+import net.corda.core.transactions.NotaryChangeWireTransaction.Component.*
+import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.toBase58String
import java.security.PublicKey
@@ -28,10 +32,18 @@ import java.security.PublicKey
*/
@CordaSerializable
data class NotaryChangeWireTransaction(
- override val inputs: List,
- override val notary: Party,
- val newNotary: Party
+ /**
+ * Contains all of the transaction components in serialized form.
+ * This is used for calculating the transaction id in a deterministic fashion, since re-serializing properties
+ * may result in a different byte sequence depending on the serialization context.
+ */
+ val serializedComponents: List
) : CoreTransaction() {
+ override val inputs: List = serializedComponents[INPUTS.ordinal].deserialize()
+ override val notary: Party = serializedComponents[NOTARY.ordinal].deserialize()
+ /** Identity of the notary service to reassign the states to.*/
+ val newNotary: Party = serializedComponents[NEW_NOTARY.ordinal].deserialize()
+
/**
* This transaction does not contain any output states, outputs can be obtained by resolving a
* [NotaryChangeLedgerTransaction] and applying the notary modification to inputs.
@@ -49,16 +61,29 @@ data class NotaryChangeWireTransaction(
* A privacy salt is not really required in this case, because we already used nonces in normal transactions and
* thus input state refs will always be unique. Also, filtering doesn't apply on this type of transactions.
*/
- override val id: SecureHash by lazy { serializedHash(inputs + notary + newNotary) }
+ override val id: SecureHash by lazy {
+ serializedComponents.map { component ->
+ component.bytes.sha256()
+ }.reduce { combinedHash, componentHash ->
+ combinedHash.hashConcat(componentHash)
+ }
+ }
/** Resolves input states and builds a [NotaryChangeLedgerTransaction]. */
- fun resolve(services: ServicesForResolution, sigs: List) : NotaryChangeLedgerTransaction {
+ fun resolve(services: ServicesForResolution, sigs: List): NotaryChangeLedgerTransaction {
val resolvedInputs = services.loadStates(inputs.toSet()).toList()
return NotaryChangeLedgerTransaction(resolvedInputs, notary, newNotary, id, sigs)
}
/** Resolves input states and builds a [NotaryChangeLedgerTransaction]. */
fun resolve(services: ServiceHub, sigs: List) = resolve(services as ServicesForResolution, sigs)
+
+ enum class Component {
+ INPUTS, NOTARY, NEW_NOTARY
+ }
+
+ @Deprecated("Required only for backwards compatibility purposes. This type of transaction should not be constructed outside Corda code.", ReplaceWith("NotaryChangeTransactionBuilder"), DeprecationLevel.WARNING)
+ constructor(inputs: List, notary: Party, newNotary: Party) : this(listOf(inputs, notary, newNotary).map { it.serialize() })
}
/**
diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt
index 4bccc65b9b..8000a3aaa6 100644
--- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt
+++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt
@@ -112,7 +112,7 @@ open class TransactionBuilder(
// with an explicit [AttachmentConstraint]
val resolvedOutputs = outputs.map { state ->
when {
- state.constraint !is AutomaticHashConstraint -> state
+ state.constraint !== AutomaticHashConstraint -> state
useWhitelistedByZoneAttachmentConstraint(state.contract, services.networkParameters) -> state.copy(constraint = WhitelistedByZoneAttachmentConstraint)
else -> services.cordappProvider.getContractAttachmentID(state.contract)?.let {
state.copy(constraint = HashAttachmentConstraint(it))
diff --git a/docs/source/node-explorer.rst b/docs/source/node-explorer.rst
index c8e15d367b..186a1f6b86 100644
--- a/docs/source/node-explorer.rst
+++ b/docs/source/node-explorer.rst
@@ -13,7 +13,7 @@ Running the UI
**Other**::
./gradlew tools:explorer:run
-
+
Running demo nodes
------------------
@@ -79,11 +79,11 @@ The Demo Nodes can be started in one of three modes:
.. note:: 5 Corda nodes will be created on the following port on localhost by default.
- * Notary -> 20001 (Does not accept logins)
- * Alice -> 20004
- * Bob -> 20007
- * UK Bank Plc -> 20010 (*Issuer node*)
- * USA Bank Corp -> 20013 (*Issuer node*)
+ * Notary -> 20005 (Does not accept logins)
+ * UK Bank Plc -> 20011 (*Issuer node*)
+ * USA Bank Corp -> 20008 (*Issuer node*)
+ * Alice -> 20017
+ * Bob -> 20014
Explorer login credentials to the Issuer nodes are defaulted to ``manager`` and ``test``.
Explorer login credentials to the Participants nodes are defaulted to ``user1`` and ``test``.
@@ -102,21 +102,21 @@ Please note you are not allowed to login to the notary.
Interface
---------
Login
- User can login to any Corda node using the explorer. Alternatively, ``gradlew explorer:runDemoNodes`` can be used to start up demo nodes for testing.
+ User can login to any Corda node using the explorer. Alternatively, ``gradlew explorer:runDemoNodes`` can be used to start up demo nodes for testing.
Corda node address, username and password are required for login, the address is defaulted to localhost:0 if leave blank.
Username and password can be configured via the ``rpcUsers`` field in node's configuration file.
-
+
.. image:: resources/explorer/login.png
:scale: 50 %
:align: center
-
+
Dashboard
The dashboard shows the top level state of node and vault.
Currently, it shows your cash balance and the numbers of transaction executed.
- The dashboard is intended to house widgets from different CordApps and provide useful information to system admin at a glance.
+ The dashboard is intended to house widgets from different CordApps and provide useful information to system admin at a glance.
.. image:: resources/explorer/dashboard.png
-
+
Cash
The cash view shows all currencies you currently own in a tree table format, it is grouped by issuer -> currency.
Individual cash transactions can be viewed by clicking on the table row. The user can also use the search field to narrow down the scope.
@@ -138,16 +138,16 @@ Issuer Nodes
.. image:: resources/explorer/newTransactionIssuer.png
Transactions
- The transaction view contains all transactions handled by the node in a table view. It shows basic information on the table e.g. Transaction ID,
- command type, USD equivalence value etc. User can expand the row by double clicking to view the inputs,
- outputs and the signatures details for that transaction.
-
+ The transaction view contains all transactions handled by the node in a table view. It shows basic information on the table e.g. Transaction ID,
+ command type, USD equivalence value etc. User can expand the row by double clicking to view the inputs,
+ outputs and the signatures details for that transaction.
+
.. image:: resources/explorer/transactionView.png
Network
- The network view shows the network information on the world map. Currently only the user's node is rendered on the map.
+ The network view shows the network information on the world map. Currently only the user's node is rendered on the map.
This will be extended to other peers in a future release.
- The map provides an intuitive way of visualizing the Corda network and the participants.
+ The map provides an intuitive way of visualizing the Corda network and the participants.
.. image:: resources/explorer/network.png
diff --git a/docs/source/tutorial-custom-notary.rst b/docs/source/tutorial-custom-notary.rst
index 28d5dc1158..cd102e484f 100644
--- a/docs/source/tutorial-custom-notary.rst
+++ b/docs/source/tutorial-custom-notary.rst
@@ -3,9 +3,10 @@
Writing a custom notary service (experimental)
==============================================
-.. warning:: Customising a notary service is still an experimental feature and not recommended for most use-cases. Currently,
- customising Raft or BFT notaries is not yet fully supported. If you want to write your own Raft notary you will have to
- implement a custom database connector (or use a separate database for the notary), and use a custom configuration file.
+.. warning:: Customising a notary service is still an experimental feature and not recommended for most use-cases. The APIs
+ for writing a custom notary may change in the future. Additionally, customising Raft or BFT notaries is not yet
+ fully supported. If you want to write your own Raft notary you will have to implement a custom database connector
+ (or use a separate database for the notary), and use a custom configuration file.
Similarly to writing an oracle service, the first step is to create a service class in your CorDapp and annotate it
with ``@CordaService``. The Corda node scans for any class with this annotation and initialises them. The custom notary
diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt
index 4bf2c85669..812134a060 100644
--- a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt
+++ b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt
@@ -27,26 +27,37 @@ sealed class ConnectionDirection {
) : ConnectionDirection()
}
+/** Class to set Artemis TCP configuration options. */
class ArtemisTcpTransport {
companion object {
const val VERIFY_PEER_LEGAL_NAME = "corda.verifyPeerCommonName"
- // Restrict enabled TLS cipher suites to:
- // AES128 using Galois/Counter Mode (GCM) for the block cipher being used to encrypt the message stream.
- // SHA256 as message authentication algorithm.
- // ECDHE as key exchange algorithm. DHE is also supported if one wants to completely avoid the use of ECC for TLS.
- // ECDSA and RSA for digital signatures. Our self-generated certificates all use ECDSA for handshakes,
- // but we allow classical RSA certificates to work in case:
- // a) we need to use keytool certificates in some demos,
- // b) we use cloud providers or HSMs that do not support ECC.
+ /**
+ * Corda supported TLS schemes.
+ *
+ * - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
+ *
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
+ *
- TLS_DHE_RSA_WITH_AES_128_GCM_SHA256
+ *
+ * As shown above, current version restricts enabled TLS cipher suites to:
+ * AES128 using Galois/Counter Mode (GCM) for the block cipher being used to encrypt the message stream.
+ * SHA256 as message authentication algorithm.
+ * Ephemeral Diffie Hellman key exchange for advanced forward secrecy. ECDHE is preferred, but DHE is also
+ * supported in case one wants to completely avoid the use of ECC for TLS.
+ * ECDSA and RSA for digital signatures. Our self-generated certificates all use ECDSA for handshakes,
+ * but we allow classical RSA certificates to work in case one uses external tools or cloud providers or HSMs
+ * that do not support ECC certificates.
+ */
val CIPHER_SUITES = listOf(
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256"
)
+ /** Supported TLS versions, currently TLSv1.2 only. */
val TLS_VERSIONS = listOf("TLSv1.2")
+ /** Specify [TransportConfiguration] for TCP communication. */
fun tcpTransport(
direction: ConnectionDirection,
hostAndPort: NetworkHostAndPort,
diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingComponent.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingComponent.kt
index abf4727f6e..50dad2c62d 100644
--- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingComponent.kt
+++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingComponent.kt
@@ -41,13 +41,6 @@ class ArtemisMessagingComponent {
const val BRIDGE_NOTIFY = "${INTERNAL_PREFIX}bridge.notify"
const val NOTIFICATIONS_ADDRESS = "${INTERNAL_PREFIX}activemq.notifications"
- /**
- * In the operation mode where we have an out of process bridge we cannot correctly populate the Artemis validated user header
- * as the TLS does not terminate directly onto Artemis. We therefore use this internal only header to forward
- * the equivalent information from the Float.
- */
- val bridgedCertificateSubject = SimpleString("sender-subject-name")
-
object P2PMessagingHeaders {
// This is a "property" attached to an Artemis MQ message object, which contains our own notion of "topic".
// We should probably try to unify our notion of "topic" (really, just a string that identifies an endpoint
diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkMap.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkMap.kt
index b1326b17a5..368dbd1a4b 100644
--- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkMap.kt
+++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkMap.kt
@@ -54,6 +54,10 @@ data class ParametersUpdate(
val updateDeadline: Instant
)
+/** Verify that a Network Map certificate is issued by Root CA and its [CertRole] is correct. */
+// TODO: Current implementation works under the assumption that there are no intermediate CAs between Root and
+// Network Map. Consider a more flexible implementation without the above assumption.
+
fun SignedDataWithCert.verifiedNetworkMapCert(rootCert: X509Certificate): T {
require(CertRole.extract(sig.by) == CertRole.NETWORK_MAP) { "Incorrect cert role: ${CertRole.extract(sig.by)}" }
X509Utilities.validateCertificateChain(rootCert, sig.by, rootCert)
diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/PropertySerializers.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/PropertySerializers.kt
index 31c4f76c41..5e7e17765c 100644
--- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/PropertySerializers.kt
+++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/PropertySerializers.kt
@@ -44,7 +44,12 @@ class PublicPropertyReader(private val readMethod: Method?) : PropertyReader() {
// is: https://youtrack.jetbrains.com/issue/KT-13077
// TODO: Revisit this when Kotlin issue is fixed.
- loggerFor().error("Unexpected internal Kotlin error", e)
+ // So this used to report as an error, but given we serialise exceptions all the time it
+ // provides for very scary log files so move this to trace level
+ loggerFor().let { logger ->
+ logger.trace("Using kotlin introspection on internal type ${this.declaringClass}")
+ logger.trace("Unexpected internal Kotlin error", e)
+ }
return true
}
}
@@ -80,7 +85,13 @@ class PrivatePropertyReader(val field: Field, parentType: Type) : PropertyReader
// This might happen for some types, e.g. kotlin.Throwable? - the root cause of the issue
// is: https://youtrack.jetbrains.com/issue/KT-13077
// TODO: Revisit this when Kotlin issue is fixed.
- loggerFor().error("Unexpected internal Kotlin error", e)
+
+ // So this used to report as an error, but given we serialise exceptions all the time it
+ // provides for very scary log files so move this to trace level
+ loggerFor().let { logger ->
+ logger.trace("Using kotlin introspection on internal type ${field}")
+ logger.trace("Unexpected internal Kotlin error", e)
+ }
true
}
}
diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/DefaultKryoCustomizer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/DefaultKryoCustomizer.kt
index a3d6719b07..7cf9f9df3f 100644
--- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/DefaultKryoCustomizer.kt
+++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/DefaultKryoCustomizer.kt
@@ -32,10 +32,7 @@ import net.corda.core.serialization.MissingAttachmentsException
import net.corda.core.serialization.SerializationWhitelist
import net.corda.core.serialization.SerializeAsToken
import net.corda.core.serialization.SerializedBytes
-import net.corda.core.transactions.ContractUpgradeWireTransaction
-import net.corda.core.transactions.NotaryChangeWireTransaction
-import net.corda.core.transactions.SignedTransaction
-import net.corda.core.transactions.WireTransaction
+import net.corda.core.transactions.*
import net.corda.core.utilities.NonEmptySet
import net.corda.core.utilities.toNonEmptySet
import net.corda.nodeapi.internal.serialization.CordaClassResolver
@@ -139,6 +136,7 @@ object DefaultKryoCustomizer {
register(java.lang.invoke.SerializedLambda::class.java)
register(ClosureSerializer.Closure::class.java, CordaClosureBlacklistSerializer)
register(ContractUpgradeWireTransaction::class.java, ContractUpgradeWireTransactionSerializer)
+ register(ContractUpgradeFilteredTransaction::class.java, ContractUpgradeFilteredTransactionSerializer)
for (whitelistProvider in serializationWhitelists) {
val types = whitelistProvider.whitelist
diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/Kryo.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/Kryo.kt
index a54af3007c..8aa8b16e27 100644
--- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/Kryo.kt
+++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/Kryo.kt
@@ -18,14 +18,10 @@ import com.esotericsoftware.kryo.serializers.CompatibleFieldSerializer
import com.esotericsoftware.kryo.serializers.FieldSerializer
import com.esotericsoftware.kryo.util.MapReferenceResolver
import net.corda.core.concurrent.CordaFuture
-import net.corda.core.contracts.ContractState
import net.corda.core.contracts.PrivacySalt
-import net.corda.core.contracts.StateRef
-import net.corda.core.contracts.TransactionState
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.TransactionSignature
-import net.corda.core.identity.Party
import net.corda.core.internal.uncheckedCast
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.SerializationContext.UseCase.Checkpoint
@@ -35,6 +31,7 @@ import net.corda.core.serialization.SerializedBytes
import net.corda.core.toFuture
import net.corda.core.toObservable
import net.corda.core.transactions.*
+import net.corda.core.utilities.OpaqueBytes
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
import net.corda.core.utilities.SgxSupport
import net.corda.nodeapi.internal.serialization.CordaClassResolver
@@ -268,40 +265,41 @@ object WireTransactionSerializer : Serializer() {
@ThreadSafe
object NotaryChangeWireTransactionSerializer : Serializer() {
override fun write(kryo: Kryo, output: Output, obj: NotaryChangeWireTransaction) {
- kryo.writeClassAndObject(output, obj.inputs)
- kryo.writeClassAndObject(output, obj.notary)
- kryo.writeClassAndObject(output, obj.newNotary)
+ kryo.writeClassAndObject(output, obj.serializedComponents)
}
override fun read(kryo: Kryo, input: Input, type: Class): NotaryChangeWireTransaction {
- val inputs: List = uncheckedCast(kryo.readClassAndObject(input))
- val notary = kryo.readClassAndObject(input) as Party
- val newNotary = kryo.readClassAndObject(input) as Party
-
- return NotaryChangeWireTransaction(inputs, notary, newNotary)
+ val components : List = uncheckedCast(kryo.readClassAndObject(input))
+ return NotaryChangeWireTransaction(components)
}
}
@ThreadSafe
object ContractUpgradeWireTransactionSerializer : Serializer() {
override fun write(kryo: Kryo, output: Output, obj: ContractUpgradeWireTransaction) {
- kryo.writeClassAndObject(output, obj.inputs)
- kryo.writeClassAndObject(output, obj.notary)
- kryo.writeClassAndObject(output, obj.legacyContractAttachmentId)
- kryo.writeClassAndObject(output, obj.upgradeContractClassName)
- kryo.writeClassAndObject(output, obj.upgradedContractAttachmentId)
+ kryo.writeClassAndObject(output, obj.serializedComponents)
kryo.writeClassAndObject(output, obj.privacySalt)
}
override fun read(kryo: Kryo, input: Input, type: Class): ContractUpgradeWireTransaction {
- val inputs: List = uncheckedCast(kryo.readClassAndObject(input))
- val notary = kryo.readClassAndObject(input) as Party
- val legacyContractAttachment = kryo.readClassAndObject(input) as SecureHash
- val upgradeContractClassName = kryo.readClassAndObject(input) as String
- val upgradedContractAttachment = kryo.readClassAndObject(input) as SecureHash
+ val components: List = uncheckedCast(kryo.readClassAndObject(input))
val privacySalt = kryo.readClassAndObject(input) as PrivacySalt
- return ContractUpgradeWireTransaction(inputs, notary, legacyContractAttachment, upgradeContractClassName, upgradedContractAttachment, privacySalt)
+ return ContractUpgradeWireTransaction(components, privacySalt)
+ }
+}
+
+@ThreadSafe
+object ContractUpgradeFilteredTransactionSerializer : Serializer() {
+ override fun write(kryo: Kryo, output: Output, obj: ContractUpgradeFilteredTransaction) {
+ kryo.writeClassAndObject(output, obj.visibleComponents)
+ kryo.writeClassAndObject(output, obj.hiddenComponents)
+ }
+
+ override fun read(kryo: Kryo, input: Input, type: Class): ContractUpgradeFilteredTransaction {
+ val visibleComponents: Map = uncheckedCast(kryo.readClassAndObject(input))
+ val hiddenComponents: Map = uncheckedCast(kryo.readClassAndObject(input))
+ return ContractUpgradeFilteredTransaction(visibleComponents, hiddenComponents)
}
}
diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt
index cc80d5f0a8..60f2eb4627 100644
--- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt
+++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt
@@ -15,6 +15,7 @@ package net.corda.nodeapi.internal.serialization.amqp
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.client.rpc.RPCException
+import net.corda.core.CordaException
import net.corda.core.CordaRuntimeException
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
@@ -1187,5 +1188,13 @@ class SerializationOutputTests(private val compression: CordaSerializationEncodi
PrivateAckWrapper.serialize()
}
+ @Test
+ fun throwable() {
+ class TestException(message: String?, cause: Throwable?) : CordaException(message, cause)
+ val testExcp = TestException("hello", Throwable().apply { stackTrace = Thread.currentThread().stackTrace } )
+ val factory = testDefaultFactoryNoEvolution()
+ SerializationOutput(factory).serialize(testExcp)
+
+ }
}
diff --git a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt
index 2d2d1cf558..54fb916a6e 100644
--- a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt
+++ b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt
@@ -16,6 +16,7 @@ import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateRef
import net.corda.core.crypto.CompositeKey
+import net.corda.core.crypto.sha256
import net.corda.core.flows.NotaryError
import net.corda.core.flows.NotaryException
import net.corda.core.flows.NotaryFlow
@@ -162,13 +163,12 @@ class BFTNotaryServiceTests : IntegrationTest() {
}.single()
spendTxs.zip(results).forEach { (tx, result) ->
if (result is Try.Failure) {
- val error = (result.exception as NotaryException).error as NotaryError.Conflict
+ val exception = result.exception as NotaryException
+ val error = exception.error as NotaryError.Conflict
assertEquals(tx.id, error.txId)
- val (stateRef, consumingTx) = error.conflict.verified().stateHistory.entries.single()
+ val (stateRef, cause) = error.consumedStates.entries.single()
assertEquals(StateRef(issueTx.id, 0), stateRef)
- assertEquals(spendTxs[successfulIndex].id, consumingTx.id)
- assertEquals(0, consumingTx.inputIndex)
- assertEquals(info.singleIdentity(), consumingTx.requestingParty)
+ assertEquals(spendTxs[successfulIndex].id.sha256(), cause.hashOfTransactionId)
}
}
}
diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt
index 33d3493941..df8b072444 100644
--- a/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt
+++ b/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt
@@ -14,16 +14,11 @@ import co.paralleluniverse.fibers.Suspendable
import com.google.common.util.concurrent.SettableFuture
import net.corda.core.contracts.StateRef
import net.corda.core.crypto.Crypto
-import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SecureHash
-import net.corda.core.flows.FlowLogic
-import net.corda.core.flows.FlowSession
-import net.corda.core.flows.NotaryError
-import net.corda.core.flows.NotaryException
+import net.corda.core.crypto.SignedData
+import net.corda.core.flows.*
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
-import net.corda.core.flows.NotarisationPayload
-import net.corda.core.flows.NotarisationRequest
import net.corda.core.node.services.NotaryService
import net.corda.core.node.services.UniquenessProvider
import net.corda.core.schemas.PersistentStateRef
@@ -91,18 +86,22 @@ class BFTNonValidatingNotaryService(
@Suspendable
override fun call(): Void? {
val payload = otherSideSession.receive().unwrap { it }
- val signatures = commit(payload)
- otherSideSession.send(signatures)
+ val response = commit(payload)
+ otherSideSession.send(response)
return null
}
- private fun commit(payload: NotarisationPayload): List {
+ private fun commit(payload: NotarisationPayload): NotarisationResponse {
val response = service.commitTransaction(payload, otherSideSession.counterparty)
when (response) {
- is BFTSMaRt.ClusterResponse.Error -> throw NotaryException(response.error)
+ is BFTSMaRt.ClusterResponse.Error -> {
+ // TODO: here we assume that all error will be the same, but there might be invalid onces from mailicious nodes
+ val responseError = response.errors.first().verified()
+ throw NotaryException(responseError, payload.coreTransaction.id)
+ }
is BFTSMaRt.ClusterResponse.Signatures -> {
log.debug("All input states of transaction ${payload.coreTransaction.id} have been committed")
- return response.txSignatures
+ return NotarisationResponse(response.txSignatures)
}
}
}
@@ -160,13 +159,16 @@ class BFTNonValidatingNotaryService(
val inputs = transaction.inputs
val notary = transaction.notary
if (transaction is FilteredTransaction) NotaryService.validateTimeWindow(services.clock, transaction.timeWindow)
- if (notary !in services.myInfo.legalIdentities) throw NotaryException(NotaryError.WrongNotary)
+ if (notary !in services.myInfo.legalIdentities) throw NotaryInternalException(NotaryError.WrongNotary)
commitInputStates(inputs, id, callerIdentity)
log.debug { "Inputs committed successfully, signing $id" }
BFTSMaRt.ReplicaResponse.Signature(sign(id))
- } catch (e: NotaryException) {
+ } catch (e: NotaryInternalException) {
log.debug { "Error processing transaction: ${e.error}" }
- BFTSMaRt.ReplicaResponse.Error(e.error)
+ val serializedError = e.error.serialize()
+ val errorSignature = sign(serializedError.bytes)
+ val signedError = SignedData(serializedError, errorSignature)
+ BFTSMaRt.ReplicaResponse.Error(signedError)
}
}
diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt b/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt
index 1eea988a61..851a9e8b75 100644
--- a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt
+++ b/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt
@@ -24,10 +24,8 @@ import bftsmart.tom.server.defaultservices.DefaultReplier
import bftsmart.tom.util.Extractor
import net.corda.core.contracts.StateRef
import net.corda.core.crypto.*
-import net.corda.core.flows.NotaryError
-import net.corda.core.flows.NotaryException
+import net.corda.core.flows.*
import net.corda.core.identity.Party
-import net.corda.core.flows.NotarisationPayload
import net.corda.core.internal.declaredField
import net.corda.core.internal.toTypedArray
import net.corda.core.node.services.UniquenessProvider
@@ -66,15 +64,15 @@ object BFTSMaRt {
/** Sent from [Replica] to [Client]. */
@CordaSerializable
sealed class ReplicaResponse {
- data class Error(val error: NotaryError) : ReplicaResponse()
- data class Signature(val txSignature: DigitalSignature) : ReplicaResponse()
+ data class Error(val error: SignedData) : ReplicaResponse()
+ data class Signature(val txSignature: TransactionSignature) : ReplicaResponse()
}
/** An aggregate response from all replica ([Replica]) replies sent from [Client] back to the calling application. */
@CordaSerializable
sealed class ClusterResponse {
- data class Error(val error: NotaryError) : ClusterResponse()
- data class Signatures(val txSignatures: List) : ClusterResponse()
+ data class Error(val errors: List>) : ClusterResponse()
+ data class Signatures(val txSignatures: List) : ClusterResponse()
}
interface Cluster {
@@ -146,7 +144,7 @@ object BFTSMaRt {
ClusterResponse.Signatures(accepted.map { it.txSignature })
} else {
log.debug { "Cluster response - error: ${rejected.first().error}" }
- ClusterResponse.Error(rejected.first().error)
+ ClusterResponse.Error(rejected.map { it.error })
}
val messageContent = aggregateResponse.serialize().bytes
@@ -242,10 +240,9 @@ object BFTSMaRt {
}
} else {
log.debug { "Conflict detected – the following inputs have already been committed: ${conflicts.keys.joinToString()}" }
- val conflict = UniquenessProvider.Conflict(conflicts)
- val conflictData = conflict.serialize()
- val signedConflict = SignedData(conflictData, sign(conflictData.bytes))
- throw NotaryException(NotaryError.Conflict(txId, signedConflict))
+ val conflict = conflicts.mapValues { StateConsumptionDetails(it.value.id.sha256()) }
+ val error = NotaryError.Conflict(txId, conflict)
+ throw NotaryInternalException(error)
}
}
}
diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/MySQLUniquenessProvider.kt b/node/src/main/kotlin/net/corda/node/services/transactions/MySQLUniquenessProvider.kt
index 39eb96ece9..66e5caaba0 100644
--- a/node/src/main/kotlin/net/corda/node/services/transactions/MySQLUniquenessProvider.kt
+++ b/node/src/main/kotlin/net/corda/node/services/transactions/MySQLUniquenessProvider.kt
@@ -17,20 +17,19 @@ import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import net.corda.core.contracts.StateRef
import net.corda.core.crypto.SecureHash
-import net.corda.core.identity.CordaX500Name
+import net.corda.core.crypto.sha256
+import net.corda.core.flows.NotaryError
+import net.corda.core.flows.NotaryInternalException
+import net.corda.core.flows.StateConsumptionDetails
import net.corda.core.identity.Party
-import net.corda.core.node.services.UniquenessException
import net.corda.core.node.services.UniquenessProvider
import net.corda.core.serialization.SingletonSerializeAsToken
-import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.utilities.loggerFor
import net.corda.node.services.config.MySQLConfiguration
-import java.security.PublicKey
import java.sql.BatchUpdateException
import java.sql.Connection
import java.sql.SQLTransientConnectionException
-import java.util.*
import java.util.concurrent.TimeUnit
/**
@@ -117,7 +116,7 @@ class MySQLUniquenessProvider(
} catch (e: BatchUpdateException) {
log.info("Unable to commit input states, finding conflicts, txId: $txId", e)
conflictCounter.inc()
- retryTransaction(FindConflicts(states))
+ retryTransaction(FindConflicts(txId, states))
} finally {
val dt = s.stop().elapsed(TimeUnit.MILLISECONDS)
commitTimer.update(dt, TimeUnit.MILLISECONDS)
@@ -173,26 +172,22 @@ class MySQLUniquenessProvider(
}
}
- private class FindConflicts(val states: List) : RetryableTransaction {
+ private class FindConflicts(val txId: SecureHash, val states: List) : RetryableTransaction {
override fun run(conn: Connection) {
- val conflicts = mutableMapOf()
+ val conflicts = mutableMapOf()
states.forEach {
val st = conn.prepareStatement(findStatement).apply {
setBytes(1, it.txhash.bytes)
setInt(2, it.index)
}
val result = st.executeQuery()
-
if (result.next()) {
val consumingTxId = SecureHash.SHA256(result.getBytes(1))
- val inputIndex = result.getInt(2)
- val partyName = CordaX500Name.parse(result.getString(3))
- val partyKey: PublicKey = result.getBytes(4).deserialize()
- conflicts[it] = UniquenessProvider.ConsumingTx(consumingTxId, inputIndex, Party(partyName, partyKey))
+ conflicts[it] = StateConsumptionDetails(consumingTxId.sha256())
}
}
conn.commit()
- if (conflicts.isNotEmpty()) throw UniquenessException(UniquenessProvider.Conflict(conflicts))
+ if (conflicts.isNotEmpty()) throw NotaryInternalException(NotaryError.Conflict(txId, conflicts))
}
}
}
\ No newline at end of file
diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt b/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt
index 61f2653245..c9f998c831 100644
--- a/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt
+++ b/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt
@@ -13,10 +13,13 @@ package net.corda.node.services.transactions
import net.corda.core.contracts.StateRef
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash
+import net.corda.core.crypto.sha256
+import net.corda.core.flows.NotaryError
+import net.corda.core.flows.NotaryInternalException
+import net.corda.core.flows.StateConsumptionDetails
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.internal.ThreadBox
-import net.corda.core.node.services.UniquenessException
import net.corda.core.node.services.UniquenessProvider
import net.corda.core.schemas.PersistentStateRef
import net.corda.core.serialization.SingletonSerializeAsToken
@@ -78,8 +81,9 @@ class PersistentUniquenessProvider : UniquenessProvider, SingletonSerializeAsTok
toPersistentEntityKey = { PersistentStateRef(it.txhash.toString(), it.index) },
fromPersistentEntity = {
//TODO null check will become obsolete after making DB/JPA columns not nullable
- var txId = it.id.txId ?: throw IllegalStateException("DB returned null SecureHash transactionId")
- var index = it.id.index ?: throw IllegalStateException("DB returned null SecureHash index")
+ val txId = it.id.txId
+ ?: throw IllegalStateException("DB returned null SecureHash transactionId")
+ val index = it.id.index ?: throw IllegalStateException("DB returned null SecureHash index")
Pair(StateRef(txhash = SecureHash.parse(txId), index = index),
UniquenessProvider.ConsumingTx(
id = SecureHash.parse(it.consumingTxHash),
@@ -101,7 +105,6 @@ class PersistentUniquenessProvider : UniquenessProvider, SingletonSerializeAsTok
}
override fun commit(states: List, txId: SecureHash, callerIdentity: Party) {
-
val conflict = mutex.locked {
val conflictingStates = LinkedHashMap()
for (inputState in states) {
@@ -110,7 +113,8 @@ class PersistentUniquenessProvider : UniquenessProvider, SingletonSerializeAsTok
}
if (conflictingStates.isNotEmpty()) {
log.debug("Failure, input states already committed: ${conflictingStates.keys}")
- UniquenessProvider.Conflict(conflictingStates)
+ val conflict = conflictingStates.mapValues { StateConsumptionDetails(it.value.id.sha256()) }
+ conflict
} else {
states.forEachIndexed { i, stateRef ->
committedStates[stateRef] = UniquenessProvider.ConsumingTx(txId, i, callerIdentity)
@@ -120,6 +124,6 @@ class PersistentUniquenessProvider : UniquenessProvider, SingletonSerializeAsTok
}
}
- if (conflict != null) throw UniquenessException(conflict)
+ if (conflict != null) throw NotaryInternalException(NotaryError.Conflict(txId, conflict))
}
}
diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt
index 100181d554..4319d357f2 100644
--- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt
+++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt
@@ -29,8 +29,11 @@ import io.atomix.copycat.server.storage.Storage
import io.atomix.copycat.server.storage.StorageLevel
import net.corda.core.contracts.StateRef
import net.corda.core.crypto.SecureHash
+import net.corda.core.crypto.sha256
+import net.corda.core.flows.NotaryError
+import net.corda.core.flows.NotaryInternalException
+import net.corda.core.flows.StateConsumptionDetails
import net.corda.core.identity.Party
-import net.corda.core.node.services.UniquenessException
import net.corda.core.node.services.UniquenessProvider
import net.corda.core.serialization.SerializationDefaults
import net.corda.core.serialization.SingletonSerializeAsToken
@@ -214,7 +217,11 @@ class RaftUniquenessProvider(private val transportConfiguration: NodeSSLConfigur
val commitCommand = DistributedImmutableMap.Commands.PutAll(encode(entries))
val conflicts = client.submit(commitCommand).get()
- if (conflicts.isNotEmpty()) throw UniquenessException(UniquenessProvider.Conflict(decode(conflicts)))
+ if (conflicts.isNotEmpty()) {
+ val conflictingStates = decode(conflicts).mapValues { StateConsumptionDetails(it.value.id.sha256()) }
+ val error = NotaryError.Conflict(txId, conflictingStates)
+ throw NotaryInternalException(error)
+ }
log.debug("All input states of transaction $txId have been committed")
}
diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt
index 4c2c529360..24b55bead5 100644
--- a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt
+++ b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt
@@ -47,7 +47,7 @@ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthor
} catch (e: Exception) {
throw when (e) {
is TransactionVerificationException,
- is SignatureException -> NotaryException(NotaryError.TransactionInvalid(e))
+ is SignatureException -> NotaryInternalException(NotaryError.TransactionInvalid(e))
else -> e
}
}
@@ -77,7 +77,7 @@ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthor
try {
tx.verifySignaturesExcept(service.notaryIdentityKey)
} catch (e: SignatureException) {
- throw NotaryException(NotaryError.TransactionInvalid(e))
+ throw NotaryInternalException(NotaryError.TransactionInvalid(e))
}
}
}
diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt
index df92fb8b10..e76d3b4c9d 100644
--- a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt
+++ b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt
@@ -13,10 +13,7 @@ package net.corda.node.services.transactions
import net.corda.core.concurrent.CordaFuture
import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.StateRef
-import net.corda.core.crypto.Crypto
-import net.corda.core.crypto.SecureHash
-import net.corda.core.crypto.TransactionSignature
-import net.corda.core.crypto.sign
+import net.corda.core.crypto.*
import net.corda.core.flows.*
import net.corda.core.identity.Party
import net.corda.core.internal.generateSignature
@@ -147,32 +144,45 @@ class NotaryServiceTests {
@Test
fun `should report conflict when inputs are reused across transactions`() {
- val inputState = issueState(aliceNode.services, alice)
- val stx = run {
- val tx = TransactionBuilder(notary)
- .addInputState(inputState)
- .addCommand(dummyCommand(alice.owningKey))
- aliceNode.services.signInitialTransaction(tx)
+ val firstState = issueState(aliceNode.services, alice)
+ val secondState = issueState(aliceNode.services, alice)
+
+ fun spendState(state: StateAndRef<*>): SignedTransaction {
+ val stx = run {
+ val tx = TransactionBuilder(notary)
+ .addInputState(state)
+ .addCommand(dummyCommand(alice.owningKey))
+ aliceNode.services.signInitialTransaction(tx)
+ }
+ aliceNode.services.startFlow(NotaryFlow.Client(stx))
+ mockNet.runNetwork()
+ return stx
}
- val stx2 = run {
+
+ val firstSpendTx = spendState(firstState)
+ val secondSpendTx = spendState(secondState)
+
+ val doubleSpendTx = run {
val tx = TransactionBuilder(notary)
- .addInputState(inputState)
.addInputState(issueState(aliceNode.services, alice))
+ .addInputState(firstState)
+ .addInputState(secondState)
.addCommand(dummyCommand(alice.owningKey))
aliceNode.services.signInitialTransaction(tx)
}
- val firstSpend = NotaryFlow.Client(stx)
- val secondSpend = NotaryFlow.Client(stx2) // Double spend the inputState in a second transaction.
- aliceNode.services.startFlow(firstSpend)
- val future = aliceNode.services.startFlow(secondSpend)
-
+ val doubleSpend = NotaryFlow.Client(doubleSpendTx) // Double spend the inputState in a second transaction.
+ val future = aliceNode.services.startFlow(doubleSpend)
mockNet.runNetwork()
val ex = assertFailsWith(NotaryException::class) { future.resultFuture.getOrThrow() }
val notaryError = ex.error as NotaryError.Conflict
- assertEquals(notaryError.txId, stx2.id)
- notaryError.conflict.verified()
+ assertEquals(notaryError.txId, doubleSpendTx.id)
+ with(notaryError) {
+ assertEquals(consumedStates.size, 2)
+ assertEquals(consumedStates[firstState.ref]!!.hashOfTransactionId, firstSpendTx.id.sha256())
+ assertEquals(consumedStates[secondState.ref]!!.hashOfTransactionId, secondSpendTx.id.sha256())
+ }
}
@Test
diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt
index c6170839d1..59d6a7a341 100644
--- a/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt
+++ b/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt
@@ -11,8 +11,10 @@
package net.corda.node.services.transactions
import net.corda.core.crypto.SecureHash
+import net.corda.core.crypto.sha256
+import net.corda.core.flows.NotaryInternalException
+import net.corda.core.flows.NotaryError
import net.corda.core.identity.CordaX500Name
-import net.corda.core.node.services.UniquenessException
import net.corda.node.internal.configureDatabase
import net.corda.node.services.schema.NodeSchemaService
import net.corda.nodeapi.internal.persistence.CordaPersistence
@@ -70,12 +72,11 @@ class PersistentUniquenessProviderTests {
val inputs = listOf(inputState)
provider.commit(inputs, txID, identity)
- val ex = assertFailsWith { provider.commit(inputs, txID, identity) }
+ val ex = assertFailsWith { provider.commit(inputs, txID, identity) }
+ val error = ex.error as NotaryError.Conflict
- val consumingTx = ex.error.stateHistory[inputState]!!
- assertEquals(consumingTx.id, txID)
- assertEquals(consumingTx.inputIndex, inputs.indexOf(inputState))
- assertEquals(consumingTx.requestingParty, identity)
+ val conflictCause = error.consumedStates[inputState]!!
+ assertEquals(conflictCause.hashOfTransactionId, txID.sha256())
}
}
}
diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt
index 9856b0982a..4dfcc2192a 100644
--- a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt
+++ b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt
@@ -24,7 +24,6 @@ import net.corda.core.node.ServiceHub
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.getOrThrow
-import net.corda.node.services.api.StartedNodeServices
import net.corda.node.services.issueInvalidState
import net.corda.testing.contracts.DummyContract
import net.corda.testing.core.ALICE_NAME
@@ -90,14 +89,13 @@ class ValidatingNotaryServiceTests {
aliceNode.services.signInitialTransaction(tx)
}
- val ex = assertFailsWith(NotaryException::class) {
+ // Expecting SignaturesMissingException instead of NotaryException, since the exception should originate from
+ // the client flow.
+ val ex = assertFailsWith {
val future = runClient(stx)
future.getOrThrow()
}
- val notaryError = ex.error as NotaryError.TransactionInvalid
- assertThat(notaryError.cause).isInstanceOf(SignedTransaction.SignaturesMissingException::class.java)
-
- val missingKeys = (notaryError.cause as SignedTransaction.SignaturesMissingException).missing
+ val missingKeys = ex.missing
assertEquals(setOf(expectedMissingKey), missingKeys)
}
diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt
index c9793b305f..d439a72c7c 100644
--- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt
+++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt
@@ -14,10 +14,14 @@ import co.paralleluniverse.fibers.Suspendable
import com.nhaarman.mockito_kotlin.argThat
import com.nhaarman.mockito_kotlin.doNothing
import com.nhaarman.mockito_kotlin.whenever
-import net.corda.core.contracts.*
+import net.corda.core.contracts.Amount
+import net.corda.core.contracts.Issued
+import net.corda.core.contracts.StateAndRef
+import net.corda.core.contracts.StateRef
import net.corda.core.crypto.NullKeys
import net.corda.core.crypto.generateKeyPair
import net.corda.core.identity.*
+import net.corda.core.internal.NotaryChangeTransactionBuilder
import net.corda.core.internal.packageName
import net.corda.core.node.StatesToRecord
import net.corda.core.node.services.StatesNotAvailableException
@@ -27,7 +31,6 @@ import net.corda.core.node.services.queryBy
import net.corda.core.node.services.vault.PageSpecification
import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.node.services.vault.QueryCriteria.*
-import net.corda.core.transactions.NotaryChangeWireTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.NonEmptySet
@@ -617,7 +620,7 @@ class NodeVaultServiceTest {
// Change notary
services.identityService.verifyAndRegisterIdentity(DUMMY_NOTARY_IDENTITY)
val newNotary = DUMMY_NOTARY
- val changeNotaryTx = NotaryChangeWireTransaction(listOf(initialCashState.ref), issueStx.notary!!, newNotary)
+ val changeNotaryTx = NotaryChangeTransactionBuilder(listOf(initialCashState.ref), issueStx.notary!!, newNotary).build()
val cashStateWithNewNotary = StateAndRef(initialCashState.state.copy(notary = newNotary), StateRef(changeNotaryTx.id, 0))
database.transaction {
diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt
index ce9345b05d..96370fb8a5 100644
--- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt
+++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt
@@ -14,14 +14,11 @@ 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.NotarisationPayload
-import net.corda.core.flows.NotarisationRequest
import net.corda.core.internal.ResolveTransactionsFlow
import net.corda.core.internal.validateRequest
import net.corda.core.node.AppServiceHub
import net.corda.core.node.services.CordaService
import net.corda.core.node.services.TrustedAuthorityNotaryService
-import net.corda.core.transactions.CoreTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionWithSignatures
import net.corda.core.transactions.WireTransaction
@@ -68,7 +65,7 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating
} catch (e: Exception) {
throw when (e) {
is TransactionVerificationException,
- is SignatureException -> NotaryException(NotaryError.TransactionInvalid(e))
+ is SignatureException -> NotaryInternalException(NotaryError.TransactionInvalid(e))
else -> e
}
}
@@ -99,7 +96,7 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating
try {
tx.verifySignaturesExcept(service.notaryIdentityKey)
} catch (e: SignatureException) {
- throw NotaryException(NotaryError.TransactionInvalid(e))
+ throw NotaryInternalException(NotaryError.TransactionInvalid(e))
}
}
diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/CashViewer.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/CashViewer.kt
index 97e094b40d..992b288f99 100644
--- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/CashViewer.kt
+++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/CashViewer.kt
@@ -21,6 +21,7 @@ import javafx.collections.ObservableList
import javafx.geometry.Insets
import javafx.scene.Parent
import javafx.scene.chart.NumberAxis
+import javafx.scene.chart.XYChart
import javafx.scene.control.*
import javafx.scene.input.MouseButton
import javafx.scene.layout.BorderPane
@@ -323,12 +324,26 @@ class CashViewer : CordaView("Cash") {
linechart(null, xAxis, yAxis) {
series("USD") {
sumAmount.addListener { _, _, _ ->
+ val lastAmount = data.last().value?.yValue
+ val currAmount = sumAmount.value.toDecimal()
val lastTimeStamp = data.last().value?.xValue
- if (lastTimeStamp == null || System.currentTimeMillis() - lastTimeStamp.toLong() > 1.seconds.toMillis()) {
- data(System.currentTimeMillis(), sumAmount.value.quantity)
- runInFxApplicationThread {
- // Modify data in UI thread.
- if (data.size > 300) data.remove(0, 1)
+ val currentTimeStamp = System.currentTimeMillis()
+
+ // If amount is not the same - always add a data point.
+ if (lastAmount == null || lastAmount != currAmount) {
+ // If update arrived in very close succession to the previous one - kill the last point received to eliminate un-necessary noise on the graph.
+ if(lastTimeStamp != null && currentTimeStamp - lastTimeStamp.toLong() < 1.seconds.toMillis()) {
+ data.safelyTransition {
+ remove(size - 1, size)
+ }
+ }
+
+ // Add a new data point.
+ data(currentTimeStamp, currAmount)
+
+ // Limit population of data points to make graph painting faster.
+ data.safelyTransition {
+ if (size > 300) remove(0, 1)
}
}
}
@@ -337,5 +352,12 @@ class CashViewer : CordaView("Cash") {
animated = false
}
}
+
+ private fun ObservableList>.safelyTransition(block: ObservableList>.() -> Unit) {
+ runInFxApplicationThread {
+ // Modify data in UI thread to properly propagate to GUI.
+ this.block()
+ }
+ }
}
}