Encumbrances implemented by reference to an index of an output state

This commit is contained in:
jamescarlyle 2016-08-18 19:41:17 +01:00
parent 43625308fa
commit ff1a1c4848
6 changed files with 172 additions and 3 deletions

View File

@ -120,6 +120,12 @@ public class JavaCommercialPaper implements Contract {
public List<PublicKey> getParticipants() {
return ImmutableList.of(this.owner);
}
@Nullable
@Override
public Integer getEncumbrance() {
return null;
}
}
public interface Clause {

View File

@ -74,12 +74,14 @@ class Cash : OnLedgerAsset<Currency, Cash.State>() {
class ConserveAmount : AbstractConserveAmount<State, 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,
/** Cash may be encumbered by an additional state. */
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

@ -111,6 +111,11 @@ interface ContractState {
* list should just contain the owner.
*/
val participants: List<PublicKey>
/** The encumbrance state, if present, forces additional controls over this state, since the encumbrance state contract
* will also be verified.
*/
val encumbrance: Int? get() = null
}
/**

View File

@ -77,6 +77,15 @@ 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)
}
}
}
override fun getRequiredSigners(tx: LedgerTransaction) = tx.commands.flatMap { it.signers }.toSet()

View File

@ -99,5 +99,7 @@ sealed class TransactionVerificationException(val tx: LedgerTransaction, cause:
class InvalidNotaryChange(tx: LedgerTransaction) : TransactionVerificationException(tx, null)
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) : TransactionVerificationException(tx, null)
}
class TransactionMissingEncumbranceException(tx: LedgerTransaction) : TransactionVerificationException(tx, null)
}

View File

@ -0,0 +1,145 @@
package com.r3corda.core.contracts
import com.r3corda.contracts.asset.Cash
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.serialization.OpaqueBytes
import com.r3corda.core.testing.*
import org.junit.Test
import java.security.PublicKey
import java.time.Instant
import java.time.temporal.ChronoUnit
import kotlin.test.assertFailsWith
val TEST_TIMELOCK_ID = TransactionEncumbranceTests.TestTimeLock()
class TransactionEncumbranceTests {
val defaultRef = OpaqueBytes(ByteArray(1, {1}))
val defaultIssuer = MEGA_CORP.ref(defaultRef)
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 timestamp: Timestamp? = tx.timestamp
val timeLockCommand = tx.commands.select<TestTimeLock.Commands>().first()
if (timeLockCommand.value is TestTimeLock.Commands.Exit) {
val time = 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>().first().validFrom)
}
}
}
data class State(
val validFrom: Instant
) : ContractState {
override val participants: List<PublicKey>
get() = throw UnsupportedOperationException()
override val contract: Contract = TEST_TIMELOCK_ID
}
interface Commands : CommandData {
class Issue : TypeOnlyCommandData(), Commands
class Exit : TypeOnlyCommandData(), Commands
}
}
@Test
fun trivial() {
// A transaction containing an input state that is encumbered must fail if the encumbrance is missing.
assertFailsWith(TransactionVerificationException.TransactionMissingEncumbranceException::class) {
transaction {
input { encumberedState }
output { unencumberedState }
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
this.verifies()
}
}
// A transaction containing an input state that is encumbered must fail if the encumbrance is not in the correct position.
assertFailsWith(TransactionVerificationException.TransactionMissingEncumbranceException::class) {
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() }
command(DUMMY_PUBKEY_1) { TestTimeLock.Commands.Exit() }
timestamp(FIVE_PM)
this.verifies()
}
}
}
// A transaction containing an input state that is encumbered must fail if the encumbrance is not in the correct transaction.
assertFailsWith(TransactionVerificationException.TransactionMissingEncumbranceException::class) {
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() }
command(DUMMY_PUBKEY_1) { TestTimeLock.Commands.Exit() }
timestamp(FIVE_PM)
this.verifies()
}
}
}
// 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 { stateWithNewOwner }
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
command(DUMMY_PUBKEY_1) { TestTimeLock.Commands.Exit() }
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.
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 { stateWithNewOwner }
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
command(DUMMY_PUBKEY_1) { TestTimeLock.Commands.Exit() }
timestamp(FOUR_PM)
this `fails with` "the time specified in the time-lock has passed"
}
}
}
}