mirror of
https://github.com/corda/corda.git
synced 2025-05-31 22:50:53 +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.
|
* 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 */
|
||||||
|
@ -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,
|
||||||
|
@ -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 {
|
||||||
|
@ -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")
|
||||||
|
@ -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,
|
||||||
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
|
@ -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()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
|
||||||
|
@ -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");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user