From 696b9741ddb63775b7e202592acd42abb04858eb Mon Sep 17 00:00:00 2001 From: Matthew Nesbit Date: Thu, 11 Aug 2016 10:27:25 +0100 Subject: [PATCH] Remove ClashingThreads exception by tightening up unique id on LinearState to be a secure random value, with constraints that it cannot be duplicated. Also, rename to linearId rather than the confusing thread. Try providing a helper interface to encourage enforcing LinearState rules Fixup after rebase Change to using Clauses for verifying LinearState standard properties Fix whitespace change Tidy up ClauseVerifier after PR comments Change from SecureHash to a TradeIdentifier class Change TradeIdentifier to UniqueIdentifier --- .../main/kotlin/com/r3corda/contracts/IRS.kt | 28 ++++++++------ .../kotlin/com/r3corda/contracts/IRSTests.kt | 15 +++++--- .../r3corda/core/contracts/FinanceTypes.kt | 18 +++++++++ .../com/r3corda/core/contracts/Structures.kt | 37 ++++++++++++++++--- .../r3corda/core/node/services/Services.kt | 4 +- .../r3corda/core/testing/DummyLinearState.kt | 26 +++++++++---- .../core/testing/InMemoryWalletService.kt | 34 +---------------- .../kotlin/com/r3corda/contracts/Invoice.kt | 6 +-- .../node/internal/testing/IRSSimulation.kt | 4 +- .../node/services/NodeSchedulerServiceTest.kt | 2 +- .../node/services/WalletWithCashTest.kt | 30 ++++++--------- 11 files changed, 116 insertions(+), 88 deletions(-) diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt b/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt index d6f36ada78..269144c110 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt @@ -450,14 +450,18 @@ class InterestRateSwap() : Contract { fun extractCommands(tx: TransactionForContract): Collection> = tx.commands.select() - override fun verify(tx: TransactionForContract) = verifyClauses(tx, listOf(Clause.Timestamped(), Clause.Group()), extractCommands(tx)) + override fun verify(tx: TransactionForContract) = verifyClauses(tx, + listOf(Clause.Timestamped(), + Clause.Group(), + LinearState.ClauseVerifier(State::class.java)), + extractCommands(tx)) interface Clause { /** * Common superclass for IRS contract clauses, which defines behaviour on match/no-match, and provides * helper functions for the clauses. */ - abstract class AbstractIRSClause : GroupClause { + abstract class AbstractIRSClause : GroupClause { override val ifMatched = MatchBehaviour.END override val ifNotMatched = MatchBehaviour.CONTINUE @@ -502,13 +506,13 @@ class InterestRateSwap() : Contract { } } - class Group : GroupClauseVerifier() { + class Group : GroupClauseVerifier() { override val ifMatched = MatchBehaviour.END override val ifNotMatched = MatchBehaviour.ERROR - override fun groupStates(tx: TransactionForContract): List> + override fun groupStates(tx: TransactionForContract): List> // Group by Trade ID for in / out states - = tx.groupStates() { state -> state.common.tradeID } + = tx.groupStates() { state -> state.linearId } override val clauses = listOf(Agree(), Fix(), Pay(), Mature()) } @@ -532,7 +536,7 @@ class InterestRateSwap() : Contract { inputs: List, outputs: List, commands: Collection>, - token: String): Set { + token: UniqueIdentifier): Set { val command = tx.commands.requireSingleCommand() val irs = outputs.filterIsInstance().single() requireThat { @@ -568,7 +572,7 @@ class InterestRateSwap() : Contract { inputs: List, outputs: List, commands: Collection>, - token: String): Set { + token: UniqueIdentifier): Set { val command = tx.commands.requireSingleCommand() val irs = outputs.filterIsInstance().single() val prevIrs = inputs.filterIsInstance().single() @@ -613,7 +617,7 @@ class InterestRateSwap() : Contract { inputs: List, outputs: List, commands: Collection>, - token: String): Set { + token: UniqueIdentifier): Set { val command = tx.commands.requireSingleCommand() requireThat { "Payments not supported / verifiable yet" by false @@ -629,11 +633,12 @@ class InterestRateSwap() : Contract { inputs: List, outputs: List, commands: Collection>, - token: String): Set { + token: UniqueIdentifier): Set { val command = tx.commands.requireSingleCommand() val irs = inputs.filterIsInstance().single() requireThat { "No more fixings to be applied" by (irs.calculation.nextFixingDate() == null) + "The irs is fully consumed and there is no id matched output state" by outputs.isEmpty() } return setOf(command.value) @@ -656,11 +661,12 @@ class InterestRateSwap() : Contract { val fixedLeg: FixedLeg, val floatingLeg: FloatingLeg, val calculation: Calculation, - val common: Common + val common: Common, + override val linearId: UniqueIdentifier = UniqueIdentifier(common.tradeID) ) : FixableDealState, SchedulableState { override val contract = IRS_PROGRAM_ID - override val thread = SecureHash.sha256(common.tradeID) + override val ref = common.tradeID override val participants: List diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt index 753f922259..c489d98ae6 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt @@ -1,6 +1,7 @@ package com.r3corda.contracts import com.r3corda.core.contracts.* +import com.r3corda.core.crypto.SecureHash import com.r3corda.core.node.services.testing.MockServices import com.r3corda.core.seconds import com.r3corda.core.testing.* @@ -394,9 +395,10 @@ class IRSTests { @Test fun `ensure failure occurs when there are inbound states for an agreement command`() { + val irs = singleIRS() transaction { - input() { singleIRS() } - output("irs post agreement") { singleIRS() } + input() { irs } + output("irs post agreement") { irs } command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } timestamp(TEST_TX_TIME) this `fails with` "There are no in states for an agreement" @@ -665,10 +667,11 @@ class IRSTests { transaction("Agreement") { output("irs post agreement2") { irs.copy( - irs.fixedLeg, - irs.floatingLeg, - irs.calculation, - irs.common.copy(tradeID = "t2") + linearId = UniqueIdentifier("t2"), + fixedLeg = irs.fixedLeg, + floatingLeg = irs.floatingLeg, + calculation = irs.calculation, + common = irs.common.copy(tradeID = "t2") ) } command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/FinanceTypes.kt b/core/src/main/kotlin/com/r3corda/core/contracts/FinanceTypes.kt index 72b7711f4f..a875ad6463 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/FinanceTypes.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/FinanceTypes.kt @@ -434,4 +434,22 @@ data class Commodity(val symbol: String, fun getInstance(symbol: String): Commodity? = registry[symbol] } +} + +/** + * This class provides a truly unique identifier of a trade, state, or other business object. + * @param externalId If there is an existing weak identifer e.g. trade reference id. + * This should be set here the first time a UniqueIdentifier identifier is created as part of an issue, + * or ledger on-boarding activity. This ensure that the human readable identity is paired with the strong id. + * @param id Should never be set by user code and left as default initialised. + * So that the first time a state is issued this should be given a new UUID. + * Subsequent copies and evolutions of a state should just copy the externalId and Id fields unmodified. + */ +data class UniqueIdentifier(val externalId: String? = null, val id: UUID = UUID.randomUUID()) { + override fun toString(): String { + if (externalId != null) { + return "${externalId}_${id.toString()}" + } + return id.toString() + } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt index a245f9071d..69e0195de2 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt @@ -1,5 +1,7 @@ package com.r3corda.core.contracts +import com.r3corda.core.contracts.clauses.MatchBehaviour +import com.r3corda.core.contracts.clauses.SingleClause import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.toStringShort @@ -196,16 +198,41 @@ data class ScheduledStateRef(val ref: StateRef, override val scheduledAt: Instan data class ScheduledActivity(val logicRef: ProtocolLogicRef, override val scheduledAt: Instant) : Scheduled /** - * A state that evolves by superseding itself, all of which share the common "thread". + * A state that evolves by superseding itself, all of which share the common "linearId". * * This simplifies the job of tracking the current version of certain types of state in e.g. a wallet. */ -interface LinearState : ContractState { - /** Unique thread id within the wallets of all parties */ - val thread: SecureHash +interface LinearState: ContractState { + /** + * Unique id shared by all LinearState states throughout history within the wallets of all parties. + * Verify methods should check that one input and one output share the id in a transaction, + * except at issuance/termination. + */ + val linearId: UniqueIdentifier - /** true if this should be tracked by our wallet(s) */ + /** + * True if this should be tracked by our wallet(s). + * */ fun isRelevant(ourKeys: Set): Boolean + + /** + * Standard clause to verify the LinearState safety properties. + */ + class ClauseVerifier(val stateClass: Class) : SingleClause { + override val ifMatched = MatchBehaviour.CONTINUE + override val ifNotMatched = MatchBehaviour.ERROR + override val requiredCommands = emptySet>() + + override fun verify(tx: TransactionForContract, commands: Collection>): Set { + val inputs = tx.inputs.filterIsInstance(stateClass) + val inputIds = inputs.map { it.linearId }.distinct() + require(inputIds.count() == inputs.count()) { "LinearStates cannot be merged" } + val outputs = tx.outputs.filterIsInstance(stateClass) + val outputIds = outputs.map { it.linearId }.distinct() + require(outputIds.count() == outputs.count()) { "LinearStates cannot be split" } + return emptySet() + } + } } interface SchedulableState : ContractState { diff --git a/core/src/main/kotlin/com/r3corda/core/node/services/Services.kt b/core/src/main/kotlin/com/r3corda/core/node/services/Services.kt index 0bc35c81cc..802a8624b1 100644 --- a/core/src/main/kotlin/com/r3corda/core/node/services/Services.kt +++ b/core/src/main/kotlin/com/r3corda/core/node/services/Services.kt @@ -85,13 +85,13 @@ interface WalletService { /** * Returns a snapshot of the heads of LinearStates. */ - val linearHeads: Map> + val linearHeads: Map> // TODO: When KT-10399 is fixed, rename this and remove the inline version below. /** Returns the [linearHeads] only when the type of the state would be considered an 'instanceof' the given type. */ @Suppress("UNCHECKED_CAST") - fun linearHeadsOfType_(stateType: Class): Map> { + fun linearHeadsOfType_(stateType: Class): Map> { return linearHeads.filterValues { stateType.isInstance(it.state.data) }.mapValues { StateAndRef(it.value.state as TransactionState, it.value.ref) } } diff --git a/core/src/main/kotlin/com/r3corda/core/testing/DummyLinearState.kt b/core/src/main/kotlin/com/r3corda/core/testing/DummyLinearState.kt index 4868efcb59..85b1ba64ea 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/DummyLinearState.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/DummyLinearState.kt @@ -2,16 +2,28 @@ package com.r3corda.core.testing import com.r3corda.core.contracts.Contract import com.r3corda.core.contracts.LinearState +import com.r3corda.core.contracts.UniqueIdentifier +import com.r3corda.core.contracts.TransactionForContract +import com.r3corda.core.contracts.clauses.verifyClauses import com.r3corda.core.crypto.SecureHash import java.security.PublicKey -class DummyLinearState( - override val thread: SecureHash = SecureHash.randomSHA256(), - override val contract: Contract = AlwaysSucceedContract(), - override val participants: List = listOf(), - val nonce: SecureHash = SecureHash.randomSHA256()) : LinearState { +class DummyLinearContract: Contract { + override val legalContractReference: SecureHash = SecureHash.sha256("Test") - override fun isRelevant(ourKeys: Set): Boolean { - return participants.any { ourKeys.contains(it) } + override fun verify(tx: TransactionForContract) = verifyClauses(tx, + listOf(LinearState.ClauseVerifier(State::class.java)), + emptyList()) + + + class State( + override val linearId: UniqueIdentifier = UniqueIdentifier(), + override val contract: Contract = DummyLinearContract(), + override val participants: List = listOf(), + val nonce: SecureHash = SecureHash.randomSHA256()) : LinearState { + + override fun isRelevant(ourKeys: Set): Boolean { + return participants.any { ourKeys.contains(it) } + } } } diff --git a/core/src/main/kotlin/com/r3corda/core/testing/InMemoryWalletService.kt b/core/src/main/kotlin/com/r3corda/core/testing/InMemoryWalletService.kt index 07f7818e4f..8306960bf5 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/InMemoryWalletService.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/InMemoryWalletService.kt @@ -22,9 +22,6 @@ import javax.annotation.concurrent.ThreadSafe */ @ThreadSafe open class InMemoryWalletService(protected val services: ServiceHub) : SingletonSerializeAsToken(), WalletService { - class ClashingThreads(threads: Set, transactions: Iterable) : - Exception("There are multiple linear head states after processing transactions $transactions. The clashing thread(s): $threads") - open protected val log = loggerFor() // Variables inside InnerState are protected with a lock by the ThreadBox and aren't in scope unless you're @@ -46,9 +43,9 @@ open class InMemoryWalletService(protected val services: ServiceHub) : Singleton /** * Returns a snapshot of the heads of LinearStates. */ - override val linearHeads: Map> + override val linearHeads: Map> get() = currentWallet.let { wallet -> - wallet.states.filterStatesOfType().associateBy { it.state.data.thread }.mapValues { it.value } + wallet.states.filterStatesOfType().associateBy { it.state.data.linearId }.mapValues { it.value } } override fun notifyAll(txns: Iterable): Wallet { @@ -78,14 +75,6 @@ open class InMemoryWalletService(protected val services: ServiceHub) : Singleton Pair(wallet, combinedDelta) } - // TODO: we need to remove the clashing threads concepts and support potential duplicate threads - // because two different nodes can have two different sets of threads and so currently it's possible - // for only one party to have a clash which interferes with determinism of the transactions. - val clashingThreads = walletAndNetDelta.first.clashingThreads - if (!clashingThreads.isEmpty()) { - throw ClashingThreads(clashingThreads, txns) - } - wallet = walletAndNetDelta.first netDelta = walletAndNetDelta.second return@locked wallet @@ -133,23 +122,4 @@ open class InMemoryWalletService(protected val services: ServiceHub) : Singleton return Pair(Wallet(newStates), change) } - - companion object { - - // Returns the set of LinearState threads that clash in the wallet - val Wallet.clashingThreads: Set get() { - val clashingThreads = HashSet() - val threadsSeen = HashSet() - for (linearState in states.filterStatesOfType()) { - val thread = linearState.state.data.thread - if (threadsSeen.contains(thread)) { - clashingThreads.add(thread) - } else { - threadsSeen.add(thread) - } - } - return clashingThreads - } - - } } diff --git a/experimental/src/main/kotlin/com/r3corda/contracts/Invoice.kt b/experimental/src/main/kotlin/com/r3corda/contracts/Invoice.kt index c43e8392b5..5e4fe803eb 100644 --- a/experimental/src/main/kotlin/com/r3corda/contracts/Invoice.kt +++ b/experimental/src/main/kotlin/com/r3corda/contracts/Invoice.kt @@ -64,8 +64,8 @@ class Invoice : Contract { val owner: Party, val buyer: Party, val assigned: Boolean, - val props: InvoiceProperties - + val props: InvoiceProperties, + override val linearId: UniqueIdentifier = UniqueIdentifier() ) : LinearState { override val contract = INVOICE_PROGRAM_ID @@ -84,8 +84,6 @@ class Invoice : Contract { // iterate over the goods list and sum up the price for each val amount: Amount> get() = props.amount - override val thread = SecureHash.Companion.sha256(props.invoiceID) - override fun isRelevant(ourKeys: Set): Boolean { return owner.owningKey in ourKeys || buyer.owningKey in ourKeys } diff --git a/node/src/main/kotlin/com/r3corda/node/internal/testing/IRSSimulation.kt b/node/src/main/kotlin/com/r3corda/node/internal/testing/IRSSimulation.kt index da1c261316..9211f198c7 100644 --- a/node/src/main/kotlin/com/r3corda/node/internal/testing/IRSSimulation.kt +++ b/node/src/main/kotlin/com/r3corda/node/internal/testing/IRSSimulation.kt @@ -9,7 +9,7 @@ import com.r3corda.contracts.InterestRateSwap import com.r3corda.core.RunOnCallerThread import com.r3corda.core.contracts.SignedTransaction import com.r3corda.core.contracts.StateAndRef -import com.r3corda.core.crypto.SecureHash +import com.r3corda.core.contracts.UniqueIdentifier import com.r3corda.core.failure import com.r3corda.core.node.services.linearHeadsOfType import com.r3corda.core.node.services.testing.MockIdentityService @@ -80,7 +80,7 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten val node1: SimulatedNode = banks[i] val node2: SimulatedNode = banks[j] - val swaps: Map> = node1.services.walletService.linearHeadsOfType() + val swaps: Map> = node1.services.walletService.linearHeadsOfType() val theDealRef: StateAndRef = swaps.values.single() // Do we have any more days left in this deal's lifetime? If not, return. diff --git a/node/src/test/kotlin/com/r3corda/node/services/NodeSchedulerServiceTest.kt b/node/src/test/kotlin/com/r3corda/node/services/NodeSchedulerServiceTest.kt index 2201217c6b..465863dd7b 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/NodeSchedulerServiceTest.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/NodeSchedulerServiceTest.kt @@ -82,7 +82,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { override val participants: List get() = throw UnsupportedOperationException() - override val thread = SecureHash.sha256("does not matter but we need it to be unique ${Math.random()}") + override val linearId = UniqueIdentifier() override fun isRelevant(ourKeys: Set): Boolean = true diff --git a/node/src/test/kotlin/com/r3corda/node/services/WalletWithCashTest.kt b/node/src/test/kotlin/com/r3corda/node/services/WalletWithCashTest.kt index d27a75ecf4..af374e5bfa 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/WalletWithCashTest.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/WalletWithCashTest.kt @@ -104,56 +104,50 @@ class WalletWithCashTest { @Test - fun branchingLinearStatesFails() { + fun branchingLinearStatesFailsToVerify() { val freshKey = services.keyManagementService.freshKey() - val thread = SecureHash.sha256("thread") + val linearId = UniqueIdentifier() // Issue a linear state val dummyIssue = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { - addOutputState(DummyLinearState(thread = thread, participants = listOf(freshKey.public))) - signWith(freshKey) - signWith(DUMMY_NOTARY_KEY) - }.toSignedTransaction() - - wallet.notify(dummyIssue.tx) - assertEquals(1, wallet.currentWallet.states.toList().size) - - // Issue another linear state of the same thread (nonce different) - val dummyIssue2 = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { - addOutputState(DummyLinearState(thread = thread, participants = listOf(freshKey.public))) + addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshKey.public))) + addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshKey.public))) signWith(freshKey) signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction() assertThatThrownBy { - wallet.notify(dummyIssue2.tx) + dummyIssue.toLedgerTransaction(services).verify() } - assertEquals(1, wallet.currentWallet.states.toList().size) } @Test fun sequencingLinearStatesWorks() { val freshKey = services.keyManagementService.freshKey() - val thread = SecureHash.sha256("thread") + val linearId = UniqueIdentifier() // Issue a linear state val dummyIssue = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { - addOutputState(DummyLinearState(thread = thread, participants = listOf(freshKey.public))) + addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshKey.public))) signWith(freshKey) signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction() + dummyIssue.toLedgerTransaction(services).verify() + wallet.notify(dummyIssue.tx) assertEquals(1, wallet.currentWallet.states.toList().size) // Move the same state val dummyMove = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { - addOutputState(DummyLinearState(thread = thread, participants = listOf(freshKey.public))) + addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshKey.public))) addInputState(dummyIssue.tx.outRef(0)) signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction() + dummyIssue.toLedgerTransaction(services).verify() + wallet.notify(dummyMove.tx) assertEquals(1, wallet.currentWallet.states.toList().size) }