diff --git a/core/src/main/kotlin/core/TransactionVerification.kt b/core/src/main/kotlin/core/TransactionVerification.kt index af3f6e0abc..5f24af239d 100644 --- a/core/src/main/kotlin/core/TransactionVerification.kt +++ b/core/src/main/kotlin/core/TransactionVerification.kt @@ -14,6 +14,8 @@ import java.util.* class TransactionResolutionException(val hash: SecureHash) : Exception() class TransactionConflictException(val conflictRef: StateRef, val tx1: LedgerTransaction, val tx2: LedgerTransaction) : Exception() +// 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 @@ -49,7 +51,7 @@ class TransactionGroup(val transactions: Set, val nonVerified // Look up the output in that transaction by index. inputs.add(ltx.outputs[ref.index]) } - resolved.add(TransactionForVerification(inputs, tx.outputs, tx.commands, tx.hash)) + resolved.add(TransactionForVerification(inputs, tx.outputs, tx.attachments, tx.commands, tx.hash)) } for (tx in resolved) @@ -62,12 +64,17 @@ class TransactionGroup(val transactions: Set, val nonVerified /** A transaction in fully resolved and sig-checked form, ready for passing as input to a verification function. */ data class TransactionForVerification(val inStates: List, val outStates: List, + val attachments: List, val commands: List>, val origHash: SecureHash) { override fun hashCode() = origHash.hashCode() override fun equals(other: Any?) = other is TransactionForVerification && other.origHash == origHash /** + * Runs the contracts for this transaction. + * + * TODO: Move this out of the core data structure definitions, once unit tests are more cleanly separated. + * * @throws TransactionVerificationException if a contract throws an exception, the original is in the cause field * @throws IllegalStateException if a state refers to an unknown contract. */ @@ -77,6 +84,7 @@ data class TransactionForVerification(val inStates: List, // throws an exception, the entire transaction is invalid. val programHashes = (inStates.map { it.programRef } + outStates.map { it.programRef }).toSet() for (hash in programHashes) { + // TODO: Change this interface to ensure that attachment JARs are put on the classpath before execution. val program: Contract = programMap[hash] try { program.verify(this) diff --git a/core/src/main/kotlin/core/Transactions.kt b/core/src/main/kotlin/core/Transactions.kt index b53786b2e1..e895b78e3c 100644 --- a/core/src/main/kotlin/core/Transactions.kt +++ b/core/src/main/kotlin/core/Transactions.kt @@ -74,14 +74,6 @@ data class WireTransaction(val inputs: List, } } - fun toLedgerTransaction(identityService: IdentityService): LedgerTransaction { - val authenticatedArgs = commands.map { - val institutions = it.pubkeys.mapNotNull { pk -> identityService.partyFromKey(pk) } - AuthenticatedObject(it.pubkeys, institutions, it.data) - } - return LedgerTransaction(inputs, attachments, outputs, authenticatedArgs, id) - } - /** Serialises and returns this transaction as a [SignedTransaction] with no signatures attached. */ fun toSignedTransaction(withSigs: List): SignedTransaction { return SignedTransaction(serialized, withSigs) @@ -154,15 +146,6 @@ data class SignedTransaction(val txBits: SerializedBytes, return missing } - /** - * 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. - */ - fun verifyToLedgerTransaction(identityService: IdentityService): LedgerTransaction { - verify() - return tx.toLedgerTransaction(identityService) - } - /** Returns the same transaction but with an additional (unchecked) signature */ fun withAdditionalSignature(sig: DigitalSignature.WithKey) = copy(sigs = sigs + sig) @@ -308,12 +291,14 @@ class TransactionBuilder(private val inputs: MutableList = arrayListOf * 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, - /** A list of [Attachment] ids that need to be available for this transaction to verify. */ - val attachments: List, + /** A list of [Attachment] objects identified by the transaction that are needed for this transaction to verify. */ + val attachments: 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. */ @@ -325,7 +310,7 @@ data class LedgerTransaction( fun outRef(index: Int) = StateAndRef(outputs[index] as T, StateRef(hash, index)) fun toWireTransaction(): WireTransaction { - val wtx = WireTransaction(inputs, attachments, outputs, commands.map { Command(it.value, it.signers) }) + val wtx = WireTransaction(inputs, attachments.map { it.id }, outputs, commands.map { Command(it.value, it.signers) }) check(wtx.serialize().hash == hash) return wtx } diff --git a/src/main/kotlin/contracts/protocols/ResolveTransactionsProtocol.kt b/src/main/kotlin/contracts/protocols/ResolveTransactionsProtocol.kt index b409bfc684..0b5029a23b 100644 --- a/src/main/kotlin/contracts/protocols/ResolveTransactionsProtocol.kt +++ b/src/main/kotlin/contracts/protocols/ResolveTransactionsProtocol.kt @@ -9,10 +9,7 @@ package contracts.protocols import co.paralleluniverse.fibers.Suspendable -import core.LedgerTransaction -import core.SignedTransaction -import core.TransactionGroup -import core.WireTransaction +import core.* import core.crypto.SecureHash import core.messaging.SingleMessageRecipient import core.protocols.ProtocolLogic @@ -63,9 +60,9 @@ class ResolveTransactionsProtocol(private val txHashes: Set, if (stx != null) { // Check the signatures on the stx first. - toVerify += stx!!.verifyToLedgerTransaction(serviceHub.identityService) + toVerify += stx!!.verifyToLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments) } else if (wtx != null) { - wtx!!.toLedgerTransaction(serviceHub.identityService) + wtx!!.toLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments) } // Run all the contracts and throw an exception if any of them reject. @@ -116,10 +113,14 @@ class ResolveTransactionsProtocol(private val txHashes: Set, resolveMissingAttachments(downloads) // Resolve any legal identities from known public keys in the signatures. - val downloadedTxns = downloads.map { it.verifyToLedgerTransaction(serviceHub.identityService) } + val downloadedTxns = downloads.map { + it.verifyToLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments) + } // Do the same for transactions loaded from disk (i.e. we checked them previously). - val loadedTxns = fromDisk.map { it.verifyToLedgerTransaction(serviceHub.identityService) } + val loadedTxns = fromDisk.map { + it.verifyToLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments) + } toVerify.addAll(downloadedTxns) alreadyVerified.addAll(loadedTxns) diff --git a/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt b/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt index d368e7ef95..848dbaa33a 100644 --- a/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt +++ b/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt @@ -144,7 +144,7 @@ object TwoPartyTradeProtocol { checkDependencies(it) // This verifies that the transaction is contract-valid, even though it is missing signatures. - serviceHub.verifyTransaction(wtx.toLedgerTransaction(serviceHub.identityService)) + serviceHub.verifyTransaction(wtx.toLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments)) if (wtx.outputs.sumCashBy(myKeyPair.public) != price) throw IllegalArgumentException("Transaction is not sending us the right amounnt of cash") diff --git a/src/main/kotlin/core/Services.kt b/src/main/kotlin/core/Services.kt index fbd77069b5..b64c3eef54 100644 --- a/src/main/kotlin/core/Services.kt +++ b/src/main/kotlin/core/Services.kt @@ -164,7 +164,7 @@ interface ServiceHub { val dependencies = ltx.inputs.map { storageService.validatedTransactions[it.txhash] ?: throw TransactionResolutionException(it.txhash) } - val ltxns = dependencies.map { it.verifyToLedgerTransaction(identityService) } + val ltxns = dependencies.map { it.verifyToLedgerTransaction(identityService, storageService.attachments) } TransactionGroup(setOf(ltx), ltxns.toSet()).verify(storageService.contractPrograms) } } diff --git a/src/main/kotlin/core/TransactionTools.kt b/src/main/kotlin/core/TransactionTools.kt new file mode 100644 index 0000000000..a0b3d5dbe8 --- /dev/null +++ b/src/main/kotlin/core/TransactionTools.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members + * pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms + * set forth therein. + * + * All other rights reserved. + */ + +package core + +import java.io.FileNotFoundException + +/** + * Looks up identities and attachments from storage to generate a [LedgerTransaction]. + * + * @throws FileNotFoundException if a required transaction was not found in storage. + */ +fun WireTransaction.toLedgerTransaction(identityService: IdentityService, + attachmentStorage: AttachmentStorage): LedgerTransaction { + val authenticatedArgs = commands.map { + val institutions = it.pubkeys.mapNotNull { pk -> identityService.partyFromKey(pk) } + AuthenticatedObject(it.pubkeys, institutions, it.data) + } + val attachments = attachments.map { + attachmentStorage.openAttachment(it) ?: throw FileNotFoundException(it.toString()) + } + return LedgerTransaction(inputs, attachments, outputs, authenticatedArgs, id) +} + +/** + * 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. + */ +fun SignedTransaction.verifyToLedgerTransaction(identityService: IdentityService, + attachmentStorage: AttachmentStorage): LedgerTransaction { + verify() + return tx.toLedgerTransaction(identityService, attachmentStorage) +} diff --git a/src/test/kotlin/contracts/CommercialPaperTests.kt b/src/test/kotlin/contracts/CommercialPaperTests.kt index 9c409f6d7f..9f6b3a8a2c 100644 --- a/src/test/kotlin/contracts/CommercialPaperTests.kt +++ b/src/test/kotlin/contracts/CommercialPaperTests.kt @@ -63,6 +63,8 @@ class CommercialPaperTestsGeneric { @Parameterized.Parameter lateinit var thisTest: ICommercialPaperTestTemplate + val attachments = MockStorageService().attachments + @Test fun ok() { trade().verify() @@ -176,7 +178,7 @@ class CommercialPaperTestsGeneric { timestamp(DUMMY_TIMESTAMPER) } val stx = ptx.toSignedTransaction() - stx.verifyToLedgerTransaction(MockIdentityService) + stx.verifyToLedgerTransaction(MockIdentityService, attachments) } val (alicesWalletTX, alicesWallet) = cashOutputsToWallet( @@ -193,7 +195,7 @@ class CommercialPaperTestsGeneric { ptx.signWith(MINI_CORP_KEY) ptx.signWith(ALICE_KEY) val stx = ptx.toSignedTransaction() - stx.verifyToLedgerTransaction(MockIdentityService) + stx.verifyToLedgerTransaction(MockIdentityService, attachments) } // Won't be validated. @@ -209,7 +211,7 @@ class CommercialPaperTestsGeneric { ptx.signWith(ALICE_KEY) ptx.signWith(MINI_CORP_KEY) ptx.timestamp(DUMMY_TIMESTAMPER) - return ptx.toSignedTransaction().verifyToLedgerTransaction(MockIdentityService) + return ptx.toSignedTransaction().verifyToLedgerTransaction(MockIdentityService, attachments) } val tooEarlyRedemption = makeRedeemTX(TEST_TX_TIME + 10.days) diff --git a/src/test/kotlin/contracts/CrowdFundTests.kt b/src/test/kotlin/contracts/CrowdFundTests.kt index 3e8334f6f0..f07440d5c7 100644 --- a/src/test/kotlin/contracts/CrowdFundTests.kt +++ b/src/test/kotlin/contracts/CrowdFundTests.kt @@ -29,6 +29,8 @@ class CrowdFundTests { pledges = ArrayList() ) + val attachments = MockStorageService().attachments + @Test fun `key mismatch at issue`() { transactionGroup { @@ -114,7 +116,7 @@ class CrowdFundTests { timestamp(DUMMY_TIMESTAMPER) } val stx = ptx.toSignedTransaction() - stx.verifyToLedgerTransaction(MockIdentityService) + stx.verifyToLedgerTransaction(MockIdentityService, attachments) } // let's give Alice some funds that she can invest @@ -134,7 +136,7 @@ class CrowdFundTests { ptx.timestamp(DUMMY_TIMESTAMPER) val stx = ptx.toSignedTransaction() // this verify passes - the transaction contains an output cash, necessary to verify the fund command - stx.verifyToLedgerTransaction(MockIdentityService) + stx.verifyToLedgerTransaction(MockIdentityService, attachments) } // Won't be validated. @@ -150,7 +152,7 @@ class CrowdFundTests { ptx.signWith(MINI_CORP_KEY) ptx.timestamp(DUMMY_TIMESTAMPER) val stx = ptx.toSignedTransaction() - return stx.verifyToLedgerTransaction(MockIdentityService) + return stx.verifyToLedgerTransaction(MockIdentityService, attachments) } val tooEarlyClose = makeFundedTX(TEST_TX_TIME + 6.days) diff --git a/src/test/kotlin/core/TransactionGroupTests.kt b/src/test/kotlin/core/TransactionGroupTests.kt index a43fdb452d..81746777eb 100644 --- a/src/test/kotlin/core/TransactionGroupTests.kt +++ b/src/test/kotlin/core/TransactionGroupTests.kt @@ -152,7 +152,7 @@ class TransactionGroupTests { }.signAll() // Now go through the conversion -> verification path with them. - val ltxns = signedTxns.map { it.verifyToLedgerTransaction(MockIdentityService) }.toSet() + val ltxns = signedTxns.map { it.verifyToLedgerTransaction(MockIdentityService, MockStorageService().attachments) }.toSet() TransactionGroup(ltxns, emptySet()).verify(MockContractFactory) } } \ No newline at end of file diff --git a/src/test/kotlin/core/node/NodeWalletServiceTest.kt b/src/test/kotlin/core/node/NodeWalletServiceTest.kt index 5c697f21ef..5f23698431 100644 --- a/src/test/kotlin/core/node/NodeWalletServiceTest.kt +++ b/src/test/kotlin/core/node/NodeWalletServiceTest.kt @@ -63,7 +63,7 @@ class NodeWalletServiceTest { Cash().generateIssue(this, 100.DOLLARS, MEGA_CORP.ref(1), freshKey.public) signWith(MEGA_CORP_KEY) }.toSignedTransaction() - val myOutput = usefulTX.verifyToLedgerTransaction(MockIdentityService).outRef(0) + val myOutput = usefulTX.verifyToLedgerTransaction(MockIdentityService, MockStorageService().attachments).outRef(0) // A tx that spends our money. val spendTX = TransactionBuilder().apply { diff --git a/src/test/kotlin/core/serialization/TransactionSerializationTests.kt b/src/test/kotlin/core/serialization/TransactionSerializationTests.kt index 58179a2e8e..42eb2ac5f4 100644 --- a/src/test/kotlin/core/serialization/TransactionSerializationTests.kt +++ b/src/test/kotlin/core/serialization/TransactionSerializationTests.kt @@ -93,7 +93,7 @@ class TransactionSerializationTests { tx.timestamp(DUMMY_TIMESTAMPER) tx.signWith(TestUtils.keypair) val stx = tx.toSignedTransaction() - val ltx = stx.verifyToLedgerTransaction(MockIdentityService) + val ltx = stx.verifyToLedgerTransaction(MockIdentityService, MockStorageService().attachments) assertEquals(tx.commands().map { it.data }, ltx.commands.map { it.value }) assertEquals(tx.inputStates(), ltx.inputs) assertEquals(tx.outputStates(), ltx.outputs) diff --git a/src/test/kotlin/core/testutils/TestUtils.kt b/src/test/kotlin/core/testutils/TestUtils.kt index b81e681a0e..e7167d1e1d 100644 --- a/src/test/kotlin/core/testutils/TestUtils.kt +++ b/src/test/kotlin/core/testutils/TestUtils.kt @@ -150,7 +150,7 @@ open class TransactionForTest : AbstractTransactionForTest() { protected fun run(time: Instant) { val cmds = commandsToAuthenticatedObjects() - val tx = TransactionForVerification(inStates, outStates.map { it.state }, cmds, SecureHash.randomSHA256()) + val tx = TransactionForVerification(inStates, outStates.map { it.state }, emptyList(), cmds, SecureHash.randomSHA256()) tx.verify(MockContractFactory) } @@ -299,8 +299,8 @@ class TransactionGroupDSL(private val stateType: Class) { fun transactionGroup(body: TransactionGroupDSL.() -> Unit) {} fun toTransactionGroup() = TransactionGroup( - txns.map { it.toLedgerTransaction(MockIdentityService) }.toSet(), - rootTxns.map { it.toLedgerTransaction(MockIdentityService) }.toSet() + txns.map { it.toLedgerTransaction(MockIdentityService, MockStorageService().attachments) }.toSet(), + rootTxns.map { it.toLedgerTransaction(MockIdentityService, MockStorageService().attachments) }.toSet() ) class Failed(val index: Int, cause: Throwable) : Exception("Transaction $index didn't verify", cause)