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