mirror of
https://github.com/corda/corda.git
synced 2025-04-07 19:34:41 +00:00
Merged in james-encumbrances (pull request #289)
Encumbrances implemented by reference to an index of an output state
This commit is contained in:
commit
34890b7678
@ -124,6 +124,12 @@ public class JavaCommercialPaper implements Contract {
|
||||
public List<PublicKey> getParticipants() {
|
||||
return ImmutableList.of(this.owner);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Integer getEncumbrance() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public interface Clauses {
|
||||
|
@ -72,12 +72,13 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
|
||||
class ConserveAmount : AbstractConserveAmount<State, Commands, Currency>()
|
||||
}
|
||||
|
||||
/** A state representing a cash claim against some party */
|
||||
/** A state representing a cash claim against some party. */
|
||||
data class State(
|
||||
override val amount: Amount<Issued<Currency>>,
|
||||
|
||||
/** There must be a MoveCommand signed by this key to claim the amount */
|
||||
override val owner: PublicKey
|
||||
/** There must be a MoveCommand signed by this key to claim the amount. */
|
||||
override val owner: PublicKey,
|
||||
override val encumbrance: Int? = null
|
||||
) : FungibleAsset<Currency> {
|
||||
constructor(deposit: PartyAndReference, amount: Amount<Currency>, owner: PublicKey)
|
||||
: this(Amount(amount.quantity, Issued(deposit, amount.token)), owner)
|
||||
|
@ -15,8 +15,8 @@ import static com.r3corda.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());
|
||||
private final Cash.State outState = new Cash.State(inState.getAmount(), getDUMMY_PUBKEY_2());
|
||||
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);
|
||||
|
||||
@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()));
|
||||
tw.output(new Cash.State(issuedBy(DOLLARS(2000), defaultIssuer), getDUMMY_PUBKEY_2(), null));
|
||||
return tw.failsWith("the amounts balance");
|
||||
});
|
||||
|
||||
|
@ -113,6 +113,25 @@ interface ContractState {
|
||||
* list should just contain the owner.
|
||||
*/
|
||||
val participants: List<PublicKey>
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -78,6 +78,29 @@ sealed class TransactionType {
|
||||
throw TransactionVerificationException.ContractRejection(tx, contract, e)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate that all encumbrances exist within the set of input states.
|
||||
tx.inputs.filter { it.state.data.encumbrance != null }.forEach {
|
||||
encumberedInput ->
|
||||
if (tx.inputs.none { it.ref.txhash == encumberedInput.ref.txhash &&
|
||||
it.ref.index == encumberedInput.state.data.encumbrance }) {
|
||||
throw TransactionVerificationException.TransactionMissingEncumbranceException(
|
||||
tx, encumberedInput.state.data.encumbrance!!,
|
||||
TransactionVerificationException.Direction.INPUT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
if (encumbranceIndex == i || encumbranceIndex >= tx.outputs.size) {
|
||||
throw TransactionVerificationException.TransactionMissingEncumbranceException(
|
||||
tx, encumbranceIndex,
|
||||
TransactionVerificationException.Direction.OUTPUT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRequiredSigners(tx: LedgerTransaction) = tx.commands.flatMap { it.signers }.toSet()
|
||||
|
@ -101,4 +101,12 @@ sealed class TransactionVerificationException(val tx: LedgerTransaction, cause:
|
||||
class NotaryChangeInWrongTransactionType(tx: LedgerTransaction, val outputNotary: Party) : TransactionVerificationException(tx, null) {
|
||||
override fun toString(): String = "Found unexpected notary change in transaction. Tx notary: ${tx.notary}, found: ${outputNotary}"
|
||||
}
|
||||
class TransactionMissingEncumbranceException(tx: LedgerTransaction, val missing: Int, val inOut: Direction) : TransactionVerificationException(tx, null) {
|
||||
override val message: String?
|
||||
get() = "Missing required encumbrance ${missing} in ${inOut}"
|
||||
}
|
||||
enum class Direction {
|
||||
INPUT,
|
||||
OUTPUT
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,155 @@
|
||||
package com.r3corda.core.contracts
|
||||
|
||||
import com.r3corda.contracts.asset.Cash
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.testing.*
|
||||
import org.junit.Test
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
val TEST_TIMELOCK_ID = TransactionEncumbranceTests.TestTimeLock()
|
||||
|
||||
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(
|
||||
amount = 1000.DOLLARS `issued by` defaultIssuer,
|
||||
owner = DUMMY_PUBKEY_1
|
||||
)
|
||||
val stateWithNewOwner = encumberedState.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)
|
||||
|
||||
|
||||
class TestTimeLock : Contract {
|
||||
override val legalContractReference = SecureHash.sha256("TestTimeLock")
|
||||
override fun verify(tx: TransactionForContract) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
data class State(
|
||||
val validFrom: Instant
|
||||
) : ContractState {
|
||||
override val participants: List<PublicKey> = emptyList()
|
||||
override val contract: Contract = TEST_TIMELOCK_ID
|
||||
}
|
||||
}
|
||||
|
||||
@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"
|
||||
}
|
||||
// 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.
|
||||
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 }
|
||||
}
|
||||
// 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 }
|
||||
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
timestamp(FIVE_PM)
|
||||
this.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.
|
||||
ledger {
|
||||
unverifiedTransaction {
|
||||
output("state encumbered by 5pm time-lock") { encumberedState }
|
||||
output("5pm time-lock") { FIVE_PM_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 }
|
||||
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
timestamp(FOUR_PM)
|
||||
this `fails with` "the time specified in the time-lock has passed"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
2
docs/build/html/.buildinfo
vendored
2
docs/build/html/.buildinfo
vendored
@ -1,4 +1,4 @@
|
||||
# Sphinx build info version 1
|
||||
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
|
||||
config: 02da61908148262295ef57918c434b8d
|
||||
config: 0e8996317ea97eb6f5837df02a05a760
|
||||
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
||||
|
64
docs/build/html/_sources/tutorial-contract.txt
vendored
64
docs/build/html/_sources/tutorial-contract.txt
vendored
@ -719,6 +719,70 @@ considered relevant by your wallet (e.g. because you own it), then the node can
|
||||
of creating a transaction and taking it through the life cycle. You can learn more about this in the article
|
||||
":doc:`event-scheduling`".
|
||||
|
||||
Encumbrances
|
||||
------------
|
||||
|
||||
All contract states may be *encumbered* by up to one other state, which we call an **encumbrance**.
|
||||
|
||||
The encumbrance state, if present, forces additional controls over the encumbered state, since the encumbrance state contract
|
||||
will also be verified during the execution of the transaction. For example, a contract state could be encumbered
|
||||
with a time-lock contract state; the 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 its encumbrance by index, and the referred encumbrance state
|
||||
is an output state in a particular position on the same transaction that created the encumbered state. Note that an
|
||||
encumbered state that is being consumed must have its encumbrance consumed in the same transaction, otherwise the
|
||||
transaction is not valid.
|
||||
|
||||
The encumbrance reference is optional in the ``ContractState`` interface:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
val encumbrance: Int? get() = null
|
||||
|
||||
.. sourcecode:: java
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Integer getEncumbrance() {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
The time-lock contract mentioned above can be implemented very simply:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
class TestTimeLock : Contract {
|
||||
...
|
||||
override fun verify(tx: TransactionForContract) {
|
||||
val timestamp: Timestamp? = tx.timestamp
|
||||
...
|
||||
requireThat {
|
||||
"the time specified in the time-lock has passed" by
|
||||
(time >= tx.inputs.filterIsInstance<TestTimeLock.State>().single().validFrom)
|
||||
}
|
||||
}
|
||||
...
|
||||
}
|
||||
|
||||
We can then set up an encumbered state:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
val encumberedState = Cash.State(amount = 1000.DOLLARS `issued by` defaultIssuer, owner = DUMMY_PUBKEY_1, encumbrance = 1)
|
||||
val fourPmTimelock = TestTimeLock.State(Instant.parse("2015-04-17T16:00:00.00Z"))
|
||||
|
||||
When we construct a transaction that generates the encumbered state, we must place the encumbrance in the corresponding output
|
||||
position of that transaction. And when we subsequently consume that encumbered state, the same encumbrance state must be
|
||||
available somewhere within the input set of states.
|
||||
|
||||
Clauses
|
||||
-------
|
||||
|
||||
|
4
docs/build/html/_static/basic.css
vendored
4
docs/build/html/_static/basic.css
vendored
@ -85,10 +85,6 @@ div.sphinxsidebar #searchbox input[type="text"] {
|
||||
width: 170px;
|
||||
}
|
||||
|
||||
div.sphinxsidebar #searchbox input[type="submit"] {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
max-width: 100%;
|
||||
|
@ -719,6 +719,80 @@ considered relevant by your wallet (e.g. because you own it), then the node can
|
||||
of creating a transaction and taking it through the life cycle. You can learn more about this in the article
|
||||
":doc:`event-scheduling`".
|
||||
|
||||
Encumbrances
|
||||
------------
|
||||
|
||||
All contract states may be *encumbered* by up to one other state, which we call an **encumbrance**.
|
||||
|
||||
The encumbrance state, if present, forces additional controls over the encumbered state, since the encumbrance state contract
|
||||
will also be verified during the execution of the transaction. For example, a contract state could be encumbered
|
||||
with a time-lock contract state; the 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 its encumbrance by index, and the referred encumbrance state
|
||||
is an output state in a particular position on the same transaction that created the encumbered state. Note that an
|
||||
encumbered state that is being consumed must have its encumbrance consumed in the same transaction, otherwise the
|
||||
transaction is not valid.
|
||||
|
||||
The encumbrance reference is optional in the ``ContractState`` interface:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
val encumbrance: Int? get() = null
|
||||
|
||||
.. sourcecode:: java
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Integer getEncumbrance() {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
The time-lock contract mentioned above can be implemented very simply:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
class TestTimeLock : Contract {
|
||||
...
|
||||
override fun verify(tx: TransactionForContract) {
|
||||
val time = tx.timestamp.before ?: throw IllegalStateException(...)
|
||||
...
|
||||
requireThat {
|
||||
"the time specified in the time-lock has passed" by
|
||||
(time >= tx.inputs.filterIsInstance<TestTimeLock.State>().single().validFrom)
|
||||
}
|
||||
}
|
||||
...
|
||||
}
|
||||
|
||||
We can then set up an encumbered state:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
val encumberedState = Cash.State(amount = 1000.DOLLARS `issued by` defaultIssuer, owner = DUMMY_PUBKEY_1, encumbrance = 1)
|
||||
val fourPmTimelock = TestTimeLock.State(Instant.parse("2015-04-17T16:00:00.00Z"))
|
||||
|
||||
When we construct a transaction that generates the encumbered state, we must place the encumbrance in the corresponding output
|
||||
position of that transaction. And when we subsequently consume that encumbered state, the same encumbrance state must be
|
||||
available somewhere within the input set of states.
|
||||
|
||||
In future, we will consider the concept of a *covenant*. This is where the encumbrance travels alongside each iteration of
|
||||
the encumbered state. For example, a cash state may be encumbered with a *domicile* encumbrance, which checks the domicile of
|
||||
the identity of the owner that the cash state is being moved to, in order to uphold sanction screening regulations, and prevent
|
||||
cash being paid to parties domiciled in e.g. North Korea. In this case, the encumbrance should be permanently attached to
|
||||
the all future cash states stemming from this one.
|
||||
|
||||
We will also consider marking states that are capable of being encumbrances as such. This will prevent states being used
|
||||
as encumbrances inadvertently. For example, the time-lock above would be usable as an encumbrance, but it makes no sense to
|
||||
be able to encumber a cash state with another one.
|
||||
|
||||
Clauses
|
||||
-------
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user