From 3d391ec8c2522ead25fb5f9cb2700e232035cc99 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Thu, 11 Aug 2016 16:29:26 +0200 Subject: [PATCH] Add unit tests for the resolve transactions protocol --- core/build.gradle | 6 + .../r3corda/core/contracts/DummyContract.kt | 19 ++- .../protocols/ResolveTransactionsProtocol.kt | 19 ++- .../ResolveTransactionsProtocolTest.kt | 128 ++++++++++++++++++ .../ResolveTransactionsProtocolTest.kt | 4 - .../r3corda/node/internal/testing/MockNode.kt | 33 ++++- .../node/services/NotaryChangeTests.kt | 4 +- 7 files changed, 195 insertions(+), 18 deletions(-) create mode 100644 core/src/test/kotlin/com/r3corda/core/protocols/ResolveTransactionsProtocolTest.kt delete mode 100644 core/src/test/kotlin/com/r3corda/protocols/ResolveTransactionsProtocolTest.kt diff --git a/core/build.gradle b/core/build.gradle index c7ef1a91a3..fa24af3928 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -18,6 +18,9 @@ repositories { maven { url 'http://oss.sonatype.org/content/repositories/snapshots' } + maven { + url 'https://dl.bintray.com/kotlin/exposed' + } } sourceSets { @@ -35,6 +38,9 @@ dependencies { // Guava: Google test library (collections test suite) testCompile "com.google.guava:guava-testlib:19.0" + // Bring in the MockNode infrastructure for writing protocol unit tests. + testCompile project(":node") + compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/DummyContract.kt b/core/src/main/kotlin/com/r3corda/core/contracts/DummyContract.kt index 66358cebd5..174a58f100 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/DummyContract.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/DummyContract.kt @@ -46,8 +46,21 @@ class DummyContract : Contract { // The "empty contract" override val legalContractReference: SecureHash = SecureHash.sha256("") - fun generateInitial(owner: PartyAndReference, magicNumber: Int, notary: Party): TransactionBuilder { - val state = SingleOwnerState(magicNumber, owner.party.owningKey) - return TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Create(), owner.party.owningKey)) + companion object { + @JvmStatic + fun generateInitial(owner: PartyAndReference, magicNumber: Int, notary: Party): TransactionBuilder { + val state = SingleOwnerState(magicNumber, owner.party.owningKey) + return TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Create(), owner.party.owningKey)) + } + + fun move(prior: StateAndRef, newOwner: PublicKey): TransactionBuilder { + val priorState = prior.state.data + val (cmd, state) = priorState.withNewOwner(newOwner) + return TransactionType.General.Builder(notary = prior.state.notary).withItems( + /* INPUT */ prior, + /* COMMAND */ Command(cmd, priorState.owner), + /* OUTPUT */ state + ) + } } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/r3corda/protocols/ResolveTransactionsProtocol.kt b/core/src/main/kotlin/com/r3corda/protocols/ResolveTransactionsProtocol.kt index db7a51c80d..c8efaadc81 100644 --- a/core/src/main/kotlin/com/r3corda/protocols/ResolveTransactionsProtocol.kt +++ b/core/src/main/kotlin/com/r3corda/protocols/ResolveTransactionsProtocol.kt @@ -41,6 +41,10 @@ class ResolveTransactionsProtocol(private val txHashes: Set, private var stx: SignedTransaction? = null private var wtx: WireTransaction? = null + // TODO: Figure out a more appropriate DOS limit here, 5000 is simply a very bad guess. + /** The maximum number of transactions this protocol will try to download before bailing out. */ + var transactionCountLimit = 5000 + /** * Resolve the full history of a transaction and verify it with its dependencies. */ @@ -65,11 +69,11 @@ class ResolveTransactionsProtocol(private val txHashes: Set, // redundantly next time we attempt verification. val result = ArrayList() - for (tx in newTxns) { + for (stx in newTxns) { // Resolve to a LedgerTransaction and then run all contracts. - val ltx = tx.toLedgerTransaction(serviceHub) + val ltx = stx.toLedgerTransaction(serviceHub) ltx.verify() - serviceHub.recordTransactions(tx) + serviceHub.recordTransactions(stx) result += ltx } @@ -114,6 +118,8 @@ class ResolveTransactionsProtocol(private val txHashes: Set, nextRequests.addAll(depsToCheck) val resultQ = LinkedHashMap() + val limit = transactionCountLimit + check(limit > 0) { "$limit is not a valid count limit" } var limitCounter = 0 while (nextRequests.isNotEmpty()) { // Don't re-download the same tx when we haven't verified it yet but it's referenced multiple times in the @@ -136,10 +142,8 @@ class ResolveTransactionsProtocol(private val txHashes: Set, val inputHashes = downloads.flatMap { it.tx.inputs }.map { it.txhash } nextRequests.addAll(inputHashes) - // TODO: Figure out a more appropriate DOS limit here, 5000 is simply a very bad guess. - // TODO: Unit test the DoS limit. limitCounter = limitCounter checkedAdd nextRequests.size - if (limitCounter > 5000) + if (limitCounter > limit) throw ExcessivelyLargeTransactionGraph() } @@ -156,6 +160,7 @@ class ResolveTransactionsProtocol(private val txHashes: Set, val missingAttachments = downloads.flatMap { wtx -> wtx.attachments.filter { serviceHub.storageService.attachments.openAttachment(it) == null } } - subProtocol(FetchAttachmentsProtocol(missingAttachments.toSet(), otherSide)) + if (missingAttachments.isNotEmpty()) + subProtocol(FetchAttachmentsProtocol(missingAttachments.toSet(), otherSide)) } } diff --git a/core/src/test/kotlin/com/r3corda/core/protocols/ResolveTransactionsProtocolTest.kt b/core/src/test/kotlin/com/r3corda/core/protocols/ResolveTransactionsProtocolTest.kt new file mode 100644 index 0000000000..ff233e66b4 --- /dev/null +++ b/core/src/test/kotlin/com/r3corda/core/protocols/ResolveTransactionsProtocolTest.kt @@ -0,0 +1,128 @@ +package com.r3corda.core.protocols + +import com.r3corda.core.contracts.DummyContract +import com.r3corda.core.contracts.SignedTransaction +import com.r3corda.core.crypto.NullSignature +import com.r3corda.core.crypto.Party +import com.r3corda.core.crypto.SecureHash +import com.r3corda.core.serialization.opaque +import com.r3corda.core.testing.* +import com.r3corda.node.internal.testing.MockNetwork +import com.r3corda.protocols.ResolveTransactionsProtocol +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.security.SignatureException +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class ResolveTransactionsProtocolTest { + lateinit var net: MockNetwork + lateinit var a: MockNetwork.MockNode + lateinit var b: MockNetwork.MockNode + lateinit var notary: Party + + @Before + fun setup() { + net = MockNetwork() + val nodes = net.createSomeNodes() + a = nodes.partyNodes[0] + b = nodes.partyNodes[1] + notary = nodes.notaryNode.info.identity + net.runNetwork() + } + + @After + fun tearDown() { + net.stopNodes() + } + + @Test + fun `resolve from two hashes`() { + val (stx1, stx2) = makeTransactions() + val p = ResolveTransactionsProtocol(setOf(stx2.id), a.info.identity) + val future = b.services.startProtocol("resolve", p) + net.runNetwork() + val results = future.get() + assertEquals(listOf(stx1.id, stx2.id), results.map { it.id }) + assertEquals(stx1, b.storage.validatedTransactions.getTransaction(stx1.id)) + assertEquals(stx2, b.storage.validatedTransactions.getTransaction(stx2.id)) + } + + @Test + fun `dependency with an error`() { + val stx = makeTransactions(signFirstTX = false).second + val p = ResolveTransactionsProtocol(setOf(stx.id), a.info.identity) + val future = b.services.startProtocol("resolve", p) + net.runNetwork() + assertFailsWith(SignatureException::class) { + rootCauseExceptions { future.get() } + } + } + + @Test + fun `resolve from a signed transaction`() { + val (stx1, stx2) = makeTransactions() + val p = ResolveTransactionsProtocol(stx2, a.info.identity) + val future = b.services.startProtocol("resolve", p) + net.runNetwork() + future.get() + assertEquals(stx1, b.storage.validatedTransactions.getTransaction(stx1.id)) + // But stx2 wasn't inserted, just stx1. + assertNull(b.storage.validatedTransactions.getTransaction(stx2.id)) + } + + @Test + fun `denial of service check`() { + // Chain lots of txns together. + val stx2 = makeTransactions().second + val count = 50 + var cursor = stx2 + repeat(count) { + val stx = DummyContract.move(cursor.tx.outRef(0), MINI_CORP_PUBKEY) + .addSignatureUnchecked(NullSignature) + .toSignedTransaction(false) + a.services.recordTransactions(stx) + cursor = stx + } + val p = ResolveTransactionsProtocol(setOf(cursor.id), a.info.identity) + p.transactionCountLimit = 40 + val future = b.services.startProtocol("resolve", p) + net.runNetwork() + assertFailsWith { + rootCauseExceptions { future.get() } + } + } + + @Test + fun attachment() { + val id = a.services.storageService.attachments.importAttachment("Some test file".toByteArray().opaque().open()) + val stx2 = makeTransactions(withAttachment = id).second + val p = ResolveTransactionsProtocol(stx2, a.info.identity) + val future = b.services.startProtocol("resolve", p) + net.runNetwork() + future.get() + assertNotNull(b.services.storageService.attachments.openAttachment(id)) + } + + private fun makeTransactions(signFirstTX: Boolean = true, withAttachment: SecureHash? = null): Pair { + // Make a chain of custody of dummy states and insert into node A. + val dummy1: SignedTransaction = DummyContract.generateInitial(MEGA_CORP.ref(1), 0, notary).let { + if (withAttachment != null) + it.addAttachment(withAttachment) + if (signFirstTX) + it.signWith(MEGA_CORP_KEY) + it.signWith(DUMMY_NOTARY_KEY) + it.toSignedTransaction(false) + } + val dummy2: SignedTransaction = DummyContract.move(dummy1.tx.outRef(0), MINI_CORP_PUBKEY).let { + it.signWith(MEGA_CORP_KEY) + it.signWith(DUMMY_NOTARY_KEY) + it.toSignedTransaction() + } + a.services.recordTransactions(dummy1, dummy2) + return Pair(dummy1, dummy2) + } +} diff --git a/core/src/test/kotlin/com/r3corda/protocols/ResolveTransactionsProtocolTest.kt b/core/src/test/kotlin/com/r3corda/protocols/ResolveTransactionsProtocolTest.kt deleted file mode 100644 index 3099ad332e..0000000000 --- a/core/src/test/kotlin/com/r3corda/protocols/ResolveTransactionsProtocolTest.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.r3corda.protocols - -class ResolveTransactionsProtocolTest { -} \ No newline at end of file diff --git a/node/src/main/kotlin/com/r3corda/node/internal/testing/MockNode.kt b/node/src/main/kotlin/com/r3corda/node/internal/testing/MockNode.kt index f0b35331a1..ec27a176ac 100644 --- a/node/src/main/kotlin/com/r3corda/node/internal/testing/MockNode.kt +++ b/node/src/main/kotlin/com/r3corda/node/internal/testing/MockNode.kt @@ -11,6 +11,7 @@ import com.r3corda.core.node.services.ServiceType import com.r3corda.core.node.services.WalletService import com.r3corda.core.node.services.testing.MockIdentityService import com.r3corda.core.node.services.testing.makeTestDataSourceProperties +import com.r3corda.core.testing.DUMMY_NOTARY_KEY import com.r3corda.core.testing.InMemoryWalletService import com.r3corda.core.utilities.loggerFor import com.r3corda.node.internal.AbstractNode @@ -160,6 +161,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, } } + // TODO: Move this to using createSomeNodes which doesn't conflate network services with network users. /** * Sets up a two node network, in which the first node runs network map and notary services and the other * doesn't. @@ -172,8 +174,35 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, ) } - fun createNotaryNode(legalName: String? = null, keyPair: KeyPair? = null) = createNode(null, -1, defaultFactory, true, legalName, keyPair, false, NetworkMapService.Type, SimpleNotaryService.Type) - fun createPartyNode(networkMapAddr: NodeInfo, legalName: String? = null, keyPair: KeyPair? = null) = createNode(networkMapAddr, -1, defaultFactory, true, legalName, keyPair) + /** + * A bundle that separates the generic user nodes and service-providing nodes. A real network might not be so + * clearly separated, but this is convenient for testing. + */ + data class BasketOfNodes(val partyNodes: List, val notaryNode: MockNode, val mapNode: MockNode) + + /** + * Sets up a network with the requested number of nodes (defaulting to two), with one or more service nodes that + * run a notary, network map, any oracles etc. Can't be combined with [createTwoNodes]. + */ + fun createSomeNodes(numPartyNodes: Int = 2, nodeFactory: Factory = defaultFactory, notaryKeyPair: KeyPair? = DUMMY_NOTARY_KEY): BasketOfNodes { + require(nodes.isEmpty()) + val mapNode = createNode(null, nodeFactory = nodeFactory, advertisedServices = NetworkMapService.Type) + val notaryNode = createNode(mapNode.info, nodeFactory = nodeFactory, keyPair = notaryKeyPair, + advertisedServices = SimpleNotaryService.Type) + val nodes = ArrayList() + repeat(numPartyNodes) { + nodes += createPartyNode(mapNode.info) + } + return BasketOfNodes(nodes, notaryNode, mapNode) + } + + fun createNotaryNode(legalName: String? = null, keyPair: KeyPair? = null): MockNode { + return createNode(null, -1, defaultFactory, true, legalName, keyPair, false, NetworkMapService.Type, SimpleNotaryService.Type) + } + + fun createPartyNode(networkMapAddr: NodeInfo, legalName: String? = null, keyPair: KeyPair? = null): MockNode { + return createNode(networkMapAddr, -1, defaultFactory, true, legalName, keyPair) + } @Suppress("unused") // This is used from the network visualiser tool. fun addressToNode(address: SingleMessageRecipient): MockNode = nodes.single { it.net.myAddress == address } diff --git a/node/src/test/kotlin/com/r3corda/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/com/r3corda/node/services/NotaryChangeTests.kt index 5e81eec3a3..94bcddb9df 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/NotaryChangeTests.kt @@ -96,7 +96,7 @@ class NotaryChangeTests { } fun issueState(node: AbstractNode): StateAndRef<*> { - val tx = DummyContract().generateInitial(node.info.identity.ref(0), Random().nextInt(), DUMMY_NOTARY) + val tx = DummyContract.generateInitial(node.info.identity.ref(0), Random().nextInt(), DUMMY_NOTARY) tx.signWith(node.storage.myLegalIdentityKey) tx.signWith(DUMMY_NOTARY_KEY) val stx = tx.toSignedTransaction() @@ -119,7 +119,7 @@ fun issueMultiPartyState(nodeA: AbstractNode, nodeB: AbstractNode): StateAndRef< } fun issueInvalidState(node: AbstractNode, notary: Party = DUMMY_NOTARY): StateAndRef<*> { - val tx = DummyContract().generateInitial(node.info.identity.ref(0), Random().nextInt(), notary) + val tx = DummyContract.generateInitial(node.info.identity.ref(0), Random().nextInt(), notary) tx.setTime(Instant.now(), 30.seconds) tx.signWith(node.storage.myLegalIdentityKey) val stx = tx.toSignedTransaction(false)