ENT-9823 Rename handleDoubleSpend -> propagateDoubleSpendErrorToPeers (#7338)

This commit is contained in:
Jose Coll 2023-04-20 15:34:46 +01:00 committed by GitHub
parent fffc3e4c5d
commit 0bd4364653
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 78 additions and 17 deletions

View File

@ -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 {

View File

@ -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) {