mirror of
https://github.com/corda/corda.git
synced 2025-04-06 19:07:08 +00:00
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
This commit is contained in:
parent
3f63aecf6c
commit
696b9741dd
@ -450,14 +450,18 @@ class InterestRateSwap() : Contract {
|
||||
fun extractCommands(tx: TransactionForContract): Collection<AuthenticatedObject<CommandData>>
|
||||
= tx.commands.select<Commands>()
|
||||
|
||||
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<State, String> {
|
||||
abstract class AbstractIRSClause : GroupClause<State, UniqueIdentifier> {
|
||||
override val ifMatched = MatchBehaviour.END
|
||||
override val ifNotMatched = MatchBehaviour.CONTINUE
|
||||
|
||||
@ -502,13 +506,13 @@ class InterestRateSwap() : Contract {
|
||||
}
|
||||
}
|
||||
|
||||
class Group : GroupClauseVerifier<State, String>() {
|
||||
class Group : GroupClauseVerifier<State, UniqueIdentifier>() {
|
||||
override val ifMatched = MatchBehaviour.END
|
||||
override val ifNotMatched = MatchBehaviour.ERROR
|
||||
|
||||
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, String>>
|
||||
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, UniqueIdentifier>>
|
||||
// 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<State>,
|
||||
outputs: List<State>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: String): Set<CommandData> {
|
||||
token: UniqueIdentifier): Set<CommandData> {
|
||||
val command = tx.commands.requireSingleCommand<Commands.Agree>()
|
||||
val irs = outputs.filterIsInstance<State>().single()
|
||||
requireThat {
|
||||
@ -568,7 +572,7 @@ class InterestRateSwap() : Contract {
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: String): Set<CommandData> {
|
||||
token: UniqueIdentifier): Set<CommandData> {
|
||||
val command = tx.commands.requireSingleCommand<Commands.Refix>()
|
||||
val irs = outputs.filterIsInstance<State>().single()
|
||||
val prevIrs = inputs.filterIsInstance<State>().single()
|
||||
@ -613,7 +617,7 @@ class InterestRateSwap() : Contract {
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: String): Set<CommandData> {
|
||||
token: UniqueIdentifier): Set<CommandData> {
|
||||
val command = tx.commands.requireSingleCommand<Commands.Pay>()
|
||||
requireThat {
|
||||
"Payments not supported / verifiable yet" by false
|
||||
@ -629,11 +633,12 @@ class InterestRateSwap() : Contract {
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: String): Set<CommandData> {
|
||||
token: UniqueIdentifier): Set<CommandData> {
|
||||
val command = tx.commands.requireSingleCommand<Commands.Mature>()
|
||||
val irs = inputs.filterIsInstance<State>().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<PublicKey>
|
||||
|
@ -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() }
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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<PublicKey>): Boolean
|
||||
|
||||
/**
|
||||
* Standard clause to verify the LinearState safety properties.
|
||||
*/
|
||||
class ClauseVerifier<S: LinearState>(val stateClass: Class<S>) : SingleClause {
|
||||
override val ifMatched = MatchBehaviour.CONTINUE
|
||||
override val ifNotMatched = MatchBehaviour.ERROR
|
||||
override val requiredCommands = emptySet<Class<out CommandData>>()
|
||||
|
||||
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> {
|
||||
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 {
|
||||
|
@ -85,13 +85,13 @@ interface WalletService {
|
||||
/**
|
||||
* Returns a snapshot of the heads of LinearStates.
|
||||
*/
|
||||
val linearHeads: Map<SecureHash, StateAndRef<LinearState>>
|
||||
val linearHeads: Map<UniqueIdentifier, StateAndRef<LinearState>>
|
||||
|
||||
// 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 <T : LinearState> linearHeadsOfType_(stateType: Class<T>): Map<SecureHash, StateAndRef<T>> {
|
||||
fun <T : LinearState> linearHeadsOfType_(stateType: Class<T>): Map<UniqueIdentifier, StateAndRef<T>> {
|
||||
return linearHeads.filterValues { stateType.isInstance(it.state.data) }.mapValues { StateAndRef(it.value.state as TransactionState<T>, it.value.ref) }
|
||||
}
|
||||
|
||||
|
@ -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<PublicKey> = listOf(),
|
||||
val nonce: SecureHash = SecureHash.randomSHA256()) : LinearState {
|
||||
class DummyLinearContract: Contract {
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("Test")
|
||||
|
||||
override fun isRelevant(ourKeys: Set<PublicKey>): 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<PublicKey> = listOf(),
|
||||
val nonce: SecureHash = SecureHash.randomSHA256()) : LinearState {
|
||||
|
||||
override fun isRelevant(ourKeys: Set<PublicKey>): Boolean {
|
||||
return participants.any { ourKeys.contains(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,9 +22,6 @@ import javax.annotation.concurrent.ThreadSafe
|
||||
*/
|
||||
@ThreadSafe
|
||||
open class InMemoryWalletService(protected val services: ServiceHub) : SingletonSerializeAsToken(), WalletService {
|
||||
class ClashingThreads(threads: Set<SecureHash>, transactions: Iterable<WireTransaction>) :
|
||||
Exception("There are multiple linear head states after processing transactions $transactions. The clashing thread(s): $threads")
|
||||
|
||||
open protected val log = loggerFor<InMemoryWalletService>()
|
||||
|
||||
// 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<SecureHash, StateAndRef<LinearState>>
|
||||
override val linearHeads: Map<UniqueIdentifier, StateAndRef<LinearState>>
|
||||
get() = currentWallet.let { wallet ->
|
||||
wallet.states.filterStatesOfType<LinearState>().associateBy { it.state.data.thread }.mapValues { it.value }
|
||||
wallet.states.filterStatesOfType<LinearState>().associateBy { it.state.data.linearId }.mapValues { it.value }
|
||||
}
|
||||
|
||||
override fun notifyAll(txns: Iterable<WireTransaction>): 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<SecureHash> get() {
|
||||
val clashingThreads = HashSet<SecureHash>()
|
||||
val threadsSeen = HashSet<SecureHash>()
|
||||
for (linearState in states.filterStatesOfType<LinearState>()) {
|
||||
val thread = linearState.state.data.thread
|
||||
if (threadsSeen.contains(thread)) {
|
||||
clashingThreads.add(thread)
|
||||
} else {
|
||||
threadsSeen.add(thread)
|
||||
}
|
||||
}
|
||||
return clashingThreads
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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<Issued<Currency>> get() = props.amount
|
||||
|
||||
override val thread = SecureHash.Companion.sha256(props.invoiceID)
|
||||
|
||||
override fun isRelevant(ourKeys: Set<PublicKey>): Boolean {
|
||||
return owner.owningKey in ourKeys || buyer.owningKey in ourKeys
|
||||
}
|
||||
|
@ -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<SecureHash, StateAndRef<InterestRateSwap.State>> = node1.services.walletService.linearHeadsOfType<InterestRateSwap.State>()
|
||||
val swaps: Map<UniqueIdentifier, StateAndRef<InterestRateSwap.State>> = node1.services.walletService.linearHeadsOfType<InterestRateSwap.State>()
|
||||
val theDealRef: StateAndRef<InterestRateSwap.State> = swaps.values.single()
|
||||
|
||||
// Do we have any more days left in this deal's lifetime? If not, return.
|
||||
|
@ -82,7 +82,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
|
||||
override val participants: List<PublicKey>
|
||||
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<PublicKey>): Boolean = true
|
||||
|
||||
|
@ -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<LinearState>(0))
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}.toSignedTransaction()
|
||||
|
||||
dummyIssue.toLedgerTransaction(services).verify()
|
||||
|
||||
wallet.notify(dummyMove.tx)
|
||||
assertEquals(1, wallet.currentWallet.states.toList().size)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user