mirror of
https://github.com/corda/corda.git
synced 2025-02-20 17:33:15 +00:00
ENT-9823 Rename handleDoubleSpend -> propagateDoubleSpendErrorToPeers (#7338)
This commit is contained in:
parent
fffc3e4c5d
commit
0bd4364653
@ -22,6 +22,7 @@ import net.corda.core.flows.NotaryException
|
||||
import net.corda.core.flows.NotarySigCheck
|
||||
import net.corda.core.flows.ReceiveFinalityFlow
|
||||
import net.corda.core.flows.ReceiveTransactionFlow
|
||||
import net.corda.core.flows.SendTransactionFlow
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.flows.TransactionStatus
|
||||
import net.corda.core.flows.UnexpectedFlowEndException
|
||||
@ -184,9 +185,10 @@ class FinalityFlowTests : WithFinality {
|
||||
catch (e: NotaryException) {
|
||||
val stxId = (e.error as NotaryError.Conflict).txId
|
||||
assertNull(aliceNode.services.validatedTransactions.getTransactionInternal(stxId))
|
||||
// Note: double spend error not propagated to peers by default
|
||||
val (_, txnDsStatusBob) = bobNode.services.validatedTransactions.getTransactionInternal(stxId) ?: fail()
|
||||
assertEquals(TransactionStatus.MISSING_NOTARY_SIG, txnDsStatusBob)
|
||||
// Note: double spend error not propagated to peers by default (corDapp PV = 3)
|
||||
// Un-notarised txn clean-up occurs in ReceiveFinalityFlow upon receipt of UnexpectedFlowEndException
|
||||
assertNull(aliceNode.services.validatedTransactions.getTransactionInternal(stxId))
|
||||
assertTxnRemovedFromDatabase(aliceNode, stxId)
|
||||
}
|
||||
}
|
||||
|
||||
@ -203,9 +205,22 @@ class FinalityFlowTests : WithFinality {
|
||||
assertEquals(TransactionStatus.VERIFIED, txnStatusBob)
|
||||
|
||||
try {
|
||||
aliceNode.startFlowAndRunNetwork(SpendFlow(ref, bobNode.info.singleIdentity(), handleDoubleSpend = true)).resultFuture.getOrThrow()
|
||||
aliceNode.startFlowAndRunNetwork(SpendFlow(ref, bobNode.info.singleIdentity(), propagateDoubleSpendErrorToPeers = true)).resultFuture.getOrThrow()
|
||||
}
|
||||
catch (e: NotaryException) {
|
||||
// note: ReceiveFinalityFlow un-notarised transaction clean-up takes place upon catching NotaryError.Conflict
|
||||
val stxId = (e.error as NotaryError.Conflict).txId
|
||||
assertNull(aliceNode.services.validatedTransactions.getTransactionInternal(stxId))
|
||||
assertTxnRemovedFromDatabase(aliceNode, stxId)
|
||||
assertNull(bobNode.services.validatedTransactions.getTransactionInternal(stxId))
|
||||
assertTxnRemovedFromDatabase(bobNode, stxId)
|
||||
}
|
||||
|
||||
try {
|
||||
aliceNode.startFlowAndRunNetwork(SpendFlow(ref, bobNode.info.singleIdentity(), propagateDoubleSpendErrorToPeers = false)).resultFuture.getOrThrow()
|
||||
}
|
||||
catch (e: NotaryException) {
|
||||
// note: ReceiveFinalityFlow un-notarised transaction clean-up takes place upon catching UnexpectedFlowEndException
|
||||
val stxId = (e.error as NotaryError.Conflict).txId
|
||||
assertNull(aliceNode.services.validatedTransactions.getTransactionInternal(stxId))
|
||||
assertTxnRemovedFromDatabase(aliceNode, stxId)
|
||||
@ -304,6 +319,23 @@ class FinalityFlowTests : WithFinality {
|
||||
assertEquals(TransactionStatus.VERIFIED, txnStatusBobYetAgain)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `two phase finality flow successfully removes un-notarised transaction where initiator fails to send notary signature`() {
|
||||
val bobNode = createBob(platformVersion = PlatformVersionSwitches.TWO_PHASE_FINALITY)
|
||||
|
||||
val ref = aliceNode.startFlowAndRunNetwork(IssueFlow(notary)).resultFuture.getOrThrow()
|
||||
try {
|
||||
aliceNode.startFlowAndRunNetwork(MimicFinalityFailureFlow(ref, bobNode.info.singleIdentity())).resultFuture.getOrThrow()
|
||||
}
|
||||
catch (e: UnexpectedFlowEndException) {
|
||||
val stxId = SecureHash.parse(e.message)
|
||||
assertNull(aliceNode.services.validatedTransactions.getTransactionInternal(stxId))
|
||||
assertTxnRemovedFromDatabase(aliceNode, stxId)
|
||||
assertNull(bobNode.services.validatedTransactions.getTransactionInternal(stxId))
|
||||
assertTxnRemovedFromDatabase(bobNode, stxId)
|
||||
}
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
class IssueFlow(val notary: Party) : FlowLogic<StateAndRef<DummyContract.SingleOwnerState>>() {
|
||||
|
||||
@ -320,7 +352,7 @@ class FinalityFlowTests : WithFinality {
|
||||
@StartableByRPC
|
||||
@InitiatingFlow
|
||||
class SpendFlow(private val stateAndRef: StateAndRef<DummyContract.SingleOwnerState>, private val newOwner: Party,
|
||||
private val handleDoubleSpend: Boolean? = null) : FlowLogic<SignedTransaction>() {
|
||||
private val propagateDoubleSpendErrorToPeers: Boolean? = null) : FlowLogic<SignedTransaction>() {
|
||||
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
@ -328,7 +360,7 @@ class FinalityFlowTests : WithFinality {
|
||||
val signedTransaction = serviceHub.signInitialTransaction(txBuilder, ourIdentity.owningKey)
|
||||
val sessionWithCounterParty = initiateFlow(newOwner)
|
||||
sessionWithCounterParty.sendAndReceive<String>("initial-message")
|
||||
return subFlow(FinalityFlow(signedTransaction, setOf(sessionWithCounterParty), handleDoubleSpend = handleDoubleSpend))
|
||||
return subFlow(FinalityFlow(signedTransaction, setOf(sessionWithCounterParty), propagateDoubleSpendErrorToPeers = propagateDoubleSpendErrorToPeers))
|
||||
}
|
||||
}
|
||||
|
||||
@ -418,6 +450,27 @@ class FinalityFlowTests : WithFinality {
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatingFlow
|
||||
class MimicFinalityFailureFlow(private val stateAndRef: StateAndRef<DummyContract.SingleOwnerState>, private val newOwner: Party) : FlowLogic<SignedTransaction>() {
|
||||
// Mimic FinalityFlow but trigger UnexpectedFlowEndException in ReceiveFinality whilst awaiting receipt of notary signature
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val txBuilder = DummyContract.move(stateAndRef, newOwner)
|
||||
val stxn = serviceHub.signInitialTransaction(txBuilder, ourIdentity.owningKey)
|
||||
val sessionWithCounterParty = initiateFlow(newOwner)
|
||||
subFlow(SendTransactionFlow(sessionWithCounterParty, stxn))
|
||||
throw UnexpectedFlowEndException("${stxn.id}")
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(MimicFinalityFailureFlow::class)
|
||||
class TriggerReceiveFinalityFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
subFlow(ReceiveFinalityFlow(otherSide))
|
||||
}
|
||||
}
|
||||
|
||||
class FinalisationFailedException(val notarisedTxn: SignedTransaction) : FlowException("Failed to finalise transaction with notary signature.")
|
||||
|
||||
private fun createBob(cordapps: List<TestCordappInternal> = emptyList(), platformVersion: Int = PLATFORM_VERSION): TestStartedNode {
|
||||
|
@ -56,13 +56,13 @@ class FinalityFlow private constructor(val transaction: SignedTransaction,
|
||||
private val sessions: Collection<FlowSession>,
|
||||
private val newApi: Boolean,
|
||||
private val statesToRecord: StatesToRecord = ONLY_RELEVANT,
|
||||
private val handleDoubleSpend: Boolean? = null) : FlowLogic<SignedTransaction>() {
|
||||
private val propagateDoubleSpendErrorToPeers: Boolean? = null) : FlowLogic<SignedTransaction>() {
|
||||
|
||||
@CordaInternal
|
||||
data class ExtraConstructorArgs(val oldParticipants: Collection<Party>, val sessions: Collection<FlowSession>, val newApi: Boolean, val statesToRecord: StatesToRecord)
|
||||
data class ExtraConstructorArgs(val oldParticipants: Collection<Party>, val sessions: Collection<FlowSession>, val newApi: Boolean, val statesToRecord: StatesToRecord, val propagateDoubleSpendErrorToPeers: Boolean?)
|
||||
|
||||
@CordaInternal
|
||||
fun getExtraConstructorArgs() = ExtraConstructorArgs(oldParticipants, sessions, newApi, statesToRecord)
|
||||
fun getExtraConstructorArgs() = ExtraConstructorArgs(oldParticipants, sessions, newApi, statesToRecord, propagateDoubleSpendErrorToPeers)
|
||||
|
||||
@Deprecated(DEPRECATION_MSG)
|
||||
constructor(transaction: SignedTransaction, extraRecipients: Set<Party>, progressTracker: ProgressTracker) : this(
|
||||
@ -91,15 +91,15 @@ class FinalityFlow private constructor(val transaction: SignedTransaction,
|
||||
* @param transaction What to commit.
|
||||
* @param sessions A collection of [FlowSession]s for each non-local participant of the transaction. Sessions to non-participants can
|
||||
* also be provided.
|
||||
* @param handleDoubleSpend Whether to catch and propagate Double Spend exception to peers.
|
||||
* @param propagateDoubleSpendErrorToPeers Whether to catch and propagate Double Spend exception to peers.
|
||||
*/
|
||||
@JvmOverloads
|
||||
constructor(
|
||||
transaction: SignedTransaction,
|
||||
sessions: Collection<FlowSession>,
|
||||
progressTracker: ProgressTracker = tracker(),
|
||||
handleDoubleSpend: Boolean? = null
|
||||
) : this(transaction, emptyList(), progressTracker, sessions, true, handleDoubleSpend = handleDoubleSpend)
|
||||
propagateDoubleSpendErrorToPeers: Boolean? = null
|
||||
) : this(transaction, emptyList(), progressTracker, sessions, true, propagateDoubleSpendErrorToPeers = propagateDoubleSpendErrorToPeers)
|
||||
|
||||
/**
|
||||
* Notarise the given transaction and broadcast it to all the participants.
|
||||
@ -108,7 +108,7 @@ class FinalityFlow private constructor(val transaction: SignedTransaction,
|
||||
* @param sessions A collection of [FlowSession]s for each non-local participant of the transaction. Sessions to non-participants can
|
||||
* also be provided.
|
||||
* @param statesToRecord Which states to commit to the vault.
|
||||
* @param handleDoubleSpend Whether to catch and propagate Double Spend exception to peers.
|
||||
* @param propagateDoubleSpendErrorToPeers Whether to catch and propagate Double Spend exception to peers.
|
||||
*/
|
||||
@JvmOverloads
|
||||
constructor(
|
||||
@ -116,8 +116,8 @@ class FinalityFlow private constructor(val transaction: SignedTransaction,
|
||||
sessions: Collection<FlowSession>,
|
||||
statesToRecord: StatesToRecord,
|
||||
progressTracker: ProgressTracker = tracker(),
|
||||
handleDoubleSpend: Boolean? = null
|
||||
) : this(transaction, emptyList(), progressTracker, sessions, true, statesToRecord, handleDoubleSpend = handleDoubleSpend)
|
||||
propagateDoubleSpendErrorToPeers: Boolean? = null
|
||||
) : this(transaction, emptyList(), progressTracker, sessions, true, statesToRecord, propagateDoubleSpendErrorToPeers = propagateDoubleSpendErrorToPeers)
|
||||
|
||||
/**
|
||||
* Notarise the given transaction and broadcast it to all the participants.
|
||||
@ -237,9 +237,9 @@ class FinalityFlow private constructor(val transaction: SignedTransaction,
|
||||
catch (e: NotaryException) {
|
||||
if (e.error is NotaryError.Conflict && useTwoPhaseFinality) {
|
||||
(serviceHub as ServiceHubCoreInternal).removeUnnotarisedTransaction(e.error.txId)
|
||||
val overrideHandleDoubleSpend = handleDoubleSpend ?:
|
||||
val overridePropagateDoubleSpendErrorToPeers = propagateDoubleSpendErrorToPeers ?:
|
||||
(serviceHub.cordappProvider.getAppContext().cordapp.targetPlatformVersion >= PlatformVersionSwitches.TWO_PHASE_FINALITY)
|
||||
if (overrideHandleDoubleSpend && newPlatformSessions.isNotEmpty()) {
|
||||
if (overridePropagateDoubleSpendErrorToPeers && newPlatformSessions.isNotEmpty()) {
|
||||
broadcastDoubleSpendError(newPlatformSessions, e)
|
||||
} else sleep(Duration.ZERO) // force checkpoint to persist db update.
|
||||
}
|
||||
@ -490,6 +490,14 @@ class ReceiveFinalityFlow @JvmOverloads constructor(private val otherSideSession
|
||||
sleep(Duration.ZERO) // force checkpoint to persist db update.
|
||||
}
|
||||
throw throwable
|
||||
} catch (e: UnexpectedFlowEndException) {
|
||||
(serviceHub as ServiceHubCoreInternal).removeUnnotarisedTransaction(stx.id)
|
||||
sleep(Duration.ZERO) // force checkpoint to persist db update.
|
||||
throw UnexpectedFlowEndException(
|
||||
"${otherSideSession.counterparty} has finished prematurely whilst awaiting transaction notary signature.",
|
||||
e.cause,
|
||||
e.originalErrorId
|
||||
)
|
||||
}
|
||||
} else {
|
||||
serviceHub.telemetryServiceInternal.span("${this::class.java.name}#recordTransactions", flowLogic = this) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user