mirror of
https://github.com/corda/corda.git
synced 2024-12-19 04:57:58 +00:00
Encumbrances implemented by reference to an index of an output state
This commit is contained in:
parent
43625308fa
commit
ff1a1c4848
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user