mirror of
https://github.com/corda/corda.git
synced 2025-04-07 11:27:01 +00:00
[CORDA-2130] Encumbered states should always be assigned to the same notary (#4158)
This commit is contained in:
parent
74c80aafd6
commit
4c25250fc8
@ -136,6 +136,16 @@ sealed class TransactionVerificationException(val txId: SecureHash, message: Str
|
||||
"is not satisfied. Encumbered states should also be referenced as an encumbrance of another state to form " +
|
||||
"a full cycle. Offending indices $nonMatching", null)
|
||||
|
||||
/**
|
||||
* All encumbered states should be assigned to the same notary. This is due to the fact that multi-notary
|
||||
* transactions are not supported and thus two encumbered states with different notaries cannot be consumed
|
||||
* in the same transaction.
|
||||
*/
|
||||
@KeepForDJVM
|
||||
class TransactionNotaryMismatchEncumbranceException(txId: SecureHash, encumberedIndex: Int, encumbranceIndex: Int, encumberedNotary: Party, encumbranceNotary: Party)
|
||||
: TransactionVerificationException(txId, "Encumbered output states assigned to different notaries found. " +
|
||||
"Output state with index $encumberedIndex is assigned to notary [$encumberedNotary], while its encumbrance with index $encumbranceIndex is assigned to notary [$encumbranceNotary]", null)
|
||||
|
||||
/** Whether the inputs or outputs list contains an encumbrance issue, see [TransactionMissingEncumbranceException]. */
|
||||
@CordaSerializable
|
||||
@KeepForDJVM
|
||||
|
@ -14,6 +14,7 @@ import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.Try
|
||||
import java.util.*
|
||||
import java.util.function.Predicate
|
||||
import kotlin.collections.HashSet
|
||||
|
||||
/**
|
||||
* A LedgerTransaction is derived from a [WireTransaction]. It is the result of doing the following operations:
|
||||
@ -232,10 +233,39 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
// Check that in the outputs,
|
||||
// a) an encumbered state does not refer to itself as the encumbrance
|
||||
// b) the number of outputs can contain the encumbrance
|
||||
// c) the bi-directionality (full cycle) property is satisfied.
|
||||
// c) the bi-directionality (full cycle) property is satisfied
|
||||
// d) encumbered output states are assigned to the same notary.
|
||||
val statesAndEncumbrance = outputs.withIndex().filter { it.value.encumbrance != null }.map { Pair(it.index, it.value.encumbrance!!) }
|
||||
if (!statesAndEncumbrance.isEmpty()) {
|
||||
checkOutputEncumbrances(statesAndEncumbrance)
|
||||
checkBidirectionalOutputEncumbrances(statesAndEncumbrance)
|
||||
checkNotariesOutputEncumbrance(statesAndEncumbrance)
|
||||
}
|
||||
}
|
||||
|
||||
// Method to check if all encumbered states are assigned to the same notary Party.
|
||||
// This method should be invoked after [checkBidirectionalOutputEncumbrances], because it assumes that the
|
||||
// bi-directionality property is already satisfied.
|
||||
private fun checkNotariesOutputEncumbrance(statesAndEncumbrance: List<Pair<Int, Int>>) {
|
||||
// We only check for transactions in which notary is null (i.e., issuing transactions).
|
||||
// Note that if a notary is defined for a transaction, we already check if all outputs are assigned
|
||||
// to the same notary (transaction's notary) in [checkNoNotaryChange()].
|
||||
if (notary == null) {
|
||||
// indicesAlreadyChecked is used to bypass already checked indices and to avoid cycles.
|
||||
val indicesAlreadyChecked = HashSet<Int>()
|
||||
statesAndEncumbrance.forEach {
|
||||
checkNotary(it.first, indicesAlreadyChecked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private tailrec fun checkNotary(index: Int, indicesAlreadyChecked: HashSet<Int>) {
|
||||
if (indicesAlreadyChecked.add(index)) {
|
||||
val encumbranceIndex = outputs[index].encumbrance!!
|
||||
if (outputs[index].notary != outputs[encumbranceIndex].notary) {
|
||||
throw TransactionVerificationException.TransactionNotaryMismatchEncumbranceException(id, index, encumbranceIndex, outputs[index].notary, outputs[encumbranceIndex].notary)
|
||||
} else {
|
||||
checkNotary(encumbranceIndex, indicesAlreadyChecked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -273,7 +303,7 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
// b -> c and c -> b
|
||||
// c -> a b -> a
|
||||
// and form a full cycle, meaning that the bi-directionality property is satisfied.
|
||||
private fun checkOutputEncumbrances(statesAndEncumbrance: List<Pair<Int, Int>>) {
|
||||
private fun checkBidirectionalOutputEncumbrances(statesAndEncumbrance: List<Pair<Int, Int>>) {
|
||||
// [Set] of "from" (encumbered states).
|
||||
val encumberedSet = mutableSetOf<Int>()
|
||||
// [Set] of "to" (encumbrance states).
|
||||
|
@ -2,10 +2,7 @@ package net.corda.core.transactions
|
||||
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.contracts.Contract
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.TransactionVerificationException
|
||||
import net.corda.core.contracts.requireThat
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.finance.DOLLARS
|
||||
@ -18,6 +15,7 @@ import net.corda.testing.core.TestIdentity
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.ledger
|
||||
import org.assertj.core.api.AssertionsForClassTypes
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.time.Instant
|
||||
@ -33,6 +31,7 @@ class TransactionEncumbranceTests {
|
||||
|
||||
private companion object {
|
||||
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
|
||||
val DUMMY_NOTARY2 = TestIdentity(DUMMY_NOTARY_NAME.copy(organisation = "${DUMMY_NOTARY_NAME.organisation}2"), 30).party
|
||||
val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
|
||||
val MINI_CORP = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")).party
|
||||
val MEGA_CORP get() = megaCorp.party
|
||||
@ -77,7 +76,7 @@ class TransactionEncumbranceTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `states can be bi-directionally encumbered`() {
|
||||
fun `states must be bi-directionally encumbered`() {
|
||||
// Basic encumbrance example for encumbrance index links 0 -> 1 and 1 -> 0
|
||||
ledgerServices.ledger(DUMMY_NOTARY) {
|
||||
transaction {
|
||||
@ -316,4 +315,42 @@ class TransactionEncumbranceTests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `encumbered states cannot be assigned to different notaries`() {
|
||||
// Single encumbrance with different notaries.
|
||||
assertFailsWith<TransactionVerificationException.TransactionNotaryMismatchEncumbranceException> {
|
||||
TransactionBuilder()
|
||||
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 1, AutomaticHashConstraint)
|
||||
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY2, 0, AutomaticHashConstraint)
|
||||
.addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey)
|
||||
.toLedgerTransaction(ledgerServices)
|
||||
}
|
||||
|
||||
// More complex encumbrance (full cycle of size 4) where one of the encumbered states is assigned to a different notary.
|
||||
// 0 -> 1, 1 -> 3, 3 -> 2, 2 -> 0
|
||||
// We expect that state at index 3 cannot be encumbered with the state at index 2, due to mismatched notaries.
|
||||
AssertionsForClassTypes.assertThatExceptionOfType(TransactionVerificationException.TransactionNotaryMismatchEncumbranceException::class.java).isThrownBy {
|
||||
TransactionBuilder()
|
||||
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 1, AutomaticHashConstraint)
|
||||
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 3, AutomaticHashConstraint)
|
||||
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY2, 0, AutomaticHashConstraint)
|
||||
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 2, AutomaticHashConstraint)
|
||||
.addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey)
|
||||
.toLedgerTransaction(ledgerServices)
|
||||
}.withMessageContaining("index 3 is assigned to notary [O=Notary Service, L=Zurich, C=CH], while its encumbrance with index 2 is assigned to notary [O=Notary Service2, L=Zurich, C=CH]")
|
||||
|
||||
// Two different encumbrance chains, where only one fails due to mismatched notary.
|
||||
// 0 -> 1, 1 -> 0, 2 -> 3, 3 -> 2 where encumbered states with indices 2 and 3, respectively, are assigned
|
||||
// to different notaries.
|
||||
AssertionsForClassTypes.assertThatExceptionOfType(TransactionVerificationException.TransactionNotaryMismatchEncumbranceException::class.java).isThrownBy {
|
||||
TransactionBuilder()
|
||||
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 1, AutomaticHashConstraint)
|
||||
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 0, AutomaticHashConstraint)
|
||||
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 3, AutomaticHashConstraint)
|
||||
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY2, 2, AutomaticHashConstraint)
|
||||
.addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey)
|
||||
.toLedgerTransaction(ledgerServices)
|
||||
}.withMessageContaining("index 2 is assigned to notary [O=Notary Service, L=Zurich, C=CH], while its encumbrance with index 3 is assigned to notary [O=Notary Service2, L=Zurich, C=CH]")
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user