From 3cc0cef9c5352b60555962c25c58146cef91de7a Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Thu, 23 Jun 2016 14:17:08 +0100 Subject: [PATCH 1/8] core: Typo --- core/src/main/kotlin/com/r3corda/core/node/services/Services.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 70cd2d8500..a7b4ddca41 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 @@ -23,7 +23,7 @@ val TOPIC_DEFAULT_POSTFIX = ".0" * change out from underneath you, even though the canonical currently-best-known wallet may change as we learn * about new transactions from our peers and generate new transactions that consume states ourselves. * - * This absract class has no references to Cash contracts. + * This abstract class has no references to Cash contracts. */ class Wallet(val states: List>) { @Suppress("UNCHECKED_CAST") From 2c7b86fee243503686748285b08050bf01b2a683 Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Thu, 23 Jun 2016 14:18:12 +0100 Subject: [PATCH 2/8] core: Add LinearState thread clash check to InMemoryWalletService.notifyAll --- .../core/testing/InMemoryWalletService.kt | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) 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 8c1ec72b30..37beeab2bf 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/InMemoryWalletService.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/InMemoryWalletService.kt @@ -22,6 +22,8 @@ import javax.annotation.concurrent.ThreadSafe */ @ThreadSafe open class InMemoryWalletService(private val services: ServiceHub) : SingletonSerializeAsToken(), WalletService { + class ClashingThreads(threads: Set) : + Exception("There are multiple linear states pointing to the same thread. The clashing thread(s): $threads") private val log = loggerFor() // Variables inside InnerState are protected with a lock by the ThreadBox and aren't in scope unless you're @@ -44,7 +46,7 @@ open class InMemoryWalletService(private val services: ServiceHub) : SingletonSe * Returns a snapshot of the heads of LinearStates */ override val linearHeads: Map> - get() = mutex.locked { wallet }.let { wallet -> + get() = currentWallet.let { wallet -> wallet.states.filterStatesOfType().associateBy { it.state.data.thread }.mapValues { it.value } } @@ -74,10 +76,17 @@ open class InMemoryWalletService(private val services: ServiceHub) : SingletonSe val combinedDelta = delta + walletAndDelta.second Pair(wallet, combinedDelta) } + + val clashingThreads = walletAndNetDelta.first.clashingThreads + if (!clashingThreads.isEmpty()) { + throw ClashingThreads(clashingThreads) + } + wallet = walletAndNetDelta.first netDelta = walletAndNetDelta.second return@locked wallet } + if (netDelta != Wallet.NoUpdate) { _updatesPublisher.onNext(netDelta) } @@ -120,4 +129,23 @@ open class InMemoryWalletService(private val services: ServiceHub) : SingletonSe return Pair(Wallet(newStates), change) } -} \ No newline at end of file + + 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 + } + + } +} From f233780e08c574a08a25f03072b2420ee261938b Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Thu, 23 Jun 2016 14:19:52 +0100 Subject: [PATCH 3/8] core: Add DummyLinearState and AlwaysSucceedContract for testing --- .../core/testing/AlwaysSucceedContract.kt | 10 ++++++++++ .../r3corda/core/testing/DummyLinearState.kt | 17 +++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 core/src/main/kotlin/com/r3corda/core/testing/AlwaysSucceedContract.kt create mode 100644 core/src/main/kotlin/com/r3corda/core/testing/DummyLinearState.kt diff --git a/core/src/main/kotlin/com/r3corda/core/testing/AlwaysSucceedContract.kt b/core/src/main/kotlin/com/r3corda/core/testing/AlwaysSucceedContract.kt new file mode 100644 index 0000000000..646ec06801 --- /dev/null +++ b/core/src/main/kotlin/com/r3corda/core/testing/AlwaysSucceedContract.kt @@ -0,0 +1,10 @@ +package com.r3corda.core.testing + +import com.r3corda.core.contracts.Contract +import com.r3corda.core.contracts.TransactionForContract +import com.r3corda.core.crypto.SecureHash + +class AlwaysSucceedContract(override val legalContractReference: SecureHash = SecureHash.sha256("Always succeed contract")) : Contract { + override fun verify(tx: TransactionForContract) { + } +} diff --git a/core/src/main/kotlin/com/r3corda/core/testing/DummyLinearState.kt b/core/src/main/kotlin/com/r3corda/core/testing/DummyLinearState.kt new file mode 100644 index 0000000000..ed50fb7aee --- /dev/null +++ b/core/src/main/kotlin/com/r3corda/core/testing/DummyLinearState.kt @@ -0,0 +1,17 @@ +package com.r3corda.core.testing + +import com.r3corda.core.contracts.Contract +import com.r3corda.core.contracts.LinearState +import com.r3corda.core.crypto.SecureHash +import java.security.PublicKey +import java.util.* + +class DummyLinearState( + override val thread: SecureHash = SecureHash.randomSHA256(), + override val contract: Contract = AlwaysSucceedContract(), + override val participants: List = listOf()) : LinearState { + + override fun isRelevant(ourKeys: Set): Boolean { + return participants.any { ourKeys.contains(it) } + } +} From 3a84e2fe9d2067d87d7c81396c5dec1308095e51 Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Thu, 23 Jun 2016 14:20:21 +0100 Subject: [PATCH 4/8] node: Add test for LinearState thread clash --- .../node/services/WalletWithCashTest.kt | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) 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 100a072288..afc95f777f 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/WalletWithCashTest.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/WalletWithCashTest.kt @@ -4,6 +4,7 @@ import com.r3corda.contracts.cash.Cash import com.r3corda.contracts.cash.cashBalances import com.r3corda.contracts.testing.fillWithSomeTestCash import com.r3corda.core.contracts.* +import com.r3corda.core.crypto.SecureHash import com.r3corda.core.node.ServiceHub import com.r3corda.core.node.services.testing.MockKeyManagementService import com.r3corda.core.node.services.testing.MockStorageService @@ -14,6 +15,7 @@ import com.r3corda.node.services.wallet.NodeWalletService import org.junit.After import org.junit.Before import org.junit.Test +import org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.* import kotlin.test.assertEquals import kotlin.test.assertNull @@ -96,4 +98,25 @@ class WalletWithCashTest { // TODO: Flesh out these tests as needed. } + + + @Test + fun addingSeveralTransactionsOfTheSameLinearStateThreadFails() { + val (wallet, services) = make() + + val freshKey = services.keyManagementService.freshKey() + + val thread = SecureHash.sha256("thread") + val dummyIssue = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { + addOutputState(DummyLinearState(thread = thread, participants = listOf(freshKey.public))) + signWith(freshKey) + }.toSignedTransaction() + + wallet.notify(dummyIssue.tx) + assertEquals(1, wallet.currentWallet.states.size) + assertThatThrownBy { + wallet.notify(dummyIssue.tx) + } + assertEquals(1, wallet.currentWallet.states.size) + } } From 6bab0eb79fa256c4e64ef339a543c5840e538cc4 Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Thu, 23 Jun 2016 17:44:38 +0100 Subject: [PATCH 5/8] core: Add comment about Wallet.states --- .../main/kotlin/com/r3corda/core/node/services/Services.kt | 4 ++++ 1 file changed, 4 insertions(+) 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 a7b4ddca41..8bb335b309 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 @@ -24,6 +24,10 @@ val TOPIC_DEFAULT_POSTFIX = ".0" * about new transactions from our peers and generate new transactions that consume states ourselves. * * This abstract class has no references to Cash contracts. + * + * [states] Holds the list of states that are *active* and *relevant*. + * Active means they haven't been consumed yet (or we don't know about it). + * Relevant means they contain at least one of our pubkeys */ class Wallet(val states: List>) { @Suppress("UNCHECKED_CAST") From 57270c8c6644d5f28da76951e00746ef82f9d04b Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Thu, 23 Jun 2016 17:44:52 +0100 Subject: [PATCH 6/8] core: Add nonce to DummyLinearState --- .../main/kotlin/com/r3corda/core/testing/DummyLinearState.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 ed50fb7aee..756e1c4494 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/DummyLinearState.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/DummyLinearState.kt @@ -9,7 +9,8 @@ import java.util.* class DummyLinearState( override val thread: SecureHash = SecureHash.randomSHA256(), override val contract: Contract = AlwaysSucceedContract(), - override val participants: List = listOf()) : LinearState { + override val participants: List = listOf(), + val nonce: SecureHash = SecureHash.randomSHA256()) : LinearState { override fun isRelevant(ourKeys: Set): Boolean { return participants.any { ourKeys.contains(it) } From ac69f566c92d12bcdad13458af43bf18b2f959d5 Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Thu, 23 Jun 2016 17:45:15 +0100 Subject: [PATCH 7/8] node: Add another test testing correct LinearState sequencing --- .../node/services/WalletWithCashTest.kt | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) 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 afc95f777f..1107f610ab 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/WalletWithCashTest.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/WalletWithCashTest.kt @@ -101,12 +101,14 @@ class WalletWithCashTest { @Test - fun addingSeveralTransactionsOfTheSameLinearStateThreadFails() { + fun branchingLinearStatesFails() { val (wallet, services) = make() val freshKey = services.keyManagementService.freshKey() val thread = SecureHash.sha256("thread") + + // Issue a linear state val dummyIssue = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { addOutputState(DummyLinearState(thread = thread, participants = listOf(freshKey.public))) signWith(freshKey) @@ -114,9 +116,44 @@ class WalletWithCashTest { wallet.notify(dummyIssue.tx) assertEquals(1, wallet.currentWallet.states.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))) + signWith(freshKey) + }.toSignedTransaction() + assertThatThrownBy { - wallet.notify(dummyIssue.tx) + wallet.notify(dummyIssue2.tx) } assertEquals(1, wallet.currentWallet.states.size) } + + @Test + fun sequencingLinearStatesWorks() { + val (wallet, services) = make() + + val freshKey = services.keyManagementService.freshKey() + + val thread = SecureHash.sha256("thread") + + // Issue a linear state + val dummyIssue = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { + addOutputState(DummyLinearState(thread = thread, participants = listOf(freshKey.public))) + signWith(freshKey) + }.toSignedTransaction() + + wallet.notify(dummyIssue.tx) + assertEquals(1, wallet.currentWallet.states.size) + + // Move the same state + val dummyMove = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { + addOutputState(DummyLinearState(thread = thread, participants = listOf(freshKey.public))) + addInputState(dummyIssue.tx.outRef(0)) + signWith(DUMMY_NOTARY_KEY) + }.toSignedTransaction() + + wallet.notify(dummyMove.tx) + assertEquals(1, wallet.currentWallet.states.size) + } } From 2d8d5571c20542c8db19cff3c5f473ea309bce03 Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Thu, 23 Jun 2016 18:02:51 +0100 Subject: [PATCH 8/8] core: Add more info to ClashingThreads exception --- .../com/r3corda/core/testing/InMemoryWalletService.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 37beeab2bf..45bf312a3d 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/InMemoryWalletService.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/InMemoryWalletService.kt @@ -22,8 +22,8 @@ import javax.annotation.concurrent.ThreadSafe */ @ThreadSafe open class InMemoryWalletService(private val services: ServiceHub) : SingletonSerializeAsToken(), WalletService { - class ClashingThreads(threads: Set) : - Exception("There are multiple linear states pointing to the same thread. The clashing thread(s): $threads") + class ClashingThreads(threads: Set, transactions: Iterable) : + Exception("There are multiple linear head states after processing transactions $transactions. The clashing thread(s): $threads") private val log = loggerFor() // Variables inside InnerState are protected with a lock by the ThreadBox and aren't in scope unless you're @@ -79,7 +79,7 @@ open class InMemoryWalletService(private val services: ServiceHub) : SingletonSe val clashingThreads = walletAndNetDelta.first.clashingThreads if (!clashingThreads.isEmpty()) { - throw ClashingThreads(clashingThreads) + throw ClashingThreads(clashingThreads, txns) } wallet = walletAndNetDelta.first