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.
This commit is contained in:
Mike Hearn 2016-03-02 15:35:46 +01:00
parent 1123c28f02
commit 6cb86ab840
12 changed files with 79 additions and 43 deletions

View File

@ -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<LedgerTransaction>, 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<LedgerTransaction>, 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<ContractState>,
val outStates: List<ContractState>,
val attachments: List<Attachment>,
val commands: List<AuthenticatedObject<CommandData>>,
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<ContractState>,
// 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)

View File

@ -74,14 +74,6 @@ data class WireTransaction(val inputs: List<StateRef>,
}
}
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<DigitalSignature.WithKey>): SignedTransaction {
return SignedTransaction(serialized, withSigs)
@ -154,15 +146,6 @@ data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
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<StateRef> = 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<StateRef>,
/** A list of [Attachment] ids that need to be available for this transaction to verify. */
val attachments: List<SecureHash>,
/** A list of [Attachment] objects identified by the transaction that are needed for this transaction to verify. */
val attachments: List<Attachment>,
/** The states that will be generated by the execution of this transaction. */
val outputs: List<ContractState>,
/** Arbitrary data passed to the program of each input state. */
@ -325,7 +310,7 @@ data class LedgerTransaction(
fun <T : ContractState> 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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,6 +29,8 @@ class CrowdFundTests {
pledges = ArrayList<CrowdFund.Pledge>()
)
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)

View File

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

View File

@ -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<Cash.State>(0)
val myOutput = usefulTX.verifyToLedgerTransaction(MockIdentityService, MockStorageService().attachments).outRef<Cash.State>(0)
// A tx that spends our money.
val spendTX = TransactionBuilder().apply {

View File

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

View File

@ -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<T : ContractState>(private val stateType: Class<T>) {
fun transactionGroup(body: TransactionGroupDSL<T>.() -> 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)