From 35aaac0c15a051d571ecbdff83e5f816ff45a02c Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Wed, 5 Oct 2016 12:19:06 +0100 Subject: [PATCH] Remove trade finance contract code --- .../r3corda/contracts/tradefinance/Notice.kt | 12 - .../contracts/tradefinance/Receivable.kt | 305 ------------------ .../contracts/tradefinance/ReceivableTests.kt | 153 --------- 3 files changed, 470 deletions(-) delete mode 100644 contracts/src/main/kotlin/com/r3corda/contracts/tradefinance/Notice.kt delete mode 100644 contracts/src/main/kotlin/com/r3corda/contracts/tradefinance/Receivable.kt delete mode 100644 contracts/src/test/kotlin/com/r3corda/contracts/tradefinance/ReceivableTests.kt diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/tradefinance/Notice.kt b/contracts/src/main/kotlin/com/r3corda/contracts/tradefinance/Notice.kt deleted file mode 100644 index 15581a7228..0000000000 --- a/contracts/src/main/kotlin/com/r3corda/contracts/tradefinance/Notice.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.r3corda.contracts.tradefinance - -import java.security.PublicKey -import java.util.* - -/** - * A notice which can be attached to a receivable. - */ -sealed class Notice(val id: UUID, val owner: PublicKey) { - class OwnershipInterest(id: UUID, owner: PublicKey) : Notice(id, owner) - class Objection(id: UUID, owner: PublicKey) : Notice(id, owner) -} \ No newline at end of file diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/tradefinance/Receivable.kt b/contracts/src/main/kotlin/com/r3corda/contracts/tradefinance/Receivable.kt deleted file mode 100644 index cddf5da957..0000000000 --- a/contracts/src/main/kotlin/com/r3corda/contracts/tradefinance/Receivable.kt +++ /dev/null @@ -1,305 +0,0 @@ -package com.r3corda.contracts.tradefinance - -import com.r3corda.core.contracts.* -import com.r3corda.core.contracts.clauses.* -import com.r3corda.core.crypto.Party -import com.r3corda.core.crypto.SecureHash -import com.r3corda.core.isOrderedAndUnique -import com.r3corda.core.random63BitValue -import com.r3corda.core.serialization.OpaqueBytes -import com.r3corda.core.utilities.NonEmptySet -import java.security.PublicKey -import java.time.Instant -import java.time.LocalDate -import java.time.ZonedDateTime -import java.util.* - -/** - * Contract for managing lifecycle of a receivable which is recorded on the distributed ledger. These are entered by - * a third party (typically a potential creditor), and then shared by the trade finance registry, allowing others to - * attach/detach notices of ownership interest/objection. - * - * States of this contract *are not* fungible, and as such special rules apply. States must be unique within the - * inputs/outputs, and strictly ordered, in order to make it easy to verify that outputs match the inputs except where - * commands mean there are changes. - */ -class Receivable : Contract { - data class State(override val linearId: UniqueIdentifier = UniqueIdentifier(), - val created: ZonedDateTime, // When the underlying receivable was raised - val registered: Instant, // When the receivable was added to the registry - val payer: Party, - val payee: Party, - val payerRef: OpaqueBytes?, - val payeeRef: OpaqueBytes?, - val value: Amount>, - val attachments: Set, - val notices: List, - override val owner: PublicKey) : OwnableState, LinearState { - override val contract: Contract = Receivable() - override val participants: List = listOf(owner) - override fun isRelevant(ourKeys: Set): Boolean - = ourKeys.contains(payer.owningKey) || ourKeys.contains(payee.owningKey) || ourKeys.contains(owner) - override fun withNewOwner(newOwner: PublicKey): Pair - = Pair(Commands.Move(null, mapOf(Pair(linearId, newOwner))), copy(owner = newOwner)) - } - - interface Commands : CommandData { - val changed: Iterable - data class Issue(override val changed: NonEmptySet, - override val nonce: Long = random63BitValue()) : IssueCommand, Commands - data class Move(override val contractHash: SecureHash?, val changes: Map) : MoveCommand, Commands { - override val changed: Iterable = changes.keys - } - data class Note(val changes: Map>) : Commands { - override val changed: Iterable = changes.keys - } - // TODO: Write Amend clause, possibly to merge into Move - /* data class Amend(val id: UniqueIdentifier, - val payer: Party, - val payee: Party, - val payerRef: OpaqueBytes?, - val payeeRef: OpaqueBytes?, - val value: Amount>, - val attachments: Set) : Commands */ - data class Exit(override val changed: NonEmptySet) : Commands - } - - data class Diff(val added: List, val removed: List) - - interface Clauses { - /** - * Assert that each input/output state is unique within that list of states, and that states are ordered. There - * should never be the same receivable twice in a transaction. Uniqueness is also enforced by the notary, - * but we get the check as a side-effect of comparing states, so the duplication is acceptable. - */ - class StatesAreOrderedAndUnique : Clause() { - override fun verify(tx: TransactionForContract, - inputs: List, - outputs: List, - commands: List>, - groupingKey: Unit?): Set { - // Enforce that states are ordered, so that the transaction can only be assembled in one way - requireThat { - "input receivables are ordered and unique" by inputs.isOrderedAndUnique { linearId } - "output receivables are ordered and unique" by outputs.isOrderedAndUnique { linearId } - } - return emptySet() - } - } - - /** - * Check that all inputs are present as outputs, and that all owners for new outputs have signed the command. - */ - class Issue : Clause() { - override val requiredCommands: Set> = setOf(Commands.Issue::class.java) - - override fun verify(tx: TransactionForContract, - inputs: List, - outputs: List, - commands: List>, - groupingKey: Unit?): Set { - require(groupingKey == null) - // TODO: Take in matched commands as a parameter - val command = commands.requireSingleCommand() - val timestamp = tx.timestamp - - // Records for receivables are never fungible, so we just want to make sure all inputs exist as - // outputs, and there are new outputs. - requireThat { - "there are more output states than input states" by (outputs.size > inputs.size) - // TODO: Should timestamps perhaps be enforced on all receivable transactions? - "the transaction has a timestamp" by (timestamp != null) - } - - val expectedOutputs = ArrayList(inputs) - val keysThatSigned = command.signers - val owningPubKeys = HashSet() - outputs - .filter { it.linearId in command.value.changed } - .forEach { state -> - val registrationInLocalZone = state.registered.atZone(state.created.zone) - requireThat { - "the receivable is registered after it was created" by (state.created < registrationInLocalZone) - // TODO: Should narrow the window on how long ago the registration can be compared to the transaction - "the receivable is registered before the transaction date" by (state.registered < timestamp?.before) - } - owningPubKeys.add(state.owner) - expectedOutputs.add(state) - } - // Re-sort the outputs now we've finished changing them - expectedOutputs.sortBy { state -> state.linearId } - requireThat { - "the owning keys are the same as the signing keys" by keysThatSigned.containsAll(owningPubKeys) - "outputs match inputs with expected changes applied" by outputs.equals(expectedOutputs) - } - - return setOf(command.value as Commands) - } - } - - /** - * Check that inputs and outputs are exactly the same, except for ownership changes specified in the command. - * The command must be signed by the previous owners of all changed input states. - */ - class Move : Clause() { - override val requiredCommands: Set> = setOf(Commands.Move::class.java) - - override fun verify(tx: TransactionForContract, - inputs: List, - outputs: List, - commands: List>, - groupingKey: Unit?): Set { - require(groupingKey == null) - // TODO: Take in matched commands as a parameter - val moveCommand = commands.requireSingleCommand() - val changes = moveCommand.value.changes - // Rebuild the outputs we expect, then compare. Receivables are not fungible, so inputs and outputs - // must match one to one - val expectedOutputs: List = inputs.map { input -> - val newOwner = changes[input.linearId] - if (newOwner != null) { - input.copy(owner = newOwner) - } else { - input - } - } - requireThat { - "inputs are not empty" by inputs.isNotEmpty() - "outputs match inputs with expected changes applied" by outputs.equals(expectedOutputs) - } - // Do standard move command checks including the signature checks - verifyMoveCommand(inputs, commands) - return setOf(moveCommand.value as Commands) - } - } - - /** - * Add and/or remove notices on receivables. All input states must match output states, except for the - * changed notices. - */ - class Note : Clause() { - override val requiredCommands: Set> = setOf(Commands.Note::class.java) - - override fun verify(tx: TransactionForContract, - inputs: List, - outputs: List, - commands: List>, - groupingKey: Unit?): Set { - require(groupingKey == null) - // TODO: Take in matched commands as a parameter - val command = commands.requireSingleCommand() - // Rebuild the outputs we expect, then compare. Receivables are not fungible, so inputs and outputs - // must match one to one - val (expectedOutputs, owningPubKeys) = deriveOutputStates(inputs, command) - val keysThatSigned = command.signers - requireThat { - "inputs are not empty" by inputs.isNotEmpty() - "outputs match inputs with expected changes applied" by outputs.equals(expectedOutputs) - "the owning keys are the same as the signing keys" by keysThatSigned.containsAll(owningPubKeys) - } - return setOf(command.value as Commands) - } - - fun deriveOutputStates(inputs: List, - command: AuthenticatedObject): Pair, Set> { - val changes = command.value.changes - val seenNotices = HashSet() - val outputs = inputs.map { input -> - val stateChanges = changes[input.linearId] - if (stateChanges != null) { - val notices = ArrayList(input.notices) - stateChanges.added.forEach { notice -> - require(!seenNotices.contains(notice)) { "Notices can only appear once in the add and/or remove lists" } - require(!notices.contains(notice)) { "Notice is already present on the receivable" } - seenNotices.add(notice) - notices.add(notice) - } - stateChanges.removed.forEach { notice -> - require(!seenNotices.contains(notice)) { "Notices can only appear once in the add and/or remove lists" } - require(notices.remove(notice)) { "Notice is not present on the receivable" } - seenNotices.add(notice) - } - input.copy(notices = notices) - } else { - input - } - } - return Pair(outputs, seenNotices.map { it.owner }.toSet() ) - } - } - - /** - * Remove a receivable from the ledger. This can only be done once all notices have been removed. - */ - class Exit : Clause() { - override val requiredCommands: Set> = setOf(Commands.Exit::class.java) - - override fun verify(tx: TransactionForContract, - inputs: List, - outputs: List, - commands: List>, - groupingKey: Unit?): Set { - require(groupingKey == null) - // TODO: Take in matched commands as a parameter - val command = commands.requireSingleCommand() - val unmatchedIds = HashSet(command.value.changed) - val owningPubKeys = HashSet() - val expectedOutputs = inputs.filter { input -> - if (unmatchedIds.contains(input.linearId)) { - requireThat { - "there are no notices on receivables to be removed from the ledger" by input.notices.isEmpty() - } - unmatchedIds.remove(input.linearId) - owningPubKeys.add(input.owner) - false - } else { - true - } - } - val keysThatSigned = command.signers - requireThat { - "inputs are not empty" by inputs.isNotEmpty() - "outputs match inputs with expected changes applied" by outputs.equals(expectedOutputs) - "the owning keys are the same as the signing keys" by keysThatSigned.containsAll(owningPubKeys) - } - return setOf(command.value as Commands) - } - } - - // TODO: Amend clause, which replaces the Move clause - - /** - * Default clause, which checks the inputs and outputs match. Normally this wouldn't be expected to trigger, - * as other commands would handle the transaction, but this exists in case the states need to be witnessed by - * other contracts within the transaction but not modified. - */ - class InputsAndOutputsMatch : Clause() { - override fun verify(tx: TransactionForContract, - inputs: List, - outputs: List, - commands: List>, - groupingKey: Unit?): Set { - require(groupingKey == null) - require(inputs.equals(outputs)) { "Inputs and outputs must match unless commands indicate otherwise" } - return emptySet() - } - } - } - - override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/receivables.html") - fun extractCommands(commands: Collection>): List> - = commands.select() - override fun verify(tx: TransactionForContract) - = verifyClause(tx, FilterOn( - AllComposition( - Clauses.StatesAreOrderedAndUnique(), // TODO: This is varient of the LinearState.ClauseVerifier, and we should move it up there - FirstComposition( - Clauses.Issue(), - Clauses.Exit(), - Clauses.Note(), - Clauses.Move(), - Clauses.InputsAndOutputsMatch() - ) - ), { states -> states.filterIsInstance() }), - extractCommands(tx.commands)) -} diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/tradefinance/ReceivableTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/tradefinance/ReceivableTests.kt deleted file mode 100644 index cc1246d961..0000000000 --- a/contracts/src/test/kotlin/com/r3corda/contracts/tradefinance/ReceivableTests.kt +++ /dev/null @@ -1,153 +0,0 @@ -package com.r3corda.contracts.tradefinance - -import com.r3corda.contracts.asset.DUMMY_CASH_ISSUER -import com.r3corda.core.contracts.* -import com.r3corda.core.serialization.OpaqueBytes -import com.r3corda.core.utilities.NonEmptySet -import com.r3corda.core.utilities.TEST_TX_TIME -import com.r3corda.testing.* -import org.junit.Test -import java.time.Duration -import java.time.ZoneId -import java.util.* - -class ReceivableTests { - val inStates = arrayOf( - Receivable.State( - UniqueIdentifier.fromString("9e688c58-a548-3b8e-af69-c9e1005ad0bf"), - (TEST_TX_TIME - Duration.ofDays(2)).atZone(ZoneId.of("UTC")), - TEST_TX_TIME - Duration.ofDays(1), - ALICE, - BOB, - OpaqueBytes(ByteArray(1, { 1 })), - OpaqueBytes(ByteArray(1, { 2 })), - Amount>(1000L, USD `issued by` DUMMY_CASH_ISSUER), - emptySet(), - emptyList(), - MEGA_CORP_PUBKEY - ), - Receivable.State( - UniqueIdentifier.fromString("55a54008-ad1b-3589-aa21-0d2629c1df41"), - (TEST_TX_TIME - Duration.ofDays(2)).atZone(ZoneId.of("UTC")), - TEST_TX_TIME - Duration.ofDays(1), - ALICE, - BOB, - OpaqueBytes(ByteArray(1, { 3 })), - OpaqueBytes(ByteArray(1, { 4 })), - Amount>(2000L, GBP `issued by` DUMMY_CASH_ISSUER), - emptySet(), - emptyList(), - MEGA_CORP_PUBKEY - ) - ) - - @Test - fun trivial() { - transaction { - input { inStates[0] } - timestamp(TEST_TX_TIME) - this `fails with` "Inputs and outputs must match unless commands indicate otherwise" - - tweak { - output { inStates[0] } - verifies() - } - - tweak { - output { inStates[1] } - this `fails with` "Inputs and outputs must match unless commands indicate otherwise" - } - } - - transaction { - output { inStates[0] } - timestamp(TEST_TX_TIME) - this `fails with` "Inputs and outputs must match unless commands indicate otherwise" - } - } - - @Test - fun `order and uniqueness is enforced`() { - transaction { - input { inStates[0] } - input { inStates[1] } - output { inStates[0] } - output { inStates[1] } - timestamp(TEST_TX_TIME) - verifies() - } - - transaction { - input { inStates[1] } - input { inStates[0] } - output { inStates[0] } - output { inStates[1] } - timestamp(TEST_TX_TIME) - this `fails with` "receivables are ordered and unique" - } - - transaction { - input { inStates[0] } - input { inStates[0] } - output { inStates[0] } - output { inStates[0] } - timestamp(TEST_TX_TIME) - this `fails with` "receivables are ordered and unique" - } - } - - @Test - fun `issue`() { - // Testing that arbitrary new outputs are rejected is covered in trivial() - transaction { - output { inStates[0] } - command(MEGA_CORP_PUBKEY, Receivable.Commands.Issue(NonEmptySet(inStates[0].linearId))) - timestamp(TEST_TX_TIME) - verifies() - } - transaction { - output { inStates[0] } - command(ALICE_PUBKEY, Receivable.Commands.Issue(NonEmptySet(inStates[0].linearId))) - timestamp(TEST_TX_TIME) - this `fails with` "the owning keys are the same as the signing keys" - } - } - - @Test - fun `move`() { - transaction { - input { inStates[0] } - output { inStates[0].copy(owner = MINI_CORP_PUBKEY) } - timestamp(TEST_TX_TIME) - this `fails with` "Inputs and outputs must match unless commands indicate otherwise" - tweak { - command(MEGA_CORP_PUBKEY, Receivable.Commands.Move(null, mapOf(Pair(inStates[0].linearId, MINI_CORP_PUBKEY)))) - verifies() - } - // Test that moves enforce the correct new owner - tweak { - command(MEGA_CORP_PUBKEY, Receivable.Commands.Move(null, mapOf(Pair(inStates[0].linearId, ALICE_PUBKEY)))) - this `fails with` "outputs match inputs with expected changes applied" - } - } - } - - @Test - fun `exit`() { - // Testing that arbitrary disappearing outputs are rejected is covered in trivial() - transaction { - input { inStates[0] } - timestamp(TEST_TX_TIME) - command(MEGA_CORP_PUBKEY, Receivable.Commands.Exit(NonEmptySet(inStates[0].linearId))) - verifies() - } - transaction { - input { inStates[0] } - timestamp(TEST_TX_TIME) - command(ALICE_PUBKEY, Receivable.Commands.Exit(NonEmptySet(inStates[0].linearId))) - this `fails with` "the owning keys are the same as the signing keys" - } - } - - // TODO: Test adding and removing notices -} \ No newline at end of file