Skip to content

Commit

Permalink
Merge remote-tracking branch 'private/master' into feature/tudor_cons…
Browse files Browse the repository at this point in the history
…traints

# Conflicts:
#	core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt
#	core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt
#	core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt
#	node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt
  • Loading branch information
[email protected] committed Nov 14, 2018
2 parents 8aaf120 + d5fa859 commit 1e27f0c
Show file tree
Hide file tree
Showing 37 changed files with 1,079 additions and 268 deletions.
3 changes: 3 additions & 0 deletions .ci/api-current.txt
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,9 @@ public final class net.corda.core.contracts.AutomaticHashConstraint extends java
public boolean isSatisfiedBy(net.corda.core.contracts.Attachment)
public static final net.corda.core.contracts.AutomaticHashConstraint INSTANCE
##
public @interface net.corda.core.contracts.BelongsToContract
public abstract Class<? extends net.corda.core.contracts.Contract> value()
##
@CordaSerializable
public final class net.corda.core.contracts.Command extends java.lang.Object
public <init>(T, java.security.PublicKey)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,15 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
fun `SignedTransaction (WireTransaction)`() {
val attachmentId = SecureHash.randomSHA256()
doReturn(attachmentId).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID)
val attachmentStorage = rigorousMock<AttachmentStorage>()
doReturn(attachmentStorage).whenever(services).attachments
val attachment = rigorousMock<ContractAttachment>()
doReturn(attachment).whenever(attachmentStorage).openAttachment(attachmentId)
doReturn(attachmentId).whenever(attachment).id
doReturn(emptyList<Party>()).whenever(attachment).signers
doReturn(setOf(DummyContract.PROGRAM_ID)).whenever(attachment).allContracts
doReturn("app").whenever(attachment).uploader

