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