mirror of
https://github.com/corda/corda.git
synced 2025-05-28 13:14:24 +00:00
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:
parent
08e391579c
commit
b9d5081af6
@ -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 */
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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")
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
||||
|
@ -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");
|
||||
});
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user