val wtx = TransactionBuilder(
notary = DUMMY_NOTARY,
inputs = mutableListOf(StateRef(SecureHash.randomSHA256(), 1)),
Expand Down
121 changes: 110 additions & 11 deletions core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,78 @@ import net.corda.core.KeepForDJVM
import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint.isSatisfiedBy
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.isFulfilledBy
import net.corda.core.crypto.keys
import net.corda.core.internal.AttachmentWithContext
import net.corda.core.internal.isUploaderTrusted
import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.warnOnce
import org.slf4j.LoggerFactory
import java.lang.annotation.Inherited
import java.security.PublicKey

/** Constrain which contract-code-containing attachment can be used with a [ContractState]. */
/**
* This annotation should only be added to [Contract] classes.
* If the annotation is present, then we assume that [Contract.verify] will ensure that the output states have an acceptable constraint.
* If the annotation is missing, then the default - secure - constraint propagation logic is enforced by the platform.
*/
@Target(AnnotationTarget.CLASS)
@Inherited
annotation class NoConstraintPropagation

/**
* Constrain which contract-code-containing attachment can be used with a [Contract].
* */
@CordaSerializable
@DoNotImplement
interface AttachmentConstraint {
/** Returns whether the given contract attachment can be used with the [ContractState] associated with this constraint object. */
fun isSatisfiedBy(attachment: Attachment): Boolean

/**
* This method will be used in conjunction with [NoConstraintPropagation]. It is run during transaction verification when the contract is not annotated with [NoConstraintPropagation].
* When constraints propagation is enabled, constraints set on output states need to follow certain rules with regards to constraints of input states.
*
* Rules:
* * It is allowed for output states to inherit the exact same constraint as the input states.
* * The [AlwaysAcceptAttachmentConstraint] is not allowed to transition to a different constraint, as that could be used to hide malicious behaviour.
* * Nothing can be migrated from the [HashAttachmentConstraint] except a [HashAttachmentConstraint] with the same hash.
* * Anything (except the [AlwaysAcceptAttachmentConstraint]) can be transitioned to a [HashAttachmentConstraint].
* * You can transition from the [WhitelistedByZoneAttachmentConstraint] to the [SignatureAttachmentConstraint] only if all signers of the JAR are required to sign in the future.
*
* TODO - SignatureConstraint third party signers.
*/
fun canBeTransitionedFrom(input: AttachmentConstraint, attachment: ContractAttachment): Boolean {
val output = this
return when {
// These branches should not happen, as this has been already checked.
input is AutomaticPlaceholderConstraint || output is AutomaticPlaceholderConstraint -> throw IllegalArgumentException("Illegal constraint: AutomaticPlaceholderConstraint.")
input is AutomaticHashConstraint || output is AutomaticHashConstraint -> throw IllegalArgumentException("Illegal constraint: AutomaticHashConstraint.")

// Transition to the same constraint.
input == output -> true

// You can't transition from the AlwaysAcceptAttachmentConstraint to anything else, as it could hide something illegal.
input is AlwaysAcceptAttachmentConstraint && output !is AlwaysAcceptAttachmentConstraint -> false

// Nothing can be migrated from the HashConstraint except a HashConstraint with the same Hash. (This check is redundant, but added for clarity)
// TODO - this might change if we decide to allow migration to the SignatureConstraint.
input is HashAttachmentConstraint && output is HashAttachmentConstraint -> input == output
input is HashAttachmentConstraint && output !is HashAttachmentConstraint -> false

// Anything (except the AlwaysAcceptAttachmentConstraint) can be transformed to a HashAttachmentConstraint.
input !is HashAttachmentConstraint && output is HashAttachmentConstraint -> true

// The SignatureAttachmentConstraint allows migration from a Signature constraint with the same key.
// TODO - we don't support currently third party signers. When we do, the output key will have to be stronger then the input key.
input is SignatureAttachmentConstraint && output is SignatureAttachmentConstraint -> input.key == output.key

// You can transition from the WhitelistConstraint to the SignatureConstraint only if all signers of the JAR are required to sign in the future.
input is WhitelistedByZoneAttachmentConstraint && output is SignatureAttachmentConstraint ->
attachment.signers.isNotEmpty() && output.key.keys.containsAll(attachment.signers)

else -> false
}
}
}

/** An [AttachmentConstraint] where [isSatisfiedBy] always returns true. */
Expand Down Expand Up @@ -47,26 +108,64 @@ data class HashAttachmentConstraint(val attachmentId: SecureHash) : AttachmentCo
object WhitelistedByZoneAttachmentConstraint : AttachmentConstraint {
override fun isSatisfiedBy(attachment: Attachment): Boolean {
return if (attachment is AttachmentWithContext) {
val whitelist = attachment.whitelistedContractImplementations ?: throw IllegalStateException("Unable to verify WhitelistedByZoneAttachmentConstraint - whitelist not specified")
val whitelist = attachment.whitelistedContractImplementations
?: throw IllegalStateException("Unable to verify WhitelistedByZoneAttachmentConstraint - whitelist not specified")
attachment.id in (whitelist[attachment.stateContract] ?: emptyList())
} else false
}
}

@KeepForDJVM
@Deprecated("The name is no longer valid as multiple constraints were added.", replaceWith = ReplaceWith("AutomaticPlaceholderConstraint"), level = DeprecationLevel.WARNING)
object AutomaticHashConstraint : AttachmentConstraint {
override fun isSatisfiedBy(attachment: Attachment): Boolean {
throw UnsupportedOperationException("Contracts cannot be satisfied by an AutomaticHashConstraint placeholder.")
}
}

/**
* This [AttachmentConstraint] is a convenience class that will be automatically resolved to a [HashAttachmentConstraint].
* The resolution occurs in [TransactionBuilder.toWireTransaction] and uses the [TransactionState.contract] value
* to find a corresponding loaded [Cordapp] that contains such a contract, and then uses that [Cordapp] as the
* [Attachment].
* This [AttachmentConstraint] is a convenience class that acts as a placeholder and will be automatically resolved by the platform when set on an output state.
* It is the default constraint of all output states.
*
* If, for any reason, this class is not automatically resolved the default implementation is to fail, because the
* intent of this class is that it should be replaced by a correct [HashAttachmentConstraint] and verify against an
* actual [Attachment].
* The resolution occurs in [TransactionBuilder.toWireTransaction] and is based on the input states and the attachments.
* If the [Contract] was not annotated with [NoConstraintPropagation], then the platform will ensure the correct constraint propagation.
*/
@KeepForDJVM
object AutomaticHashConstraint : AttachmentConstraint {
object AutomaticPlaceholderConstraint : AttachmentConstraint {
override fun isSatisfiedBy(attachment: Attachment): Boolean {
throw UnsupportedOperationException("Contracts cannot be satisfied by an AutomaticHashConstraint placeholder")
throw UnsupportedOperationException("Contracts cannot be satisfied by an AutomaticPlaceholderConstraint placeholder.")
}
}

private val logger = LoggerFactory.getLogger(AttachmentConstraint::class.java)
private val validConstraints = setOf(
AlwaysAcceptAttachmentConstraint::class,
HashAttachmentConstraint::class,
WhitelistedByZoneAttachmentConstraint::class,
SignatureAttachmentConstraint::class)

/**
* Fails if the constraint is not of a known type.
* Only the Corda core is allowed to implement the [AttachmentConstraint] interface.
*/
internal fun checkConstraintValidity(state: TransactionState<*>) {
require(state.constraint::class in validConstraints) { "Found state ${state.contract} with an illegal constraint: ${state.constraint}" }
if (state.constraint is AlwaysAcceptAttachmentConstraint) {
logger.warnOnce("Found state ${state.contract} that is constrained by the insecure: AlwaysAcceptAttachmentConstraint.")
}
}

/**
* Check for the [NoConstraintPropagation] annotation on the contractClassName.
* If it's present it means that the automatic secure core behaviour is not applied, and it's up to the contract developer to enforce a secure propagation logic.
*/
internal fun ContractClassName.contractHasAutomaticConstraintPropagation(classLoader: ClassLoader? = null) =
(classLoader ?: NoConstraintPropagation::class.java.classLoader)
.loadClass(this).getAnnotation(NoConstraintPropagation::class.java) == null

fun ContractClassName.warnContractWithoutConstraintPropagation(classLoader: ClassLoader? = null) {
if (!this.contractHasAutomaticConstraintPropagation(classLoader)) {
logger.warnOnce("Found contract $this with automatic constraint propagation disabled.")
}
}

Expand Down
32 changes: 32 additions & 0 deletions core/src/main/kotlin/net/corda/core/contracts/BelongsToContract.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package net.corda.core.contracts
import kotlin.reflect.KClass
/**
* This annotation is required by any [ContractState] which needs to ensure that it is only ever processed as part of a
* [TransactionState] referencing the specified [Contract]. It may be omitted in the case that the [ContractState] class
* is defined as an inner class of its owning [Contract] class, in which case the "X belongs to Y" relationship is taken
* to be implicitly declared.
*
* During verification of transactions, prior to their being written into the ledger, all input and output states are
* checked to ensure that their [ContractState]s match with their [Contract]s as specified either by this annotation, or
* by their inner/outer class relationship.
*
* The transaction will write a warning to the log if any mismatch is detected.
*
* @param value The class of the [Contract] to which states of the annotated [ContractState] belong.
*/
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class BelongsToContract(val value: KClass<out Contract>)
/**
* Obtain the typename of the required [ContractClass] associated with the target [ContractState], using the
* [BelongsToContract] annotation by default, but falling through to checking the state's enclosing class if there is
* one and it inherits from [Contract].
*/
val ContractState.requiredContractClassName: String? get() {
val annotation = javaClass.getAnnotation(BelongsToContract::class.java)
if (annotation != null) {
return annotation.value.java.typeName
}
val enclosingClass = javaClass.enclosingClass ?: return null
return if (Contract::class.java.isAssignableFrom(enclosingClass)) enclosingClass.typeName else null
}
36 changes: 34 additions & 2 deletions core/src/main/kotlin/net/corda/core/contracts/TransactionState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package net.corda.core.contracts
import net.corda.core.KeepForDJVM
import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.loggerFor

// DOCSTART 1
typealias ContractClassName = String
Expand All @@ -26,7 +27,14 @@ data class TransactionState<out T : ContractState> @JvmOverloads constructor(
* sent across, and run, from the network from within a sandbox environment.
*/
// TODO: Implement the contract sandbox loading of the contract attachments
val contract: ContractClassName,
val contract: ContractClassName = requireNotNull(data.requiredContractClassName) {
//TODO: add link to docsite page, when there is one.
"""
Unable to infer Contract class name because state class ${data::class.java.name} is not annotated with
@BelongsToContract, and does not have an enclosing class which implements Contract. Either annotate ${data::class.java.name}
with @BelongsToContract, or supply an explicit contract parameter to TransactionState().
""".trimIndent().replace('\n', ' ')
},
/** Identity of the notary that ensures the state is not used as an input to a transaction more than once */
val notary: Party,
/**
Expand All @@ -50,5 +58,29 @@ data class TransactionState<out T : ContractState> @JvmOverloads constructor(
/**
* A validator for the contract attachments on the transaction.
*/
val constraint: AttachmentConstraint = AutomaticHashConstraint)
val constraint: AttachmentConstraint = AutomaticPlaceholderConstraint) {

private companion object {
val logger = loggerFor<TransactionState<*>>()
}

init {
when {
data.requiredContractClassName == null -> logger.warn(
"""
State class ${data::class.java.name} is not annotated with @BelongsToContract,
and does not have an enclosing class which implements Contract. Annotate ${data::class.java.simpleName}
with @BelongsToContract(${contract.split("\\.\\$").last()}.class) to remove this warning.
""".trimIndent().replace('\n', ' ')
)
data.requiredContractClassName != contract -> logger.warn(
"""
State class ${data::class.java.name} belongs to contract ${data.requiredContractClassName},
but is bundled with contract $contract in TransactionState. Annotate ${data::class.java.simpleName}
with @BelongsToContract(${contract.split("\\.\\$").last()}.class) to remove this warning.
""".trimIndent().replace('\n', ' ')
)
}
}
}
// DOCEND 1
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class AttachmentResolutionException(val hash: SecureHash) : FlowException("Attac
*/
@Suppress("MemberVisibilityCanBePrivate")
@CordaSerializable
sealed class TransactionVerificationException(val txId: SecureHash, message: String, cause: Throwable?)
abstract class TransactionVerificationException(val txId: SecureHash, message: String, cause: Throwable?)
: FlowException("$message, transaction: $txId", cause) {

/**
Expand All @@ -51,6 +51,19 @@ sealed class TransactionVerificationException(val txId: SecureHash, message: Str
constructor(txId: SecureHash, contract: Contract, cause: Throwable) : this(txId, contract.javaClass.name, cause)
}

/**
* This exception happens when a transaction was not built correctly.
* When a contract is not annotated with [NoConstraintPropagation], then the platform ensures that the constraints of output states transition correctly from input states.
*
* @property txId The transaction.
* @property contractClass The fully qualified class name of the failing contract.
* @property inputConstraint The constraint of the input state.
* @property outputConstraint The constraint of the outputs state.
*/
@KeepForDJVM
class ConstraintPropagationRejection(txId: SecureHash, val contractClass: String, inputConstraint: AttachmentConstraint, outputConstraint: AttachmentConstraint)
: TransactionVerificationException(txId, "Contract constraints for $contractClass are not propagated correctly. The outputConstraint: $outputConstraint is not a valid transition from the input constraint: $inputConstraint.", null)

/**
* 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any)
/**
* This is a wildcard payload to be used by the invoker of the [DataVendingFlow] to allow unlimited access to its vault.
*
* Todo Fails with a serialization exception if it is not a list. Why?
* TODO Fails with a serialization exception if it is not a list. Why?
*/
@CordaSerializable
object RetrieveAnyTransactionPayload : ArrayList<Any>()
Loading

0 comments on commit 1e27f0c

Please sign in to comment.