From 701fc853ad2978c4e3268842bd7a8283bb023722 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Fri, 29 Jul 2016 15:52:04 +0200 Subject: [PATCH] Refactor the core transaction types to improve clarity, simplify verification and prepare for sandboxing. Changes include: - LedgerTransaction is now much more central: it represents a fully resolved and looked-up tx, with the inputs available. - TransactionGroup and TransactionForVerification are gone. There is a temporary TransactionForContract class for backwards compatibility but it will also be gone soon. - ResolveTransactionsProtocol is simplified, and now commits a tx to the database as soon as it's determined to be valid. - ServiceHub is now passed in more consistently to verification code, so we can use more services in future more easily e.g. a sandboxing service. - A variety of APIs have been tweaked or documented better. --- .../protocols/TwoPartyTradeProtocol.kt | 28 +-- .../r3corda/contracts/CommercialPaperTests.kt | 77 +++---- .../kotlin/com/r3corda/contracts/IRSTests.kt | 31 ++- .../contracts/asset/ObligationTests.kt | 4 +- .../core/contracts/TransactionTools.kt | 37 ++-- .../core/contracts/TransactionTypes.kt | 38 ++-- .../core/contracts/TransactionVerification.kt | 72 +------ .../r3corda/core/contracts/Transactions.kt | 66 +++--- .../com/r3corda/core/node/ServiceHub.kt | 20 +- .../com/r3corda/core/testing/TestDSL.kt | 50 +---- .../AbstractStateReplacementProtocol.kt | 25 +-- .../protocols/ResolveTransactionsProtocol.kt | 132 ++++++------ .../r3corda/protocols/TwoPartyDealProtocol.kt | 18 +- .../protocols/ValidatingNotaryProtocol.kt | 17 +- .../core/contracts/TransactionGroupTests.kt | 194 ------------------ .../TransactionSerializationTests.kt | 9 +- .../contracts/AccountReceivableTests.kt | 17 +- .../contracts/BillOfLadingAgreementTests.kt | 34 ++- .../kotlin/com/r3corda/contracts/LOCTests.kt | 19 +- .../messaging/TwoPartyTradeProtocolTests.kt | 18 +- .../node/services/WalletWithCashTest.kt | 3 +- 21 files changed, 315 insertions(+), 594 deletions(-) delete mode 100644 core/src/test/kotlin/com/r3corda/core/contracts/TransactionGroupTests.kt diff --git a/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt b/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt index fe06b16bd3..7a83042ef7 100644 --- a/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt +++ b/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt @@ -4,9 +4,7 @@ import co.paralleluniverse.fibers.Suspendable import com.r3corda.contracts.asset.Cash import com.r3corda.contracts.asset.sumCashBy import com.r3corda.core.contracts.* -import com.r3corda.core.crypto.DigitalSignature -import com.r3corda.core.crypto.Party -import com.r3corda.core.crypto.signWithECDSA +import com.r3corda.core.crypto.* import com.r3corda.core.node.NodeInfo import com.r3corda.core.protocols.ProtocolLogic import com.r3corda.core.random63BitValue @@ -121,17 +119,17 @@ object TwoPartyTradeProtocol { progressTracker.nextStep() // Check that the tx proposed by the buyer is valid. - val missingSigs = it.verify(throwIfSignaturesAreMissing = false) - if (missingSigs != setOf(myKeyPair.public, notaryNode.identity.owningKey)) - throw SignatureException("The set of missing signatures is not as expected: $missingSigs") + val missingSigs: Set = it.verifySignatures(throwIfSignaturesAreMissing = false) + val expected = setOf(myKeyPair.public, notaryNode.identity.owningKey) + if (missingSigs != expected) + throw SignatureException("The set of missing signatures is not as expected: ${missingSigs.toStringsShort()} vs [${myKeyPair.public.toStringShort()}, ${notaryNode.identity.owningKey.toStringShort()}]") val wtx: WireTransaction = it.tx logger.trace { "Received partially signed transaction: ${it.id}" } - checkDependencies(it) - - // This verifies that the transaction is contract-valid, even though it is missing signatures. - serviceHub.verifyTransaction(wtx.toLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments)) + // Download and check all the things that this transaction depends on and verify it is contract-valid, + // even though it is missing signatures. + subProtocol(ResolveTransactionsProtocol(wtx, otherSide)) if (wtx.outputs.map { it.data }.sumCashBy(myKeyPair.public).withoutIssuer() != price) throw IllegalArgumentException("Transaction is not sending us the right amount of cash") @@ -150,14 +148,6 @@ object TwoPartyTradeProtocol { } } - @Suspendable - private fun checkDependencies(stx: SignedTransaction) { - // Download and check all the transactions that this transaction depends on, but do not check this - // transaction itself. - val dependencyTxIDs = stx.tx.inputs.map { it.txhash }.toSet() - subProtocol(ResolveTransactionsProtocol(dependencyTxIDs, otherSide)) - } - open fun signWithOurKey(partialTX: SignedTransaction): DigitalSignature.WithKey { progressTracker.currentStep = SIGNING return myKeyPair.signWithECDSA(partialTX.txBits) @@ -206,7 +196,7 @@ object TwoPartyTradeProtocol { logger.trace { "Got signatures from seller, verifying ... " } val fullySigned = stx + signatures.sellerSig + signatures.notarySig - fullySigned.verify() + fullySigned.verifySignatures() logger.trace { "Signatures received are valid. Trade complete! :-)" } return fullySigned diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt index 666fa850d5..3812555d4b 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt @@ -1,11 +1,12 @@ package com.r3corda.contracts import com.r3corda.contracts.asset.* +import com.r3corda.contracts.testing.fillWithSomeTestCash import com.r3corda.core.contracts.* import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.SecureHash import com.r3corda.core.days -import com.r3corda.core.node.services.testing.MockStorageService +import com.r3corda.core.node.services.testing.MockServices import com.r3corda.core.seconds import com.r3corda.core.testing.* import org.junit.Test @@ -72,7 +73,6 @@ class CommercialPaperTestsGeneric { @Parameterized.Parameter lateinit var thisTest: ICommercialPaperTestTemplate - val attachments = MockStorageService().attachments val issuer = MEGA_CORP.ref(123) @Test @@ -190,59 +190,62 @@ class CommercialPaperTestsGeneric { @Test fun `issue move and then redeem`() { - // MiniCorp issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself. - val issueTX: LedgerTransaction = run { - val ptx = CommercialPaper().generateIssue(MINI_CORP.ref(123), 10000.DOLLARS `issued by` DUMMY_CASH_ISSUER, - TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply { + val aliceServices = MockServices() + val alicesWallet = aliceServices.fillWithSomeTestCash(9000.DOLLARS) + + val bigCorpServices = MockServices() + val bigCorpWallet = bigCorpServices.fillWithSomeTestCash(13000.DOLLARS) + + // Propagate the cash transactions to each side. + aliceServices.recordTransactions(bigCorpWallet.states.map { bigCorpServices.storageService.validatedTransactions.getTransaction(it.ref.txhash)!! }) + bigCorpServices.recordTransactions(alicesWallet.states.map { aliceServices.storageService.validatedTransactions.getTransaction(it.ref.txhash)!! }) + + // BigCorp™ issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself. + val faceValue = 10000.DOLLARS `issued by` DUMMY_CASH_ISSUER + val issuance = bigCorpServices.storageService.myLegalIdentity.ref(1) + val issueTX: SignedTransaction = + CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply { setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds) - signWith(MINI_CORP_KEY) + signWith(bigCorpServices.key) signWith(DUMMY_NOTARY_KEY) - } - val stx = ptx.toSignedTransaction() - stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, attachments) - } + }.toSignedTransaction() - val (alicesWalletTX, alicesWallet) = cashOutputsToWallet( - 3000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY, - 3000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY, - 3000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY - ) - - // Alice pays $9000 to MiniCorp to own some of their debt. - val moveTX: LedgerTransaction = run { + // Alice pays $9000 to BigCorp to own some of their debt. + val moveTX: SignedTransaction = run { val ptx = TransactionType.General.Builder() - Cash().generateSpend(ptx, 9000.DOLLARS, MINI_CORP_PUBKEY, alicesWallet) - CommercialPaper().generateMove(ptx, issueTX.outRef(0), ALICE_PUBKEY) - ptx.signWith(MINI_CORP_KEY) - ptx.signWith(ALICE_KEY) + Cash().generateSpend(ptx, 9000.DOLLARS, bigCorpServices.key.public, alicesWallet.statesOfType()) + CommercialPaper().generateMove(ptx, issueTX.tx.outRef(0), aliceServices.key.public) + ptx.signWith(bigCorpServices.key) + ptx.signWith(aliceServices.key) ptx.signWith(DUMMY_NOTARY_KEY) - ptx.toSignedTransaction().verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, attachments) + ptx.toSignedTransaction() } - // Won't be validated. - val (corpWalletTX, corpWallet) = cashOutputsToWallet( - 9000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` MINI_CORP_PUBKEY `with notary` DUMMY_NOTARY, - 4000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` MINI_CORP_PUBKEY `with notary` DUMMY_NOTARY - ) - - fun makeRedeemTX(time: Instant): LedgerTransaction { + fun makeRedeemTX(time: Instant): SignedTransaction { val ptx = TransactionType.General.Builder() ptx.setTime(time, DUMMY_NOTARY, 30.seconds) - CommercialPaper().generateRedeem(ptx, moveTX.outRef(1), corpWallet) - ptx.signWith(ALICE_KEY) - ptx.signWith(MINI_CORP_KEY) + CommercialPaper().generateRedeem(ptx, moveTX.tx.outRef(1), bigCorpWallet.statesOfType()) + ptx.signWith(aliceServices.key) + ptx.signWith(bigCorpServices.key) ptx.signWith(DUMMY_NOTARY_KEY) - return ptx.toSignedTransaction().verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, attachments) + return ptx.toSignedTransaction() } val tooEarlyRedemption = makeRedeemTX(TEST_TX_TIME + 10.days) val validRedemption = makeRedeemTX(TEST_TX_TIME + 31.days) + // Verify the txns are valid and insert into both sides. + listOf(issueTX, moveTX).forEach { + it.toLedgerTransaction(aliceServices).verify() + aliceServices.recordTransactions(it) + bigCorpServices.recordTransactions(it) + } + val e = assertFailsWith(TransactionVerificationException::class) { - TransactionGroup(setOf(issueTX, moveTX, tooEarlyRedemption), setOf(corpWalletTX, alicesWalletTX)).verify() + tooEarlyRedemption.toLedgerTransaction(aliceServices).verify() } assertTrue(e.cause!!.message!!.contains("paper must have matured")) - TransactionGroup(setOf(issueTX, moveTX, validRedemption), setOf(corpWalletTX, alicesWalletTX)).verify() + validRedemption.toLedgerTransaction(aliceServices).verify() } } diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt index b63d3ad218..c1f3c960fa 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt @@ -1,7 +1,7 @@ package com.r3corda.contracts import com.r3corda.core.contracts.* -import com.r3corda.core.node.services.testing.MockStorageService +import com.r3corda.core.node.services.testing.MockServices import com.r3corda.core.seconds import com.r3corda.core.testing.* import org.junit.Test @@ -195,9 +195,6 @@ fun createDummyIRS(irsSelect: Int): InterestRateSwap.State { } class IRSTests { - - val attachments = MockStorageService().attachments - @Test fun ok() { trade().verifies() @@ -211,9 +208,9 @@ class IRSTests { /** * Generate an IRS txn - we'll need it for a few things. */ - fun generateIRSTxn(irsSelect: Int): LedgerTransaction { + fun generateIRSTxn(irsSelect: Int): SignedTransaction { val dummyIRS = createDummyIRS(irsSelect) - val genTX: LedgerTransaction = run { + val genTX: SignedTransaction = run { val gtx = InterestRateSwap().generateAgreement( fixedLeg = dummyIRS.fixedLeg, floatingLeg = dummyIRS.floatingLeg, @@ -225,7 +222,7 @@ class IRSTests { signWith(MINI_CORP_KEY) signWith(DUMMY_NOTARY_KEY) } - gtx.toSignedTransaction().verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, attachments) + gtx.toSignedTransaction() } return genTX } @@ -243,7 +240,7 @@ class IRSTests { * Utility so I don't have to keep typing this. */ fun singleIRS(irsSelector: Int = 1): InterestRateSwap.State { - return generateIRSTxn(irsSelector).outputs.map { it.data }.filterIsInstance().single() + return generateIRSTxn(irsSelector).tx.outputs.map { it.data }.filterIsInstance().single() } /** @@ -293,30 +290,30 @@ class IRSTests { */ @Test fun generateIRSandFixSome() { + val services = MockServices() var previousTXN = generateIRSTxn(1) - fun currentIRS() = previousTXN.outputs.map { it.data }.filterIsInstance().single() - - val txns = HashSet() - txns += previousTXN + previousTXN.toLedgerTransaction(services).verify() + services.recordTransactions(previousTXN) + fun currentIRS() = previousTXN.tx.outputs.map { it.data }.filterIsInstance().single() while (true) { val nextFix: FixOf = currentIRS().nextFixingOf() ?: break - val fixTX: LedgerTransaction = run { + val fixTX: SignedTransaction = run { val tx = TransactionType.General.Builder() val fixing = Fix(nextFix, "0.052".percent.value) - InterestRateSwap().generateFix(tx, previousTXN.outRef(0), fixing) + InterestRateSwap().generateFix(tx, previousTXN.tx.outRef(0), fixing) with(tx) { setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds) signWith(MEGA_CORP_KEY) signWith(MINI_CORP_KEY) signWith(DUMMY_NOTARY_KEY) } - tx.toSignedTransaction().verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, attachments) + tx.toSignedTransaction() } + fixTX.toLedgerTransaction(services).verify() + services.recordTransactions(fixTX) previousTXN = fixTX - txns += fixTX } - TransactionGroup(txns, emptySet()).verify() } // Move these later as they aren't IRS specific. diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt index f06834db58..314e19a2df 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt @@ -289,7 +289,7 @@ class ObligationTests { }.toSignedTransaction() assertEquals(1, tx.tx.outputs.size) assertEquals(stateAndRef.state.data.copy(lifecycle = Lifecycle.DEFAULTED), tx.tx.outputs[0].data) - assertTrue(tx.verify().isEmpty()) + tx.verifySignatures() // And set it back stateAndRef = tx.tx.outRef>(0) @@ -300,7 +300,7 @@ class ObligationTests { }.toSignedTransaction() assertEquals(1, tx.tx.outputs.size) assertEquals(stateAndRef.state.data.copy(lifecycle = Lifecycle.NORMAL), tx.tx.outputs[0].data) - assertTrue(tx.verify().isEmpty()) + tx.verifySignatures() } /** Test generating a transaction to settle an obligation. */ diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTools.kt b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTools.kt index 11b360e78a..936182580a 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTools.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTools.kt @@ -1,32 +1,39 @@ package com.r3corda.core.contracts -import com.r3corda.core.node.services.AttachmentStorage -import com.r3corda.core.node.services.IdentityService +import com.r3corda.core.node.ServiceHub import java.io.FileNotFoundException +// TODO: Move these into the actual classes (i.e. where people would expect to find them) and split Transactions.kt into multiple files + /** - * Looks up identities and attachments from storage to generate a [LedgerTransaction]. + * Looks up identities and attachments from storage to generate a [LedgerTransaction]. A transaction is expected to + * have been fully resolved using the resolution protocol by this point. * * @throws FileNotFoundException if a required attachment was not found in storage. + * @throws TransactionResolutionException if an input points to a transaction not found in storage. */ -fun WireTransaction.toLedgerTransaction(identityService: IdentityService, - attachmentStorage: AttachmentStorage): LedgerTransaction { +fun WireTransaction.toLedgerTransaction(services: ServiceHub): LedgerTransaction { + // Look up random keys to authenticated identities. This is just a stub placeholder and will all change in future. val authenticatedArgs = commands.map { - val institutions = it.signers.mapNotNull { pk -> identityService.partyFromKey(pk) } - AuthenticatedObject(it.signers, institutions, it.value) + val parties = it.signers.mapNotNull { pk -> services.identityService.partyFromKey(pk) } + AuthenticatedObject(it.signers, parties, it.value) } + // Open attachments specified in this transaction. If we haven't downloaded them, we fail. val attachments = attachments.map { - attachmentStorage.openAttachment(it) ?: throw FileNotFoundException(it.toString()) + services.storageService.attachments.openAttachment(it) ?: throw FileNotFoundException(it.toString()) } - return LedgerTransaction(inputs, outputs, authenticatedArgs, attachments, id, signers, type) + val resolvedInputs = inputs.map { StateAndRef(services.loadState(it), it) } + return LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, signers, type) } /** - * Calls [verify] to check all required signatures are present, and then uses the passed [IdentityService] to call - * [WireTransaction.toLedgerTransaction] to look up well known identities from pubkeys. + * Calls [verify] to check all required signatures are present, and then calls [WireTransaction.toLedgerTransaction] + * with the passed in [ServiceHub] to resolve the dependencies, returning an unverified LedgerTransaction. + * + * @throws FileNotFoundException if a required attachment was not found in storage. + * @throws TransactionResolutionException if an input points to a transaction not found in storage. */ -fun SignedTransaction.verifyToLedgerTransaction(identityService: IdentityService, - attachmentStorage: AttachmentStorage): LedgerTransaction { - verify() - return tx.toLedgerTransaction(identityService, attachmentStorage) +fun SignedTransaction.toLedgerTransaction(services: ServiceHub): LedgerTransaction { + verifySignatures() + return tx.toLedgerTransaction(services) } diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTypes.kt b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTypes.kt index 86304863bd..2e0f4174fb 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTypes.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionTypes.kt @@ -16,18 +16,17 @@ sealed class TransactionType { * * Note: Presence of _signatures_ is not checked, only the public keys to be signed for. */ - fun verify(tx: TransactionForVerification) { + fun verify(tx: LedgerTransaction) { val missing = verifySigners(tx) if (missing.isNotEmpty()) throw TransactionVerificationException.SignersMissing(tx, missing.toList()) - verifyTransaction(tx) } /** Check that the list of signers includes all the necessary keys */ - fun verifySigners(tx: TransactionForVerification): Set { + fun verifySigners(tx: LedgerTransaction): Set { val timestamp = tx.commands.noneOrSingle { it.value is TimestampCommand } val timestampKey = timestamp?.signers.orEmpty() - val notaryKey = (tx.inputs.map { it.notary.owningKey } + timestampKey).toSet() + val notaryKey = (tx.inputs.map { it.state.notary.owningKey } + timestampKey).toSet() if (notaryKey.size > 1) throw TransactionVerificationException.MoreThanOneNotary(tx) val requiredKeys = getRequiredSigners(tx) + notaryKey @@ -40,10 +39,10 @@ sealed class TransactionType { * Return the list of public keys that that require signatures for the transaction type. * Note: the notary key is checked separately for all transactions and need not be included. */ - abstract fun getRequiredSigners(tx: TransactionForVerification): Set + abstract fun getRequiredSigners(tx: LedgerTransaction): Set /** Implement type specific transaction validation logic */ - abstract fun verifyTransaction(tx: TransactionForVerification) + abstract fun verifyTransaction(tx: LedgerTransaction) /** A general transaction type where transaction validity is determined by custom contract code */ class General : TransactionType() { @@ -54,10 +53,11 @@ sealed class TransactionType { * Check the transaction is contract-valid by running the verify() for each input and output state contract. * If any contract fails to verify, the whole transaction is considered to be invalid. */ - override fun verifyTransaction(tx: TransactionForVerification) { + override fun verifyTransaction(tx: LedgerTransaction) { // TODO: Check that notary is unchanged val ctx = tx.toTransactionForContract() + // TODO: This will all be replaced in future once the sandbox and contract constraints work is done. val contracts = (ctx.inputs.map { it.contract } + ctx.outputs.map { it.contract }).toSet() for (contract in contracts) { try { @@ -68,10 +68,7 @@ sealed class TransactionType { } } - override fun getRequiredSigners(tx: TransactionForVerification): Set { - val commandKeys = tx.commands.flatMap { it.signers }.toSet() - return commandKeys - } + override fun getRequiredSigners(tx: LedgerTransaction) = tx.commands.flatMap { it.signers }.toSet() } /** @@ -91,14 +88,16 @@ sealed class TransactionType { } /** - * Check that the difference between inputs and outputs is only the notary field, - * and that all required signing public keys are present. + * Check that the difference between inputs and outputs is only the notary field, and that all required signing + * public keys are present. + * + * @throws TransactionVerificationException.InvalidNotaryChange if the validity check fails. */ - override fun verifyTransaction(tx: TransactionForVerification) { + override fun verifyTransaction(tx: LedgerTransaction) { try { - tx.inputs.zip(tx.outputs).forEach { - check(it.first.data == it.second.data) - check(it.first.notary != it.second.notary) + for ((input, output) in tx.inputs.zip(tx.outputs)) { + check(input.state.data == output.data) + check(input.state.notary != output.notary) } check(tx.commands.isEmpty()) } catch (e: IllegalStateException) { @@ -106,9 +105,6 @@ sealed class TransactionType { } } - override fun getRequiredSigners(tx: TransactionForVerification): Set { - val participantKeys = tx.inputs.flatMap { it.data.participants }.toSet() - return participantKeys - } + override fun getRequiredSigners(tx: LedgerTransaction) = tx.inputs.flatMap { it.state.data.participants }.toSet() } } diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionVerification.kt b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionVerification.kt index 9844c78c82..9dd43dadef 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionVerification.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionVerification.kt @@ -8,50 +8,6 @@ import java.util.* // TODO: Consider moving this out of the core module and providing a different way for unit tests to test contracts. -/** - * A TransactionGroup defines a directed acyclic graph of transactions that can be resolved with each other and then - * verified. Successful verification does not imply the non-existence of other conflicting transactions: simply that - * this subgraph does not contain conflicts and is accepted by the involved contracts. - * - * The inputs of the provided transactions must be resolvable either within the [transactions] set, or from the - * [nonVerifiedRoots] set. Transactions in the non-verified set are ignored other than for looking up input states. - */ -class TransactionGroup(val transactions: Set, val nonVerifiedRoots: Set) { - /** - * Verifies the group and returns the set of resolved transactions. - */ - fun verify(): Set { - // Check that every input can be resolved to an output. - // Check that no output is referenced by more than one input. - // Cycles should be impossible due to the use of hashes as pointers. - check(transactions.intersect(nonVerifiedRoots).isEmpty()) - - val hashToTXMap: Map> = (transactions + nonVerifiedRoots).groupBy { it.id } - val refToConsumingTXMap = hashMapOf() - - val resolved = HashSet(transactions.size) - for (tx in transactions) { - val inputs = ArrayList>(tx.inputs.size) - for (ref in tx.inputs) { - val conflict = refToConsumingTXMap[ref] - if (conflict != null) - throw TransactionConflictException(ref, tx, conflict) - refToConsumingTXMap[ref] = tx - - // Look up the connecting transaction. - val ltx = hashToTXMap[ref.txhash]?.single() ?: throw TransactionResolutionException(ref.txhash) - // Look up the output in that transaction by index. - inputs.add(ltx.outputs[ref.index]) - } - resolved.add(TransactionForVerification(inputs, tx.outputs, tx.attachments, tx.commands, tx.id, tx.signers, tx.type)) - } - - for (tx in resolved) - tx.verify() - return resolved - } -} - /** A transaction in fully resolved and sig-checked form, ready for passing as input to a verification function. */ data class TransactionForVerification(val inputs: List>, val outputs: List>, @@ -62,20 +18,6 @@ data class TransactionForVerification(val inputs: List, fun getTimestampByName(vararg authorityName: String): TimestampCommand? = commands.getTimestampByName(*authorityName) } -class TransactionResolutionException(val hash: SecureHash) : Exception() +class TransactionResolutionException(val hash: SecureHash) : Exception() { + override fun toString() = "Transaction resolution failure for $hash" +} class TransactionConflictException(val conflictRef: StateRef, val tx1: LedgerTransaction, val tx2: LedgerTransaction) : Exception() -sealed class TransactionVerificationException(val tx: TransactionForVerification, cause: Throwable?) : Exception(cause) { - class ContractRejection(tx: TransactionForVerification, val contract: Contract, cause: Throwable?) : TransactionVerificationException(tx, cause) - class MoreThanOneNotary(tx: TransactionForVerification) : TransactionVerificationException(tx, null) - class SignersMissing(tx: TransactionForVerification, val missing: List) : TransactionVerificationException(tx, null) { +sealed class TransactionVerificationException(val tx: LedgerTransaction, cause: Throwable?) : Exception(cause) { + class ContractRejection(tx: LedgerTransaction, val contract: Contract, cause: Throwable?) : TransactionVerificationException(tx, cause) + class MoreThanOneNotary(tx: LedgerTransaction) : TransactionVerificationException(tx, null) + class SignersMissing(tx: LedgerTransaction, val missing: List) : TransactionVerificationException(tx, null) { override fun toString() = "Signers missing: ${missing.map { it.toStringShort() }}" } - class InvalidNotaryChange(tx: TransactionForVerification) : TransactionVerificationException(tx, null) + class InvalidNotaryChange(tx: LedgerTransaction) : TransactionVerificationException(tx, null) } diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/Transactions.kt b/core/src/main/kotlin/com/r3corda/core/contracts/Transactions.kt index e955efbfaf..151ed36402 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/Transactions.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/Transactions.kt @@ -21,17 +21,23 @@ import java.security.SignatureException * a public key that is mentioned inside a transaction command. SignedTransaction is the top level transaction type * and the type most frequently passed around the network and stored. The identity of a transaction is the hash * of a WireTransaction, therefore if you are storing data keyed by WT hash be aware that multiple different SWTs may - * map to the same key (and they could be different in important ways!). + * map to the same key (and they could be different in important ways, like validity!). The signatures on a + * SignedTransaction might be invalid or missing: the type does not imply validity. * * WireTransaction is a transaction in a form ready to be serialised/unserialised. A WireTransaction can be hashed * in various ways to calculate a *signature hash* (or sighash), this is the hash that is signed by the various involved - * keypairs. Note that a sighash is not the same thing as a *transaction id*, which is the hash of the entire - * WireTransaction i.e. the outermost serialised form with everything included. + * keypairs. A WireTransaction can be safely deserialised from inside a SignedTransaction outside of the sandbox, + * because it consists of only platform defined types. Any user defined object graphs are kept as byte arrays. A + * WireTransaction may be invalid and missing its dependencies (other transactions + attachments). * - * LedgerTransaction is derived from WireTransaction. It is the result of doing some basic key lookups on WireCommand - * to see if any keys are from a recognised party, thus converting the WireCommand objects into - * AuthenticatedObject. Currently we just assume a hard coded pubkey->party map. In future it'd make more - * sense to use a certificate scheme and so that logic would get more complex. + * LedgerTransaction is derived from WireTransaction. It is the result of doing the following operations: + * + * - Downloading and locally storing all the dependencies of the transaction. + * - Doing some basic key lookups on WireCommand to see if any keys are from a recognised party, thus converting the + * WireCommand objects into AuthenticatedObject. Currently we just assume a hard coded pubkey->party map. + * In future it'd make more sense to use a certificate scheme and so that logic would get more complex. + * - Deserialising the included states, sandboxing the contract classes, and generally ensuring the embedded code is + * safe for interaction. * * All the above refer to inputs using a (txhash, output index) pair. * @@ -73,7 +79,7 @@ data class WireTransaction(val inputs: List, override fun toString(): String { val buf = StringBuilder() - buf.appendln("Transaction:") + buf.appendln("Transaction $id:") for (input in inputs) buf.appendln("${Emoji.rightArrow}INPUT: $input") for (output in outputs) buf.appendln("${Emoji.leftArrow}OUTPUT: $output") for (command in commands) buf.appendln("${Emoji.diamond}COMMAND: $command") @@ -97,18 +103,6 @@ data class SignedTransaction(val txBits: SerializedBytes, /** A transaction ID is the hash of the [WireTransaction]. Thus adding or removing a signature does not change it. */ override val id: SecureHash get() = txBits.hash - /** - * Verifies the given signatures against the serialized transaction data. Does NOT deserialise or check the contents - * to ensure there are no missing signatures: use verify() to do that. This weaker version can be useful for - * checking a partially signed transaction being prepared by multiple co-operating parties. - * - * @throws SignatureException if the signature is invalid or does not match. - */ - fun verifySignatures() { - for (sig in sigs) - sig.verifyWithECDSA(txBits.bits) - } - /** * Verify the signatures, deserialise the wire transaction and then check that the set of signatures found contains * the set of pubkeys in the signers list. If any signatures are missing, either throws an exception (by default) or @@ -116,9 +110,12 @@ data class SignedTransaction(val txBits: SerializedBytes, * * @throws SignatureException if a signature is invalid, does not match or if any signature is missing. */ - fun verify(throwIfSignaturesAreMissing: Boolean = true): Set { - verifySignatures() + fun verifySignatures(throwIfSignaturesAreMissing: Boolean = true): Set { + // Embedded WireTransaction is not deserialised until after we check the signatures. + for (sig in sigs) + sig.verifyWithECDSA(txBits.bits) + // Now examine the contents and ensure the sigs we have line up with the advertised list of signers. val missing = getMissingSignatures() if (missing.isNotEmpty() && throwIfSignaturesAreMissing) throw SignatureException("Missing signatures on transaction ${id.prefixChars()} for: ${missing.map { it.toStringShort() }}") @@ -157,12 +154,10 @@ data class SignedTransaction(val txBits: SerializedBytes, * A LedgerTransaction wraps the data needed to calculate one or more successor states from a set of input states. * It is the first step after extraction from a WireTransaction. The signatures at this point have been lined up * with the commands from the wire, and verified/looked up. - * - * TODO: This class needs a bit more thought. Should inputs be fully resolved by this point too? */ data class LedgerTransaction( /** The input states which will be consumed/invalidated by the execution of this transaction. */ - val inputs: List, + val inputs: List>, /** The states that will be generated by the execution of this transaction. */ val outputs: List>, /** Arbitrary data passed to the program of each input state. */ @@ -171,9 +166,28 @@ data class LedgerTransaction( val attachments: List, /** The hash of the original serialised WireTransaction */ override val id: SecureHash, + /** The notary key and the command keys together: a signed transaction must provide signatures for all of these. */ val signers: List, val type: TransactionType ) : NamedByHash { @Suppress("UNCHECKED_CAST") fun outRef(index: Int) = StateAndRef(outputs[index] as TransactionState, StateRef(id, index)) -} + + // TODO: Remove this concept. + // There isn't really a good justification for hiding this data from the contract, it's just a backwards compat hack. + /** Strips the transaction down to a form that is usable by the contract verify functions */ + fun toTransactionForContract(): TransactionForContract { + return TransactionForContract(inputs.map { it.state.data }, outputs.map { it.data }, attachments, commands, id, + inputs.map { it.state.notary }.singleOrNull()) + } + + /** + * Verifies this transaction and throws an exception if not valid, depending on the type. For general transactions: + * + * - The contracts are run with the transaction as the input. + * - The list of keys mentioned in commands is compared against the signers list. + * + * @throws TransactionVerificationException if anything goes wrong. + */ + fun verify() = type.verify(this) +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/r3corda/core/node/ServiceHub.kt b/core/src/main/kotlin/com/r3corda/core/node/ServiceHub.kt index 0800dc8f87..5a6349e954 100644 --- a/core/src/main/kotlin/com/r3corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/com/r3corda/core/node/ServiceHub.kt @@ -1,7 +1,10 @@ package com.r3corda.core.node import com.google.common.util.concurrent.ListenableFuture -import com.r3corda.core.contracts.* +import com.r3corda.core.contracts.SignedTransaction +import com.r3corda.core.contracts.StateRef +import com.r3corda.core.contracts.TransactionResolutionException +import com.r3corda.core.contracts.TransactionState import com.r3corda.core.messaging.MessagingService import com.r3corda.core.node.services.* import com.r3corda.core.protocols.ProtocolLogic @@ -25,19 +28,6 @@ interface ServiceHub { val schedulerService: SchedulerService val clock: Clock - /** - * Given a [LedgerTransaction], looks up all its dependencies in the local database, uses the identity service to map - * the [SignedTransaction]s the DB gives back into [LedgerTransaction]s, and then runs the smart contracts for the - * transaction. If no exception is thrown, the transaction is valid. - */ - fun verifyTransaction(ltx: LedgerTransaction) { - val dependencies = ltx.inputs.map { - storageService.validatedTransactions.getTransaction(it.txhash) ?: throw TransactionResolutionException(it.txhash) - } - val ltxns = dependencies.map { it.verifyToLedgerTransaction(identityService, storageService.attachments) } - TransactionGroup(setOf(ltx), ltxns.toSet()).verify() - } - /** * Given a list of [SignedTransaction]s, writes them to the local storage for validated transactions and then * sends them to the wallet for further processing. @@ -70,4 +60,4 @@ interface ServiceHub { * @throws IllegalProtocolLogicException or IllegalArgumentException if there are problems with the [logicType] or [args]. */ fun invokeProtocolAsync(logicType: Class>, vararg args: Any?): ListenableFuture -} +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt b/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt index e3f18ea53d..b905375f76 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt @@ -1,10 +1,7 @@ package com.r3corda.core.testing import com.r3corda.core.contracts.* -import com.r3corda.core.crypto.DigitalSignature -import com.r3corda.core.crypto.Party -import com.r3corda.core.crypto.SecureHash -import com.r3corda.core.crypto.signWithECDSA +import com.r3corda.core.crypto.* import com.r3corda.core.node.ServiceHub import com.r3corda.core.serialization.serialize import java.io.InputStream @@ -133,8 +130,7 @@ data class TestTransactionDSLInterpreter private constructor( } override fun verifies(): EnforceVerifyOrFail { - val resolvedTransaction = ledgerInterpreter.resolveWireTransaction(toWireTransaction()) - resolvedTransaction.verify() + toWireTransaction().toLedgerTransaction(services).verify() return EnforceVerifyOrFail.Token } @@ -185,26 +181,6 @@ data class TestLedgerDSLInterpreter private constructor ( nonVerifiedTransactionWithLocations = HashMap(nonVerifiedTransactionWithLocations) ) - internal fun resolveWireTransaction(wireTransaction: WireTransaction): TransactionForVerification { - return wireTransaction.run { - val authenticatedCommands = commands.map { - AuthenticatedObject(it.signers, it.signers.mapNotNull { services.identityService.partyFromKey(it) }, it.value) - } - val resolvedInputStates = inputs.map { resolveStateRef(it) } - val resolvedAttachments = attachments.map { resolveAttachment(it) } - TransactionForVerification( - inputs = resolvedInputStates, - outputs = outputs, - commands = authenticatedCommands, - origHash = wireTransaction.serialized.hash, - attachments = resolvedAttachments, - signers = signers.toList(), - type = type - ) - - } - } - internal inline fun resolveStateRef(stateRef: StateRef): TransactionState { val transactionWithLocation = transactionWithLocations[stateRef.txhash] ?: @@ -230,16 +206,6 @@ data class TestLedgerDSLInterpreter private constructor ( return transactionInterpreter } - fun toTransactionGroup(): TransactionGroup { - val ledgerTransactions = transactionWithLocations.map { - it.value.transaction.toLedgerTransaction(services.identityService, services.storageService.attachments) - } - val nonVerifiedLedgerTransactions = nonVerifiedTransactionWithLocations.map { - it.value.transaction.toLedgerTransaction(services.identityService, services.storageService.attachments) - } - return TransactionGroup(ledgerTransactions.toSet(), nonVerifiedLedgerTransactions.toSet()) - } - fun transactionName(transactionHash: SecureHash): String? { val transactionWithLocation = transactionWithLocations[transactionHash] return if (transactionWithLocation != null) { @@ -298,16 +264,18 @@ data class TestLedgerDSLInterpreter private constructor ( } override fun verifies(): EnforceVerifyOrFail { - val transactionGroup = toTransactionGroup() try { - transactionGroup.verify() + services.recordTransactions(transactionsUnverified.map { SignedTransaction(it.serialized, listOf(NullSignature)) }) + for ((key, value) in transactionWithLocations) { + value.transaction.toLedgerTransaction(services).verify() + services.recordTransactions(SignedTransaction(value.transaction.serialized, listOf(NullSignature))) + } + return EnforceVerifyOrFail.Token } catch (exception: TransactionVerificationException) { - val transactionWithLocation = transactionWithLocations[exception.tx.origHash] + val transactionWithLocation = transactionWithLocations[exception.tx.id] val transactionName = transactionWithLocation?.label ?: transactionWithLocation?.location ?: "" throw VerifiesFailed(transactionName, exception) } - - return EnforceVerifyOrFail.Token } override fun retrieveOutputStateAndRef(clazz: Class, label: String): StateAndRef { diff --git a/core/src/main/kotlin/com/r3corda/protocols/AbstractStateReplacementProtocol.kt b/core/src/main/kotlin/com/r3corda/protocols/AbstractStateReplacementProtocol.kt index ff38cb4ea6..7931dd09cf 100644 --- a/core/src/main/kotlin/com/r3corda/protocols/AbstractStateReplacementProtocol.kt +++ b/core/src/main/kotlin/com/r3corda/protocols/AbstractStateReplacementProtocol.kt @@ -10,9 +10,8 @@ import com.r3corda.core.node.NodeInfo import com.r3corda.core.protocols.ProtocolLogic import com.r3corda.core.random63BitValue import com.r3corda.core.utilities.ProgressTracker -import com.r3corda.protocols.NotaryProtocol -import com.r3corda.protocols.PartyRequestMessage -import com.r3corda.protocols.ResolveTransactionsProtocol +import com.r3corda.protocols.AbstractStateReplacementProtocol.Acceptor +import com.r3corda.protocols.AbstractStateReplacementProtocol.Instigator import java.security.PublicKey /** @@ -164,13 +163,14 @@ abstract class AbstractStateReplacementProtocol { val response = Result.noError(mySignature) val swapSignatures = sendAndReceive>(otherSide, sessionIdForSend, sessionIdForReceive, response) + // TODO: This step should not be necessary, as signatures are re-checked in verifySignatures. val allSignatures = swapSignatures.validate { signatures -> signatures.forEach { it.verifyWithECDSA(stx.txBits) } signatures } val finalTx = stx + allSignatures - finalTx.verify() + finalTx.verifySignatures() serviceHub.recordTransactions(listOf(finalTx)) } @@ -191,7 +191,9 @@ abstract class AbstractStateReplacementProtocol { private fun verifyTx(stx: SignedTransaction) { checkMySignatureRequired(stx.tx) checkDependenciesValid(stx) - checkValid(stx) + // We expect stx to have insufficient signatures, so we convert the WireTransaction to the LedgerTransaction + // here, thus bypassing the sufficient-signatures check. + stx.tx.toLedgerTransaction(serviceHub).verify() } private fun checkMySignatureRequired(tx: WireTransaction) { @@ -202,19 +204,10 @@ abstract class AbstractStateReplacementProtocol { @Suspendable private fun checkDependenciesValid(stx: SignedTransaction) { - val dependencyTxIDs = stx.tx.inputs.map { it.txhash }.toSet() - subProtocol(ResolveTransactionsProtocol(dependencyTxIDs, otherSide)) + subProtocol(ResolveTransactionsProtocol(stx.tx, otherSide)) } - private fun checkValid(stx: SignedTransaction) { - val ltx = stx.tx.toLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments) - serviceHub.verifyTransaction(ltx) - } - - private fun sign(stx: SignedTransaction): DigitalSignature.WithKey { - val myKeyPair = serviceHub.storageService.myLegalIdentityKey - return myKeyPair.signWithECDSA(stx.txBits) - } + private fun sign(stx: SignedTransaction) = serviceHub.storageService.myLegalIdentityKey.signWithECDSA(stx.txBits) } // TODO: similar classes occur in other places (NotaryProtocol), need to consolidate diff --git a/core/src/main/kotlin/com/r3corda/protocols/ResolveTransactionsProtocol.kt b/core/src/main/kotlin/com/r3corda/protocols/ResolveTransactionsProtocol.kt index 3ed8ceb66c..12be7a81b4 100644 --- a/core/src/main/kotlin/com/r3corda/protocols/ResolveTransactionsProtocol.kt +++ b/core/src/main/kotlin/com/r3corda/protocols/ResolveTransactionsProtocol.kt @@ -1,27 +1,35 @@ package com.r3corda.protocols import co.paralleluniverse.fibers.Suspendable -import com.r3corda.core.contracts.* +import com.r3corda.core.checkedAdd +import com.r3corda.core.contracts.LedgerTransaction +import com.r3corda.core.contracts.SignedTransaction +import com.r3corda.core.contracts.WireTransaction +import com.r3corda.core.contracts.toLedgerTransaction import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.SecureHash import com.r3corda.core.protocols.ProtocolLogic import java.util.* -// NB: This code is unit tested by TwoPartyTradeProtocolTests +// TODO: This code is currently unit tested by TwoPartyTradeProtocolTests, it should have its own tests. + +// TODO: It may be a clearer API if we make the primary c'tor private here, and only allow a single tx to be "resolved". /** - * This protocol fetches each transaction identified by the given hashes from either disk or network, along with all - * their dependencies, and verifies them together using a single [TransactionGroup]. If no exception is thrown, then - * all the transactions have been successfully verified and inserted into the local database. + * This protocol is used to verify the validity of a transaction by recursively checking the validity of all the + * dependencies. Once a transaction is checked it's inserted into local storage so it can be relayed and won't be + * checked again. * * A couple of constructors are provided that accept a single transaction. When these are used, the dependencies of that * transaction are resolved and then the transaction itself is verified. Again, if successful, the results are inserted * into the database as long as a [SignedTransaction] was provided. If only the [WireTransaction] form was provided - * then this isn't enough to put into the local database, so only the dependencies are inserted. This way to use the - * protocol is helpful when resolving and verifying a finished but partially signed transaction. + * then this isn't enough to put into the local database, so only the dependencies are checked and inserted. This way + * to use the protocol is helpful when resolving and verifying a finished but partially signed transaction. + * + * The protocol returns a list of verified [LedgerTransaction] objects, in a depth-first order. */ class ResolveTransactionsProtocol(private val txHashes: Set, - private val otherSide: Party) : ProtocolLogic() { + private val otherSide: Party) : ProtocolLogic>() { companion object { private fun dependencyIDs(wtx: WireTransaction) = wtx.inputs.map { it.txhash }.toSet() @@ -48,45 +56,49 @@ class ResolveTransactionsProtocol(private val txHashes: Set, } @Suspendable - override fun call(): Unit { - val toVerify = HashSet() - val alreadyVerified = HashSet() - val downloadedSignedTxns = ArrayList() + override fun call(): List { + val newTxns: Iterable = downloadDependencies(txHashes) - // This fills out toVerify, alreadyVerified (roots) and downloadedSignedTxns. - fetchDependenciesAndCheckSignatures(txHashes, toVerify, alreadyVerified, downloadedSignedTxns) + // For each transaction, verify it and insert it into the database. As we are iterating over them in a + // depth-first order, we should not encounter any verification failures due to missing data. If we fail + // half way through, it's no big deal, although it might result in us attempting to re-download data + // redundantly next time we attempt verification. + val result = ArrayList() - if (stx != null) { - // Check the signatures on the stx first. - toVerify += stx!!.verifyToLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments) - } else if (wtx != null) { - wtx!!.toLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments) + for (tx in newTxns) { + // Resolve to a LedgerTransaction and then run all contracts. + val ltx = tx.toLedgerTransaction(serviceHub) + ltx.verify() + serviceHub.recordTransactions(tx) + result += ltx } - // Run all the contracts and throw an exception if any of them reject. - TransactionGroup(toVerify, alreadyVerified).verify() + // If this protocol is resolving a specific transaction, make sure we have its attachments and then verify + // it as well, but don't insert to the database. Note that when we were given a SignedTransaction (stx != null) + // we *could* insert, because successful verification implies we have everything we need here, and it might + // be a clearer API if we do that. But for consistency with the other c'tor we currently do not. + stx?.let { + fetchMissingAttachments(listOf(it.tx)) + val ltx = it.toLedgerTransaction(serviceHub) + ltx.verify() + result += ltx + } + wtx?.let { + fetchMissingAttachments(listOf(it)) + val ltx = it.toLedgerTransaction(serviceHub) + ltx.verify() + result += ltx + } - // Now write all the transactions we just validated back to the database for next time, including - // signatures so we can serve up these transactions to other peers when we, in turn, send one that - // depends on them onto another peer. - // - // It may seem tempting to write transactions to the database as we receive them, instead of all at once - // here at the end. Doing it this way avoids cases where a transaction is in the database but its - // dependencies aren't, or an unvalidated and possibly broken tx is there. - serviceHub.recordTransactions(downloadedSignedTxns) + return result } override val topic: String get() = throw UnsupportedOperationException() @Suspendable - private fun fetchDependenciesAndCheckSignatures(depsToCheck: Set, - toVerify: HashSet, - alreadyVerified: HashSet, - downloadedSignedTxns: ArrayList) { - // Maintain a work queue of all hashes to load/download, initialised with our starting set. - // Then either fetch them from the database or request them from the other side. Look up the - // signatures against our identity database, filtering the transactions into 'already checked' - // and 'need to check' sets. + private fun downloadDependencies(depsToCheck: Set): List { + // Maintain a work queue of all hashes to load/download, initialised with our starting set. Then do a breadth + // first traversal across the dependency graph. // // TODO: This approach has two problems. Analyze and resolve them: // @@ -103,45 +115,49 @@ class ResolveTransactionsProtocol(private val txHashes: Set, val nextRequests = LinkedHashSet() // Keep things unique but ordered, for unit test stability. nextRequests.addAll(depsToCheck) + val resultQ = LinkedHashMap() var limitCounter = 0 while (nextRequests.isNotEmpty()) { - val (fromDisk, downloads) = subProtocol(FetchTransactionsProtocol(nextRequests, otherSide)) + // Don't re-download the same tx when we haven't verified it yet but it's referenced multiple times in the + // graph we're traversing. + val notAlreadyFetched = nextRequests.filterNot { it in resultQ }.toSet() nextRequests.clear() - // TODO: This could be done in parallel with other fetches for extra speed. - resolveMissingAttachments(downloads) + if (notAlreadyFetched.isEmpty()) // Done early. + break - // Resolve any legal identities from known public keys in the signatures. - val downloadedTxns = downloads.map { - it.verifyToLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments) - } + // Request the standalone transaction data (which may refer to things we don't yet have). + val downloads: List = subProtocol(FetchTransactionsProtocol(notAlreadyFetched, otherSide)).downloaded - // Do the same for transactions loaded from disk (i.e. we checked them previously). - val loadedTxns = fromDisk.map { - it.verifyToLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments) - } + fetchMissingAttachments(downloads.map { it.tx }) - toVerify.addAll(downloadedTxns) - alreadyVerified.addAll(loadedTxns) - downloadedSignedTxns.addAll(downloads) + for (stx in downloads) + check(resultQ.putIfAbsent(stx.id, stx) == null) // Assert checks the filter at the start. - // And now add all the input states to the work queue for database or remote resolution. - nextRequests.addAll(downloadedTxns.flatMap { it.inputs }.map { it.txhash }) + // Add all input states to the work queue. + val inputHashes = downloads.flatMap { it.tx.inputs }.map { it.txhash } + nextRequests.addAll(inputHashes) - // And loop around ... - // TODO: Figure out a more appropriate DOS limit here, 5000 is simply a guess. + // TODO: Figure out a more appropriate DOS limit here, 5000 is simply a very bad guess. // TODO: Unit test the DoS limit. - limitCounter += nextRequests.size + limitCounter = limitCounter checkedAdd nextRequests.size if (limitCounter > 5000) throw ExcessivelyLargeTransactionGraph() } + + return resultQ.values.reversed() } + /** + * Returns a list of all the dependencies of the given transactions, deepest first i.e. the last downloaded comes + * first in the returned list and thus doesn't have any unverified dependencies. + */ @Suspendable - private fun resolveMissingAttachments(downloads: List) { + private fun fetchMissingAttachments(downloads: List) { + // TODO: This could be done in parallel with other fetches for extra speed. val missingAttachments = downloads.flatMap { stx -> - stx.tx.attachments.filter { serviceHub.storageService.attachments.openAttachment(it) == null } + stx.attachments.filter { serviceHub.storageService.attachments.openAttachment(it) == null } } subProtocol(FetchAttachmentsProtocol(missingAttachments.toSet(), otherSide)) } diff --git a/core/src/main/kotlin/com/r3corda/protocols/TwoPartyDealProtocol.kt b/core/src/main/kotlin/com/r3corda/protocols/TwoPartyDealProtocol.kt index 727421be9e..4130267340 100644 --- a/core/src/main/kotlin/com/r3corda/protocols/TwoPartyDealProtocol.kt +++ b/core/src/main/kotlin/com/r3corda/protocols/TwoPartyDealProtocol.kt @@ -98,21 +98,21 @@ object TwoPartyDealProtocol { fun verifyPartialTransaction(untrustedPartialTX: UntrustworthyData): SignedTransaction { progressTracker.currentStep = VERIFYING - untrustedPartialTX.validate { + untrustedPartialTX.validate { stx -> progressTracker.nextStep() // Check that the tx proposed by the buyer is valid. - val missingSigs = it.verify(throwIfSignaturesAreMissing = false) + val missingSigs = stx.verifySignatures(throwIfSignaturesAreMissing = false) if (missingSigs != setOf(myKeyPair.public, notaryNode.identity.owningKey)) throw SignatureException("The set of missing signatures is not as expected: $missingSigs") - val wtx: WireTransaction = it.tx - logger.trace { "Received partially signed transaction: ${it.id}" } + val wtx: WireTransaction = stx.tx + logger.trace { "Received partially signed transaction: ${stx.id}" } - checkDependencies(it) + checkDependencies(stx) // This verifies that the transaction is contract-valid, even though it is missing signatures. - serviceHub.verifyTransaction(wtx.toLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments)) + wtx.toLedgerTransaction(serviceHub).verify() // There are all sorts of funny games a malicious secondary might play here, we should fix them: // @@ -124,7 +124,7 @@ object TwoPartyDealProtocol { // but the goal of this code is not to be fully secure (yet), but rather, just to find good ways to // express protocol state machines on top of the messaging layer. - return it + return stx } } @@ -226,7 +226,7 @@ object TwoPartyDealProtocol { logger.trace { "Got signatures from other party, verifying ... " } val fullySigned = stx + signatures.sellerSig + signatures.notarySig - fullySigned.verify() + fullySigned.verifySignatures() logger.trace { "Signatures received are valid. Deal transaction complete! :-)" } @@ -471,4 +471,4 @@ object TwoPartyDealProtocol { } } -} +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/r3corda/protocols/ValidatingNotaryProtocol.kt b/core/src/main/kotlin/com/r3corda/protocols/ValidatingNotaryProtocol.kt index 1845128786..e38067a464 100644 --- a/core/src/main/kotlin/com/r3corda/protocols/ValidatingNotaryProtocol.kt +++ b/core/src/main/kotlin/com/r3corda/protocols/ValidatingNotaryProtocol.kt @@ -23,11 +23,11 @@ class ValidatingNotaryProtocol(otherSide: Party, uniquenessProvider: UniquenessProvider) : NotaryProtocol.Service(otherSide, sessionIdForSend, sessionIdForReceive, timestampChecker, uniquenessProvider) { @Suspendable override fun beforeCommit(stx: SignedTransaction, reqIdentity: Party) { - val wtx = stx.tx try { checkSignatures(stx) - validateDependencies(reqIdentity, wtx) - checkContractValid(wtx) + val wtx = stx.tx + resolveTransaction(reqIdentity, wtx) + wtx.toLedgerTransaction(serviceHub).verify() } catch (e: Exception) { when (e) { is TransactionVerificationException, @@ -39,18 +39,13 @@ class ValidatingNotaryProtocol(otherSide: Party, private fun checkSignatures(stx: SignedTransaction) { val myKey = serviceHub.storageService.myLegalIdentity.owningKey - val missing = stx.verify(false) - myKey + val missing = stx.verifySignatures(throwIfSignaturesAreMissing = false) - myKey if (missing.isNotEmpty()) throw NotaryException(NotaryError.SignaturesMissing(missing.toList())) } - private fun checkContractValid(wtx: WireTransaction) { - val ltx = wtx.toLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments) - serviceHub.verifyTransaction(ltx) - } - @Suspendable - private fun validateDependencies(reqIdentity: Party, wtx: WireTransaction) { + private fun resolveTransaction(reqIdentity: Party, wtx: WireTransaction) { subProtocol(ResolveTransactionsProtocol(wtx, reqIdentity)) } -} +} \ No newline at end of file diff --git a/core/src/test/kotlin/com/r3corda/core/contracts/TransactionGroupTests.kt b/core/src/test/kotlin/com/r3corda/core/contracts/TransactionGroupTests.kt deleted file mode 100644 index 97562c648b..0000000000 --- a/core/src/test/kotlin/com/r3corda/core/contracts/TransactionGroupTests.kt +++ /dev/null @@ -1,194 +0,0 @@ -package com.r3corda.core.contracts - -import com.r3corda.core.crypto.Party -import com.r3corda.core.crypto.SecureHash -import com.r3corda.core.crypto.newSecureRandom -import com.r3corda.core.node.services.testing.MockStorageService -import com.r3corda.core.testing.* -import org.junit.Test -import java.security.PublicKey -import java.util.* -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertNotEquals - -val TEST_PROGRAM_ID = TransactionGroupTests.TestCash() - -class TransactionGroupTests { - val A_THOUSAND_POUNDS = TestCash.State(MINI_CORP.ref(1, 2, 3), 1000.POUNDS, MINI_CORP_PUBKEY) - - class TestCash : Contract { - override val legalContractReference = SecureHash.sha256("TestCash") - - override fun verify(tx: TransactionForContract) { - } - - data class State( - val deposit: PartyAndReference, - val amount: Amount, - override val owner: PublicKey) : OwnableState { - override val contract: Contract = TEST_PROGRAM_ID - override val participants: List - get() = listOf(owner) - - override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner)) - } - - interface Commands : CommandData { - class Move() : TypeOnlyCommandData(), Commands - data class Issue(val nonce: Long = newSecureRandom().nextLong()) : Commands - data class Exit(val amount: Amount) : Commands - } - } - - infix fun TestCash.State.`owned by`(owner: PublicKey) = copy(owner = owner) - infix fun TestCash.State.`with notary`(notary: Party) = TransactionState(this, notary) - - @Test - fun success() { - ledger { - unverifiedTransaction { - output("£1000") { A_THOUSAND_POUNDS } - } - - transaction { - input("£1000") - output("alice's £1000") { A_THOUSAND_POUNDS `owned by` ALICE_PUBKEY } - command(MINI_CORP_PUBKEY) { TestCash.Commands.Move() } - this.verifies() - } - - transaction { - input("alice's £1000") - command(ALICE_PUBKEY) { TestCash.Commands.Move() } - command(MINI_CORP_PUBKEY) { TestCash.Commands.Exit(1000.POUNDS) } - this.verifies() - } - - this.verifies() - } - } - - @Test - fun conflict() { - ledger { - val t = transaction { - output("cash") { A_THOUSAND_POUNDS } - command(MINI_CORP_PUBKEY) { TestCash.Commands.Issue() } - this.verifies() - } - - val conflict1 = transaction { - input("cash") - val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` BOB_PUBKEY - output { HALF } - output { HALF } - command(MINI_CORP_PUBKEY) { TestCash.Commands.Move() } - this.verifies() - } - - verifies() - - // Alice tries to double spend back to herself. - val conflict2 = transaction { - input("cash") - val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` ALICE_PUBKEY - output { HALF } - output { HALF } - command(MINI_CORP_PUBKEY) { TestCash.Commands.Move() } - this.verifies() - } - - assertNotEquals(conflict1, conflict2) - - val e = assertFailsWith(TransactionConflictException::class) { - verifies() - } - assertEquals(StateRef(t.id, 0), e.conflictRef) - assertEquals(setOf(conflict1.id, conflict2.id), setOf(e.tx1.id, e.tx2.id)) - } - } - - @Test - fun disconnected() { - // Check that if we have a transaction in the group that doesn't connect to anything else, it's rejected. - val tg = ledger { - transaction { - output("cash") { A_THOUSAND_POUNDS } - command(MINI_CORP_PUBKEY) { TestCash.Commands.Issue() } - this.verifies() - } - - transaction { - input("cash") - output { A_THOUSAND_POUNDS `owned by` BOB_PUBKEY } - this.verifies() - } - } - - val input = StateAndRef(A_THOUSAND_POUNDS `with notary` DUMMY_NOTARY, generateStateRef()) - tg.apply { - transaction { - assertFailsWith(TransactionResolutionException::class) { - input(input.ref) - } - this.verifies() - } - } - } - - @Test - fun duplicatedInputs() { - // Check that a transaction cannot refer to the same input more than once. - ledger { - unverifiedTransaction { - output("£1000") { A_THOUSAND_POUNDS } - } - - transaction { - input("£1000") - input("£1000") - output { A_THOUSAND_POUNDS.copy(amount = A_THOUSAND_POUNDS.amount * 2) } - command(MINI_CORP_PUBKEY) { TestCash.Commands.Move() } - this.verifies() - } - - assertFailsWith(TransactionConflictException::class) { - verifies() - } - } - } - - @Test - fun signGroup() { - ledger { - transaction { - output("£1000") { A_THOUSAND_POUNDS } - command(MINI_CORP_PUBKEY) { TestCash.Commands.Issue() } - this.verifies() - } - - transaction { - input("£1000") - output("alice's £1000") { A_THOUSAND_POUNDS `owned by` ALICE_PUBKEY } - command(MINI_CORP_PUBKEY) { TestCash.Commands.Move() } - this.verifies() - } - - transaction { - input("alice's £1000") - command(ALICE_PUBKEY) { TestCash.Commands.Move() } - command(MINI_CORP_PUBKEY) { TestCash.Commands.Exit(1000.POUNDS) } - this.verifies() - } - - val signedTxns: List = signAll() - - // Now go through the conversion -> verification path with them. - val ltxns = signedTxns.map { - it.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, MockStorageService().attachments) - }.toSet() - TransactionGroup(ltxns, emptySet()).verify() - } - } -} diff --git a/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt b/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt index e335db44a0..5da8cc4e11 100644 --- a/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt +++ b/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt @@ -3,7 +3,6 @@ package com.r3corda.core.serialization import com.r3corda.core.contracts.* import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.newSecureRandom -import com.r3corda.core.node.services.testing.MockStorageService import com.r3corda.core.seconds import com.r3corda.core.testing.* import org.junit.Before @@ -97,7 +96,7 @@ class TransactionSerializationTests { tx2.signWith(DUMMY_NOTARY_KEY) tx2.signWith(DUMMY_KEY_2) - signedTX.copy(sigs = tx2.toSignedTransaction().sigs).verify() + signedTX.copy(sigs = tx2.toSignedTransaction().sigs).verifySignatures() } } @@ -107,10 +106,6 @@ class TransactionSerializationTests { tx.signWith(DUMMY_KEY_1) tx.signWith(DUMMY_NOTARY_KEY) val stx = tx.toSignedTransaction() - val ltx = stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, MockStorageService().attachments) - assertEquals(tx.commands().map { it.value }, ltx.commands.map { it.value }) - assertEquals(tx.inputStates(), ltx.inputs) - assertEquals(tx.outputStates(), ltx.outputs) - assertEquals(TEST_TX_TIME, ltx.commands.getTimestampBy(DUMMY_NOTARY)!!.midpoint) + assertEquals(TEST_TX_TIME, (stx.tx.commands[1].value as TimestampCommand).midpoint) } } diff --git a/experimental/src/test/kotlin/com/r3corda/contracts/AccountReceivableTests.kt b/experimental/src/test/kotlin/com/r3corda/contracts/AccountReceivableTests.kt index e80be276c2..f9d5de3202 100644 --- a/experimental/src/test/kotlin/com/r3corda/contracts/AccountReceivableTests.kt +++ b/experimental/src/test/kotlin/com/r3corda/contracts/AccountReceivableTests.kt @@ -3,15 +3,14 @@ package com.r3corda.contracts import com.r3corda.core.contracts.DOLLARS import com.r3corda.core.contracts.LedgerTransaction import com.r3corda.core.contracts.`issued by` -import com.r3corda.core.contracts.verifyToLedgerTransaction -import com.r3corda.core.node.services.testing.MockStorageService +import com.r3corda.core.contracts.toLedgerTransaction +import com.r3corda.core.node.services.testing.MockServices import com.r3corda.core.seconds import com.r3corda.core.serialization.OpaqueBytes import com.r3corda.core.testing.* +import org.junit.Before import java.time.Instant import java.time.ZoneOffset -//import java.util.* -//import kotlin.test.fail /** * unit test cases that confirms the correct behavior of the AccountReceivable smart contract @@ -29,6 +28,8 @@ class AccountReceivableTests { val notary = DUMMY_NOTARY + lateinit var services: MockServices + val invoiceProperties = Invoice.InvoiceProperties( invoiceID = "123", seller = LocDataStructures.Company( @@ -67,6 +68,11 @@ class AccountReceivableTests { PAST, FUTURE } + @Before + fun setup() { + services = MockServices() + } + fun generateInvoiceIssueTxn(kind: WhatKind = WhatKind.FUTURE): LedgerTransaction { val genTX: LedgerTransaction = run { val pastProp = initialInvoiceState.props.copy(invoiceDate = @@ -83,8 +89,9 @@ class AccountReceivableTests { signWith(MINI_CORP_KEY) signWith(DUMMY_NOTARY_KEY) } - gtx.toSignedTransaction().verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, MockStorageService().attachments) + gtx.toSignedTransaction().toLedgerTransaction(services) } + genTX.verify() return genTX } diff --git a/experimental/src/test/kotlin/com/r3corda/contracts/BillOfLadingAgreementTests.kt b/experimental/src/test/kotlin/com/r3corda/contracts/BillOfLadingAgreementTests.kt index 4c0c169302..e22564b3e7 100644 --- a/experimental/src/test/kotlin/com/r3corda/contracts/BillOfLadingAgreementTests.kt +++ b/experimental/src/test/kotlin/com/r3corda/contracts/BillOfLadingAgreementTests.kt @@ -2,8 +2,9 @@ package com.r3corda.contracts import com.r3corda.core.contracts.* import com.r3corda.core.crypto.SecureHash -import com.r3corda.core.node.services.testing.MockStorageService +import com.r3corda.core.node.services.testing.MockServices import com.r3corda.core.testing.* +import org.junit.Before import org.junit.Test import java.time.Instant import java.time.LocalDate @@ -42,7 +43,12 @@ class BillOfLadingAgreementTests { props =pros ) - val attachments = MockStorageService().attachments + lateinit var services: MockServices + + @Before + fun setup() { + services = MockServices() + } //Generation method tests @@ -52,15 +58,14 @@ class BillOfLadingAgreementTests { signWith(ALICE_KEY) signWith(DUMMY_NOTARY_KEY) } - val stx = ptx.toSignedTransaction() - stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE,attachments) + ptx.toSignedTransaction().toLedgerTransaction(services).verify() } @Test(expected = IllegalStateException::class) fun issueGenerationMethod_Unsigned() { val ptx = BillOfLadingAgreement().generateIssue(Bill.owner, Bill.beneficiary, Bill.props, DUMMY_NOTARY) val stx = ptx.toSignedTransaction() - stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE,attachments) + ptx.toSignedTransaction().toLedgerTransaction(services).verify() } @Test(expected = IllegalStateException::class) @@ -68,9 +73,7 @@ class BillOfLadingAgreementTests { val ptx = BillOfLadingAgreement().generateIssue(Bill.owner, Bill.beneficiary, Bill.props, DUMMY_NOTARY).apply { signWith(BOB_KEY) } - val stx = ptx.toSignedTransaction() - stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE,attachments) - + ptx.toSignedTransaction().toLedgerTransaction(services).verify() } // @Test // TODO: Fix Test @@ -85,8 +88,7 @@ class BillOfLadingAgreementTests { ptx.signWith(MEGA_CORP_KEY) //Signed by owner ptx.signWith(BOB_KEY) //and beneficiary // ptx.signWith(CHARLIE_KEY) // ?????? - val stx = ptx.toSignedTransaction() - stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE,attachments) + ptx.toSignedTransaction().toLedgerTransaction(services).verify() } @Test(expected = IllegalStateException::class) @@ -98,8 +100,7 @@ class BillOfLadingAgreementTests { ) BillOfLadingAgreement().generateTransferAndEndorse(ptx,sr,CHARLIE_PUBKEY, CHARLIE) ptx.signWith(MEGA_CORP_KEY) //Signed by owner - val stx = ptx.toSignedTransaction() - stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE,attachments) + ptx.toSignedTransaction().toLedgerTransaction(services).verify() } @Test(expected = IllegalStateException::class) @@ -111,8 +112,7 @@ class BillOfLadingAgreementTests { ) BillOfLadingAgreement().generateTransferAndEndorse(ptx,sr,CHARLIE_PUBKEY, CHARLIE) ptx.signWith(BOB_KEY) //Signed by beneficiary - val stx = ptx.toSignedTransaction() - stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE,attachments) + ptx.toSignedTransaction().toLedgerTransaction(services).verify() } // @Test // TODO Fix Test @@ -124,8 +124,7 @@ class BillOfLadingAgreementTests { ) BillOfLadingAgreement().generateTransferPossession(ptx,sr,CHARLIE_PUBKEY) ptx.signWith(MEGA_CORP_KEY) //Signed by owner - val stx = ptx.toSignedTransaction() - stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE,attachments) + ptx.toSignedTransaction().toLedgerTransaction(services).verify() } @Test(expected = IllegalStateException::class) @@ -136,8 +135,7 @@ class BillOfLadingAgreementTests { StateRef(SecureHash.randomSHA256(), Random().nextInt(32)) ) BillOfLadingAgreement().generateTransferPossession(ptx,sr,CHARLIE_PUBKEY) - val stx = ptx.toSignedTransaction() - stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE,attachments) + ptx.toSignedTransaction().toLedgerTransaction(services).verify() } //Custom transaction tests diff --git a/experimental/src/test/kotlin/com/r3corda/contracts/LOCTests.kt b/experimental/src/test/kotlin/com/r3corda/contracts/LOCTests.kt index 3c1f7d8c78..4c0b8fca94 100644 --- a/experimental/src/test/kotlin/com/r3corda/contracts/LOCTests.kt +++ b/experimental/src/test/kotlin/com/r3corda/contracts/LOCTests.kt @@ -3,8 +3,10 @@ package com.r3corda.contracts import com.r3corda.contracts.asset.Cash import com.r3corda.core.contracts.* import com.r3corda.core.crypto.SecureHash +import com.r3corda.core.node.services.testing.MockServices import com.r3corda.core.serialization.OpaqueBytes import com.r3corda.core.testing.* +import org.junit.Before import org.junit.Test import java.time.Instant import java.time.LocalDate @@ -114,23 +116,26 @@ class LOCTests { ) ) + lateinit var services: MockServices + @Before + fun setup() { + services = MockServices() + } @Test fun issueSignedByBank() { - val ptx = LOC().generateIssue(LOCstate.beneficiaryPaid, LOCstate.issued, LOCstate.terminated, LOCstate.props, DUMMY_NOTARY).apply { + val ptx = LOC().generateIssue(LOCstate.beneficiaryPaid, true, LOCstate.terminated, LOCstate.props, DUMMY_NOTARY).apply { signWith(MEGA_CORP_KEY) signWith(DUMMY_NOTARY_KEY) } - val stx = ptx.toSignedTransaction() - stx.verify() + ptx.toSignedTransaction().toLedgerTransaction(services).verify() } @Test(expected = IllegalStateException::class) fun issueUnsigned() { val ptx = LOC().generateIssue(LOCstate.beneficiaryPaid, LOCstate.issued, LOCstate.terminated, LOCstate.props, DUMMY_NOTARY) - val stx = ptx.toSignedTransaction() - stx.verify() + ptx.toSignedTransaction().toLedgerTransaction(services).verify() } @Test(expected = IllegalStateException::class) @@ -138,9 +143,7 @@ class LOCTests { val ptx = LOC().generateIssue(LOCstate.beneficiaryPaid, LOCstate.issued, LOCstate.terminated, LOCstate.props, DUMMY_NOTARY).apply { signWith(BOB_KEY) } - val stx = ptx.toSignedTransaction() - stx.verify() - + ptx.toSignedTransaction().toLedgerTransaction(services).verify() } diff --git a/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt b/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt index 0beef77c22..9d9a61f38c 100644 --- a/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt @@ -257,11 +257,10 @@ class TwoPartyTradeProtocolTests { } val attachmentID = attachment(ByteArrayInputStream(stream.toByteArray())) - val issuer = MEGA_CORP.ref(1) - val bobsFakeCash = fillUpForBuyer(false, bobNode.keyManagement.freshKey().public, issuer).second + val bobsFakeCash = fillUpForBuyer(false, bobNode.keyManagement.freshKey().public).second val bobsSignedTxns = insertFakeTransactions(bobsFakeCash, bobNode.services) val alicesFakePaper = fillUpForSeller(false, aliceNode.storage.myLegalIdentity.owningKey, - 1200.DOLLARS `issued by` issuer, notaryNode.info.identity, attachmentID).second + 1200.DOLLARS `issued by` DUMMY_CASH_ISSUER, notaryNode.info.identity, attachmentID).second val alicesSignedTxns = insertFakeTransactions(alicesFakePaper, aliceNode.services, aliceNode.storage.myLegalIdentityKey) val buyerSessionID = random63BitValue() @@ -326,9 +325,11 @@ class TwoPartyTradeProtocolTests { TxRecord.Get(bobsFakeCash[0].id), // Bob answers with the transactions that are now all verifiable, as Alice bottomed out. // Bob's transactions are valid, so she commits to the database - TxRecord.Add(bobsSignedTxns[bobsFakeCash[1].id]!!), - TxRecord.Add(bobsSignedTxns[bobsFakeCash[2].id]!!), TxRecord.Add(bobsSignedTxns[bobsFakeCash[0].id]!!), + TxRecord.Get(bobsFakeCash[0].id), // Verify + TxRecord.Add(bobsSignedTxns[bobsFakeCash[2].id]!!), + TxRecord.Get(bobsFakeCash[0].id), // Verify + TxRecord.Add(bobsSignedTxns[bobsFakeCash[1].id]!!), // Now she verifies the transaction is contract-valid (not signature valid) which means // looking up the states again. TxRecord.Get(bobsFakeCash[1].id), @@ -413,7 +414,7 @@ class TwoPartyTradeProtocolTests { wtxToSign: List, services: ServiceHub, vararg extraKeys: KeyPair): Map { - val signed: List = signAll(wtxToSign, extraKeys.toList()) + val signed: List = signAll(wtxToSign, extraKeys.toList() + DUMMY_CASH_ISSUER_KEY) services.recordTransactions(signed) val validatedTransactions = services.storageService.validatedTransactions if (validatedTransactions is RecordingTransactionStorage) { @@ -425,16 +426,15 @@ class TwoPartyTradeProtocolTests { private fun LedgerDSL.fillUpForBuyer( withError: Boolean, owner: PublicKey = BOB_PUBKEY, - issuer: PartyAndReference = MEGA_CORP.ref(1)): Pair> { + issuer: PartyAndReference = DUMMY_CASH_ISSUER): Pair> { // Bob (Buyer) has some cash he got from the Bank of Elbonia, Alice (Seller) has some commercial paper she // wants to sell to Bob. - val eb1 = transaction { // Issued money to itself. output("elbonian money 1") { 800.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY } output("elbonian money 2") { 1000.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY } if (!withError) - command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() } + command(DUMMY_CASH_ISSUER_KEY.public) { Cash.Commands.Issue() } timestamp(TEST_TX_TIME) if (withError) { this.fails() 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 4582b89fa3..6742e64e8d 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/WalletWithCashTest.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/WalletWithCashTest.kt @@ -7,7 +7,6 @@ import com.r3corda.contracts.testing.fillWithSomeTestCash import com.r3corda.core.contracts.* import com.r3corda.core.crypto.SecureHash import com.r3corda.core.node.services.WalletService -import com.r3corda.core.node.services.testing.MockStorageService import com.r3corda.core.node.services.testing.MockServices import com.r3corda.core.testing.* import com.r3corda.core.utilities.BriefLogFormatter @@ -70,7 +69,7 @@ class WalletWithCashTest { Cash().generateIssue(this, 100.DOLLARS `issued by` MEGA_CORP.ref(1), freshKey.public, DUMMY_NOTARY) signWith(MEGA_CORP_KEY) }.toSignedTransaction() - val myOutput = usefulTX.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, MockStorageService().attachments).outRef(0) + val myOutput = usefulTX.toLedgerTransaction(services).outRef(0) // A tx that spends our money. val spendTX = TransactionType.General.Builder().apply {