[CORDA-738] Ensure encumbrances are bi-directional (#4089)

This commit is contained in:
Konstantinos Chalkias 2018-10-19 18:34:32 +01:00 committed by GitHub
parent e10119031c
commit 72cab90577
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 271 additions and 79 deletions

View File

@ -116,13 +116,32 @@ sealed class TransactionVerificationException(val txId: SecureHash, message: Str
class TransactionMissingEncumbranceException(txId: SecureHash, val missing: Int, val inOut: Direction)
: TransactionVerificationException(txId, "Missing required encumbrance $missing in $inOut", null)
/**
* If two or more states refer to another state (as their encumbrance), then the bi-directionality property cannot
* be satisfied.
*/
@KeepForDJVM
class TransactionDuplicateEncumbranceException(txId: SecureHash, index: Int)
: TransactionVerificationException(txId, "The bi-directionality property of encumbered output states " +
"is not satisfied. Index $index is referenced more than once", null)
/**
* An encumbered state should also be referenced as the encumbrance of another state in order to satisfy the
* bi-directionality property (a full cycle should be present).
*/
@KeepForDJVM
class TransactionNonMatchingEncumbranceException(txId: SecureHash, nonMatching: Collection<Int>)
: TransactionVerificationException(txId, "The bi-directionality property of encumbered output states " +
"is not satisfied. Encumbered states should also be referenced as an encumbrance of another state to form " +
"a full cycle. Offending indices $nonMatching", null)
/** Whether the inputs or outputs list contains an encumbrance issue, see [TransactionMissingEncumbranceException]. */
@CordaSerializable
@KeepForDJVM
enum class Direction {
/** Issue in the inputs list */
/** Issue in the inputs list. */
INPUT,
/** Issue in the outputs list */
/** Issue in the outputs list. */
OUTPUT
}

View File

@ -62,6 +62,7 @@ abstract class AbstractStateReplacementFlow {
@Throws(StateReplacementException::class)
override fun call(): StateAndRef<T> {
val (stx) = assembleTx()
stx.verify(serviceHub, checkSufficientSignatures = false)
val participantSessions = getParticipantSessions()
progressTracker.currentStep = SIGNING

View File

@ -46,14 +46,14 @@ class NotaryChangeFlow<out T : ContractState>(
return AbstractStateReplacementFlow.UpgradeTx(stx)
}
/** Resolves the encumbrance state chain for the given [state] */
/** Resolves the encumbrance state chain for the given [state]. */
private fun resolveEncumbrances(state: StateAndRef<T>): List<StateAndRef<T>> {
val states = mutableListOf(state)
val states = mutableSetOf(state)
while (states.last().state.encumbrance != null) {
val encumbranceStateRef = StateRef(states.last().ref.txhash, states.last().state.encumbrance!!)
val encumbranceState = serviceHub.toStateAndRef<T>(encumbranceStateRef)
states.add(encumbranceState)
if (!states.add(encumbranceState)) break // Stop if there is a cycle.
}
return states
return states.toList()
}
}

View File

@ -169,30 +169,79 @@ data class LedgerTransaction @JvmOverloads constructor(
private fun checkEncumbrancesValid() {
// Validate that all encumbrances exist within the set of input states.
val encumberedInputs = inputs.filter { it.state.encumbrance != null }
encumberedInputs.forEach { (state, ref) ->
val encumbranceStateExists = inputs.any {
it.ref.txhash == ref.txhash && it.ref.index == state.encumbrance
}
if (!encumbranceStateExists) {
inputs.filter { it.state.encumbrance != null }
.forEach { (state, ref) -> checkInputEncumbranceStateExists(state, ref) }
// 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.
val statesAndEncumbrance = outputs.withIndex().filter { it.value.encumbrance != null }.map { Pair(it.index, it.value.encumbrance!!) }
if (!statesAndEncumbrance.isEmpty()) {
checkOutputEncumbrances(statesAndEncumbrance)
}
}
private fun checkInputEncumbranceStateExists(state: TransactionState<ContractState>, ref: StateRef) {
val encumbranceStateExists = inputs.any {
it.ref.txhash == ref.txhash && it.ref.index == state.encumbrance
}
if (!encumbranceStateExists) {
throw TransactionVerificationException.TransactionMissingEncumbranceException(
id,
state.encumbrance!!,
TransactionVerificationException.Direction.INPUT
)
}
}
// Using basic graph theory, a full cycle of encumbered (co-dependent) states should exist to achieve bi-directional
// encumbrances. This property is important to ensure that no states involved in an encumbrance-relationship
// can be spent on their own. Briefly, if any of the states is having more than one encumbrance references by
// other states, a full cycle detection will fail. As a result, all of the encumbered states must be present
// as "from" and "to" only once (or zero times if no encumbrance takes place). For instance,
// a -> b
// c -> b and a -> b
// b -> a b -> c
// do not satisfy the bi-directionality (full cycle) property.
//
// In the first example "b" appears twice in encumbrance ("to") list and "c" exists in the encumbered ("from") list only.
// Due the above, one could consume "a" and "b" in the same transaction and then, because "b" is already consumed, "c" cannot be spent.
//
// Similarly, the second example does not form a full cycle because "a" and "c" exist in one of the lists only.
// As a result, one can consume "b" and "c" in the same transactions, which will make "a" impossible to be spent.
//
// On other hand the following are valid constructions:
// a -> b a -> c
// 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>>) {
// [Set] of "from" (encumbered states).
val encumberedSet = mutableSetOf<Int>()
// [Set] of "to" (encumbrance states).
val encumbranceSet = mutableSetOf<Int>()
// Update both [Set]s.
statesAndEncumbrance.forEach { (statePosition, encumbrance) ->
// Check it does not refer to itself.
if (statePosition == encumbrance || encumbrance >= outputs.size) {
throw TransactionVerificationException.TransactionMissingEncumbranceException(
id,
state.encumbrance!!,
TransactionVerificationException.Direction.INPUT
)
encumbrance,
TransactionVerificationException.Direction.OUTPUT)
} else {
encumberedSet.add(statePosition) // Guaranteed to have unique elements.
if (!encumbranceSet.add(encumbrance)) {
throw TransactionVerificationException.TransactionDuplicateEncumbranceException(id, encumbrance)
}
}
}
// Check that, in the outputs, an encumbered state does not refer to itself as the encumbrance,
// and that the number of outputs can contain the encumbrance.
for ((i, output) in outputs.withIndex()) {
val encumbranceIndex = output.encumbrance ?: continue
if (encumbranceIndex == i || encumbranceIndex >= outputs.size) {
throw TransactionVerificationException.TransactionMissingEncumbranceException(
id,
encumbranceIndex,
TransactionVerificationException.Direction.OUTPUT)
}
// At this stage we have ensured that "from" and "to" [Set]s are equal in size, but we should check their
// elements do indeed match. If they don't match, we return their symmetric difference (disjunctive union).
val symmetricDifference = (encumberedSet union encumbranceSet).subtract(encumberedSet intersect encumbranceSet)
if (symmetricDifference.isNotEmpty()) {
// At least one encumbered state is not in the [encumbranceSet] and vice versa.
throw TransactionVerificationException.TransactionNonMatchingEncumbranceException(id, symmetricDifference)
}
}

View File

@ -102,13 +102,21 @@ data class NotaryChangeLedgerTransaction(
override val references: List<StateAndRef<ContractState>> = emptyList()
/** We compute the outputs on demand by applying the notary field modification to the inputs */
/** We compute the outputs on demand by applying the notary field modification to the inputs. */
override val outputs: List<TransactionState<ContractState>>
get() = inputs.mapIndexed { pos, (state) ->
get() = computeOutputs()
private fun computeOutputs(): List<TransactionState<ContractState>> {
val inputPositionIndex: Map<StateRef, Int> = inputs.mapIndexed { index, stateAndRef -> stateAndRef.ref to index }.toMap()
return inputs.map { (state, ref) ->
if (state.encumbrance != null) {
state.copy(notary = newNotary, encumbrance = pos + 1)
val encumbranceStateRef = StateRef(ref.txhash, state.encumbrance)
val encumbrancePosition = inputPositionIndex[encumbranceStateRef]
?: throw IllegalStateException("Unable to generate output states transaction not constructed correctly.")
state.copy(notary = newNotary, encumbrance = encumbrancePosition)
} else state.copy(notary = newNotary)
}
}
override val requiredSigningKeys: Set<PublicKey>
get() = inputs.flatMap { it.state.data.participants }.map { it.owningKey }.toSet() + notary.owningKey
@ -118,18 +126,16 @@ data class NotaryChangeLedgerTransaction(
}
/**
* Check that encumbrances have been included in the inputs. The [NotaryChangeFlow] guarantees that an encumbrance
* will follow its encumbered state in the inputs.
* Check that encumbrances have been included in the inputs.
*/
private fun checkEncumbrances() {
inputs.forEachIndexed { i, (state, ref) ->
state.encumbrance?.let {
val nextIndex = i + 1
fun nextStateIsEncumbrance() = (inputs[nextIndex].ref.txhash == ref.txhash) && (inputs[nextIndex].ref.index == it)
if (nextIndex >= inputs.size || !nextStateIsEncumbrance()) {
val encumberedStates = inputs.asSequence().filter { it.state.encumbrance != null }.associateBy { it.ref }
if (encumberedStates.isNotEmpty()) {
inputs.forEach { (state, ref) ->
if (StateRef(ref.txhash, state.encumbrance!!) !in encumberedStates) {
throw TransactionVerificationException.TransactionMissingEncumbranceException(
id,
it,
state.encumbrance,
TransactionVerificationException.Direction.INPUT)
}
}

View File

@ -134,4 +134,3 @@ fun <V> Future<V>.getOrThrow(timeout: Duration? = null): V = try {
} catch (e: ExecutionException) {
throw e.cause!!
}

View File

@ -4,6 +4,7 @@ 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.identity.AbstractParty
import net.corda.core.identity.CordaX500Name
@ -21,33 +22,43 @@ import org.junit.Rule
import org.junit.Test
import java.time.Instant
import java.time.temporal.ChronoUnit
import kotlin.test.assertFailsWith
const val TEST_TIMELOCK_ID = "net.corda.core.transactions.TransactionEncumbranceTests\$DummyTimeLock"
class TransactionEncumbranceTests {
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
private companion object {
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
val MINI_CORP = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")).party
val MEGA_CORP get() = megaCorp.party
val MEGA_CORP_PUBKEY get() = megaCorp.publicKey
val defaultIssuer = MEGA_CORP.ref(1)
val state = Cash.State(
amount = 1000.DOLLARS `issued by` defaultIssuer,
owner = MEGA_CORP
)
val stateWithNewOwner = state.copy(owner = MINI_CORP)
val extraCashState = state.copy(amount = state.amount * 3)
val FOUR_PM: Instant = Instant.parse("2015-04-17T16:00:00.00Z")
val FIVE_PM: Instant = FOUR_PM.plus(1, ChronoUnit.HOURS)
val timeLock = DummyTimeLock.State(FIVE_PM)
val ledgerServices = MockServices(listOf("net.corda.core.transactions", "net.corda.finance.contracts.asset"), MEGA_CORP.name,
rigorousMock<IdentityServiceInternal>().also {
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
})
}
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
val defaultIssuer = MEGA_CORP.ref(1)
val state = Cash.State(
amount = 1000.DOLLARS `issued by` defaultIssuer,
owner = MEGA_CORP
)
val stateWithNewOwner = state.copy(owner = MINI_CORP)
val FOUR_PM: Instant = Instant.parse("2015-04-17T16:00:00.00Z")
val FIVE_PM: Instant = FOUR_PM.plus(1, ChronoUnit.HOURS)
val timeLock = DummyTimeLock.State(FIVE_PM)
class DummyTimeLock : Contract {
override fun verify(tx: LedgerTransaction) {
val timeLockInput = tx.inputsOfType<State>().singleOrNull() ?: return
@ -65,23 +76,136 @@ class TransactionEncumbranceTests {
}
}
private val ledgerServices = MockServices(listOf("net.corda.core.transactions", "net.corda.finance.contracts.asset"), MEGA_CORP.name,
rigorousMock<IdentityServiceInternal>().also {
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
})
@Test
fun `state can be encumbered`() {
fun `states can be bi-directionally encumbered`() {
// Basic encumbrance example for encumbrance index links 0 -> 1 and 1 -> 0
ledgerServices.ledger(DUMMY_NOTARY) {
transaction {
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
input(Cash.PROGRAM_ID, state)
output(Cash.PROGRAM_ID, encumbrance = 1, contractState = stateWithNewOwner)
output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock)
output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock", encumbrance = 1, contractState = stateWithNewOwner)
output(TEST_TIMELOCK_ID, "5pm time-lock", 0, timeLock)
command(MEGA_CORP.owningKey, Cash.Commands.Move())
verifies()
}
}
// Full cycle example with 4 elements 0 -> 1, 1 -> 2, 2 -> 3 and 3 -> 0
// All 3 Cash states and the TimeLock are linked and should be consumed in the same transaction.
// Note that all of the Cash states are encumbered both together and with time lock.
ledgerServices.ledger(DUMMY_NOTARY) {
transaction {
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
input(Cash.PROGRAM_ID, extraCashState)
output(Cash.PROGRAM_ID, "state encumbered by state 1", encumbrance = 1, contractState = stateWithNewOwner)
output(Cash.PROGRAM_ID, "state encumbered by state 2", encumbrance = 2, contractState = stateWithNewOwner)
output(Cash.PROGRAM_ID, "state encumbered by state 3", encumbrance = 3, contractState = stateWithNewOwner)
output(TEST_TIMELOCK_ID, "5pm time-lock", 0, timeLock)
command(MEGA_CORP.owningKey, Cash.Commands.Move())
verifies()
}
}
// A transaction that includes multiple independent encumbrance chains.
// Each Cash state is encumbered with its own TimeLock.
// Note that all of the Cash states are encumbered both together and with time lock.
ledgerServices.ledger(DUMMY_NOTARY) {
transaction {
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
input(Cash.PROGRAM_ID, extraCashState)
output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock A", encumbrance = 3, contractState = stateWithNewOwner)
output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock B", encumbrance = 4, contractState = stateWithNewOwner)
output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock C", encumbrance = 5, contractState = stateWithNewOwner)
output(TEST_TIMELOCK_ID, "5pm time-lock A", 0, timeLock)
output(TEST_TIMELOCK_ID, "5pm time-lock B", 1, timeLock)
output(TEST_TIMELOCK_ID, "5pm time-lock C", 2, timeLock)
command(MEGA_CORP.owningKey, Cash.Commands.Move())
verifies()
}
}
// Full cycle example with 4 elements (different combination) 0 -> 3, 1 -> 2, 2 -> 0 and 3 -> 1
ledgerServices.ledger(DUMMY_NOTARY) {
transaction {
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
input(Cash.PROGRAM_ID, extraCashState)
output(Cash.PROGRAM_ID, "state encumbered by state 3", encumbrance = 3, contractState = stateWithNewOwner)
output(Cash.PROGRAM_ID, "state encumbered by state 2", encumbrance = 2, contractState = stateWithNewOwner)
output(Cash.PROGRAM_ID, "state encumbered by state 0", encumbrance = 0, contractState = stateWithNewOwner)
output(TEST_TIMELOCK_ID, "5pm time-lock", 1, timeLock)
command(MEGA_CORP.owningKey, Cash.Commands.Move())
verifies()
}
}
}
@Test
fun `non bi-directional encumbrance will fail`() {
// Single encumbrance with no back link.
assertFailsWith<TransactionVerificationException.TransactionNonMatchingEncumbranceException> {
ledgerServices.ledger(DUMMY_NOTARY) {
transaction {
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
input(Cash.PROGRAM_ID, state)
output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock", encumbrance = 1, contractState = stateWithNewOwner)
output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock)
command(MEGA_CORP.owningKey, Cash.Commands.Move())
verifies()
}
}
}
// Full cycle fails due to duplicate encumbrance reference.
// 0 -> 1, 1 -> 3, 2 -> 3 (thus 3 is referenced two times).
assertFailsWith<TransactionVerificationException.TransactionDuplicateEncumbranceException> {
ledgerServices.ledger(DUMMY_NOTARY) {
transaction {
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
input(Cash.PROGRAM_ID, state)
output(Cash.PROGRAM_ID, "state encumbered by state 1", encumbrance = 1, contractState = stateWithNewOwner)
output(Cash.PROGRAM_ID, "state encumbered by state 3", encumbrance = 3, contractState = stateWithNewOwner)
output(Cash.PROGRAM_ID, "state encumbered by state 3 again", encumbrance = 3, contractState = stateWithNewOwner)
output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock)
command(MEGA_CORP.owningKey, Cash.Commands.Move())
verifies()
}
}
}
// No Full cycle due to non-matching encumbered-encumbrance elements.
// 0 -> 1, 1 -> 3, 2 -> 0 (thus offending indices [2, 3], because 2 is not referenced and 3 is not encumbered).
assertFailsWith<TransactionVerificationException.TransactionNonMatchingEncumbranceException> {
ledgerServices.ledger(DUMMY_NOTARY) {
transaction {
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
input(Cash.PROGRAM_ID, state)
output(Cash.PROGRAM_ID, "state encumbered by state 1", encumbrance = 1, contractState = stateWithNewOwner)
output(Cash.PROGRAM_ID, "state encumbered by state 3", encumbrance = 3, contractState = stateWithNewOwner)
output(Cash.PROGRAM_ID, "state encumbered by state 0", encumbrance = 0, contractState = stateWithNewOwner)
output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock)
command(MEGA_CORP.owningKey, Cash.Commands.Move())
verifies()
}
}
}
// No Full cycle in one of the encumbrance chains due to non-matching encumbered-encumbrance elements.
// 0 -> 2, 2 -> 0 is valid. On the other hand, there is 1 -> 3 only and 3 -> 1 does not exist.
// (thus offending indices [1, 3], because 1 is not referenced and 3 is not encumbered).
assertFailsWith<TransactionVerificationException.TransactionNonMatchingEncumbranceException> {
ledgerServices.ledger(DUMMY_NOTARY) {
transaction {
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
input(Cash.PROGRAM_ID, state)
output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock A", encumbrance = 2, contractState = stateWithNewOwner)
output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock B", encumbrance = 3, contractState = stateWithNewOwner)
output(TEST_TIMELOCK_ID, "5pm time-lock A", 0, timeLock)
output(TEST_TIMELOCK_ID, "5pm time-lock B", timeLock)
command(MEGA_CORP.owningKey, Cash.Commands.Move())
verifies()
}
}
}
}
@Test
@ -132,7 +256,7 @@ class TransactionEncumbranceTests {
unverifiedTransaction {
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock", encumbrance = 1, contractState = state)
output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock)
output(TEST_TIMELOCK_ID, "5pm time-lock",0, timeLock)
}
transaction {
attachments(Cash.PROGRAM_ID)
@ -151,7 +275,7 @@ class TransactionEncumbranceTests {
transaction {
attachments(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID, state)
output(Cash.PROGRAM_ID, encumbrance = 0, contractState = stateWithNewOwner)
output(Cash.PROGRAM_ID, "state encumbered by itself", encumbrance = 0, contractState = stateWithNewOwner)
command(MEGA_CORP.owningKey, Cash.Commands.Move())
this `fails with` "Missing required encumbrance 0 in OUTPUT"
}
@ -164,7 +288,7 @@ class TransactionEncumbranceTests {
transaction {
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
input(Cash.PROGRAM_ID, state)
output(TEST_TIMELOCK_ID, encumbrance = 2, contractState = stateWithNewOwner)
output(TEST_TIMELOCK_ID, "state encumbered by state 2 which does not exist", encumbrance = 2, contractState = stateWithNewOwner)
output(TEST_TIMELOCK_ID, timeLock)
command(MEGA_CORP.owningKey, Cash.Commands.Move())
this `fails with` "Missing required encumbrance 2 in OUTPUT"
@ -178,7 +302,7 @@ class TransactionEncumbranceTests {
unverifiedTransaction {
attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID)
output(Cash.PROGRAM_ID, "state encumbered by some other state", encumbrance = 1, contractState = state)
output(Cash.PROGRAM_ID, "some other state", state)
output(Cash.PROGRAM_ID, "some other state", encumbrance = 0, contractState = state)
output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock)
}
transaction {

View File

@ -109,20 +109,13 @@ class NotaryChangeTests {
// Check that all encumbrances have been propagated to the outputs
val originalOutputs = issueTx.outputStates
val newOutputs = notaryChangeTx.outputStates
assertTrue(originalOutputs.minus(newOutputs).isEmpty())
assertTrue(originalOutputs.size == newOutputs.size && originalOutputs.containsAll(newOutputs))
// Check that encumbrance links aren't broken after notary change
val encumbranceLink = HashMap<ContractState, ContractState?>()
issueTx.outputs.forEach {
val currentState = it.data
val encumbranceState = it.encumbrance?.let { issueTx.outputs[it].data }
encumbranceLink[currentState] = encumbranceState
}
notaryChangeTx.outputs.forEach {
val currentState = it.data
val encumbranceState = it.encumbrance?.let { notaryChangeTx.outputs[it].data }
assertEquals(encumbranceLink[currentState], encumbranceState)
}
// Check if encumbrance linking between states has not changed.
val originalLinkedStates = issueTx.outputs.asSequence().filter { it.encumbrance != null }.map { Pair(it.data, issueTx.outputs[it.encumbrance!!].data) }.toSet()
val notaryChangeLinkedStates = notaryChangeTx.outputs.asSequence().filter { it.encumbrance != null }.map { Pair(it.data, notaryChangeTx.outputs[it.encumbrance!!].data) }.toSet()
assertTrue { originalLinkedStates.size == notaryChangeLinkedStates.size && originalLinkedStates.containsAll(notaryChangeLinkedStates) }
}
@Test
@ -172,10 +165,11 @@ class NotaryChangeTests {
val stateB = DummyContract.SingleOwnerState(Random().nextInt(), owner.party)
val stateC = DummyContract.SingleOwnerState(Random().nextInt(), owner.party)
// Ensure encumbrances form a cycle.
val tx = TransactionBuilder(null).apply {
addCommand(Command(DummyContract.Commands.Create(), owner.party.owningKey))
addOutputState(stateA, DummyContract.PROGRAM_ID, notaryIdentity, encumbrance = 2) // Encumbered by stateB
addOutputState(stateC, DummyContract.PROGRAM_ID, notaryIdentity)
addOutputState(stateC, DummyContract.PROGRAM_ID, notaryIdentity, encumbrance = 0) // Encumbered by stateA
addOutputState(stateB, DummyContract.PROGRAM_ID, notaryIdentity, encumbrance = 1) // Encumbered by stateC
}
val stx = services.signInitialTransaction(tx)