Add unit tests for the resolve transactions protocol

This commit is contained in:
Mike Hearn 2016-08-11 16:29:26 +02:00
parent 709fe096b3
commit 3d391ec8c2
7 changed files with 195 additions and 18 deletions

View File

@ -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"

View File

@ -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<DummyContract.SingleOwnerState>, 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
)
}
}
}

View File

@ -41,6 +41,10 @@ class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
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<SecureHash>,
// redundantly next time we attempt verification.
val result = ArrayList<LedgerTransaction>()
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<SecureHash>,
nextRequests.addAll(depsToCheck)
val resultQ = LinkedHashMap<SecureHash, SignedTransaction>()
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<SecureHash>,
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<SecureHash>,
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))
}
}

View File

@ -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<ResolveTransactionsProtocol.ExcessivelyLargeTransactionGraph> {
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<SignedTransaction, SignedTransaction> {
// 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)
}
}

View File

@ -1,4 +0,0 @@
package com.r3corda.protocols
class ResolveTransactionsProtocolTest {
}

View File

@ -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<MockNode>, 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<MockNode>()
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 }

View File

@ -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)