mirror of
https://github.com/corda/corda.git
synced 2025-06-18 15:18:16 +00:00
Make NotaryFlow idempotent
Alternatively, we could make the underlying UniquenessProviders idempotent.
This commit is contained in:
@ -133,9 +133,18 @@ object NotaryFlow {
|
|||||||
try {
|
try {
|
||||||
uniquenessProvider.commit(tx.inputs, tx.id, otherSide)
|
uniquenessProvider.commit(tx.inputs, tx.id, otherSide)
|
||||||
} catch (e: UniquenessException) {
|
} catch (e: UniquenessException) {
|
||||||
val conflictData = e.error.serialize()
|
// Allow re-committing the transaction to make the NotaryFlow idempotent. Alternatively, we could make
|
||||||
val signedConflict = SignedData(conflictData, sign(conflictData.bytes))
|
// the underlying UniquenessProviders idempotent.
|
||||||
throw NotaryException(NotaryError.Conflict(tx, signedConflict))
|
val conflicts = tx.inputs.filterIndexed { i, stateRef ->
|
||||||
|
val consumingTx = e.error.stateHistory[stateRef]
|
||||||
|
consumingTx != null && consumingTx != UniquenessProvider.ConsumingTx(tx.id, i, otherSide)
|
||||||
|
}
|
||||||
|
if (conflicts.isNotEmpty()) {
|
||||||
|
// TODO: Create a new UniquenessException that only contains the conflicts filtered above.
|
||||||
|
val conflictData = e.error.serialize()
|
||||||
|
val signedConflict = SignedData(conflictData, sign(conflictData.bytes))
|
||||||
|
throw NotaryException(NotaryError.Conflict(tx, signedConflict))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,7 +85,7 @@ class NotaryServiceTests {
|
|||||||
assertThat(ex.error).isInstanceOf(NotaryError.TimestampInvalid::class.java)
|
assertThat(ex.error).isInstanceOf(NotaryError.TimestampInvalid::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun `should report conflict for a duplicate transaction`() {
|
@Test fun `should sign identical transaction multiple times (signing is idempotent)`() {
|
||||||
val stx = run {
|
val stx = run {
|
||||||
val inputState = issueState(clientNode)
|
val inputState = issueState(clientNode)
|
||||||
val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState)
|
val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState)
|
||||||
@ -93,8 +93,32 @@ class NotaryServiceTests {
|
|||||||
tx.toSignedTransaction(false)
|
tx.toSignedTransaction(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val firstAttempt = NotaryFlow.Client(stx)
|
||||||
|
val secondAttempt = NotaryFlow.Client(stx)
|
||||||
|
clientNode.services.startFlow(firstAttempt)
|
||||||
|
val future = clientNode.services.startFlow(secondAttempt)
|
||||||
|
|
||||||
|
net.runNetwork()
|
||||||
|
|
||||||
|
future.resultFuture.getOrThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `should report conflict when inputs are reused accross transactions`() {
|
||||||
|
val inputState = issueState(clientNode)
|
||||||
|
val stx = run {
|
||||||
|
val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState)
|
||||||
|
tx.signWith(clientNode.keyPair!!)
|
||||||
|
tx.toSignedTransaction(false)
|
||||||
|
}
|
||||||
|
val stx2 = run {
|
||||||
|
val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState)
|
||||||
|
tx.addInputState(issueState(clientNode))
|
||||||
|
tx.signWith(clientNode.keyPair!!)
|
||||||
|
tx.toSignedTransaction(false)
|
||||||
|
}
|
||||||
|
|
||||||
val firstSpend = NotaryFlow.Client(stx)
|
val firstSpend = NotaryFlow.Client(stx)
|
||||||
val secondSpend = NotaryFlow.Client(stx)
|
val secondSpend = NotaryFlow.Client(stx2) // Double spend the inputState in a second transaction.
|
||||||
clientNode.services.startFlow(firstSpend)
|
clientNode.services.startFlow(firstSpend)
|
||||||
val future = clientNode.services.startFlow(secondSpend)
|
val future = clientNode.services.startFlow(secondSpend)
|
||||||
|
|
||||||
@ -102,11 +126,10 @@ class NotaryServiceTests {
|
|||||||
|
|
||||||
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.tx, stx.tx)
|
assertEquals(notaryError.tx, stx2.tx)
|
||||||
notaryError.conflict.verified()
|
notaryError.conflict.verified()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun runNotaryClient(stx: SignedTransaction): ListenableFuture<DigitalSignature.WithKey> {
|
private fun runNotaryClient(stx: SignedTransaction): ListenableFuture<DigitalSignature.WithKey> {
|
||||||
val flow = NotaryFlow.Client(stx)
|
val flow = NotaryFlow.Client(stx)
|
||||||
val future = clientNode.services.startFlow(flow).resultFuture
|
val future = clientNode.services.startFlow(flow).resultFuture
|
||||||
|
Reference in New Issue
Block a user