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() {
|
public List<PublicKey> getParticipants() {
|
||||||
return ImmutableList.of(this.owner);
|
return ImmutableList.of(this.owner);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public Integer getEncumbrance() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface Clause {
|
public interface Clause {
|
||||||
|
@ -74,12 +74,14 @@ class Cash : OnLedgerAsset<Currency, Cash.State>() {
|
|||||||
class ConserveAmount : AbstractConserveAmount<State, Currency>()
|
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(
|
data class 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: PublicKey
|
override val owner: PublicKey,
|
||||||
|
/** Cash may be encumbered by an additional state. */
|
||||||
|
override val encumbrance: Int? = null
|
||||||
) : FungibleAsset<Currency> {
|
) : FungibleAsset<Currency> {
|
||||||
constructor(deposit: PartyAndReference, amount: Amount<Currency>, owner: PublicKey)
|
constructor(deposit: PartyAndReference, amount: Amount<Currency>, owner: PublicKey)
|
||||||
: this(Amount(amount.quantity, Issued(deposit, amount.token)), owner)
|
: this(Amount(amount.quantity, Issued(deposit, amount.token)), owner)
|
||||||
|
@ -111,6 +111,11 @@ interface ContractState {
|
|||||||
* list should just contain the owner.
|
* list should just contain the owner.
|
||||||
*/
|
*/
|
||||||
val participants: List<PublicKey>
|
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)
|
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()
|
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 InvalidNotaryChange(tx: LedgerTransaction) : TransactionVerificationException(tx, null)
|
||||||
class NotaryChangeInWrongTransactionType(tx: LedgerTransaction, val outputNotary: Party) : 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}"
|
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