Merged in mike-ledgertx-refactoring (pull request #264)

Refactor the core transaction types
This commit is contained in:
Mike Hearn
2016-08-08 18:02:32 +02:00
24 changed files with 349 additions and 615 deletions

View File

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

View File

@ -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<PublicKey> {
fun verifySigners(tx: LedgerTransaction): Set<PublicKey> {
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<PublicKey>
abstract fun getRequiredSigners(tx: LedgerTransaction): Set<PublicKey>
/** 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<PublicKey> {
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<PublicKey> {
val participantKeys = tx.inputs.flatMap { it.data.participants }.toSet()
return participantKeys
}
override fun getRequiredSigners(tx: LedgerTransaction) = tx.inputs.flatMap { it.state.data.participants }.toSet()
}
}

View File

@ -8,76 +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<LedgerTransaction>, val nonVerifiedRoots: Set<LedgerTransaction>) {
/**
* Verifies the group and returns the set of resolved transactions.
*/
fun verify(): Set<TransactionForVerification> {
// 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<SecureHash, List<LedgerTransaction>> = (transactions + nonVerifiedRoots).groupBy { it.id }
val refToConsumingTXMap = hashMapOf<StateRef, LedgerTransaction>()
val resolved = HashSet<TransactionForVerification>(transactions.size)
for (tx in transactions) {
val inputs = ArrayList<TransactionState<ContractState>>(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<TransactionState<ContractState>>,
val outputs: List<TransactionState<ContractState>>,
val attachments: List<Attachment>,
val commands: List<AuthenticatedObject<CommandData>>,
val origHash: SecureHash,
val signers: List<PublicKey>,
val type: TransactionType) {
override fun hashCode() = origHash.hashCode()
override fun equals(other: Any?) = other is TransactionForVerification && other.origHash == origHash
/**
* Verifies that the transaction is valid by running type-specific validation logic.
*
* TODO: Move this out of the core data structure definitions, once unit tests are more cleanly separated.
*
* @throws TransactionVerificationException if validation logic fails or if a contract throws an exception
* (the original is in the cause field).
*/
@Throws(TransactionVerificationException::class)
fun verify() = type.verify(this)
fun toTransactionForContract() = TransactionForContract(inputs.map { it.data }, outputs.map { it.data },
attachments, commands, origHash, inputs.map { it.notary }.singleOrNull())
}
/**
* A transaction to be passed as input to a contract verification function. Defines helper methods to
* simplify verification logic in contracts.
@ -171,14 +101,16 @@ data class TransactionForContract(val inputs: List<ContractState>,
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<PublicKey>) : 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<PublicKey>) : 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)
}

View File

@ -20,24 +20,27 @@ import java.security.SignatureException
* SignedTransaction wraps a serialized WireTransaction. It contains one or more signatures, each one for
* 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!).
* of a WireTransaction, therefore if you are storing data keyed by WT hash be aware that multiple different STs may
* 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.
*
* 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<Command>. 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.
* - Resolving the input states and loading them into memory.
* - Doing some basic key lookups on WireCommand to see if any keys are from a recognised party, thus converting the
* WireCommand objects into AuthenticatedObject<Command>. 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 output states.
*
* All the above refer to inputs using a (txhash, output index) pair.
*
* TransactionForVerification is the same as LedgerTransaction but with the input states looked up from a local
* database and replaced with the real objects. Likewise, attachments are fully resolved at this point.
* TFV is the form that is finally fed into the contracts.
* There is also TransactionForContract, which is a lightly red-acted form of LedgerTransaction that's fed into the
* contract's verify function. It may be removed in future.
*/
/** Transaction ready for serialisation, without any signatures attached. */
@ -73,7 +76,7 @@ data class WireTransaction(val inputs: List<StateRef>,
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 +100,6 @@ data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
/** 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 +107,12 @@ data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
*
* @throws SignatureException if a signature is invalid, does not match or if any signature is missing.
*/
fun verify(throwIfSignaturesAreMissing: Boolean = true): Set<PublicKey> {
verifySignatures()
fun verifySignatures(throwIfSignaturesAreMissing: Boolean = true): Set<PublicKey> {
// 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() }}")
@ -144,7 +138,7 @@ data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
/**
* Returns the set of missing signatures - a signature must be present for each signer public key.
*/
fun getMissingSignatures(): Set<PublicKey> {
private fun getMissingSignatures(): Set<PublicKey> {
val requiredKeys = tx.signers.toSet()
val sigKeys = sigs.map { it.by }.toSet()
@ -157,12 +151,10 @@ data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
* 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>,
val inputs: List<StateAndRef<*>>,
/** The states that will be generated by the execution of this transaction. */
val outputs: List<TransactionState<*>>,
/** Arbitrary data passed to the program of each input state. */
@ -171,9 +163,28 @@ data class LedgerTransaction(
val attachments: List<Attachment>,
/** 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<PublicKey>,
val type: TransactionType
) : NamedByHash {
@Suppress("UNCHECKED_CAST")
fun <T : ContractState> outRef(index: Int) = StateAndRef(outputs[index] as TransactionState<T>, 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)
}

View File

@ -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 <T : Any> invokeProtocolAsync(logicType: Class<out ProtocolLogic<T>>, vararg args: Any?): ListenableFuture<T>
}
}

View File

@ -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<ContractState>(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 <reified S : ContractState> resolveStateRef(stateRef: StateRef): TransactionState<S> {
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 ?: "<unknown>"
throw VerifiesFailed(transactionName, exception)
}
return EnforceVerifyOrFail.Token
}
override fun <S : ContractState> retrieveOutputStateAndRef(clazz: Class<S>, label: String): StateAndRef<S> {

View File

@ -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<T> {
val response = Result.noError(mySignature)
val swapSignatures = sendAndReceive<List<DigitalSignature.WithKey>>(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<T> {
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<T> {
@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

View File

@ -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<SecureHash>,
private val otherSide: Party) : ProtocolLogic<Unit>() {
private val otherSide: Party) : ProtocolLogic<List<LedgerTransaction>>() {
companion object {
private fun dependencyIDs(wtx: WireTransaction) = wtx.inputs.map { it.txhash }.toSet()
@ -48,45 +56,46 @@ class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
}
@Suspendable
override fun call(): Unit {
val toVerify = HashSet<LedgerTransaction>()
val alreadyVerified = HashSet<LedgerTransaction>()
val downloadedSignedTxns = ArrayList<SignedTransaction>()
override fun call(): List<LedgerTransaction> {
val newTxns: Iterable<SignedTransaction> = 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<LedgerTransaction>()
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()
// 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.
// 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.
//
// 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)
// If 'stx' is set, then 'wtx' is the contents (from the c'tor).
stx?.verifySignatures()
wtx?.let {
fetchMissingAttachments(listOf(it))
val ltx = it.toLedgerTransaction(serviceHub)
ltx.verify()
result += ltx
}
return result
}
override val topic: String get() = throw UnsupportedOperationException()
@Suspendable
private fun fetchDependenciesAndCheckSignatures(depsToCheck: Set<SecureHash>,
toVerify: HashSet<LedgerTransaction>,
alreadyVerified: HashSet<LedgerTransaction>,
downloadedSignedTxns: ArrayList<SignedTransaction>) {
// 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<SecureHash>): List<SignedTransaction> {
// 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 +112,49 @@ class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
val nextRequests = LinkedHashSet<SecureHash>() // Keep things unique but ordered, for unit test stability.
nextRequests.addAll(depsToCheck)
val resultQ = LinkedHashMap<SecureHash, SignedTransaction>()
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<SignedTransaction> = 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<SignedTransaction>) {
val missingAttachments = downloads.flatMap { stx ->
stx.tx.attachments.filter { serviceHub.storageService.attachments.openAttachment(it) == null }
private fun fetchMissingAttachments(downloads: List<WireTransaction>) {
// TODO: This could be done in parallel with other fetches for extra speed.
val missingAttachments = downloads.flatMap { wtx ->
wtx.attachments.filter { serviceHub.storageService.attachments.openAttachment(it) == null }
}
subProtocol(FetchAttachmentsProtocol(missingAttachments.toSet(), otherSide))
}

View File

@ -98,21 +98,21 @@ object TwoPartyDealProtocol {
fun verifyPartialTransaction(untrustedPartialTX: UntrustworthyData<SignedTransaction>): 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 {
}
}
}
}

View File

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

View File

@ -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<Currency>,
override val owner: PublicKey) : OwnableState {
override val contract: Contract = TEST_PROGRAM_ID
override val participants: List<PublicKey>
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<Currency>) : 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<SignedTransaction> = 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()
}
}
}

View File

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