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.
*/
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 */
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 */

View File

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

View File

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

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 assembleTx(): Pair<SignedTransaction, List<CompositeKey>>
abstract protected fun assembleTx(): Pair<SignedTransaction, Iterable<CompositeKey>>
@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 participantNode = serviceHub.networkMapCache.getNodeByLegalIdentityKey(it) ?:
throw IllegalStateException("Participant $it to state $originalState not found on the network")

View File

@ -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<Party>() {
override fun assembleProposal(stateRef: StateRef, modification: Party, stx: SignedTransaction): AbstractStateReplacementFlow.Proposal<Party>
= Proposal(stateRef, modification, stx)
override fun assembleTx(): Pair<SignedTransaction, List<CompositeKey>> {
override fun assembleTx(): Pair<SignedTransaction, Iterable<CompositeKey>> {
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<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
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<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,

View File

@ -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<State>().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<TestTimeLock.State>().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"
}
}
}
}

View File

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

View File

@ -123,12 +123,6 @@ public class JavaCommercialPaper implements Contract {
public List<CompositeKey> 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<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);
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<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 {
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<State> 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()));
}
}

View File

@ -82,8 +82,7 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
override val amount: Amount<Issued<Currency>>,
/** 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<Currency>, QueryableState {
constructor(deposit: PartyAndReference, amount: Amount<Currency>, owner: CompositeKey)
: 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 {
return when (schema) {
is CashSchemaV1 -> CashSchemaV1.PersistentCashState(
encumbrance = this.encumbrance,
owner = this.owner.toBase58String(),
pennies = this.amount.quantity,
currency = this.amount.token.product.currencyCode,

View File

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

View File

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

View File

@ -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<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:
// - A participant is offline/can't be found on the network
// - The requesting party is not a participant

View File

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

View File

@ -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<out T : TransactionDSLInterpreter>(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