mirror of
https://github.com/corda/corda.git
synced 2025-02-01 00:45:59 +00:00
Fix the serialisability of TransactionVerificationException and subtypes. (#2695)
Add KDocs for the exceptions in this file, and deprecate exceptions that aren't actually used.
This commit is contained in:
parent
bf712a893e
commit
3985211352
@ -7,48 +7,129 @@ import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.NonEmptySet
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
* The node asked a remote peer for the transaction identified by [hash] because it is a dependency of a transaction
|
||||
* being resolved, but the remote peer would not provide it.
|
||||
*
|
||||
* @property hash Merkle root of the transaction being resolved, see [net.corda.core.transactions.WireTransaction.id]
|
||||
*/
|
||||
class TransactionResolutionException(val hash: SecureHash) : FlowException("Transaction resolution failure for $hash")
|
||||
|
||||
/**
|
||||
* The node asked a remote peer for the attachment identified by [hash] because it is a dependency of a transaction
|
||||
* being resolved, but the remote peer would not provide it.
|
||||
*
|
||||
* @property hash Hash of the bytes of the attachment, see [Attachment.id]
|
||||
*/
|
||||
class AttachmentResolutionException(val hash: SecureHash) : FlowException("Attachment resolution failure for $hash")
|
||||
|
||||
/**
|
||||
* Indicates that some aspect of the transaction named by [txId] violates the platform rules. The exact type of failure
|
||||
* is expressed using a subclass. TransactionVerificationException is a [FlowException] and thus when thrown inside
|
||||
* a flow, the details of the failure will be serialised, propagated to the peer and rethrown.
|
||||
*
|
||||
* @property txId the Merkle root hash (identifier) of the transaction that failed verification.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
@CordaSerializable
|
||||
sealed class TransactionVerificationException(val txId: SecureHash, message: String, cause: Throwable?)
|
||||
: FlowException("$message, transaction: $txId", cause) {
|
||||
|
||||
class ContractRejection(txId: SecureHash, contract: Contract, cause: Throwable)
|
||||
: TransactionVerificationException(txId, "Contract verification failed: ${cause.message}, contract: $contract", cause)
|
||||
/**
|
||||
* Indicates that one of the [Contract.verify] methods selected by the contract constraints and attachments
|
||||
* rejected the transaction by throwing an exception.
|
||||
*
|
||||
* @property contractClass The fully qualified class name of the failing contract.
|
||||
*/
|
||||
class ContractRejection(txId: SecureHash, val contractClass: String, cause: Throwable) : TransactionVerificationException(txId, "Contract verification failed: ${cause.message}, contract: $contractClass", cause) {
|
||||
constructor(txId: SecureHash, contract: Contract, cause: Throwable) : this(txId, contract.javaClass.name, cause)
|
||||
}
|
||||
|
||||
class ContractConstraintRejection(txId: SecureHash, contractClass: String)
|
||||
/**
|
||||
* The transaction attachment that contains the [contractClass] class didn't meet the constraints specified by
|
||||
* the [TransactionState.constraint] object. This usually implies a version mismatch of some kind.
|
||||
*
|
||||
* @property contractClass The fully qualified class name of the failing contract.
|
||||
*/
|
||||
class ContractConstraintRejection(txId: SecureHash, val contractClass: String)
|
||||
: TransactionVerificationException(txId, "Contract constraints failed for $contractClass", null)
|
||||
|
||||
/**
|
||||
* A state requested a contract class via its [TransactionState.contract] field that didn't appear in any attached
|
||||
* JAR at all. This usually implies the attachments were forgotten or a version mismatch.
|
||||
*
|
||||
* @property contractClass The fully qualified class name of the failing contract.
|
||||
*/
|
||||
class MissingAttachmentRejection(txId: SecureHash, val contractClass: String)
|
||||
: TransactionVerificationException(txId, "Contract constraints failed, could not find attachment for: $contractClass", null)
|
||||
|
||||
class ConflictingAttachmentsRejection(txId: SecureHash, contractClass: String)
|
||||
/**
|
||||
* Indicates this transaction violates the "no overlap" rule: two attachments are trying to provide the same file
|
||||
* path. Whereas Java classpaths would normally allow that with the first class taking precedence, this is not
|
||||
* allowed in transactions for security reasons. This usually indicates that two separate apps share a dependency,
|
||||
* in which case you could try 'shading the fat jars' to rename classes of dependencies. Or you could manually
|
||||
* attach dependency JARs when building the transaction.
|
||||
*
|
||||
* @property contractClass The fully qualified class name of the failing contract.
|
||||
*/
|
||||
class ConflictingAttachmentsRejection(txId: SecureHash, val contractClass: String)
|
||||
: TransactionVerificationException(txId, "Contract constraints failed for: $contractClass, because multiple attachments providing this contract were attached.", null)
|
||||
|
||||
class ContractCreationError(txId: SecureHash, contractClass: String, cause: Throwable)
|
||||
/**
|
||||
* A [Contract] class named by a state could not be constructed. Most likely you do not have a no-argument
|
||||
* constructor, or the class doesn't subclass [Contract].
|
||||
*
|
||||
* @property contractClass The fully qualified class name of the failing contract.
|
||||
*/
|
||||
class ContractCreationError(txId: SecureHash, val contractClass: String, cause: Throwable)
|
||||
: TransactionVerificationException(txId, "Contract verification failed: ${cause.message}, could not create contract class: $contractClass", cause)
|
||||
|
||||
class MoreThanOneNotary(txId: SecureHash)
|
||||
: TransactionVerificationException(txId, "More than one notary", null)
|
||||
/**
|
||||
* An output state has a notary that doesn't match the transaction's notary field. It must!
|
||||
*
|
||||
* @property txNotary the [Party] specified by the transaction header.
|
||||
* @property outputNotary the [Party] specified by the errant state.
|
||||
*/
|
||||
class NotaryChangeInWrongTransactionType(txId: SecureHash, val txNotary: Party, val outputNotary: Party)
|
||||
: TransactionVerificationException(txId, "Found unexpected notary change in transaction. Tx notary: $txNotary, found: $outputNotary", null)
|
||||
|
||||
class SignersMissing(txId: SecureHash, missing: List<PublicKey>)
|
||||
: TransactionVerificationException(txId, "Signers missing: ${missing.joinToString()}", null)
|
||||
/**
|
||||
* If a state is encumbered (the [TransactionState.encumbrance] field is set) then its encumbrance must be used
|
||||
* as an input to any transaction that uses it. In this way states can be tied together in chains, thus composing
|
||||
* logic. Note that encumbrances aren't fully supported by all aspects of the platform at this time so if you use
|
||||
* them, you may find transactions created by the platform don't always respect the encumbrance rule.
|
||||
*
|
||||
* @property missing the index of the state missing the encumbrance.
|
||||
* @property inOut whether the issue exists in the input list or output list.
|
||||
*/
|
||||
class TransactionMissingEncumbranceException(txId: SecureHash, val missing: Int, val inOut: Direction)
|
||||
: TransactionVerificationException(txId, "Missing required encumbrance $missing in $inOut", null)
|
||||
|
||||
/** Whether the inputs or outputs list contains an encumbrance issue, see [TransactionMissingEncumbranceException]. */
|
||||
@CordaSerializable
|
||||
enum class Direction {
|
||||
/** Issue in the inputs list */ INPUT,
|
||||
/** Issue in the outputs list */ OUTPUT
|
||||
}
|
||||
|
||||
// We could revisit and throw this more appropriate type in a future release that uses targetVersion to
|
||||
// avoid the compatibility break, because IllegalStateException isn't ideal for this. Or we could use this
|
||||
// as a cause.
|
||||
/** @suppress This class is not used: duplicate inputs throw a [IllegalStateException] instead. */
|
||||
@Deprecated("This class is not used: duplicate inputs throw a [IllegalStateException] instead.")
|
||||
class DuplicateInputStates(txId: SecureHash, val duplicates: NonEmptySet<StateRef>)
|
||||
: TransactionVerificationException(txId, "Duplicate inputs: ${duplicates.joinToString()}", null)
|
||||
|
||||
/** @suppress This class is obsolete and nothing has ever used it. */
|
||||
@Deprecated("This class is obsolete and nothing has ever used it.")
|
||||
class MoreThanOneNotary(txId: SecureHash) : TransactionVerificationException(txId, "More than one notary", null)
|
||||
|
||||
/** @suppress This class is obsolete and nothing has ever used it. */
|
||||
@Deprecated("This class is obsolete and nothing has ever used it.")
|
||||
class SignersMissing(txId: SecureHash, val missing: List<PublicKey>) : TransactionVerificationException(txId, "Signers missing: ${missing.joinToString()}", null)
|
||||
|
||||
/** @suppress This class is obsolete and nothing has ever used it. */
|
||||
@Deprecated("This class is obsolete and nothing has ever used it.")
|
||||
class InvalidNotaryChange(txId: SecureHash)
|
||||
: TransactionVerificationException(txId, "Detected a notary change. Outputs must use the same notary as inputs", null)
|
||||
|
||||
class NotaryChangeInWrongTransactionType(txId: SecureHash, txNotary: Party, outputNotary: Party)
|
||||
: TransactionVerificationException(txId, "Found unexpected notary change in transaction. Tx notary: $txNotary, found: $outputNotary", null)
|
||||
|
||||
class TransactionMissingEncumbranceException(txId: SecureHash, missing: Int, inOut: Direction)
|
||||
: TransactionVerificationException(txId, "Missing required encumbrance $missing in $inOut", null)
|
||||
|
||||
@CordaSerializable
|
||||
enum class Direction {
|
||||
INPUT,
|
||||
OUTPUT
|
||||
}
|
||||
}
|
||||
|
@ -116,10 +116,9 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
* If any contract fails to verify, the whole transaction is considered to be invalid.
|
||||
*/
|
||||
private fun verifyContracts() {
|
||||
for (contractEntry in contracts.entries) {
|
||||
val result = contractEntry.value
|
||||
for ((key, result) in contracts) {
|
||||
when (result) {
|
||||
is Try.Failure -> throw TransactionVerificationException.ContractCreationError(id, contractEntry.key, result.exception)
|
||||
is Try.Failure -> throw TransactionVerificationException.ContractCreationError(id, key, result.exception)
|
||||
is Try.Success -> {
|
||||
val contract = result.value
|
||||
try {
|
||||
|
@ -0,0 +1,108 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.nodeapi.internal.serialization.AllWhitelist
|
||||
import net.corda.nodeapi.internal.serialization.amqp.DeserializationInput
|
||||
import net.corda.nodeapi.internal.serialization.amqp.SerializationOutput
|
||||
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
|
||||
import net.corda.nodeapi.internal.serialization.amqp.custom.PublicKeySerializer
|
||||
import net.corda.testing.core.DUMMY_BANK_A_NAME
|
||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||
import net.corda.testing.core.TestIdentity
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class TransactionVerificationExceptionSerialisationTests {
|
||||
private fun defaultFactory() = SerializerFactory(
|
||||
AllWhitelist,
|
||||
ClassLoader.getSystemClassLoader()
|
||||
)
|
||||
|
||||
private val txid = SecureHash.allOnesHash
|
||||
private val factory = defaultFactory()
|
||||
|
||||
@Test
|
||||
fun contractConstraintRejectionTest() {
|
||||
val excp = TransactionVerificationException.ContractConstraintRejection(txid, "This is only a test")
|
||||
val excp2 = DeserializationInput(factory).deserialize(SerializationOutput(factory).serialize(excp))
|
||||
|
||||
assertEquals(excp.message, excp2.message)
|
||||
assertEquals(excp.cause, excp2.cause)
|
||||
assertEquals(excp.txId, excp2.txId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun contractRejectionTest() {
|
||||
class TestContract(val thing: Int) : Contract {
|
||||
override fun verify(tx: LedgerTransaction) = Unit
|
||||
}
|
||||
|
||||
val contract = TestContract(12)
|
||||
val cause = Throwable("wibble")
|
||||
|
||||
val exception = TransactionVerificationException.ContractRejection(txid, contract, cause)
|
||||
val exception2 = DeserializationInput(factory).deserialize(SerializationOutput(factory).serialize(exception))
|
||||
|
||||
assertEquals(exception.message, exception2.message)
|
||||
assertEquals(exception.cause?.message, exception2.cause?.message)
|
||||
assertEquals(exception.txId, exception2.txId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun missingAttachmentRejectionTest() {
|
||||
val exception = TransactionVerificationException.MissingAttachmentRejection(txid, "Some contract class")
|
||||
val exception2 = DeserializationInput(factory).deserialize(SerializationOutput(factory).serialize(exception))
|
||||
|
||||
assertEquals(exception.message, exception2.message)
|
||||
assertEquals(exception.cause?.message, exception2.cause?.message)
|
||||
assertEquals(exception.txId, exception2.txId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun conflictingAttachmentsRejectionTest() {
|
||||
val exception = TransactionVerificationException.ContractConstraintRejection(txid, "Some contract class")
|
||||
val exception2 = DeserializationInput(factory).deserialize(SerializationOutput(factory).serialize(exception))
|
||||
|
||||
assertEquals(exception.message, exception2.message)
|
||||
assertEquals(exception.cause?.message, exception2.cause?.message)
|
||||
assertEquals(exception.txId, exception2.txId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun contractCreationErrorTest() {
|
||||
val cause = Throwable("wibble")
|
||||
val exception = TransactionVerificationException.ContractCreationError(txid, "Some contract class", cause)
|
||||
val exception2 = DeserializationInput(factory).deserialize(SerializationOutput(factory).serialize(exception))
|
||||
|
||||
assertEquals(exception.message, exception2.message)
|
||||
assertEquals(exception.cause?.message, exception2.cause?.message)
|
||||
assertEquals(exception.txId, exception2.txId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun transactionMissingEncumbranceTest() {
|
||||
val exception = TransactionVerificationException.TransactionMissingEncumbranceException(
|
||||
txid, 12, TransactionVerificationException.Direction.INPUT)
|
||||
val exception2 = DeserializationInput(factory).deserialize(SerializationOutput(factory).serialize(exception))
|
||||
|
||||
assertEquals(exception.message, exception2.message)
|
||||
assertEquals(exception.cause?.message, exception2.cause?.message)
|
||||
assertEquals(exception.txId, exception2.txId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun notaryChangeInWrongTransactionTypeTest() {
|
||||
val dummyBankA = TestIdentity(DUMMY_BANK_A_NAME, 40).party
|
||||
val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20).party
|
||||
|
||||
val factory = defaultFactory()
|
||||
factory.register(PublicKeySerializer)
|
||||
val exception = TransactionVerificationException.NotaryChangeInWrongTransactionType(txid, dummyBankA, dummyNotary)
|
||||
val exception2 = DeserializationInput(factory).deserialize(SerializationOutput(factory).serialize(exception))
|
||||
|
||||
assertEquals(exception.message, exception2.message)
|
||||
assertEquals(exception.cause?.message, exception2.cause?.message)
|
||||
assertEquals(exception.txId, exception2.txId)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user