From d70cd26a7c73c75b7a18eb8f38795d1d94e066ed Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Thu, 8 Mar 2018 10:52:07 +0000 Subject: [PATCH 1/9] Kdoc/comment updates (#2626) --- .../corda/confidential/SwapIdentitiesFlow.kt | 3 +- .../net/corda/core/internal/CertRole.kt | 8 +- .../corda/core/node/services/VaultService.kt | 88 ++++++++++--------- .../core/transactions/LedgerTransaction.kt | 2 +- .../core/transactions/MerkleTransaction.kt | 22 ++--- .../core/transactions/TransactionBuilder.kt | 2 +- .../net/corda/nodeapi/ArtemisTcpTransport.kt | 27 ++++-- .../nodeapi/internal/network/NetworkMap.kt | 4 + 8 files changed, 88 insertions(+), 68 deletions(-) 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 d4ecd77987..1771c2846f 100644 --- a/confidential-identities/src/main/kotlin/net/corda/confidential/SwapIdentitiesFlow.kt +++ b/confidential-identities/src/main/kotlin/net/corda/confidential/SwapIdentitiesFlow.kt @@ -76,7 +76,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/internal/CertRole.kt b/core/src/main/kotlin/net/corda/core/internal/CertRole.kt index 82f4a42deb..29d24b80e0 100644 --- a/core/src/main/kotlin/net/corda/core/internal/CertRole.kt +++ b/core/src/main/kotlin/net/corda/core/internal/CertRole.kt @@ -24,9 +24,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), @@ -37,6 +35,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/node/services/VaultService.kt b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt index 17586bfc69..b7606475f3 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 @@ -6,13 +6,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 @@ -110,15 +111,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>, @@ -158,17 +159,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> @@ -180,10 +182,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) @@ -192,7 +194,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. */ @@ -200,35 +202,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. @@ -249,12 +251,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. @@ -271,11 +273,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). @@ -287,8 +289,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/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index 09b3b7c98c..4c2445676a 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -400,6 +400,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 be7cdf0d7b..b61daf9963 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt @@ -85,18 +85,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]) } } } } @@ -148,9 +148,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 @@ -165,9 +165,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() } } } @@ -312,14 +312,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/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index 3204e15580..12808fc299 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -102,7 +102,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/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt index 34b5bf7784..b0330f667a 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt @@ -17,26 +17,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/network/NetworkMap.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkMap.kt index 31314ca833..2e652d9279 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 @@ -44,6 +44,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) From c3c3a859d5ae3bc2213a4542fc223d38c2756146 Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Thu, 8 Mar 2018 11:11:28 +0000 Subject: [PATCH 2/9] CORDA-1117 - port rst fixes (#2762) --- docs/source/node-explorer.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/source/node-explorer.rst b/docs/source/node-explorer.rst index 48c0a96088..cc68ad2970 100644 --- a/docs/source/node-explorer.rst +++ b/docs/source/node-explorer.rst @@ -63,11 +63,11 @@ The Demo Nodes can be started in one of two modes: .. note:: 5 Corda nodes will be created on the following port on localhost by default. - * Notary -> 20003 (Does not accept logins) - * Alice -> 20006 - * Bob -> 20009 - * UK Bank Plc -> 20012 (*Issuer node*) - * USA Bank Corp -> 20015 (*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``. From 2e9027db1bd2b8733026fc030864caf0fa237642 Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Thu, 8 Mar 2018 11:44:26 +0000 Subject: [PATCH 3/9] secureRandomBytes should call getNextBytes, not generateSeed (#2587) --- core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt | 2 +- core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 96f0c85cc2..b586f5e562 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt @@ -168,7 +168,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) } /** * Get an instance of [SecureRandom] to avoid blocking, due to waiting for additional entropy, when possible. 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 9a2348c48f..cf253903e2 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt @@ -78,7 +78,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. From b3203c9f3efddcde19abc038b9ce05ae94021713 Mon Sep 17 00:00:00 2001 From: Matthew Nesbit Date: Thu, 8 Mar 2018 12:44:05 +0000 Subject: [PATCH 4/9] Remove a mistakenly duplicated constant defined on ArtemisMessagingComponent (#2765) --- .../corda/nodeapi/internal/ArtemisMessagingComponent.kt | 7 ------- 1 file changed, 7 deletions(-) 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 4c55f77e87..649dc967ae 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 @@ -31,13 +31,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 From 198fb4f2648ccc5d8802b74b2d66fdbd347c9f63 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Thu, 8 Mar 2018 23:07:41 +0000 Subject: [PATCH 5/9] CORDA-1192 - Quieten message when reflecting on a builtin Kotlin type (#2767) * CORDA-1192 - Quieten message when reflecting on a builtin Kotlin type * review comments --- .../serialization/amqp/PropertySerializers.kt | 15 +++++++++++++-- .../amqp/SerializationOutputTests.kt | 9 +++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) 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 9029dd171b..4687c1173b 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 @@ -34,7 +34,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 } } @@ -70,7 +75,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/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 e69a949dbd..5fc4359e88 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 @@ -5,6 +5,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 @@ -1177,5 +1178,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) + + } } From af60848da788c1bf0023518f112b8abbca482ccc Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Thu, 8 Mar 2018 17:23:16 +0000 Subject: [PATCH 6/9] CORDA-1197: Take into account last amount submitted when adding data points. Current logic in `CashWidget` is not handling well updates done in close succession, i.e. less than 1 second. And such frequent updates do indeed happen, e.g. from: `net.corda.client.jfx.model.ContractStateModel#cashStates` where `list` is modified twice. Also use `.toDecimal()` instead of `.quantity`to have amount represented in pounds rather than in pennies. (cherry picked from commit 952cc35) --- .../views/cordapps/cash/CashViewer.kt | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) 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 6093a235b1..1cce87e6c4 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 @@ -11,6 +11,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 @@ -313,12 +314,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) } } } @@ -327,5 +342,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() + } + } } } From a3bf4577f3ecadcbc1eb26da226946242501b161 Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Thu, 8 Mar 2018 17:59:25 +0000 Subject: [PATCH 7/9] =?UTF-8?q?CORDA-696=20-=20Ensure=20deterministic=20tr?= =?UTF-8?q?ansaction=20id=20calculation=20for=20contra=E2=80=A6=20(#2676)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The problem with the previous implementation is that the transaction would be deserialized with the schema specified in the serialized form, but the calculation of the id would involve re-serializing properties using a local serialization context which might produce a different result. --- .ci/api-current.txt | 30 ++--- .../net/corda/core/crypto/CryptoUtils.kt | 6 +- .../net/corda/core/flows/NotaryChangeFlow.kt | 6 +- .../core/internal/ContractUpgradeUtils.kt | 6 +- .../corda/core/internal/TransactionUtils.kt | 45 ++++++++ .../ContractUpgradeTransactions.kt | 104 ++++++++++++++---- .../transactions/NotaryChangeTransactions.kt | 37 ++++++- .../kryo/DefaultKryoCustomizer.kt | 6 +- .../internal/serialization/kryo/Kryo.kt | 44 ++++---- .../services/vault/NodeVaultServiceTest.kt | 9 +- 10 files changed, 208 insertions(+), 85 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 20fb45af09..6962c89071 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -3001,17 +3001,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() ## @@ -3038,8 +3036,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() @@ -3047,15 +3045,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() @@ -3063,8 +3057,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() @@ -3199,11 +3193,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/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt index b586f5e562..873184c6eb 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt @@ -222,7 +222,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/flows/NotaryChangeFlow.kt b/core/src/main/kotlin/net/corda/core/flows/NotaryChangeFlow.kt index 26525204aa..7690f01843 100644 --- a/core/src/main/kotlin/net/corda/core/flows/NotaryChangeFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/NotaryChangeFlow.kt @@ -7,7 +7,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 @@ -30,11 +30,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/internal/ContractUpgradeUtils.kt b/core/src/main/kotlin/net/corda/core/internal/ContractUpgradeUtils.kt index b085f51391..7b3283b28a 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ContractUpgradeUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ContractUpgradeUtils.kt @@ -21,18 +21,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/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/transactions/ContractUpgradeTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt index 33f15547d4..e85b50f82e 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt @@ -3,12 +3,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 @@ -18,13 +24,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" } @@ -39,11 +52,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 { @@ -56,7 +75,7 @@ data class ContractUpgradeWireTransaction( resolvedInputs, notary, legacyContractAttachment, - upgradeContractClassName, + upgradedContractClassName, upgradedContractAttachment, id, privacySalt, @@ -65,8 +84,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 } } @@ -74,19 +108,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) } /** @@ -103,7 +161,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, @@ -165,7 +223,7 @@ data class ContractUpgradeLedgerTransaction( // TODO: re-map encumbrance pointers input.state.copy( data = upgradedState, - contract = upgradeContractClassName, + contract = upgradedContractClassName, constraint = outputConstraint ) } @@ -182,7 +240,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/NotaryChangeTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt index e230adbff8..0704ec0db2 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt @@ -3,11 +3,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 @@ -18,10 +22,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. @@ -39,16 +51,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/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 5bf8e1eac4..9bb6b81142 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 @@ -22,10 +22,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 @@ -129,6 +126,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 fc0be0c8eb..e161768f65 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 @@ -8,14 +8,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 @@ -25,6 +21,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.nodeapi.internal.serialization.CordaClassResolver import net.corda.nodeapi.internal.serialization.serializationContextKey @@ -254,40 +251,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/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index 9bdcd1f24c..0c513aea82 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 @@ -4,10 +4,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 @@ -17,7 +21,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 @@ -607,7 +610,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 { From 2d31247da233b4848da8105b4fda9303bd657e5c Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Tue, 6 Mar 2018 12:22:09 +0000 Subject: [PATCH 8/9] CORDA-1171: When a double-spend occurs, do not send the consuming transaction id and requesting party back to the client - this might lead to privacy leak. Only the transaction id hash is now returned. --- .ci/api-current.txt | 10 +- .../corda/core/flows/NotarisationRequest.kt | 13 ++- .../kotlin/net/corda/core/flows/NotaryFlow.kt | 108 ++++++++++-------- .../net/corda/core/internal/NotaryUtils.kt | 16 +++ .../corda/core/node/services/NotaryService.kt | 46 ++++---- .../core/node/services/UniquenessProvider.kt | 12 +- docs/source/tutorial-custom-notary.rst | 7 +- .../node/services/BFTNotaryServiceTests.kt | 12 +- .../BFTNonValidatingNotaryService.kt | 32 +++--- .../node/services/transactions/BFTSMaRt.kt | 21 ++-- .../PersistentUniquenessProvider.kt | 16 ++- .../transactions/RaftUniquenessProvider.kt | 11 +- .../transactions/ValidatingNotaryFlow.kt | 4 +- .../transactions/NotaryServiceTests.kt | 48 +++++--- .../PersistentUniquenessProviderTests.kt | 13 ++- .../ValidatingNotaryServiceTests.kt | 10 +- .../corda/notarydemo/MyCustomNotaryService.kt | 7 +- 17 files changed, 214 insertions(+), 172 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/core/internal/NotaryUtils.kt diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 6962c89071..51726ff636 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -1375,12 +1375,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() @@ -1431,13 +1429,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() 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 166d38dc68..24ea4bd675 100644 --- a/core/src/main/kotlin/net/corda/core/flows/NotarisationRequest.kt +++ b/core/src/main/kotlin/net/corda/core/flows/NotarisationRequest.kt @@ -1,8 +1,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 @@ -43,7 +42,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 @@ -59,7 +58,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 } @@ -98,4 +97,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/NotaryFlow.kt b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt index ccb823897b..2c3ef48413 100644 --- a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt @@ -1,27 +1,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 @@ -36,6 +33,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>() { @@ -68,44 +66,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() @@ -116,18 +102,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. @@ -156,11 +137,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 } @@ -175,14 +162,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))) } } } @@ -197,14 +184,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. */ @@ -236,3 +236,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/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/node/services/NotaryService.kt b/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt index 68badaa4e7..0cb1e9fcf6 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 @@ -8,7 +8,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 @@ -30,18 +29,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) ) } @@ -82,28 +82,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) @@ -117,6 +113,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 b4bdb7faf9..2873d22d74 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 @@ -13,24 +13,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/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/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt index be70a53734..b72ee8e6ec 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 @@ -6,6 +6,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 @@ -28,8 +29,8 @@ import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.contracts.DummyContract -import net.corda.testing.core.singleIdentity import net.corda.testing.core.dummyCommand +import net.corda.testing.core.singleIdentity import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.InternalMockNetwork.MockNode import net.corda.testing.node.internal.InternalMockNodeParameters @@ -142,13 +143,12 @@ class BFTNotaryServiceTests { }.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 a04ce94d6f..4602088edd 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 @@ -4,16 +4,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 @@ -81,18 +76,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) } } } @@ -150,13 +149,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 aa3027559b..24e3de6089 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 @@ -14,10 +14,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 @@ -56,15 +54,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 { @@ -136,7 +134,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 @@ -232,10 +230,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/PersistentUniquenessProvider.kt b/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt index 507669e91c..02aca42b85 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 @@ -3,10 +3,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 @@ -68,8 +71,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), @@ -91,7 +95,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) { @@ -100,7 +103,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) @@ -110,6 +114,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 fd0a20de0f..f9da07e825 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 @@ -19,8 +19,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 @@ -204,7 +207,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 27151c2ce7..57edcfecaf 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 @@ -37,7 +37,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 } } @@ -67,7 +67,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 39ce8f8fe8..56f0197372 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 @@ -3,10 +3,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 @@ -137,32 +134,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 071c184e99..55aa5d06e4 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 @@ -1,8 +1,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 @@ -60,12 +62,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 bc79857f21..7f7e6dbc48 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 @@ -14,7 +14,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 @@ -80,14 +79,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/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt index 090ec3b0e8..18f2c2c6be 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 @@ -4,14 +4,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 @@ -58,7 +55,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 } } @@ -89,7 +86,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)) } } From 2fbb34ba3815c7d59e0ef595f954d8ded5551e50 Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Fri, 9 Mar 2018 16:22:36 +0000 Subject: [PATCH 9/9] CORDA-1171: Update MySQL uniqueness provider to throw the correct exception --- .../transactions/MySQLUniquenessProvider.kt | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) 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