From b9d5081af611fa8b5c9ac41e1769e8bf0244bd4e Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Thu, 5 Jan 2017 17:44:31 +0000 Subject: [PATCH] Update notary change flow to support encumbrances (#101) * Update notary change flow to support encumbrances. Move encumbrance pointer from ContractState to TransactionState. * Refactor & add new encumbrance tests --- .../net/corda/core/contracts/Structures.kt | 41 ++-- .../corda/core/contracts/TransactionTypes.kt | 8 +- .../core/transactions/TransactionBuilder.kt | 3 +- .../flows/AbstractStateReplacementFlow.kt | 4 +- .../net/corda/flows/NotaryChangeFlow.kt | 63 +++++- .../contracts/TransactionEncumbranceTests.kt | 180 +++++++++--------- .../corda/docs/FxTransactionBuildTutorial.kt | 4 +- .../corda/contracts/JavaCommercialPaper.java | 16 +- .../kotlin/net/corda/contracts/asset/Cash.kt | 4 +- .../kotlin/net/corda/schemas/CashSchemaV1.kt | 3 - .../corda/contracts/asset/CashTestsJava.java | 6 +- .../corda/node/services/NotaryChangeTests.kt | 56 ++++++ .../main/kotlin/net/corda/testing/TestDSL.kt | 4 +- .../testing/TransactionDSLInterpreter.kt | 11 +- 14 files changed, 248 insertions(+), 155 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt index 1434087756..dbdfeb71da 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt @@ -114,25 +114,6 @@ interface ContractState { * list should just contain the owner. */ val participants: List - - /** - * All contract states may be _encumbered_ by up to one other state. - * - * The encumbrance state, if present, forces additional controls over the encumbered state, since the platform checks - * that the encumbrance state is present as an input in the same transaction that consumes the encumbered state, and - * the contract code and rules of the encumbrance state will also be verified during the execution of the transaction. - * For example, a cash contract state could be encumbered with a time-lock contract state; the cash state is then only - * processable in a transaction that verifies that the time specified in the encumbrance time-lock has passed. - * - * 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 - * implementation would be encumber by reference to a StateRef., which would allow the specification of encumbrance - * 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, - * otherwise the transaction is not valid. - */ - val encumbrance: Int? get() = null } /** @@ -143,13 +124,31 @@ data class TransactionState( /** The custom contract state */ val data: T, /** Identity of the notary that ensures the state is not used as an input to a transaction more than once */ - val notary: Party) { + val notary: Party, + /** + * All contract states may be _encumbered_ by up to one other state. + * + * The encumbrance state, if present, forces additional controls over the encumbered state, since the platform checks + * that the encumbrance state is present as an input in the same transaction that consumes the encumbered state, and + * the contract code and rules of the encumbrance state will also be verified during the execution of the transaction. + * For example, a cash contract state could be encumbered with a time-lock contract state; the cash state is then only + * processable in a transaction that verifies that the time specified in the encumbrance time-lock has passed. + * + * 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 + * implementation would be encumber by reference to a StateRef., which would allow the specification of encumbrance + * 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, + * otherwise the transaction is not valid. + */ + val encumbrance: Int? = null) { /** * Copies the underlying state, replacing the notary field with the new value. * To replace the notary, we need an approval (signature) from _all_ participants of the [ContractState]. */ - fun withNotary(newNotary: Party) = TransactionState(this.data, newNotary) + fun withNotary(newNotary: Party) = TransactionState(this.data, newNotary, encumbrance) } /** Wraps the [ContractState] in a [TransactionState] object */ diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionTypes.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionTypes.kt index 39d8d1830a..8b8cabf342 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionTypes.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionTypes.kt @@ -74,14 +74,14 @@ sealed class TransactionType { private fun verifyEncumbrances(tx: LedgerTransaction) { // Validate that all encumbrances exist within the set of input states. - val encumberedInputs = tx.inputs.filter { it.state.data.encumbrance != null } + val encumberedInputs = tx.inputs.filter { it.state.encumbrance != null } encumberedInputs.forEach { encumberedInput -> val encumbranceStateExists = tx.inputs.any { - it.ref.txhash == encumberedInput.ref.txhash && it.ref.index == encumberedInput.state.data.encumbrance + it.ref.txhash == encumberedInput.ref.txhash && it.ref.index == encumberedInput.state.encumbrance } if (!encumbranceStateExists) { throw TransactionVerificationException.TransactionMissingEncumbranceException( - tx, encumberedInput.state.data.encumbrance!!, + tx, encumberedInput.state.encumbrance!!, TransactionVerificationException.Direction.INPUT ) } @@ -90,7 +90,7 @@ sealed class TransactionType { // 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 tx.outputs.withIndex()) { - val encumbranceIndex = output.data.encumbrance ?: continue + val encumbranceIndex = output.encumbrance ?: continue if (encumbranceIndex == i || encumbranceIndex >= tx.outputs.size) { throw TransactionVerificationException.TransactionMissingEncumbranceException( tx, encumbranceIndex, diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index 76accc297a..a3a6c5cff8 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -159,7 +159,8 @@ open class TransactionBuilder( return outputs.size - 1 } - fun addOutputState(state: ContractState, notary: Party) = addOutputState(TransactionState(state, notary)) + @JvmOverloads + fun addOutputState(state: ContractState, notary: Party, encumbrance: Int? = null) = addOutputState(TransactionState(state, notary, encumbrance)) /** A default notary must be specified during builder construction to use this method */ fun addOutputState(state: ContractState): Int { diff --git a/core/src/main/kotlin/net/corda/flows/AbstractStateReplacementFlow.kt b/core/src/main/kotlin/net/corda/flows/AbstractStateReplacementFlow.kt index 4581779ac1..0e353d10c6 100644 --- a/core/src/main/kotlin/net/corda/flows/AbstractStateReplacementFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/AbstractStateReplacementFlow.kt @@ -67,10 +67,10 @@ abstract class AbstractStateReplacementFlow { } abstract protected fun assembleProposal(stateRef: StateRef, modification: T, stx: SignedTransaction): Proposal - abstract protected fun assembleTx(): Pair> + abstract protected fun assembleTx(): Pair> @Suspendable - private fun collectSignatures(participants: List, stx: SignedTransaction): List { + private fun collectSignatures(participants: Iterable, stx: SignedTransaction): List { val parties = participants.map { val participantNode = serviceHub.networkMapCache.getNodeByLegalIdentityKey(it) ?: throw IllegalStateException("Participant $it to state $originalState not found on the network") diff --git a/core/src/main/kotlin/net/corda/flows/NotaryChangeFlow.kt b/core/src/main/kotlin/net/corda/flows/NotaryChangeFlow.kt index 2bc8f9fd33..a677b4335a 100644 --- a/core/src/main/kotlin/net/corda/flows/NotaryChangeFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/NotaryChangeFlow.kt @@ -1,13 +1,11 @@ package net.corda.flows import co.paralleluniverse.fibers.Suspendable -import net.corda.core.contracts.ContractState -import net.corda.core.contracts.StateAndRef -import net.corda.core.contracts.StateRef -import net.corda.core.contracts.TransactionType +import net.corda.core.contracts.* import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.Party import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.UntrustworthyData import net.corda.flows.NotaryChangeFlow.Acceptor @@ -36,17 +34,66 @@ object NotaryChangeFlow : AbstractStateReplacementFlow() { override fun assembleProposal(stateRef: StateRef, modification: Party, stx: SignedTransaction): AbstractStateReplacementFlow.Proposal = Proposal(stateRef, modification, stx) - override fun assembleTx(): Pair> { + override fun assembleTx(): Pair> { val state = originalState.state - val newState = state.withNotary(modification) - val participants = state.data.participants - val tx = TransactionType.NotaryChange.Builder(originalState.state.notary).withItems(originalState, newState) + val tx = TransactionType.NotaryChange.Builder(originalState.state.notary) + + val participants: Iterable + + if (state.encumbrance == null) { + val modifiedState = TransactionState(state.data, modification) + tx.addInputState(originalState) + tx.addOutputState(modifiedState) + participants = state.data.participants + } else { + participants = resolveEncumbrances(tx) + } + val myKey = serviceHub.legalIdentityKey tx.signWith(myKey) val stx = tx.toSignedTransaction(false) + return Pair(stx, participants) } + + /** + * Adds the notary change state transitions to the [tx] builder for the [originalState] and its encumbrance + * state chain (encumbrance states might be themselves encumbered by other states). + * + * @return union of all added states' participants + */ + private fun resolveEncumbrances(tx: TransactionBuilder): Iterable { + val stateRef = originalState.ref + val txId = stateRef.txhash + val issuingTx = serviceHub.storageService.validatedTransactions.getTransaction(txId) ?: throw IllegalStateException("Transaction $txId not found") + val outputs = issuingTx.tx.outputs + + val participants = mutableSetOf() + + var nextStateIndex = stateRef.index + var newOutputPosition = tx.outputStates().size + while (true) { + val nextState = outputs[nextStateIndex] + tx.addInputState(StateAndRef(nextState, StateRef(txId, nextStateIndex))) + participants.addAll(nextState.data.participants) + + if (nextState.encumbrance == null) { + val modifiedState = TransactionState(nextState.data, modification) + tx.addOutputState(modifiedState) + break + } else { + val modifiedState = TransactionState(nextState.data, modification, newOutputPosition + 1) + tx.addOutputState(modifiedState) + nextStateIndex = nextState.encumbrance + } + + newOutputPosition++ + } + + return participants + } + } class Acceptor(otherSide: Party, diff --git a/core/src/test/kotlin/net/corda/core/contracts/TransactionEncumbranceTests.kt b/core/src/test/kotlin/net/corda/core/contracts/TransactionEncumbranceTests.kt index 880c43fca9..0ed02db3bb 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/TransactionEncumbranceTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/TransactionEncumbranceTests.kt @@ -12,32 +12,28 @@ import org.junit.Test import java.time.Instant import java.time.temporal.ChronoUnit -val TEST_TIMELOCK_ID = TransactionEncumbranceTests.TestTimeLock() +val TEST_TIMELOCK_ID = TransactionEncumbranceTests.DummyTimeLock() class TransactionEncumbranceTests { val defaultIssuer = MEGA_CORP.ref(1) - val encumberedState = Cash.State( - amount = 1000.DOLLARS `issued by` defaultIssuer, - owner = DUMMY_PUBKEY_1, - encumbrance = 1 - ) - val unencumberedState = Cash.State( + + val state = Cash.State( amount = 1000.DOLLARS `issued by` defaultIssuer, owner = DUMMY_PUBKEY_1 ) - val stateWithNewOwner = encumberedState.copy(owner = DUMMY_PUBKEY_2) + val stateWithNewOwner = state.copy(owner = DUMMY_PUBKEY_2) + val FOUR_PM = Instant.parse("2015-04-17T16:00:00.00Z") val FIVE_PM = FOUR_PM.plus(1, ChronoUnit.HOURS) - val FIVE_PM_TIMELOCK = TestTimeLock.State(FIVE_PM) + val timeLock = DummyTimeLock.State(FIVE_PM) - - class TestTimeLock : Contract { - override val legalContractReference = SecureHash.sha256("TestTimeLock") + class DummyTimeLock : Contract { + override val legalContractReference = SecureHash.sha256("DummyTimeLock") override fun verify(tx: TransactionForContract) { + val timeLockInput = tx.inputs.filterIsInstance().singleOrNull() ?: return val time = tx.timestamp?.before ?: throw IllegalArgumentException("Transactions containing time-locks must be timestamped") requireThat { - "the time specified in the time-lock has passed" by - (time >= tx.inputs.filterIsInstance().single().validFrom) + "the time specified in the time-lock has passed" by (time >= timeLockInput.validFrom) } } @@ -50,110 +46,110 @@ class TransactionEncumbranceTests { } @Test - fun trivial() { - // A transaction containing an input state that is encumbered must fail if the encumbrance has not been presented - // on the input states. - transaction { - input { encumberedState } - output { unencumberedState } - command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } - this `fails with` "Missing required encumbrance 1 in INPUT" + fun `state can be encumbered`() { + ledger { + transaction { + input { state } + output(encumbrance = 1) { stateWithNewOwner } + output("5pm time-lock") { timeLock } + command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } + verifies() + } } - // An encumbered state must not be encumbered by itself. - transaction { - input { unencumberedState } - input { unencumberedState } - output { unencumberedState } - // The encumbered state refers to an encumbrance in position 1, so what follows is wrong. - output { encumberedState } - command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } - this `fails with` "Missing required encumbrance 1 in OUTPUT" - } - // An encumbered state must not reference an index greater than the size of the output states. - // In this test, the output encumbered state refers to an encumbrance in position 1, but there is only one output. - transaction { - input { unencumberedState } - // The encumbered state refers to an encumbrance in position 1, so there should be at least 2 outputs. - output { encumberedState } - command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } - this `fails with` "Missing required encumbrance 1 in OUTPUT" - } - } @Test - fun testEncumbranceEffects() { - // This test fails because the encumbered state is pointing to the ordinary cash state as the encumbrance, - // instead of the timelock by mistake, so when we try and use it the transaction fails as we didn't include the - // encumbrance cash state. + fun `state can transition if encumbrance rules are met`() { ledger { unverifiedTransaction { - output("state encumbered by 5pm time-lock") { encumberedState } - output { unencumberedState } - output("5pm time-lock") { FIVE_PM_TIMELOCK } - } - transaction { - input("state encumbered by 5pm time-lock") - input("5pm time-lock") - output { stateWithNewOwner } - command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } - timestamp(FIVE_PM) - this `fails with` "Missing required encumbrance 1 in INPUT" - } - } - // A transaction containing an input state that is encumbered must fail if the encumbrance is not in the correct - // transaction. In this test, the intended encumbrance is presented alongside the encumbered state for consumption, - // although the encumbered state always refers to the encumbrance produced in the same transaction, and the in this case - // the encumbrance was created in a separate transaction. - ledger { - unverifiedTransaction { - output("state encumbered by 5pm time-lock") { encumberedState } - output { unencumberedState } - } - unverifiedTransaction { - output("5pm time-lock") { FIVE_PM_TIMELOCK } - } - transaction { - input("state encumbered by 5pm time-lock") - input("5pm time-lock") - output { stateWithNewOwner } - command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } - timestamp(FIVE_PM) - this `fails with` "Missing required encumbrance 1 in INPUT" - } - } - // A transaction with an input state that is encumbered must succeed if the rules of the encumbrance are met. - ledger { - unverifiedTransaction { - output("state encumbered by 5pm time-lock") { encumberedState } - output("5pm time-lock") { FIVE_PM_TIMELOCK } + output("state encumbered by 5pm time-lock") { state } + output("5pm time-lock") { timeLock } } // Un-encumber the output if the time of the transaction is later than the timelock. transaction { input("state encumbered by 5pm time-lock") input("5pm time-lock") - output { unencumberedState } + output { stateWithNewOwner } command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } timestamp(FIVE_PM) - this.verifies() + verifies() } } - // A transaction with an input state that is encumbered must fail if the rules of the encumbrance are not met. - // In this test, the time-lock encumbrance is being processed in a transaction before the time allowed. + } + + @Test + fun `state cannot transition if the encumbrance contract fails to verify`() { ledger { unverifiedTransaction { - output("state encumbered by 5pm time-lock") { encumberedState } - output("5pm time-lock") { FIVE_PM_TIMELOCK } + output("state encumbered by 5pm time-lock") { state } + output("5pm time-lock") { timeLock } } // The time of the transaction is earlier than the time specified in the encumbering timelock. transaction { input("state encumbered by 5pm time-lock") input("5pm time-lock") - output { unencumberedState } + output { state } command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } timestamp(FOUR_PM) this `fails with` "the time specified in the time-lock has passed" } } } + + @Test + fun `state must be consumed along with its encumbrance`() { + ledger { + unverifiedTransaction { + output("state encumbered by 5pm time-lock", encumbrance = 1) { state } + output("5pm time-lock") { timeLock } + } + transaction { + input("state encumbered by 5pm time-lock") + output { stateWithNewOwner } + command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } + timestamp(FIVE_PM) + this `fails with` "Missing required encumbrance 1 in INPUT" + } + } + } + + @Test + fun `state cannot be encumbered by itself`() { + transaction { + input { state } + output(encumbrance = 0) { stateWithNewOwner } + command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } + this `fails with` "Missing required encumbrance 0 in OUTPUT" + } + } + + @Test + fun `encumbrance state index must be valid`() { + transaction { + input { state } + output(encumbrance = 2) { stateWithNewOwner } + output { timeLock } + command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } + this `fails with` "Missing required encumbrance 2 in OUTPUT" + } + } + + @Test + fun `correct encumbrance state must be provided`() { + ledger { + unverifiedTransaction { + output("state encumbered by some other state", encumbrance = 1) { state } + output("some other state") { state } + output("5pm time-lock") { timeLock } + } + transaction { + input("state encumbered by some other state") + input("5pm time-lock") + output { stateWithNewOwner } + command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } + timestamp(FIVE_PM) + this `fails with` "Missing required encumbrance 1 in INPUT" + } + } + } } diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt index 1f03b7aaf8..a75ff24521 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt @@ -84,12 +84,12 @@ private fun prepareOurInputsAndOutputs(serviceHub: ServiceHub, request: FxReques val (inputs, residual) = gatherOurInputs(serviceHub, sellAmount, request.notary) // Build and an output state for the counterparty - val transferedFundsOutput = Cash.State(sellAmount, request.counterparty.owningKey, null) + val transferedFundsOutput = Cash.State(sellAmount, request.counterparty.owningKey) if (residual > 0L) { // Build an output state for the residual change back to us val residualAmount = Amount(residual, sellAmount.token) - val residualOutput = Cash.State(residualAmount, serviceHub.myInfo.legalIdentity.owningKey, null) + val residualOutput = Cash.State(residualAmount, serviceHub.myInfo.legalIdentity.owningKey) return FxResponse(inputs, listOf(transferedFundsOutput, residualOutput)) } else { return FxResponse(inputs, listOf(transferedFundsOutput)) diff --git a/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java b/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java index 6fecd8d47d..2d8d8bbf64 100644 --- a/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java +++ b/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java @@ -123,12 +123,6 @@ public class JavaCommercialPaper implements Contract { public List getParticipants() { return ImmutableList.of(this.owner); } - - @Nullable - @Override - public Integer getEncumbrance() { - return null; - } } public interface Clauses { @@ -303,12 +297,16 @@ public class JavaCommercialPaper implements Contract { return SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper"); } - public TransactionBuilder generateIssue(@NotNull PartyAndReference issuance, @NotNull Amount> faceValue, @Nullable Instant maturityDate, @NotNull Party notary) { + public TransactionBuilder generateIssue(@NotNull PartyAndReference issuance, @NotNull Amount> faceValue, @Nullable Instant maturityDate, @NotNull Party notary, Integer encumbrance) { State state = new State(issuance, issuance.getParty().getOwningKey(), faceValue, maturityDate); - TransactionState output = new TransactionState<>(state, notary); + TransactionState output = new TransactionState<>(state, notary, encumbrance); return new TransactionType.General.Builder(notary).withItems(output, new Command(new Commands.Issue(), issuance.getParty().getOwningKey())); } + public TransactionBuilder generateIssue(@NotNull PartyAndReference issuance, @NotNull Amount> faceValue, @Nullable Instant maturityDate, @NotNull Party notary) { + return generateIssue(issuance, faceValue, maturityDate, notary, null); + } + public void generateRedeem(TransactionBuilder tx, StateAndRef paper, VaultService vault) throws InsufficientBalanceException { vault.generateSpend(tx, StructuresKt.withoutIssuer(paper.getState().getData().getFaceValue()), paper.getState().getData().getOwner(), null); tx.addInputState(paper); @@ -317,7 +315,7 @@ public class JavaCommercialPaper implements Contract { public void generateMove(TransactionBuilder tx, StateAndRef paper, CompositeKey newOwner) { tx.addInputState(paper); - tx.addOutputState(new TransactionState<>(new State(paper.getState().getData().getIssuance(), newOwner, paper.getState().getData().getFaceValue(), paper.getState().getData().getMaturityDate()), paper.getState().getNotary())); + tx.addOutputState(new TransactionState<>(new State(paper.getState().getData().getIssuance(), newOwner, paper.getState().getData().getFaceValue(), paper.getState().getData().getMaturityDate()), paper.getState().getNotary(), paper.getState().getEncumbrance())); tx.addCommand(new Command(new Commands.Move(), paper.getState().getData().getOwner())); } } diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt b/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt index 83e8e94add..4ca53d34d7 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt @@ -82,8 +82,7 @@ class Cash : OnLedgerAsset() { override val amount: Amount>, /** There must be a MoveCommand signed by this key to claim the amount. */ - override val owner: CompositeKey, - override val encumbrance: Int? = null + override val owner: CompositeKey ) : FungibleAsset, QueryableState { constructor(deposit: PartyAndReference, amount: Amount, owner: CompositeKey) : this(Amount(amount.quantity, Issued(deposit, amount.token)), owner) @@ -103,7 +102,6 @@ class Cash : OnLedgerAsset() { override fun generateMappedObject(schema: MappedSchema): PersistentState { return when (schema) { is CashSchemaV1 -> CashSchemaV1.PersistentCashState( - encumbrance = this.encumbrance, owner = this.owner.toBase58String(), pennies = this.amount.quantity, currency = this.amount.token.product.currencyCode, diff --git a/finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt b/finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt index b9a3505738..8972dd9254 100644 --- a/finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt +++ b/finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt @@ -19,9 +19,6 @@ object CashSchemaV1 : MappedSchema(schemaFamily = CashSchema.javaClass, version @Entity @Table(name = "cash_states") class PersistentCashState( - @Column(name = "encumbrance") - var encumbrance: Int?, - @Column(name = "owner_key") var owner: String, diff --git a/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java b/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java index 99e4cfd5d4..4d52e97b28 100644 --- a/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java +++ b/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java @@ -15,8 +15,8 @@ import static net.corda.testing.CoreTestUtils.*; public class CashTestsJava { private final OpaqueBytes defaultRef = new OpaqueBytes(new byte[]{1}); private final PartyAndReference defaultIssuer = getMEGA_CORP().ref(defaultRef); - private final Cash.State inState = new Cash.State(issuedBy(DOLLARS(1000), defaultIssuer), getDUMMY_PUBKEY_1(), null); - private final Cash.State outState = new Cash.State(inState.getAmount(), getDUMMY_PUBKEY_2(), null); + private final Cash.State inState = new Cash.State(issuedBy(DOLLARS(1000), defaultIssuer), getDUMMY_PUBKEY_1()); + private final Cash.State outState = new Cash.State(inState.getAmount(), getDUMMY_PUBKEY_2()); @Test public void trivial() { @@ -26,7 +26,7 @@ public class CashTestsJava { tx.failsWith("the amounts balance"); tx.tweak(tw -> { - tw.output(new Cash.State(issuedBy(DOLLARS(2000), defaultIssuer), getDUMMY_PUBKEY_2(), null)); + tw.output(new Cash.State(issuedBy(DOLLARS(2000), defaultIssuer), getDUMMY_PUBKEY_2())); return tw.failsWith("the amounts balance"); }); diff --git a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt index 24601d5355..d6e67f452c 100644 --- a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt @@ -6,6 +6,7 @@ import net.corda.core.crypto.generateKeyPair import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo import net.corda.core.seconds +import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.DUMMY_NOTARY_KEY import net.corda.flows.NotaryChangeFlow.Instigator @@ -22,6 +23,7 @@ import java.time.Instant import java.util.* import kotlin.test.assertEquals import kotlin.test.assertFailsWith +import kotlin.test.assertTrue class NotaryChangeTests { lateinit var net: MockNetwork @@ -86,6 +88,60 @@ class NotaryChangeTests { assertThat(ex.error).isInstanceOf(StateReplacementRefused::class.java) } + @Test + fun `should not break encumbrance links`() { + val issueTx = issueEncumberedState(clientNodeA, oldNotaryNode) + + val state = StateAndRef(issueTx.outputs.first(), StateRef(issueTx.id, 0)) + val newNotary = newNotaryNode.info.notaryIdentity + val flow = Instigator(state, newNotary) + val future = clientNodeA.services.startFlow(flow) + net.runNetwork() + val newState = future.resultFuture.getOrThrow() + assertEquals(newState.state.notary, newNotary) + + val notaryChangeTx = clientNodeA.services.storageService.validatedTransactions.getTransaction(newState.ref.txhash)!!.tx + + // Check that all encumbrances have been propagated to the outputs + val originalOutputs = issueTx.outputs.map { it.data } + val newOutputs = notaryChangeTx.outputs.map { it.data } + assertTrue(originalOutputs.minus(newOutputs).isEmpty()) + + // Check that encumbrance links aren't broken after notary change + val encumbranceLink = HashMap() + 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) + } + } + + private fun issueEncumberedState(node: AbstractNode, notaryNode: AbstractNode): WireTransaction { + val owner = node.info.legalIdentity.ref(0) + val notary = notaryNode.info.notaryIdentity + + val stateA = DummyContract.SingleOwnerState(Random().nextInt(), owner.party.owningKey) + val stateB = DummyContract.SingleOwnerState(Random().nextInt(), owner.party.owningKey) + val stateC = DummyContract.SingleOwnerState(Random().nextInt(), owner.party.owningKey) + + val tx = TransactionType.General.Builder(null).apply { + addCommand(Command(DummyContract.Commands.Create(), owner.party.owningKey)) + addOutputState(stateA, notary, encumbrance = 2) // Encumbered by stateB + addOutputState(stateC, notary) + addOutputState(stateB, notary, encumbrance = 1) // Encumbered by stateC + } + val nodeKey = node.services.legalIdentityKey + tx.signWith(nodeKey) + val stx = tx.toSignedTransaction() + node.services.recordTransactions(listOf(stx)) + return tx.toWireTransaction() + } + // TODO: Add more test cases once we have a general flow/service exception handling mechanism: // - A participant is offline/can't be found on the network // - The requesting party is not a participant diff --git a/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt b/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt index 70ca887912..95dfcb0e74 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt @@ -115,8 +115,8 @@ data class TestTransactionDSLInterpreter private constructor( transactionBuilder.addInputState(StateAndRef(state, stateRef)) } - override fun _output(label: String?, notary: Party, contractState: ContractState) { - val outputIndex = transactionBuilder.addOutputState(contractState, notary) + override fun _output(label: String?, notary: Party, encumbrance: Int?, contractState: ContractState) { + val outputIndex = transactionBuilder.addOutputState(contractState, notary, encumbrance) if (label != null) { if (label in labelToIndexMap) { throw DuplicateOutputLabel(label) diff --git a/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt b/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt index e094a8fcf3..4cace87afe 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt @@ -31,9 +31,10 @@ interface TransactionDSLInterpreter : Verifies, OutputStateLookup { * Adds an output to the transaction. * @param label An optional label that may be later used to retrieve the output probably in other transactions. * @param notary The associated notary. + * @param encumbrance The position of the encumbrance state. * @param contractState The state itself. */ - fun _output(label: String?, notary: Party, contractState: ContractState) + fun _output(label: String?, notary: Party, encumbrance: Int?, contractState: ContractState) /** * Adds an [Attachment] reference to the transaction. @@ -85,16 +86,16 @@ class TransactionDSL(val interpreter: T) : Tr * @see TransactionDSLInterpreter._output */ @JvmOverloads - fun output(label: String? = null, notary: Party = DUMMY_NOTARY, contractStateClosure: () -> ContractState) = - _output(label, notary, contractStateClosure()) + fun output(label: String? = null, notary: Party = DUMMY_NOTARY, encumbrance: Int? = null, contractStateClosure: () -> ContractState) = + _output(label, notary, encumbrance, contractStateClosure()) /** * @see TransactionDSLInterpreter._output */ fun output(label: String, contractState: ContractState) = - _output(label, DUMMY_NOTARY, contractState) + _output(label, DUMMY_NOTARY, null, contractState) fun output(contractState: ContractState) = - _output(null, DUMMY_NOTARY, contractState) + _output(null, DUMMY_NOTARY, null, contractState) /** * @see TransactionDSLInterpreter._command