mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
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.
This commit is contained in:
parent
a3bf4577f3
commit
2d31247da2
@ -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 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
|
@net.corda.core.serialization.CordaSerializable public static final class net.corda.core.flows.NotaryError$Conflict extends net.corda.core.flows.NotaryError
|
||||||
public <init>(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.SecureHash component1()
|
||||||
@org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SignedData component2()
|
@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, net.corda.core.crypto.SignedData)
|
@org.jetbrains.annotations.NotNull public final net.corda.core.flows.NotaryError$Conflict copy(net.corda.core.crypto.SecureHash, Map)
|
||||||
public boolean equals(Object)
|
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()
|
@org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getTxId()
|
||||||
public int hashCode()
|
public int hashCode()
|
||||||
@org.jetbrains.annotations.NotNull public String toString()
|
@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
|
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
|
@net.corda.core.serialization.CordaSerializable public final class net.corda.core.flows.NotaryException extends net.corda.core.flows.FlowException
|
||||||
public <init>(net.corda.core.flows.NotaryError)
|
public <init>(net.corda.core.flows.NotaryError, net.corda.core.crypto.SecureHash)
|
||||||
@org.jetbrains.annotations.NotNull public final net.corda.core.flows.NotaryError getError()
|
@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 final class net.corda.core.flows.NotaryFlow extends java.lang.Object
|
||||||
public <init>()
|
public <init>()
|
||||||
##
|
##
|
||||||
@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 <init>(net.corda.core.transactions.SignedTransaction)
|
public <init>(net.corda.core.transactions.SignedTransaction)
|
||||||
public <init>(net.corda.core.transactions.SignedTransaction, net.corda.core.utilities.ProgressTracker)
|
public <init>(net.corda.core.transactions.SignedTransaction, net.corda.core.utilities.ProgressTracker)
|
||||||
@co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public List call()
|
@co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public List call()
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
package net.corda.core.flows
|
package net.corda.core.flows
|
||||||
|
|
||||||
import net.corda.core.contracts.StateRef
|
import net.corda.core.contracts.StateRef
|
||||||
import net.corda.core.crypto.DigitalSignature
|
import net.corda.core.crypto.*
|
||||||
import net.corda.core.crypto.SecureHash
|
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
@ -43,7 +42,7 @@ class NotarisationRequest(statesToConsume: List<StateRef>, val transactionId: Se
|
|||||||
val signature = requestSignature.digitalSignature
|
val signature = requestSignature.digitalSignature
|
||||||
if (intendedSigner.owningKey != signature.by) {
|
if (intendedSigner.owningKey != signature.by) {
|
||||||
val errorMessage = "Expected a signature by ${intendedSigner.owningKey}, but received by ${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
|
// 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
|
// 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<StateRef>, val transactionId: Se
|
|||||||
when (e) {
|
when (e) {
|
||||||
is InvalidKeyException, is SignatureException -> {
|
is InvalidKeyException, is SignatureException -> {
|
||||||
val error = NotaryError.RequestSignatureInvalid(e)
|
val error = NotaryError.RequestSignatureInvalid(e)
|
||||||
throw NotaryException(error)
|
throw NotaryInternalException(error)
|
||||||
}
|
}
|
||||||
else -> throw e
|
else -> throw e
|
||||||
}
|
}
|
||||||
@ -98,4 +97,8 @@ data class NotarisationPayload(val transaction: Any, val requestSignature: Notar
|
|||||||
* Should only be used by non-validating notaries.
|
* Should only be used by non-validating notaries.
|
||||||
*/
|
*/
|
||||||
val coreTransaction get() = transaction as CoreTransaction
|
val coreTransaction get() = transaction as CoreTransaction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Payload returned by the notary service flow to the client. */
|
||||||
|
@CordaSerializable
|
||||||
|
data class NotarisationResponse(val signatures: List<TransactionSignature>)
|
@ -1,27 +1,24 @@
|
|||||||
package net.corda.core.flows
|
package net.corda.core.flows
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import net.corda.core.DoNotImplement
|
||||||
import net.corda.core.contracts.StateRef
|
import net.corda.core.contracts.StateRef
|
||||||
import net.corda.core.contracts.TimeWindow
|
import net.corda.core.contracts.TimeWindow
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.SignedData
|
|
||||||
import net.corda.core.crypto.TransactionSignature
|
import net.corda.core.crypto.TransactionSignature
|
||||||
import net.corda.core.crypto.keys
|
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.FetchDataFlow
|
import net.corda.core.internal.FetchDataFlow
|
||||||
import net.corda.core.internal.generateSignature
|
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.NotaryService
|
||||||
import net.corda.core.node.services.TrustedAuthorityNotaryService
|
import net.corda.core.node.services.TrustedAuthorityNotaryService
|
||||||
import net.corda.core.node.services.UniquenessProvider
|
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.transactions.CoreTransaction
|
|
||||||
import net.corda.core.transactions.ContractUpgradeWireTransaction
|
import net.corda.core.transactions.ContractUpgradeWireTransaction
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.transactions.WireTransaction
|
import net.corda.core.transactions.WireTransaction
|
||||||
import net.corda.core.utilities.ProgressTracker
|
import net.corda.core.utilities.ProgressTracker
|
||||||
import net.corda.core.utilities.UntrustworthyData
|
import net.corda.core.utilities.UntrustworthyData
|
||||||
import net.corda.core.utilities.unwrap
|
import net.corda.core.utilities.unwrap
|
||||||
import java.security.SignatureException
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.function.Predicate
|
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
|
* @throws NotaryException in case the any of the inputs to the transaction have been consumed
|
||||||
* by another transaction or the time-window is invalid.
|
* by another transaction or the time-window is invalid.
|
||||||
*/
|
*/
|
||||||
|
@DoNotImplement
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
open class Client(private val stx: SignedTransaction,
|
open class Client(private val stx: SignedTransaction,
|
||||||
override val progressTracker: ProgressTracker) : FlowLogic<List<TransactionSignature>>() {
|
override val progressTracker: ProgressTracker) : FlowLogic<List<TransactionSignature>>() {
|
||||||
@ -68,44 +66,32 @@ class NotaryFlow {
|
|||||||
check(serviceHub.loadStates(stx.inputs.toSet()).all { it.state.notary == notaryParty }) {
|
check(serviceHub.loadStates(stx.inputs.toSet()).all { it.state.notary == notaryParty }) {
|
||||||
"Input states must have the same Notary"
|
"Input states must have the same Notary"
|
||||||
}
|
}
|
||||||
|
stx.resolveTransactionWithSignatures(serviceHub).verifySignaturesExcept(notaryParty.owningKey)
|
||||||
try {
|
|
||||||
stx.resolveTransactionWithSignatures(serviceHub).verifySignaturesExcept(notaryParty.owningKey)
|
|
||||||
} catch (ex: SignatureException) {
|
|
||||||
throw NotaryException(NotaryError.TransactionInvalid(ex))
|
|
||||||
}
|
|
||||||
return notaryParty
|
return notaryParty
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Notarises the transaction with the [notaryParty], obtains the notary's signature(s). */
|
/** Notarises the transaction with the [notaryParty], obtains the notary's signature(s). */
|
||||||
@Throws(NotaryException::class)
|
@Throws(NotaryException::class)
|
||||||
@Suspendable
|
@Suspendable
|
||||||
protected fun notarise(notaryParty: Party): UntrustworthyData<List<TransactionSignature>> {
|
protected fun notarise(notaryParty: Party): UntrustworthyData<NotarisationResponse> {
|
||||||
return try {
|
val session = initiateFlow(notaryParty)
|
||||||
val session = initiateFlow(notaryParty)
|
val requestSignature = NotarisationRequest(stx.inputs, stx.id).generateSignature(serviceHub)
|
||||||
val requestSignature = NotarisationRequest(stx.inputs, stx.id).generateSignature(serviceHub)
|
return if (serviceHub.networkMapCache.isValidatingNotary(notaryParty)) {
|
||||||
if (serviceHub.networkMapCache.isValidatingNotary(notaryParty)) {
|
sendAndReceiveValidating(session, requestSignature)
|
||||||
sendAndReceiveValidating(session, requestSignature)
|
} else {
|
||||||
} else {
|
sendAndReceiveNonValidating(notaryParty, session, requestSignature)
|
||||||
sendAndReceiveNonValidating(notaryParty, session, requestSignature)
|
|
||||||
}
|
|
||||||
} catch (e: NotaryException) {
|
|
||||||
if (e.error is NotaryError.Conflict) {
|
|
||||||
e.error.conflict.verified()
|
|
||||||
}
|
|
||||||
throw e
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
private fun sendAndReceiveValidating(session: FlowSession, signature: NotarisationRequestSignature): UntrustworthyData<List<TransactionSignature>> {
|
private fun sendAndReceiveValidating(session: FlowSession, signature: NotarisationRequestSignature): UntrustworthyData<NotarisationResponse> {
|
||||||
val payload = NotarisationPayload(stx, signature)
|
val payload = NotarisationPayload(stx, signature)
|
||||||
subFlow(NotarySendTransactionFlow(session, payload))
|
subFlow(NotarySendTransactionFlow(session, payload))
|
||||||
return session.receive()
|
return session.receive()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
private fun sendAndReceiveNonValidating(notaryParty: Party, session: FlowSession, signature: NotarisationRequestSignature): UntrustworthyData<List<TransactionSignature>> {
|
private fun sendAndReceiveNonValidating(notaryParty: Party, session: FlowSession, signature: NotarisationRequestSignature): UntrustworthyData<NotarisationResponse> {
|
||||||
val ctx = stx.coreTransaction
|
val ctx = stx.coreTransaction
|
||||||
val tx = when (ctx) {
|
val tx = when (ctx) {
|
||||||
is ContractUpgradeWireTransaction -> ctx.buildFilteredTransaction()
|
is ContractUpgradeWireTransaction -> ctx.buildFilteredTransaction()
|
||||||
@ -116,18 +102,13 @@ class NotaryFlow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Checks that the notary's signature(s) is/are valid. */
|
/** Checks that the notary's signature(s) is/are valid. */
|
||||||
protected fun validateResponse(response: UntrustworthyData<List<TransactionSignature>>, notaryParty: Party): List<TransactionSignature> {
|
protected fun validateResponse(response: UntrustworthyData<NotarisationResponse>, notaryParty: Party): List<TransactionSignature> {
|
||||||
return response.unwrap { signatures ->
|
return response.unwrap {
|
||||||
signatures.forEach { validateSignature(it, stx.id, notaryParty) }
|
it.validateSignatures(stx.id, notaryParty)
|
||||||
signatures
|
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
|
* The [NotarySendTransactionFlow] flow is similar to [SendTransactionFlow], but uses [NotarisationPayload] as the
|
||||||
* initial message, and retries message delivery.
|
* initial message, and retries message delivery.
|
||||||
@ -156,11 +137,17 @@ class NotaryFlow {
|
|||||||
check(serviceHub.myInfo.legalIdentities.any { serviceHub.networkMapCache.isNotary(it) }) {
|
check(serviceHub.myInfo.legalIdentities.any { serviceHub.networkMapCache.isNotary(it) }) {
|
||||||
"We are not a notary on the network"
|
"We are not a notary on the network"
|
||||||
}
|
}
|
||||||
val (id, inputs, timeWindow, notary) = receiveAndVerifyTx()
|
var txId: SecureHash? = null
|
||||||
checkNotary(notary)
|
try {
|
||||||
service.validateTimeWindow(timeWindow)
|
val parts = receiveAndVerifyTx()
|
||||||
service.commitInputStates(inputs, id, otherSideSession.counterparty)
|
txId = parts.id
|
||||||
signAndSendResponse(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
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,14 +162,14 @@ class NotaryFlow {
|
|||||||
@Suspendable
|
@Suspendable
|
||||||
protected fun checkNotary(notary: Party?) {
|
protected fun checkNotary(notary: Party?) {
|
||||||
if (notary?.owningKey != service.notaryIdentityKey) {
|
if (notary?.owningKey != service.notaryIdentityKey) {
|
||||||
throw NotaryException(NotaryError.WrongNotary)
|
throw NotaryInternalException(NotaryError.WrongNotary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
private fun signAndSendResponse(txId: SecureHash) {
|
private fun signTransactionAndSendResponse(txId: SecureHash) {
|
||||||
val signature = service.sign(txId)
|
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<StateRef>, val
|
|||||||
* Exception thrown by the notary service if any issues are encountered while trying to commit a transaction. The
|
* 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.
|
* 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. */
|
/** Specifies the cause for notarisation request failure. */
|
||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
sealed class NotaryError {
|
sealed class NotaryError {
|
||||||
/** Occurs when one or more input states of transaction with [txId] have already been consumed by another transaction. */
|
/** Occurs when one or more input states have already been consumed by another transaction. */
|
||||||
data class Conflict(val txId: SecureHash, val conflict: SignedData<UniquenessProvider.Conflict>) : NotaryError() {
|
data class Conflict(
|
||||||
override fun toString() = "One or more input states for transaction $txId have been used in another transaction"
|
/** 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<StateRef, StateConsumptionDetails>
|
||||||
|
) : 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. */
|
/** 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()
|
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
|
||||||
|
)
|
16
core/src/main/kotlin/net/corda/core/internal/NotaryUtils.kt
Normal file
16
core/src/main/kotlin/net/corda/core/internal/NotaryUtils.kt
Normal file
@ -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) }
|
||||||
|
}
|
@ -8,7 +8,6 @@ import net.corda.core.flows.*
|
|||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.node.ServiceHub
|
import net.corda.core.node.ServiceHub
|
||||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||||
import net.corda.core.serialization.serialize
|
|
||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import java.security.PublicKey
|
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.
|
* the [NotaryError.TimeWindowInvalid] error.
|
||||||
*/
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@Throws(NotaryException::class)
|
@Throws(NotaryInternalException::class)
|
||||||
fun validateTimeWindow(clock: Clock, timeWindow: TimeWindow?) {
|
fun validateTimeWindow(clock: Clock, timeWindow: TimeWindow?) {
|
||||||
if (timeWindow == null) return
|
if (timeWindow == null) return
|
||||||
val currentTime = clock.instant()
|
val currentTime = clock.instant()
|
||||||
if (currentTime !in timeWindow) {
|
if (currentTime !in timeWindow) {
|
||||||
throw NotaryException(
|
throw NotaryInternalException(
|
||||||
NotaryError.TimeWindowInvalid(currentTime, timeWindow)
|
NotaryError.TimeWindowInvalid(currentTime, timeWindow)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -82,28 +82,24 @@ abstract class TrustedAuthorityNotaryService : NotaryService() {
|
|||||||
fun commitInputStates(inputs: List<StateRef>, txId: SecureHash, caller: Party) {
|
fun commitInputStates(inputs: List<StateRef>, txId: SecureHash, caller: Party) {
|
||||||
try {
|
try {
|
||||||
uniquenessProvider.commit(inputs, txId, caller)
|
uniquenessProvider.commit(inputs, txId, caller)
|
||||||
} catch (e: UniquenessException) {
|
} catch (e: NotaryInternalException) {
|
||||||
val conflicts = inputs.filterIndexed { i, stateRef ->
|
if (e.error is NotaryError.Conflict) {
|
||||||
val consumingTx = e.error.stateHistory[stateRef]
|
val conflicts = inputs.filterIndexed { _, stateRef ->
|
||||||
consumingTx != null && consumingTx != UniquenessProvider.ConsumingTx(txId, i, caller)
|
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.
|
if (conflicts.isNotEmpty()) {
|
||||||
log.warn("Notary conflicts for $txId: $conflicts")
|
// TODO: Create a new UniquenessException that only contains the conflicts filtered above.
|
||||||
throw notaryException(txId, e)
|
log.info("Notary conflicts for $txId: $conflicts")
|
||||||
}
|
throw e
|
||||||
|
}
|
||||||
|
} else throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log.error("Internal error", e)
|
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. */
|
/** Sign a [ByteArray] input. */
|
||||||
fun sign(bits: ByteArray): DigitalSignature.WithKey {
|
fun sign(bits: ByteArray): DigitalSignature.WithKey {
|
||||||
return services.keyManagementService.sign(bits, notaryIdentityKey)
|
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.
|
// 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")
|
@Deprecated("This property is no longer used")
|
||||||
protected open val timeWindowChecker: TimeWindowChecker get() = throw UnsupportedOperationException("No default implementation, need to override")
|
@Suppress("DEPRECATION")
|
||||||
|
protected open val timeWindowChecker: TimeWindowChecker
|
||||||
|
get() = throw UnsupportedOperationException("No default implementation, need to override")
|
||||||
}
|
}
|
@ -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.
|
* A uniqueness provider is expected to be used from within the context of a flow.
|
||||||
*/
|
*/
|
||||||
interface UniquenessProvider {
|
interface UniquenessProvider {
|
||||||
/** Commits all input states of the given transaction */
|
/** Commits all input states of the given transaction. */
|
||||||
fun commit(states: List<StateRef>, txId: SecureHash, callerIdentity: Party)
|
fun commit(states: List<StateRef>, txId: SecureHash, callerIdentity: Party)
|
||||||
|
|
||||||
/** Specifies the consuming transaction for every conflicting state */
|
/** Specifies the consuming transaction for every conflicting state. */
|
||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
|
@Deprecated("No longer used due to potential privacy leak")
|
||||||
data class Conflict(val stateHistory: Map<StateRef, ConsumingTx>)
|
data class Conflict(val stateHistory: Map<StateRef, ConsumingTx>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specifies the transaction id, the position of the consumed state in the inputs, and
|
* Specifies the transaction id, the position of the consumed state in the inputs, and
|
||||||
* the caller identity requesting the commit.
|
* 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
|
@CordaSerializable
|
||||||
data class ConsumingTx(val id: SecureHash, val inputIndex: Int, val requestingParty: Party)
|
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)
|
class UniquenessException(val error: UniquenessProvider.Conflict) : CordaException(UniquenessException::class.java.name)
|
@ -3,9 +3,10 @@
|
|||||||
Writing a custom notary service (experimental)
|
Writing a custom notary service (experimental)
|
||||||
==============================================
|
==============================================
|
||||||
|
|
||||||
.. warning:: Customising a notary service is still an experimental feature and not recommended for most use-cases. Currently,
|
.. warning:: Customising a notary service is still an experimental feature and not recommended for most use-cases. The APIs
|
||||||
customising Raft or BFT notaries is not yet fully supported. If you want to write your own Raft notary you will have to
|
for writing a custom notary may change in the future. Additionally, customising Raft or BFT notaries is not yet
|
||||||
implement a custom database connector (or use a separate database for the notary), and use a custom configuration file.
|
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
|
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
|
with ``@CordaService``. The Corda node scans for any class with this annotation and initialises them. The custom notary
|
||||||
|
@ -6,6 +6,7 @@ import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint
|
|||||||
import net.corda.core.contracts.ContractState
|
import net.corda.core.contracts.ContractState
|
||||||
import net.corda.core.contracts.StateRef
|
import net.corda.core.contracts.StateRef
|
||||||
import net.corda.core.crypto.CompositeKey
|
import net.corda.core.crypto.CompositeKey
|
||||||
|
import net.corda.core.crypto.sha256
|
||||||
import net.corda.core.flows.NotaryError
|
import net.corda.core.flows.NotaryError
|
||||||
import net.corda.core.flows.NotaryException
|
import net.corda.core.flows.NotaryException
|
||||||
import net.corda.core.flows.NotaryFlow
|
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.nodeapi.internal.network.NetworkParametersCopier
|
||||||
import net.corda.testing.common.internal.testNetworkParameters
|
import net.corda.testing.common.internal.testNetworkParameters
|
||||||
import net.corda.testing.contracts.DummyContract
|
import net.corda.testing.contracts.DummyContract
|
||||||
import net.corda.testing.core.singleIdentity
|
|
||||||
import net.corda.testing.core.dummyCommand
|
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
|
||||||
import net.corda.testing.node.internal.InternalMockNetwork.MockNode
|
import net.corda.testing.node.internal.InternalMockNetwork.MockNode
|
||||||
import net.corda.testing.node.internal.InternalMockNodeParameters
|
import net.corda.testing.node.internal.InternalMockNodeParameters
|
||||||
@ -142,13 +143,12 @@ class BFTNotaryServiceTests {
|
|||||||
}.single()
|
}.single()
|
||||||
spendTxs.zip(results).forEach { (tx, result) ->
|
spendTxs.zip(results).forEach { (tx, result) ->
|
||||||
if (result is Try.Failure) {
|
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)
|
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(StateRef(issueTx.id, 0), stateRef)
|
||||||
assertEquals(spendTxs[successfulIndex].id, consumingTx.id)
|
assertEquals(spendTxs[successfulIndex].id.sha256(), cause.hashOfTransactionId)
|
||||||
assertEquals(0, consumingTx.inputIndex)
|
|
||||||
assertEquals(info.singleIdentity(), consumingTx.requestingParty)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,16 +4,11 @@ import co.paralleluniverse.fibers.Suspendable
|
|||||||
import com.google.common.util.concurrent.SettableFuture
|
import com.google.common.util.concurrent.SettableFuture
|
||||||
import net.corda.core.contracts.StateRef
|
import net.corda.core.contracts.StateRef
|
||||||
import net.corda.core.crypto.Crypto
|
import net.corda.core.crypto.Crypto
|
||||||
import net.corda.core.crypto.DigitalSignature
|
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.crypto.SignedData
|
||||||
import net.corda.core.flows.FlowSession
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.flows.NotaryError
|
|
||||||
import net.corda.core.flows.NotaryException
|
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.identity.Party
|
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.NotaryService
|
||||||
import net.corda.core.node.services.UniquenessProvider
|
import net.corda.core.node.services.UniquenessProvider
|
||||||
import net.corda.core.schemas.PersistentStateRef
|
import net.corda.core.schemas.PersistentStateRef
|
||||||
@ -81,18 +76,22 @@ class BFTNonValidatingNotaryService(
|
|||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call(): Void? {
|
override fun call(): Void? {
|
||||||
val payload = otherSideSession.receive<NotarisationPayload>().unwrap { it }
|
val payload = otherSideSession.receive<NotarisationPayload>().unwrap { it }
|
||||||
val signatures = commit(payload)
|
val response = commit(payload)
|
||||||
otherSideSession.send(signatures)
|
otherSideSession.send(response)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun commit(payload: NotarisationPayload): List<DigitalSignature> {
|
private fun commit(payload: NotarisationPayload): NotarisationResponse {
|
||||||
val response = service.commitTransaction(payload, otherSideSession.counterparty)
|
val response = service.commitTransaction(payload, otherSideSession.counterparty)
|
||||||
when (response) {
|
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 -> {
|
is BFTSMaRt.ClusterResponse.Signatures -> {
|
||||||
log.debug("All input states of transaction ${payload.coreTransaction.id} have been committed")
|
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 inputs = transaction.inputs
|
||||||
val notary = transaction.notary
|
val notary = transaction.notary
|
||||||
if (transaction is FilteredTransaction) NotaryService.validateTimeWindow(services.clock, transaction.timeWindow)
|
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)
|
commitInputStates(inputs, id, callerIdentity)
|
||||||
log.debug { "Inputs committed successfully, signing $id" }
|
log.debug { "Inputs committed successfully, signing $id" }
|
||||||
BFTSMaRt.ReplicaResponse.Signature(sign(id))
|
BFTSMaRt.ReplicaResponse.Signature(sign(id))
|
||||||
} catch (e: NotaryException) {
|
} catch (e: NotaryInternalException) {
|
||||||
log.debug { "Error processing transaction: ${e.error}" }
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,10 +14,8 @@ import bftsmart.tom.server.defaultservices.DefaultReplier
|
|||||||
import bftsmart.tom.util.Extractor
|
import bftsmart.tom.util.Extractor
|
||||||
import net.corda.core.contracts.StateRef
|
import net.corda.core.contracts.StateRef
|
||||||
import net.corda.core.crypto.*
|
import net.corda.core.crypto.*
|
||||||
import net.corda.core.flows.NotaryError
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.flows.NotaryException
|
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.flows.NotarisationPayload
|
|
||||||
import net.corda.core.internal.declaredField
|
import net.corda.core.internal.declaredField
|
||||||
import net.corda.core.internal.toTypedArray
|
import net.corda.core.internal.toTypedArray
|
||||||
import net.corda.core.node.services.UniquenessProvider
|
import net.corda.core.node.services.UniquenessProvider
|
||||||
@ -56,15 +54,15 @@ object BFTSMaRt {
|
|||||||
/** Sent from [Replica] to [Client]. */
|
/** Sent from [Replica] to [Client]. */
|
||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
sealed class ReplicaResponse {
|
sealed class ReplicaResponse {
|
||||||
data class Error(val error: NotaryError) : ReplicaResponse()
|
data class Error(val error: SignedData<NotaryError>) : ReplicaResponse()
|
||||||
data class Signature(val txSignature: DigitalSignature) : ReplicaResponse()
|
data class Signature(val txSignature: TransactionSignature) : ReplicaResponse()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** An aggregate response from all replica ([Replica]) replies sent from [Client] back to the calling application. */
|
/** An aggregate response from all replica ([Replica]) replies sent from [Client] back to the calling application. */
|
||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
sealed class ClusterResponse {
|
sealed class ClusterResponse {
|
||||||
data class Error(val error: NotaryError) : ClusterResponse()
|
data class Error(val errors: List<SignedData<NotaryError>>) : ClusterResponse()
|
||||||
data class Signatures(val txSignatures: List<DigitalSignature>) : ClusterResponse()
|
data class Signatures(val txSignatures: List<TransactionSignature>) : ClusterResponse()
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Cluster {
|
interface Cluster {
|
||||||
@ -136,7 +134,7 @@ object BFTSMaRt {
|
|||||||
ClusterResponse.Signatures(accepted.map { it.txSignature })
|
ClusterResponse.Signatures(accepted.map { it.txSignature })
|
||||||
} else {
|
} else {
|
||||||
log.debug { "Cluster response - error: ${rejected.first().error}" }
|
log.debug { "Cluster response - error: ${rejected.first().error}" }
|
||||||
ClusterResponse.Error(rejected.first().error)
|
ClusterResponse.Error(rejected.map { it.error })
|
||||||
}
|
}
|
||||||
|
|
||||||
val messageContent = aggregateResponse.serialize().bytes
|
val messageContent = aggregateResponse.serialize().bytes
|
||||||
@ -232,10 +230,9 @@ object BFTSMaRt {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.debug { "Conflict detected – the following inputs have already been committed: ${conflicts.keys.joinToString()}" }
|
log.debug { "Conflict detected – the following inputs have already been committed: ${conflicts.keys.joinToString()}" }
|
||||||
val conflict = UniquenessProvider.Conflict(conflicts)
|
val conflict = conflicts.mapValues { StateConsumptionDetails(it.value.id.sha256()) }
|
||||||
val conflictData = conflict.serialize()
|
val error = NotaryError.Conflict(txId, conflict)
|
||||||
val signedConflict = SignedData(conflictData, sign(conflictData.bytes))
|
throw NotaryInternalException(error)
|
||||||
throw NotaryException(NotaryError.Conflict(txId, signedConflict))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,13 @@ package net.corda.node.services.transactions
|
|||||||
import net.corda.core.contracts.StateRef
|
import net.corda.core.contracts.StateRef
|
||||||
import net.corda.core.crypto.Crypto
|
import net.corda.core.crypto.Crypto
|
||||||
import net.corda.core.crypto.SecureHash
|
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.CordaX500Name
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.ThreadBox
|
import net.corda.core.internal.ThreadBox
|
||||||
import net.corda.core.node.services.UniquenessException
|
|
||||||
import net.corda.core.node.services.UniquenessProvider
|
import net.corda.core.node.services.UniquenessProvider
|
||||||
import net.corda.core.schemas.PersistentStateRef
|
import net.corda.core.schemas.PersistentStateRef
|
||||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||||
@ -68,8 +71,9 @@ class PersistentUniquenessProvider : UniquenessProvider, SingletonSerializeAsTok
|
|||||||
toPersistentEntityKey = { PersistentStateRef(it.txhash.toString(), it.index) },
|
toPersistentEntityKey = { PersistentStateRef(it.txhash.toString(), it.index) },
|
||||||
fromPersistentEntity = {
|
fromPersistentEntity = {
|
||||||
//TODO null check will become obsolete after making DB/JPA columns not nullable
|
//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")
|
val txId = it.id.txId
|
||||||
var index = it.id.index ?: throw IllegalStateException("DB returned null SecureHash index")
|
?: 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),
|
Pair(StateRef(txhash = SecureHash.parse(txId), index = index),
|
||||||
UniquenessProvider.ConsumingTx(
|
UniquenessProvider.ConsumingTx(
|
||||||
id = SecureHash.parse(it.consumingTxHash),
|
id = SecureHash.parse(it.consumingTxHash),
|
||||||
@ -91,7 +95,6 @@ class PersistentUniquenessProvider : UniquenessProvider, SingletonSerializeAsTok
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun commit(states: List<StateRef>, txId: SecureHash, callerIdentity: Party) {
|
override fun commit(states: List<StateRef>, txId: SecureHash, callerIdentity: Party) {
|
||||||
|
|
||||||
val conflict = mutex.locked {
|
val conflict = mutex.locked {
|
||||||
val conflictingStates = LinkedHashMap<StateRef, UniquenessProvider.ConsumingTx>()
|
val conflictingStates = LinkedHashMap<StateRef, UniquenessProvider.ConsumingTx>()
|
||||||
for (inputState in states) {
|
for (inputState in states) {
|
||||||
@ -100,7 +103,8 @@ class PersistentUniquenessProvider : UniquenessProvider, SingletonSerializeAsTok
|
|||||||
}
|
}
|
||||||
if (conflictingStates.isNotEmpty()) {
|
if (conflictingStates.isNotEmpty()) {
|
||||||
log.debug("Failure, input states already committed: ${conflictingStates.keys}")
|
log.debug("Failure, input states already committed: ${conflictingStates.keys}")
|
||||||
UniquenessProvider.Conflict(conflictingStates)
|
val conflict = conflictingStates.mapValues { StateConsumptionDetails(it.value.id.sha256()) }
|
||||||
|
conflict
|
||||||
} else {
|
} else {
|
||||||
states.forEachIndexed { i, stateRef ->
|
states.forEachIndexed { i, stateRef ->
|
||||||
committedStates[stateRef] = UniquenessProvider.ConsumingTx(txId, i, callerIdentity)
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,8 +19,11 @@ import io.atomix.copycat.server.storage.Storage
|
|||||||
import io.atomix.copycat.server.storage.StorageLevel
|
import io.atomix.copycat.server.storage.StorageLevel
|
||||||
import net.corda.core.contracts.StateRef
|
import net.corda.core.contracts.StateRef
|
||||||
import net.corda.core.crypto.SecureHash
|
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.identity.Party
|
||||||
import net.corda.core.node.services.UniquenessException
|
|
||||||
import net.corda.core.node.services.UniquenessProvider
|
import net.corda.core.node.services.UniquenessProvider
|
||||||
import net.corda.core.serialization.SerializationDefaults
|
import net.corda.core.serialization.SerializationDefaults
|
||||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||||
@ -204,7 +207,11 @@ class RaftUniquenessProvider(private val transportConfiguration: NodeSSLConfigur
|
|||||||
val commitCommand = DistributedImmutableMap.Commands.PutAll(encode(entries))
|
val commitCommand = DistributedImmutableMap.Commands.PutAll(encode(entries))
|
||||||
val conflicts = client.submit(commitCommand).get()
|
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")
|
log.debug("All input states of transaction $txId have been committed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthor
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw when (e) {
|
throw when (e) {
|
||||||
is TransactionVerificationException,
|
is TransactionVerificationException,
|
||||||
is SignatureException -> NotaryException(NotaryError.TransactionInvalid(e))
|
is SignatureException -> NotaryInternalException(NotaryError.TransactionInvalid(e))
|
||||||
else -> e
|
else -> e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -67,7 +67,7 @@ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthor
|
|||||||
try {
|
try {
|
||||||
tx.verifySignaturesExcept(service.notaryIdentityKey)
|
tx.verifySignaturesExcept(service.notaryIdentityKey)
|
||||||
} catch (e: SignatureException) {
|
} catch (e: SignatureException) {
|
||||||
throw NotaryException(NotaryError.TransactionInvalid(e))
|
throw NotaryInternalException(NotaryError.TransactionInvalid(e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,7 @@ package net.corda.node.services.transactions
|
|||||||
import net.corda.core.concurrent.CordaFuture
|
import net.corda.core.concurrent.CordaFuture
|
||||||
import net.corda.core.contracts.StateAndRef
|
import net.corda.core.contracts.StateAndRef
|
||||||
import net.corda.core.contracts.StateRef
|
import net.corda.core.contracts.StateRef
|
||||||
import net.corda.core.crypto.Crypto
|
import net.corda.core.crypto.*
|
||||||
import net.corda.core.crypto.SecureHash
|
|
||||||
import net.corda.core.crypto.TransactionSignature
|
|
||||||
import net.corda.core.crypto.sign
|
|
||||||
import net.corda.core.flows.*
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.generateSignature
|
import net.corda.core.internal.generateSignature
|
||||||
@ -137,32 +134,45 @@ class NotaryServiceTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should report conflict when inputs are reused across transactions`() {
|
fun `should report conflict when inputs are reused across transactions`() {
|
||||||
val inputState = issueState(aliceNode.services, alice)
|
val firstState = issueState(aliceNode.services, alice)
|
||||||
val stx = run {
|
val secondState = issueState(aliceNode.services, alice)
|
||||||
val tx = TransactionBuilder(notary)
|
|
||||||
.addInputState(inputState)
|
fun spendState(state: StateAndRef<*>): SignedTransaction {
|
||||||
.addCommand(dummyCommand(alice.owningKey))
|
val stx = run {
|
||||||
aliceNode.services.signInitialTransaction(tx)
|
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)
|
val tx = TransactionBuilder(notary)
|
||||||
.addInputState(inputState)
|
|
||||||
.addInputState(issueState(aliceNode.services, alice))
|
.addInputState(issueState(aliceNode.services, alice))
|
||||||
|
.addInputState(firstState)
|
||||||
|
.addInputState(secondState)
|
||||||
.addCommand(dummyCommand(alice.owningKey))
|
.addCommand(dummyCommand(alice.owningKey))
|
||||||
aliceNode.services.signInitialTransaction(tx)
|
aliceNode.services.signInitialTransaction(tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
val firstSpend = NotaryFlow.Client(stx)
|
val doubleSpend = NotaryFlow.Client(doubleSpendTx) // Double spend the inputState in a second transaction.
|
||||||
val secondSpend = NotaryFlow.Client(stx2) // Double spend the inputState in a second transaction.
|
val future = aliceNode.services.startFlow(doubleSpend)
|
||||||
aliceNode.services.startFlow(firstSpend)
|
|
||||||
val future = aliceNode.services.startFlow(secondSpend)
|
|
||||||
|
|
||||||
mockNet.runNetwork()
|
mockNet.runNetwork()
|
||||||
|
|
||||||
val ex = assertFailsWith(NotaryException::class) { future.resultFuture.getOrThrow() }
|
val ex = assertFailsWith(NotaryException::class) { future.resultFuture.getOrThrow() }
|
||||||
val notaryError = ex.error as NotaryError.Conflict
|
val notaryError = ex.error as NotaryError.Conflict
|
||||||
assertEquals(notaryError.txId, stx2.id)
|
assertEquals(notaryError.txId, doubleSpendTx.id)
|
||||||
notaryError.conflict.verified()
|
with(notaryError) {
|
||||||
|
assertEquals(consumedStates.size, 2)
|
||||||
|
assertEquals(consumedStates[firstState.ref]!!.hashOfTransactionId, firstSpendTx.id.sha256())
|
||||||
|
assertEquals(consumedStates[secondState.ref]!!.hashOfTransactionId, secondSpendTx.id.sha256())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
package net.corda.node.services.transactions
|
package net.corda.node.services.transactions
|
||||||
|
|
||||||
import net.corda.core.crypto.SecureHash
|
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.identity.CordaX500Name
|
||||||
import net.corda.core.node.services.UniquenessException
|
|
||||||
import net.corda.node.internal.configureDatabase
|
import net.corda.node.internal.configureDatabase
|
||||||
import net.corda.node.services.schema.NodeSchemaService
|
import net.corda.node.services.schema.NodeSchemaService
|
||||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||||
@ -60,12 +62,11 @@ class PersistentUniquenessProviderTests {
|
|||||||
val inputs = listOf(inputState)
|
val inputs = listOf(inputState)
|
||||||
provider.commit(inputs, txID, identity)
|
provider.commit(inputs, txID, identity)
|
||||||
|
|
||||||
val ex = assertFailsWith<UniquenessException> { provider.commit(inputs, txID, identity) }
|
val ex = assertFailsWith<NotaryInternalException> { provider.commit(inputs, txID, identity) }
|
||||||
|
val error = ex.error as NotaryError.Conflict
|
||||||
|
|
||||||
val consumingTx = ex.error.stateHistory[inputState]!!
|
val conflictCause = error.consumedStates[inputState]!!
|
||||||
assertEquals(consumingTx.id, txID)
|
assertEquals(conflictCause.hashOfTransactionId, txID.sha256())
|
||||||
assertEquals(consumingTx.inputIndex, inputs.indexOf(inputState))
|
|
||||||
assertEquals(consumingTx.requestingParty, identity)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,6 @@ import net.corda.core.node.ServiceHub
|
|||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
import net.corda.node.services.api.StartedNodeServices
|
|
||||||
import net.corda.node.services.issueInvalidState
|
import net.corda.node.services.issueInvalidState
|
||||||
import net.corda.testing.contracts.DummyContract
|
import net.corda.testing.contracts.DummyContract
|
||||||
import net.corda.testing.core.ALICE_NAME
|
import net.corda.testing.core.ALICE_NAME
|
||||||
@ -80,14 +79,13 @@ class ValidatingNotaryServiceTests {
|
|||||||
aliceNode.services.signInitialTransaction(tx)
|
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<SignedTransaction.SignaturesMissingException> {
|
||||||
val future = runClient(stx)
|
val future = runClient(stx)
|
||||||
future.getOrThrow()
|
future.getOrThrow()
|
||||||
}
|
}
|
||||||
val notaryError = ex.error as NotaryError.TransactionInvalid
|
val missingKeys = ex.missing
|
||||||
assertThat(notaryError.cause).isInstanceOf(SignedTransaction.SignaturesMissingException::class.java)
|
|
||||||
|
|
||||||
val missingKeys = (notaryError.cause as SignedTransaction.SignaturesMissingException).missing
|
|
||||||
assertEquals(setOf(expectedMissingKey), missingKeys)
|
assertEquals(setOf(expectedMissingKey), missingKeys)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,14 +4,11 @@ import co.paralleluniverse.fibers.Suspendable
|
|||||||
import net.corda.core.contracts.TimeWindow
|
import net.corda.core.contracts.TimeWindow
|
||||||
import net.corda.core.contracts.TransactionVerificationException
|
import net.corda.core.contracts.TransactionVerificationException
|
||||||
import net.corda.core.flows.*
|
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.ResolveTransactionsFlow
|
||||||
import net.corda.core.internal.validateRequest
|
import net.corda.core.internal.validateRequest
|
||||||
import net.corda.core.node.AppServiceHub
|
import net.corda.core.node.AppServiceHub
|
||||||
import net.corda.core.node.services.CordaService
|
import net.corda.core.node.services.CordaService
|
||||||
import net.corda.core.node.services.TrustedAuthorityNotaryService
|
import net.corda.core.node.services.TrustedAuthorityNotaryService
|
||||||
import net.corda.core.transactions.CoreTransaction
|
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.transactions.TransactionWithSignatures
|
import net.corda.core.transactions.TransactionWithSignatures
|
||||||
import net.corda.core.transactions.WireTransaction
|
import net.corda.core.transactions.WireTransaction
|
||||||
@ -58,7 +55,7 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw when (e) {
|
throw when (e) {
|
||||||
is TransactionVerificationException,
|
is TransactionVerificationException,
|
||||||
is SignatureException -> NotaryException(NotaryError.TransactionInvalid(e))
|
is SignatureException -> NotaryInternalException(NotaryError.TransactionInvalid(e))
|
||||||
else -> e
|
else -> e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -89,7 +86,7 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating
|
|||||||
try {
|
try {
|
||||||
tx.verifySignaturesExcept(service.notaryIdentityKey)
|
tx.verifySignaturesExcept(service.notaryIdentityKey)
|
||||||
} catch (e: SignatureException) {
|
} catch (e: SignatureException) {
|
||||||
throw NotaryException(NotaryError.TransactionInvalid(e))
|
throw NotaryInternalException(NotaryError.TransactionInvalid(e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user