[CORDA-2130] Encumbered states should always be assigned to the same notary (#4158)

This commit is contained in:
Konstantinos Chalkias 2018-11-09 12:45:43 +00:00 committed by GitHub
parent 74c80aafd6
commit 4c25250fc8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 85 additions and 8 deletions

View File

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

View File

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

View File

@ -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]")
}
}