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
This commit is contained in:
Andrius Dagys 2017-01-05 17:44:31 +00:00 committed by GitHub
parent 08e391579c
commit b9d5081af6
14 changed files with 248 additions and 155 deletions

View File

@ -114,25 +114,6 @@ interface ContractState {
* list should just contain the owner. * list should just contain the owner.
*/ */
val participants: List<CompositeKey> val participants: List<CompositeKey>
/**
* 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<out T : ContractState>(
/** The custom contract state */ /** The custom contract state */
val data: T, val data: T,
/** Identity of the notary that ensures the state is not used as an input to a transaction more than once */ /** 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. * 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]. * 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 */ /** Wraps the [ContractState] in a [TransactionState] object */

View File

@ -74,14 +74,14 @@ sealed class TransactionType {
private fun verifyEncumbrances(tx: LedgerTransaction) { private fun verifyEncumbrances(tx: LedgerTransaction) {
// Validate that all encumbrances exist within the set of input states. // 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 -> encumberedInputs.forEach { encumberedInput ->
val encumbranceStateExists = tx.inputs.any { 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) { if (!encumbranceStateExists) {
throw TransactionVerificationException.TransactionMissingEncumbranceException( throw TransactionVerificationException.TransactionMissingEncumbranceException(
tx, encumberedInput.state.data.encumbrance!!, tx, encumberedInput.state.encumbrance!!,
TransactionVerificationException.Direction.INPUT 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, // 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. // and that the number of outputs can contain the encumbrance.
for ((i, output) in tx.outputs.withIndex()) { 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) { if (encumbranceIndex == i || encumbranceIndex >= tx.outputs.size) {
throw TransactionVerificationException.TransactionMissingEncumbranceException( throw TransactionVerificationException.TransactionMissingEncumbranceException(
tx, encumbranceIndex, tx, encumbranceIndex,

View File

@ -159,7 +159,8 @@ open class TransactionBuilder(
return outputs.size - 1 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 */ /** A default notary must be specified during builder construction to use this method */
fun addOutputState(state: ContractState): Int { fun addOutputState(state: ContractState): Int {

View File

@ -67,10 +67,10 @@ abstract class AbstractStateReplacementFlow<T> {
} }
abstract protected fun assembleProposal(stateRef: StateRef, modification: T, stx: SignedTransaction): Proposal<T> abstract protected fun assembleProposal(stateRef: StateRef, modification: T, stx: SignedTransaction): Proposal<T>
abstract protected fun assembleTx(): Pair<SignedTransaction, List<CompositeKey>> abstract protected fun assembleTx(): Pair<SignedTransaction, Iterable<CompositeKey>>
@Suspendable @Suspendable
private fun collectSignatures(participants: List<CompositeKey>, stx: SignedTransaction): List<DigitalSignature.WithKey> { private fun collectSignatures(participants: Iterable<CompositeKey>, stx: SignedTransaction): List<DigitalSignature.WithKey> {
val parties = participants.map { val parties = participants.map {
val participantNode = serviceHub.networkMapCache.getNodeByLegalIdentityKey(it) ?: val participantNode = serviceHub.networkMapCache.getNodeByLegalIdentityKey(it) ?:
throw IllegalStateException("Participant $it to state $originalState not found on the network") throw IllegalStateException("Participant $it to state $originalState not found on the network")

View File

@ -1,13 +1,11 @@
package net.corda.flows package net.corda.flows
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.ContractState import net.corda.core.contracts.*
import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.TransactionType
import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.Party import net.corda.core.crypto.Party
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.UntrustworthyData import net.corda.core.utilities.UntrustworthyData
import net.corda.flows.NotaryChangeFlow.Acceptor import net.corda.flows.NotaryChangeFlow.Acceptor
@ -36,17 +34,66 @@ object NotaryChangeFlow : AbstractStateReplacementFlow<Party>() {
override fun assembleProposal(stateRef: StateRef, modification: Party, stx: SignedTransaction): AbstractStateReplacementFlow.Proposal<Party> override fun assembleProposal(stateRef: StateRef, modification: Party, stx: SignedTransaction): AbstractStateReplacementFlow.Proposal<Party>
= Proposal(stateRef, modification, stx) = Proposal(stateRef, modification, stx)
override fun assembleTx(): Pair<SignedTransaction, List<CompositeKey>> { override fun assembleTx(): Pair<SignedTransaction, Iterable<CompositeKey>> {
val state = originalState.state val state = originalState.state
val newState = state.withNotary(modification) val tx = TransactionType.NotaryChange.Builder(originalState.state.notary)
val participants = state.data.participants
val tx = TransactionType.NotaryChange.Builder(originalState.state.notary).withItems(originalState, newState) val participants: Iterable<CompositeKey>
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 val myKey = serviceHub.legalIdentityKey
tx.signWith(myKey) tx.signWith(myKey)
val stx = tx.toSignedTransaction(false) val stx = tx.toSignedTransaction(false)
return Pair(stx, participants) 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<CompositeKey> {
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<CompositeKey>()
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, class Acceptor(otherSide: Party,

View File

@ -12,32 +12,28 @@ import org.junit.Test
import java.time.Instant import java.time.Instant
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
val TEST_TIMELOCK_ID = TransactionEncumbranceTests.TestTimeLock() val TEST_TIMELOCK_ID = TransactionEncumbranceTests.DummyTimeLock()
class TransactionEncumbranceTests { class TransactionEncumbranceTests {
val defaultIssuer = MEGA_CORP.ref(1) val defaultIssuer = MEGA_CORP.ref(1)
val encumberedState = Cash.State(
amount = 1000.DOLLARS `issued by` defaultIssuer, val state = Cash.State(
owner = DUMMY_PUBKEY_1,
encumbrance = 1
)
val unencumberedState = Cash.State(
amount = 1000.DOLLARS `issued by` defaultIssuer, amount = 1000.DOLLARS `issued by` defaultIssuer,
owner = DUMMY_PUBKEY_1 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 FOUR_PM = Instant.parse("2015-04-17T16:00:00.00Z")
val FIVE_PM = FOUR_PM.plus(1, ChronoUnit.HOURS) val FIVE_PM = FOUR_PM.plus(1, ChronoUnit.HOURS)
val FIVE_PM_TIMELOCK = TestTimeLock.State(FIVE_PM) val timeLock = DummyTimeLock.State(FIVE_PM)
class DummyTimeLock : Contract {
class TestTimeLock : Contract { override val legalContractReference = SecureHash.sha256("DummyTimeLock")
override val legalContractReference = SecureHash.sha256("TestTimeLock")
override fun verify(tx: TransactionForContract) { override fun verify(tx: TransactionForContract) {
val timeLockInput = tx.inputs.filterIsInstance<State>().singleOrNull() ?: return
val time = tx.timestamp?.before ?: throw IllegalArgumentException("Transactions containing time-locks must be timestamped") val time = tx.timestamp?.before ?: throw IllegalArgumentException("Transactions containing time-locks must be timestamped")
requireThat { requireThat {
"the time specified in the time-lock has passed" by "the time specified in the time-lock has passed" by (time >= timeLockInput.validFrom)
(time >= tx.inputs.filterIsInstance<TestTimeLock.State>().single().validFrom)
} }
} }
@ -50,110 +46,110 @@ class TransactionEncumbranceTests {
} }
@Test @Test
fun trivial() { fun `state can be encumbered`() {
// A transaction containing an input state that is encumbered must fail if the encumbrance has not been presented ledger {
// on the input states. transaction {
transaction { input { state }
input { encumberedState } output(encumbrance = 1) { stateWithNewOwner }
output { unencumberedState } output("5pm time-lock") { timeLock }
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
this `fails with` "Missing required encumbrance 1 in INPUT" 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 @Test
fun testEncumbranceEffects() { fun `state can transition if encumbrance rules are met`() {
// 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.
ledger { ledger {
unverifiedTransaction { unverifiedTransaction {
output("state encumbered by 5pm time-lock") { encumberedState } output("state encumbered by 5pm time-lock") { state }
output { unencumberedState } output("5pm time-lock") { timeLock }
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 }
} }
// Un-encumber the output if the time of the transaction is later than the timelock. // Un-encumber the output if the time of the transaction is later than the timelock.
transaction { transaction {
input("state encumbered by 5pm time-lock") input("state encumbered by 5pm time-lock")
input("5pm time-lock") input("5pm time-lock")
output { unencumberedState } output { stateWithNewOwner }
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
timestamp(FIVE_PM) 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 { ledger {
unverifiedTransaction { unverifiedTransaction {
output("state encumbered by 5pm time-lock") { encumberedState } output("state encumbered by 5pm time-lock") { state }
output("5pm time-lock") { FIVE_PM_TIMELOCK } output("5pm time-lock") { timeLock }
} }
// The time of the transaction is earlier than the time specified in the encumbering timelock. // The time of the transaction is earlier than the time specified in the encumbering timelock.
transaction { transaction {
input("state encumbered by 5pm time-lock") input("state encumbered by 5pm time-lock")
input("5pm time-lock") input("5pm time-lock")
output { unencumberedState } output { state }
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
timestamp(FOUR_PM) timestamp(FOUR_PM)
this `fails with` "the time specified in the time-lock has passed" 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"
}
}
}
} }

View File

@ -84,12 +84,12 @@ private fun prepareOurInputsAndOutputs(serviceHub: ServiceHub, request: FxReques
val (inputs, residual) = gatherOurInputs(serviceHub, sellAmount, request.notary) val (inputs, residual) = gatherOurInputs(serviceHub, sellAmount, request.notary)
// Build and an output state for the counterparty // 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) { if (residual > 0L) {
// Build an output state for the residual change back to us // Build an output state for the residual change back to us
val residualAmount = Amount(residual, sellAmount.token) 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)) return FxResponse(inputs, listOf(transferedFundsOutput, residualOutput))
} else { } else {
return FxResponse(inputs, listOf(transferedFundsOutput)) return FxResponse(inputs, listOf(transferedFundsOutput))

View File

@ -123,12 +123,6 @@ public class JavaCommercialPaper implements Contract {
public List<CompositeKey> getParticipants() { public List<CompositeKey> getParticipants() {
return ImmutableList.of(this.owner); return ImmutableList.of(this.owner);
} }
@Nullable
@Override
public Integer getEncumbrance() {
return null;
}
} }
public interface Clauses { public interface Clauses {
@ -303,12 +297,16 @@ public class JavaCommercialPaper implements Contract {
return SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper"); return SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper");
} }
public TransactionBuilder generateIssue(@NotNull PartyAndReference issuance, @NotNull Amount<Issued<Currency>> faceValue, @Nullable Instant maturityDate, @NotNull Party notary) { public TransactionBuilder generateIssue(@NotNull PartyAndReference issuance, @NotNull Amount<Issued<Currency>> faceValue, @Nullable Instant maturityDate, @NotNull Party notary, Integer encumbrance) {
State state = new State(issuance, issuance.getParty().getOwningKey(), faceValue, maturityDate); 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())); return new TransactionType.General.Builder(notary).withItems(output, new Command(new Commands.Issue(), issuance.getParty().getOwningKey()));
} }
public TransactionBuilder generateIssue(@NotNull PartyAndReference issuance, @NotNull Amount<Issued<Currency>> faceValue, @Nullable Instant maturityDate, @NotNull Party notary) {
return generateIssue(issuance, faceValue, maturityDate, notary, null);
}
public void generateRedeem(TransactionBuilder tx, StateAndRef<State> paper, VaultService vault) throws InsufficientBalanceException { public void generateRedeem(TransactionBuilder tx, StateAndRef<State> paper, VaultService vault) throws InsufficientBalanceException {
vault.generateSpend(tx, StructuresKt.withoutIssuer(paper.getState().getData().getFaceValue()), paper.getState().getData().getOwner(), null); vault.generateSpend(tx, StructuresKt.withoutIssuer(paper.getState().getData().getFaceValue()), paper.getState().getData().getOwner(), null);
tx.addInputState(paper); tx.addInputState(paper);
@ -317,7 +315,7 @@ public class JavaCommercialPaper implements Contract {
public void generateMove(TransactionBuilder tx, StateAndRef<State> paper, CompositeKey newOwner) { public void generateMove(TransactionBuilder tx, StateAndRef<State> paper, CompositeKey newOwner) {
tx.addInputState(paper); 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())); tx.addCommand(new Command(new Commands.Move(), paper.getState().getData().getOwner()));
} }
} }

View File

@ -82,8 +82,7 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
override val amount: Amount<Issued<Currency>>, override val amount: Amount<Issued<Currency>>,
/** There must be a MoveCommand signed by this key to claim the amount. */ /** There must be a MoveCommand signed by this key to claim the amount. */
override val owner: CompositeKey, override val owner: CompositeKey
override val encumbrance: Int? = null
) : FungibleAsset<Currency>, QueryableState { ) : FungibleAsset<Currency>, QueryableState {
constructor(deposit: PartyAndReference, amount: Amount<Currency>, owner: CompositeKey) constructor(deposit: PartyAndReference, amount: Amount<Currency>, owner: CompositeKey)
: this(Amount(amount.quantity, Issued(deposit, amount.token)), owner) : this(Amount(amount.quantity, Issued(deposit, amount.token)), owner)
@ -103,7 +102,6 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
override fun generateMappedObject(schema: MappedSchema): PersistentState { override fun generateMappedObject(schema: MappedSchema): PersistentState {
return when (schema) { return when (schema) {
is CashSchemaV1 -> CashSchemaV1.PersistentCashState( is CashSchemaV1 -> CashSchemaV1.PersistentCashState(
encumbrance = this.encumbrance,
owner = this.owner.toBase58String(), owner = this.owner.toBase58String(),
pennies = this.amount.quantity, pennies = this.amount.quantity,
currency = this.amount.token.product.currencyCode, currency = this.amount.token.product.currencyCode,

View File

@ -19,9 +19,6 @@ object CashSchemaV1 : MappedSchema(schemaFamily = CashSchema.javaClass, version
@Entity @Entity
@Table(name = "cash_states") @Table(name = "cash_states")
class PersistentCashState( class PersistentCashState(
@Column(name = "encumbrance")
var encumbrance: Int?,
@Column(name = "owner_key") @Column(name = "owner_key")
var owner: String, var owner: String,

View File

@ -15,8 +15,8 @@ import static net.corda.testing.CoreTestUtils.*;
public class CashTestsJava { public class CashTestsJava {
private final OpaqueBytes defaultRef = new OpaqueBytes(new byte[]{1}); private final OpaqueBytes defaultRef = new OpaqueBytes(new byte[]{1});
private final PartyAndReference defaultIssuer = getMEGA_CORP().ref(defaultRef); 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 inState = new Cash.State(issuedBy(DOLLARS(1000), defaultIssuer), getDUMMY_PUBKEY_1());
private final Cash.State outState = new Cash.State(inState.getAmount(), getDUMMY_PUBKEY_2(), null); private final Cash.State outState = new Cash.State(inState.getAmount(), getDUMMY_PUBKEY_2());
@Test @Test
public void trivial() { public void trivial() {
@ -26,7 +26,7 @@ public class CashTestsJava {
tx.failsWith("the amounts balance"); tx.failsWith("the amounts balance");
tx.tweak(tw -> { 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"); return tw.failsWith("the amounts balance");
}); });

View File

@ -6,6 +6,7 @@ import net.corda.core.crypto.generateKeyPair
import net.corda.core.getOrThrow import net.corda.core.getOrThrow
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.core.seconds 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
import net.corda.core.utilities.DUMMY_NOTARY_KEY import net.corda.core.utilities.DUMMY_NOTARY_KEY
import net.corda.flows.NotaryChangeFlow.Instigator import net.corda.flows.NotaryChangeFlow.Instigator
@ -22,6 +23,7 @@ import java.time.Instant
import java.util.* import java.util.*
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
class NotaryChangeTests { class NotaryChangeTests {
lateinit var net: MockNetwork lateinit var net: MockNetwork
@ -86,6 +88,60 @@ class NotaryChangeTests {
assertThat(ex.error).isInstanceOf(StateReplacementRefused::class.java) 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<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)
}
}
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: // 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 // - A participant is offline/can't be found on the network
// - The requesting party is not a participant // - The requesting party is not a participant

View File

@ -115,8 +115,8 @@ data class TestTransactionDSLInterpreter private constructor(
transactionBuilder.addInputState(StateAndRef(state, stateRef)) transactionBuilder.addInputState(StateAndRef(state, stateRef))
} }
override fun _output(label: String?, notary: Party, contractState: ContractState) { override fun _output(label: String?, notary: Party, encumbrance: Int?, contractState: ContractState) {
val outputIndex = transactionBuilder.addOutputState(contractState, notary) val outputIndex = transactionBuilder.addOutputState(contractState, notary, encumbrance)
if (label != null) { if (label != null) {
if (label in labelToIndexMap) { if (label in labelToIndexMap) {
throw DuplicateOutputLabel(label) throw DuplicateOutputLabel(label)

View File

@ -31,9 +31,10 @@ interface TransactionDSLInterpreter : Verifies, OutputStateLookup {
* Adds an output to the transaction. * 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 label An optional label that may be later used to retrieve the output probably in other transactions.
* @param notary The associated notary. * @param notary The associated notary.
* @param encumbrance The position of the encumbrance state.
* @param contractState The state itself. * @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. * Adds an [Attachment] reference to the transaction.
@ -85,16 +86,16 @@ class TransactionDSL<out T : TransactionDSLInterpreter>(val interpreter: T) : Tr
* @see TransactionDSLInterpreter._output * @see TransactionDSLInterpreter._output
*/ */
@JvmOverloads @JvmOverloads
fun output(label: String? = null, notary: Party = DUMMY_NOTARY, contractStateClosure: () -> ContractState) = fun output(label: String? = null, notary: Party = DUMMY_NOTARY, encumbrance: Int? = null, contractStateClosure: () -> ContractState) =
_output(label, notary, contractStateClosure()) _output(label, notary, encumbrance, contractStateClosure())
/** /**
* @see TransactionDSLInterpreter._output * @see TransactionDSLInterpreter._output
*/ */
fun output(label: String, contractState: ContractState) = fun output(label: String, contractState: ContractState) =
_output(label, DUMMY_NOTARY, contractState) _output(label, DUMMY_NOTARY, null, contractState)
fun output(contractState: ContractState) = fun output(contractState: ContractState) =
_output(null, DUMMY_NOTARY, contractState) _output(null, DUMMY_NOTARY, null, contractState)
/** /**
* @see TransactionDSLInterpreter._command * @see TransactionDSLInterpreter._command