From 6cb86ab840123b133516a9e0e24cc7bddb33b9b1 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Wed, 2 Mar 2016 15:35:46 +0100 Subject: [PATCH] Plumb attachments through to the contract verify functions (no contract uses them yet). The right way to do this is probably to put the contracts onto the classpath before execution of the contract. However, this interacts closely with the sandboxing work, which isn't yet started, so for now this will do. --- .../kotlin/core/TransactionVerification.kt | 10 ++++- core/src/main/kotlin/core/Transactions.kt | 25 +++--------- .../protocols/ResolveTransactionsProtocol.kt | 17 +++++---- .../protocols/TwoPartyTradeProtocol.kt | 2 +- src/main/kotlin/core/Services.kt | 2 +- src/main/kotlin/core/TransactionTools.kt | 38 +++++++++++++++++++ .../kotlin/contracts/CommercialPaperTests.kt | 8 ++-- src/test/kotlin/contracts/CrowdFundTests.kt | 8 ++-- src/test/kotlin/core/TransactionGroupTests.kt | 2 +- .../kotlin/core/node/NodeWalletServiceTest.kt | 2 +- .../TransactionSerializationTests.kt | 2 +- src/test/kotlin/core/testutils/TestUtils.kt | 6 +-- 12 files changed, 79 insertions(+), 43 deletions(-) create mode 100644 src/main/kotlin/core/TransactionTools.kt 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)