mirror of
https://github.com/corda/corda.git
synced 2025-02-08 03:50:34 +00:00
Tx validation detect duplicate inputs (#138)
* Move duplicate input detection to transaction verification Duplicate detection was previously part of the NotaryFlow.
This commit is contained in:
parent
44065243bb
commit
6b7edf5af6
@ -136,7 +136,7 @@ data class TransactionState<out T : ContractState> @JvmOverloads constructor(
|
|||||||
*
|
*
|
||||||
* The encumbered state refers to another by index, and the referred encumbrance state
|
* The encumbered state refers to another by index, and the referred encumbrance state
|
||||||
* is an output state in a particular position on the same transaction that created the encumbered state. An alternative
|
* is an output state in a particular position on the same transaction that created the encumbered state. An alternative
|
||||||
* implementation would be encumber by reference to a StateRef., which would allow the specification of encumbrance
|
* implementation would be encumbering by reference to a [StateRef], which would allow the specification of encumbrance
|
||||||
* by a state created in a prior transaction.
|
* by a state created in a prior transaction.
|
||||||
*
|
*
|
||||||
* Note that an encumbered state that is being consumed must have its encumbrance consumed in the same transaction,
|
* Note that an encumbered state that is being consumed must have its encumbrance consumed in the same transaction,
|
||||||
|
@ -19,6 +19,8 @@ sealed class TransactionType {
|
|||||||
*/
|
*/
|
||||||
fun verify(tx: LedgerTransaction) {
|
fun verify(tx: LedgerTransaction) {
|
||||||
require(tx.notary != null || tx.timestamp == null) { "Transactions with timestamps must be notarised." }
|
require(tx.notary != null || tx.timestamp == null) { "Transactions with timestamps must be notarised." }
|
||||||
|
val duplicates = detectDuplicateInputs(tx)
|
||||||
|
if (duplicates.isNotEmpty()) throw TransactionVerificationException.DuplicateInputStates(tx, duplicates)
|
||||||
val missing = verifySigners(tx)
|
val missing = verifySigners(tx)
|
||||||
if (missing.isNotEmpty()) throw TransactionVerificationException.SignersMissing(tx, missing.toList())
|
if (missing.isNotEmpty()) throw TransactionVerificationException.SignersMissing(tx, missing.toList())
|
||||||
verifyTransaction(tx)
|
verifyTransaction(tx)
|
||||||
@ -35,6 +37,19 @@ sealed class TransactionType {
|
|||||||
return missing
|
return missing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Check that the inputs are unique. */
|
||||||
|
private fun detectDuplicateInputs(tx: LedgerTransaction): Set<StateRef> {
|
||||||
|
var seenInputs = emptySet<StateRef>()
|
||||||
|
var duplicates = emptySet<StateRef>()
|
||||||
|
tx.inputs.forEach { state ->
|
||||||
|
if (seenInputs.contains(state.ref)) {
|
||||||
|
duplicates += state.ref
|
||||||
|
}
|
||||||
|
seenInputs += state.ref
|
||||||
|
}
|
||||||
|
return duplicates
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the list of public keys that that require signatures for the transaction type.
|
* Return the list of public keys that that require signatures for the transaction type.
|
||||||
* Note: the notary key is checked separately for all transactions and need not be included.
|
* Note: the notary key is checked separately for all transactions and need not be included.
|
||||||
|
@ -97,6 +97,9 @@ sealed class TransactionVerificationException(val tx: LedgerTransaction, cause:
|
|||||||
class SignersMissing(tx: LedgerTransaction, val missing: List<CompositeKey>) : TransactionVerificationException(tx, null) {
|
class SignersMissing(tx: LedgerTransaction, val missing: List<CompositeKey>) : TransactionVerificationException(tx, null) {
|
||||||
override fun toString() = "Signers missing: ${missing.joinToString()}"
|
override fun toString() = "Signers missing: ${missing.joinToString()}"
|
||||||
}
|
}
|
||||||
|
class DuplicateInputStates(tx: LedgerTransaction, val duplicates: Set<StateRef>) : TransactionVerificationException(tx, null) {
|
||||||
|
override fun toString() = "Duplicate inputs: ${duplicates.joinToString()}"
|
||||||
|
}
|
||||||
|
|
||||||
class InvalidNotaryChange(tx: LedgerTransaction) : TransactionVerificationException(tx, null)
|
class InvalidNotaryChange(tx: LedgerTransaction) : TransactionVerificationException(tx, null)
|
||||||
class NotaryChangeInWrongTransactionType(tx: LedgerTransaction, val outputNotary: Party) : TransactionVerificationException(tx, null) {
|
class NotaryChangeInWrongTransactionType(tx: LedgerTransaction, val outputNotary: Party) : TransactionVerificationException(tx, null) {
|
||||||
|
@ -101,8 +101,6 @@ object NotaryFlow {
|
|||||||
|
|
||||||
val result = try {
|
val result = try {
|
||||||
validateTimestamp(wtx)
|
validateTimestamp(wtx)
|
||||||
// TODO: Move the duplicate input detection to `TransactionType.verify()`.
|
|
||||||
detectDuplicateInputs(wtx)
|
|
||||||
beforeCommit(stx)
|
beforeCommit(stx)
|
||||||
commitInputStates(wtx)
|
commitInputStates(wtx)
|
||||||
val sig = sign(stx.id.bytes)
|
val sig = sign(stx.id.bytes)
|
||||||
@ -132,21 +130,6 @@ object NotaryFlow {
|
|||||||
open fun beforeCommit(stx: SignedTransaction) {
|
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
|
* 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.
|
* this method does not throw an exception when input states are present multiple times within the transaction.
|
||||||
|
@ -82,6 +82,33 @@ class TransactionTests {
|
|||||||
transaction.type.verify(transaction)
|
transaction.type.verify(transaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `transaction verification fails for duplicate inputs`() {
|
||||||
|
val baseOutState = TransactionState(DummyContract.SingleOwnerState(0, ALICE_PUBKEY), DUMMY_NOTARY)
|
||||||
|
val stateRef = StateRef(SecureHash.randomSHA256(), 0)
|
||||||
|
val stateAndRef = StateAndRef(baseOutState, stateRef)
|
||||||
|
val inputs = listOf(stateAndRef, stateAndRef)
|
||||||
|
val outputs = listOf(baseOutState)
|
||||||
|
val commands = emptyList<AuthenticatedObject<CommandData>>()
|
||||||
|
val attachments = emptyList<Attachment>()
|
||||||
|
val id = SecureHash.randomSHA256()
|
||||||
|
val signers = listOf(DUMMY_NOTARY_KEY.public.composite)
|
||||||
|
val timestamp: Timestamp? = null
|
||||||
|
val transaction: LedgerTransaction = LedgerTransaction(
|
||||||
|
inputs,
|
||||||
|
outputs,
|
||||||
|
commands,
|
||||||
|
attachments,
|
||||||
|
id,
|
||||||
|
DUMMY_NOTARY,
|
||||||
|
signers,
|
||||||
|
timestamp,
|
||||||
|
TransactionType.General()
|
||||||
|
)
|
||||||
|
|
||||||
|
assertFailsWith<TransactionVerificationException.DuplicateInputStates> { transaction.type.verify(transaction) }
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `general transactions cannot change notary`() {
|
fun `general transactions cannot change notary`() {
|
||||||
val notary: Party = DUMMY_NOTARY
|
val notary: Party = DUMMY_NOTARY
|
||||||
|
@ -103,25 +103,6 @@ class NotaryServiceTests {
|
|||||||
assertEquals(f1.resultFuture.getOrThrow(), f2.resultFuture.getOrThrow())
|
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()
|
|
||||||
|
|
||||||
val ex = assertFailsWith(NotaryException::class) { future.resultFuture.getOrThrow() }
|
|
||||||
val notaryError = ex.error as NotaryError.Conflict
|
|
||||||
assertEquals(notaryError.tx, stx.tx)
|
|
||||||
notaryError.conflict.verified()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `should report conflict when inputs are reused across transactions`() {
|
@Test fun `should report conflict when inputs are reused across transactions`() {
|
||||||
val inputState = issueState(clientNode)
|
val inputState = issueState(clientNode)
|
||||||
val stx = run {
|
val stx = run {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user