mirror of
https://github.com/corda/corda.git
synced 2025-02-21 09:51:57 +00:00
Merge pull request #96 from thschroeter/idempotent-notary-flow
Make NotaryFlow idempotent
This commit is contained in:
commit
0867a05ad7
@ -1,6 +1,7 @@
|
||||
package net.corda.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.node.services.TimestampChecker
|
||||
@ -100,6 +101,8 @@ object NotaryFlow {
|
||||
|
||||
val result = try {
|
||||
validateTimestamp(wtx)
|
||||
// TODO: Move the duplicate input detection to `TransactionType.verify()`.
|
||||
detectDuplicateInputs(wtx)
|
||||
beforeCommit(stx)
|
||||
commitInputStates(wtx)
|
||||
val sig = sign(stx.id.bytes)
|
||||
@ -129,13 +132,37 @@ object NotaryFlow {
|
||||
open fun beforeCommit(stx: SignedTransaction) {
|
||||
}
|
||||
|
||||
/** Throw NotaryException if inputs are not unique. */
|
||||
private fun detectDuplicateInputs(tx: WireTransaction) {
|
||||
var seenInputs = emptySet<StateRef>()
|
||||
var conflicts = emptyMap<StateRef, UniquenessProvider.ConsumingTx>()
|
||||
tx.inputs.forEachIndexed { i, stateRef ->
|
||||
if (seenInputs.contains(stateRef)) {
|
||||
conflicts += stateRef.to(UniquenessProvider.ConsumingTx(tx.id, i, otherSide))
|
||||
}
|
||||
seenInputs += stateRef
|
||||
}
|
||||
if (conflicts.isNotEmpty()) {
|
||||
throw notaryException(tx, UniquenessException(UniquenessProvider.Conflict(conflicts)))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A NotaryException is thrown if any of the states have been consumed by a different transaction. Note that
|
||||
* this method does not throw an exception when input states are present multiple times within the transaction.
|
||||
*/
|
||||
private fun commitInputStates(tx: WireTransaction) {
|
||||
try {
|
||||
uniquenessProvider.commit(tx.inputs, tx.id, otherSide)
|
||||
} catch (e: UniquenessException) {
|
||||
val conflictData = e.error.serialize()
|
||||
val signedConflict = SignedData(conflictData, sign(conflictData.bytes))
|
||||
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.
|
||||
throw notaryException(tx, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -143,6 +170,12 @@ object NotaryFlow {
|
||||
val mySigningKey = serviceHub.notaryIdentityKey
|
||||
return mySigningKey.signWithECDSA(bits)
|
||||
}
|
||||
|
||||
private fun notaryException(tx: WireTransaction, e: UniquenessException): NotaryException {
|
||||
val conflictData = e.error.serialize()
|
||||
val signedConflict = SignedData(conflictData, sign(conflictData.bytes))
|
||||
return NotaryException(NotaryError.Conflict(tx, signedConflict))
|
||||
}
|
||||
}
|
||||
|
||||
data class SignRequest(val tx: SignedTransaction)
|
||||
|
@ -85,7 +85,7 @@ class NotaryServiceTests {
|
||||
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 inputState = issueState(clientNode)
|
||||
val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState)
|
||||
@ -93,10 +93,26 @@ class NotaryServiceTests {
|
||||
tx.toSignedTransaction(false)
|
||||
}
|
||||
|
||||
val firstSpend = NotaryFlow.Client(stx)
|
||||
val secondSpend = NotaryFlow.Client(stx)
|
||||
clientNode.services.startFlow(firstSpend)
|
||||
val future = clientNode.services.startFlow(secondSpend)
|
||||
val firstAttempt = NotaryFlow.Client(stx)
|
||||
val secondAttempt = NotaryFlow.Client(stx)
|
||||
val f1 = clientNode.services.startFlow(firstAttempt)
|
||||
val f2 = clientNode.services.startFlow(secondAttempt)
|
||||
|
||||
net.runNetwork()
|
||||
|
||||
assertEquals(f1.resultFuture.getOrThrow(), f2.resultFuture.getOrThrow())
|
||||
}
|
||||
|
||||
@Test fun `should report conflict when inputs are reused within a single transactions`() {
|
||||
val stx = run {
|
||||
val inputState = issueState(clientNode)
|
||||
// Use inputState twice as input of a single transaction.
|
||||
val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState, inputState)
|
||||
tx.signWith(clientNode.keyPair!!)
|
||||
tx.toSignedTransaction(false)
|
||||
}
|
||||
|
||||
val future = clientNode.services.startFlow(NotaryFlow.Client(stx))
|
||||
|
||||
net.runNetwork()
|
||||
|
||||
@ -106,6 +122,32 @@ class NotaryServiceTests {
|
||||
notaryError.conflict.verified()
|
||||
}
|
||||
|
||||
@Test fun `should report conflict when inputs are reused across 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 secondSpend = NotaryFlow.Client(stx2) // Double spend the inputState in a second transaction.
|
||||
clientNode.services.startFlow(firstSpend)
|
||||
val future = clientNode.services.startFlow(secondSpend)
|
||||
|
||||
net.runNetwork()
|
||||
|
||||
val ex = assertFailsWith(NotaryException::class) { future.resultFuture.getOrThrow() }
|
||||
val notaryError = ex.error as NotaryError.Conflict
|
||||
assertEquals(notaryError.tx, stx2.tx)
|
||||
notaryError.conflict.verified()
|
||||
}
|
||||
|
||||
private fun runNotaryClient(stx: SignedTransaction): ListenableFuture<DigitalSignature.WithKey> {
|
||||
val flow = NotaryFlow.Client(stx)
|
||||
|
Loading…
x
Reference in New Issue
Block a user