Merged in james-encumbrances (pull request #289)

Encumbrances implemented by reference to an index of an output state
This commit is contained in:
Mike Hearn 2016-09-09 11:32:03 +02:00
commit 34890b7678
11 changed files with 357 additions and 11 deletions

View File

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

View File

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

View File

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

View File

@ -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
}
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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%;

View File

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