From 7ee946b98f75f4d6936b1794a49d429cddd3362e Mon Sep 17 00:00:00 2001 From: Dominic Fox <40790090+distributedleetravis@users.noreply.github.com> Date: Thu, 30 Aug 2018 10:02:18 +0100 Subject: [PATCH 1/6] ENT-2320 Introduce BelongsToContract annotation (#1) * ENT-2320 Introduce BelongsToContract annotation * Update kdoc * Eliminate duplicate warnings --- .ci/api-current.txt | 3 + .../corda/core/contracts/BelongsToContract.kt | 32 +++++ .../corda/core/contracts/TransactionState.kt | 35 ++++- .../core/transactions/LedgerTransaction.kt | 128 +++++++++++------- .../core/transactions/TransactionBuilder.kt | 56 +++++--- .../net/corda/core/utilities/KotlinUtils.kt | 12 ++ .../core/flows/ContractUpgradeFlowTest.kt | 1 + .../LedgerTransactionQueryTests.kt | 3 +- .../tutorial/testdsl/CommercialPaperTest.java | 6 +- .../docs/tutorial/testdsl/TutorialTestDSL.kt | 2 +- .../net/corda/finance/contracts/asset/Cash.kt | 1 + .../contracts/asset/ObligationTests.kt | 34 ++--- 12 files changed, 219 insertions(+), 94 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/core/contracts/BelongsToContract.kt diff --git a/.ci/api-current.txt b/.ci/api-current.txt index b0e292868f..e30e90cdcf 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -453,6 +453,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 value() +## @CordaSerializable public final class net.corda.core.contracts.Command extends java.lang.Object public (T, java.security.PublicKey) diff --git a/core/src/main/kotlin/net/corda/core/contracts/BelongsToContract.kt b/core/src/main/kotlin/net/corda/core/contracts/BelongsToContract.kt new file mode 100644 index 0000000000..b5683f51aa --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/contracts/BelongsToContract.kt @@ -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) +/** + * 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 +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionState.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionState.kt index 7ad5129538..862c457192 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionState.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionState.kt @@ -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 @@ -26,7 +27,14 @@ data class TransactionState @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, /** @@ -50,5 +58,28 @@ data class TransactionState @JvmOverloads constructor( /** * A validator for the contract attachments on the transaction. */ - val constraint: AttachmentConstraint = AutomaticHashConstraint) + val constraint: AttachmentConstraint = AutomaticHashConstraint) { + private companion object { + val logger = loggerFor>() + } + + 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 diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index fdb255348b..7492641938 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -9,9 +9,10 @@ import net.corda.core.internal.castIfPossible import net.corda.core.internal.uncheckedCast import net.corda.core.node.NetworkParameters import net.corda.core.serialization.CordaSerializable -import net.corda.core.utilities.Try +import net.corda.core.utilities.loggerFor import java.util.* import java.util.function.Predicate +import net.corda.core.utilities.warnOnce /** * A LedgerTransaction is derived from a [WireTransaction]. It is the result of doing the following operations: @@ -54,23 +55,9 @@ data class LedgerTransaction @JvmOverloads constructor( } private companion object { - private fun contractClassFor(className: ContractClassName, classLoader: ClassLoader?): Try> { - return Try.on { - (classLoader ?: this::class.java.classLoader) - .loadClass(className) - .asSubclass(Contract::class.java) - } - } - - private fun stateToContractClass(state: TransactionState): Try> { - return contractClassFor(state.contract, state.data::class.java.classLoader) - } + val logger = loggerFor() } - // Input reference state contracts are not required for verification. - private val contracts: Map>> = (inputs.map { it.state } + outputs) - .map { it.contract to stateToContractClass(it) }.toMap() - val inputStates: List get() = inputs.map { it.state.data } val referenceStates: List get() = references.map { it.state.data } @@ -88,10 +75,31 @@ data class LedgerTransaction @JvmOverloads constructor( */ @Throws(TransactionVerificationException::class) fun verify() { + validateStatesAgainstContract() verifyConstraints() verifyContracts() } + private fun allStates() = inputs.asSequence().map { it.state } + outputs.asSequence() + + /** + * For all input and output [TransactionState]s, validates that the wrapped [ContractState] matches up with the + * wrapped [Contract], as declared by the [BelongsToContract] annotation on the [ContractState]'s class. + * + * A warning will be written to the log if any mismatch is detected. + */ + private fun validateStatesAgainstContract() = allStates().forEach(::validateStateAgainstContract) + + private fun validateStateAgainstContract(state: TransactionState) { + state.data.requiredContractClassName?.let { requiredContractClassName -> + if (state.contract != requiredContractClassName) + logger.warnOnce(""" + State of class ${state.data::class.java.typeName} belongs to contract $requiredContractClassName, but + is bundled in TransactionState with ${state.contract}. + """.trimIndent().replace('\n', ' ')) + } + } + /** * Verify that all contract constraints are valid for each state before running any contract code * @@ -102,54 +110,78 @@ data class LedgerTransaction @JvmOverloads constructor( * @throws TransactionVerificationException if the constraints fail to verify */ private fun verifyConstraints() { - val contractAttachments = attachments.filterIsInstance() - (inputs.map { it.state } + outputs).forEach { state -> - val stateAttachments = contractAttachments.filter { state.contract in it.allContracts } - if (stateAttachments.isEmpty()) throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract) + val contractAttachmentsByContract = getUniqueContractAttachmentsByContract() - val uniqueAttachmentsForStateContract = stateAttachments.distinctBy { it.id } + for (state in allStates()) { + val contractAttachment = contractAttachmentsByContract[state.contract] ?: + throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract) - // In case multiple attachments have been added for the same contract, fail because this transaction will not be able to be verified - // because it will break the no-overlap rule that we have implemented in our Classloaders - if (uniqueAttachmentsForStateContract.size > 1) { - throw TransactionVerificationException.ConflictingAttachmentsRejection(id, state.contract) - } + val constraintAttachment = AttachmentWithContext(contractAttachment, state.contract, + networkParameters?.whitelistedContractImplementations) - val contractAttachment = uniqueAttachmentsForStateContract.first() - val constraintAttachment = AttachmentWithContext(contractAttachment, state.contract, networkParameters?.whitelistedContractImplementations) if (!state.constraint.isSatisfiedBy(constraintAttachment)) { throw TransactionVerificationException.ContractConstraintRejection(id, state.contract) } } } - /** - * 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. - */ - private fun verifyContracts() { - val contractInstances = ArrayList(contracts.size) - for ((key, result) in contracts) { - when (result) { - is Try.Failure -> throw TransactionVerificationException.ContractCreationError(id, key, result.exception) - is Try.Success -> { - try { - contractInstances.add(result.value.newInstance()) - } catch (e: Throwable) { - throw TransactionVerificationException.ContractCreationError(id, result.value.name, e) + private fun getUniqueContractAttachmentsByContract(): Map { + val result = mutableMapOf() + + for (attachment in attachments) { + if (attachment !is ContractAttachment) continue + + for (contract in attachment.allContracts) { + result.compute(contract) { _, previousAttachment -> + when { + previousAttachment == null -> attachment + attachment.id == previousAttachment.id -> previousAttachment + // In case multiple attachments have been added for the same contract, fail because this + // transaction will not be able to be verified because it will break the no-overlap rule + // that we have implemented in our Classloaders + else -> throw TransactionVerificationException.ConflictingAttachmentsRejection(id, contract) } } } } - contractInstances.forEach { contract -> - try { - contract.verify(this) - } catch (e: Throwable) { - throw TransactionVerificationException.ContractRejection(id, contract, e) - } + + return result + } + + /** + * 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. + */ + private fun verifyContracts() = allStates().forEach { ts -> + val contractClass = getContractClass(ts) + val contract = createContractInstance(contractClass) + + try { + contract.verify(this) + } catch (e: Exception) { + throw TransactionVerificationException.ContractRejection(id, contract, e) } } + // Obtain the contract class from the class name, wrapping any exception as a [ContractCreationError] + private fun getContractClass(ts: TransactionState): Class = + try { + (ts.data::class.java.classLoader ?: this::class.java.classLoader) + .loadClass(ts.contract) + .asSubclass(Contract::class.java) + } catch (e: Exception) { + throw TransactionVerificationException.ContractCreationError(id, ts.contract, e) + } + + // Obtain an instance of the contract class, wrapping any exception as a [ContractCreationError] + private fun createContractInstance(contractClass: Class): Contract = + try { + contractClass.newInstance() + } catch (e: Exception) { + throw TransactionVerificationException.ContractCreationError(id, contractClass.name, e) + } + + /** * Make sure the notary has stayed the same. As we can't tell how inputs and outputs connect, if there * are any inputs or reference inputs, all outputs must have the same notary. diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index 9b54509064..bcfd8f3bca 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -17,6 +17,7 @@ import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.KeyManagementService import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationFactory +import net.corda.core.utilities.loggerFor import java.security.PublicKey import java.time.Duration import java.time.Instant @@ -47,6 +48,11 @@ open class TransactionBuilder @JvmOverloads constructor( protected var privacySalt: PrivacySalt = PrivacySalt(), protected val references: MutableList = arrayListOf() ) { + + private companion object { + val logger = loggerFor() + } + private val inputsWithTransactionState = arrayListOf>() private val referencesWithTransactionState = arrayListOf>() @@ -71,7 +77,7 @@ open class TransactionBuilder @JvmOverloads constructor( // DOCSTART 1 /** A more convenient way to add items to this transaction that calls the add* methods for you based on type */ - fun withItems(vararg items: Any): TransactionBuilder { + fun withItems(vararg items: Any) = apply { for (t in items) { when (t) { is StateAndRef<*> -> addInputState(t) @@ -87,7 +93,6 @@ open class TransactionBuilder @JvmOverloads constructor( else -> throw IllegalArgumentException("Wrong argument type: ${t.javaClass}") } } - return this } // DOCEND 1 @@ -147,10 +152,10 @@ open class TransactionBuilder @JvmOverloads constructor( private fun makeAttachmentConstraint(services: ServicesForResolution, state: TransactionState): AttachmentConstraint { val attachmentId = services.cordappProvider.getContractAttachmentID(state.contract) - ?: throw MissingContractAttachments(listOf(state)) + ?: throw MissingContractAttachments(listOf(state)) val attachmentSigners = services.attachments.openAttachment(attachmentId)?.signers - ?: throw MissingContractAttachments(listOf(state)) + ?: throw MissingContractAttachments(listOf(state)) return when { attachmentSigners.isEmpty() -> HashAttachmentConstraint(attachmentId) @@ -212,7 +217,7 @@ open class TransactionBuilder @JvmOverloads constructor( * Note: Reference states are only supported on Corda networks running a minimum platform version of 4. * [toWireTransaction] will throw an [IllegalStateException] if called in such an environment. */ - open fun addReferenceState(referencedStateAndRef: ReferencedStateAndRef<*>): TransactionBuilder { + open fun addReferenceState(referencedStateAndRef: ReferencedStateAndRef<*>) = apply { val stateAndRef = referencedStateAndRef.stateAndRef referencesWithTransactionState.add(stateAndRef.state) @@ -235,34 +240,37 @@ open class TransactionBuilder @JvmOverloads constructor( checkNotary(stateAndRef) references.add(stateAndRef.ref) checkForInputsAndReferencesOverlap() - return this } /** Adds an input [StateRef] to the transaction. */ - open fun addInputState(stateAndRef: StateAndRef<*>): TransactionBuilder { + open fun addInputState(stateAndRef: StateAndRef<*>) = apply { checkNotary(stateAndRef) inputs.add(stateAndRef.ref) inputsWithTransactionState.add(stateAndRef.state) - return this } /** Adds an attachment with the specified hash to the TransactionBuilder. */ - fun addAttachment(attachmentId: SecureHash): TransactionBuilder { + fun addAttachment(attachmentId: SecureHash) = apply { attachments.add(attachmentId) - return this } /** Adds an output state to the transaction. */ - fun addOutputState(state: TransactionState<*>): TransactionBuilder { + fun addOutputState(state: TransactionState<*>) = apply { outputs.add(state) - return this } /** Adds an output state, with associated contract code (and constraints), and notary, to the transaction. */ @JvmOverloads fun addOutputState( state: ContractState, - contract: ContractClassName, + contract: ContractClassName = requireNotNull(state.requiredContractClassName) { + //TODO: add link to docsite page, when there is one. +""" +Unable to infer Contract class name because state class ${state::class.java.name} is not annotated with +@BelongsToContract, and does not have an enclosing class which implements Contract. Either annotate ${state::class.java.name} +with @BelongsToContract, or supply an explicit contract parameter to addOutputState(). +""".trimIndent().replace('\n', ' ') + }, notary: Party, encumbrance: Int? = null, constraint: AttachmentConstraint = AutomaticHashConstraint ): TransactionBuilder { @@ -272,20 +280,26 @@ open class TransactionBuilder @JvmOverloads constructor( /** A default notary must be specified during builder construction to use this method */ @JvmOverloads fun addOutputState( - state: ContractState, contract: ContractClassName, + state: ContractState, + contract: ContractClassName = requireNotNull(state.requiredContractClassName) { + //TODO: add link to docsite page, when there is one. +""" +Unable to infer Contract class name because state class ${state::class.java.name} is not annotated with +@BelongsToContract, and does not have an enclosing class which implements Contract. Either annotate ${state::class.java.name} +with @BelongsToContract, or supply an explicit contract parameter to addOutputState(). +""".trimIndent().replace('\n', ' ') + }, constraint: AttachmentConstraint = AutomaticHashConstraint - ): TransactionBuilder { + ) = apply { checkNotNull(notary) { "Need to specify a notary for the state, or set a default one on TransactionBuilder initialisation" } addOutputState(state, contract, notary!!, constraint = constraint) - return this } /** Adds a [Command] to the transaction. */ - fun addCommand(arg: Command<*>): TransactionBuilder { + fun addCommand(arg: Command<*>) = apply { commands.add(arg) - return this } /** @@ -301,10 +315,9 @@ open class TransactionBuilder @JvmOverloads constructor( * transaction must then be signed by the notary service within this window of time. In this way, the notary acts as * the Timestamp Authority. */ - fun setTimeWindow(timeWindow: TimeWindow): TransactionBuilder { + fun setTimeWindow(timeWindow: TimeWindow) = apply { check(notary != null) { "Only notarised transactions can have a time-window" } window = timeWindow - return this } /** @@ -316,9 +329,8 @@ open class TransactionBuilder @JvmOverloads constructor( */ fun setTimeWindow(time: Instant, timeTolerance: Duration) = setTimeWindow(TimeWindow.withTolerance(time, timeTolerance)) - fun setPrivacySalt(privacySalt: PrivacySalt): TransactionBuilder { + fun setPrivacySalt(privacySalt: PrivacySalt) = apply { this.privacySalt = privacySalt - return this } /** Returns an immutable list of input [StateRef]s. */ diff --git a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt index 240edbfd1e..849c937b8e 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt @@ -135,3 +135,15 @@ fun Future.getOrThrow(timeout: Duration? = null): V = try { throw e.cause!! } +private val warnings = mutableSetOf() + +/** + * Utility to help log a warning message only once. + * It's not thread safe, as in the worst case the warning will be logged twice. + */ +fun Logger.warnOnce(warning: String) { + if (warning !in warnings) { + warnings.add(warning) + this.warn(warning) + } +} \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index ead8da3ae4..62dab98d1e 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -146,6 +146,7 @@ class ContractUpgradeFlowTest : WithContracts, WithFinality { class Move : TypeOnlyCommandData() + @BelongsToContract(CashV2::class) data class State(override val amount: Amount>, val owners: List) : FungibleAsset { override val owner: AbstractParty = owners.first() override val exitKeys = (owners + amount.token.issuer.party).map { it.owningKey }.toSet() diff --git a/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt b/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt index f91eb9c344..67a04ac5f3 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt @@ -46,11 +46,12 @@ class LedgerTransactionQueryTests { data class Cmd3(val id: Int) : CommandData, Commands // Unused command, required for command not-present checks. } - + @BelongsToContract(DummyContract::class) private class StringTypeDummyState(val data: String) : ContractState { override val participants: List = emptyList() } + @BelongsToContract(DummyContract::class) private class IntTypeDummyState(val data: Int) : ContractState { override val participants: List = emptyList() } diff --git a/docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java b/docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java index 07ef61b49e..d84166613a 100644 --- a/docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java +++ b/docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java @@ -250,7 +250,7 @@ public class CommercialPaperTest { // Some CP is issued onto the ledger by MegaCorp. l.transaction("Issuance", tx -> { - tx.output(Cash.PROGRAM_ID, "paper", getPaper()); + tx.output(JCP_PROGRAM_ID, "paper", getPaper()); tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Issue()); tx.attachments(JCP_PROGRAM_ID); tx.timeWindow(TEST_TX_TIME); @@ -296,7 +296,7 @@ public class CommercialPaperTest { // Some CP is issued onto the ledger by MegaCorp. l.transaction("Issuance", tx -> { - tx.output(Cash.PROGRAM_ID, "paper", getPaper()); + tx.output(JCP_PROGRAM_ID, "paper", getPaper()); tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Issue()); tx.attachments(JCP_PROGRAM_ID); tx.timeWindow(TEST_TX_TIME); @@ -309,7 +309,7 @@ public class CommercialPaperTest { tx.output(Cash.PROGRAM_ID, "borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), megaCorp.getParty())); JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); tx.output(JCP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(alice.getParty())); - tx.command(alice.getPublicKey(), new Cash.Commands.Move()); + tx.command(alice.getPublicKey(), new Cash.Commands.Move(JavaCommercialPaper.class)); tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Move()); return tx.verifies(); }); diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt index fc04b31387..26f1f6885b 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt @@ -295,7 +295,7 @@ class CommercialPaperTest { input("alice's $900") output(Cash.PROGRAM_ID, "borrowed $900", 900.DOLLARS.CASH issuedBy issuer ownedBy megaCorp.party) output(CP_PROGRAM_ID, "alice's paper", "paper".output().withOwner(alice.party)) - command(alice.publicKey, Cash.Commands.Move()) + command(alice.publicKey, Cash.Commands.Move(CommercialPaper::class.java)) command(megaCorp.publicKey, CommercialPaper.Commands.Move()) verifies() } diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt index 73681e44b7..e95609abb2 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt @@ -50,6 +50,7 @@ class Cash : OnLedgerAsset() { // DOCSTART 1 /** A state representing a cash claim against some party. */ + @BelongsToContract(Cash::class) data class State( override val amount: Amount>, diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt index 7c5edaeea1..6d31af0ea5 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt @@ -96,11 +96,11 @@ class ObligationTests { group: LedgerDSL ) = group.apply { unverifiedTransaction { - attachments(Obligation.PROGRAM_ID) + attachments(Obligation.PROGRAM_ID, Cash.PROGRAM_ID) output(Obligation.PROGRAM_ID, "Alice's $1,000,000 obligation to Bob", oneMillionDollars.OBLIGATION between Pair(ALICE, BOB)) output(Obligation.PROGRAM_ID, "Bob's $1,000,000 obligation to Alice", oneMillionDollars.OBLIGATION between Pair(BOB, ALICE)) output(Obligation.PROGRAM_ID, "MegaCorp's $1,000,000 obligation to Bob", oneMillionDollars.OBLIGATION between Pair(MEGA_CORP, BOB)) - output(Obligation.PROGRAM_ID, "Alice's $1,000,000", 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy ALICE) + output(Cash.PROGRAM_ID, "Alice's $1,000,000", 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy ALICE) } } @@ -166,11 +166,11 @@ class ObligationTests { transaction { attachments(Obligation.PROGRAM_ID) output(Obligation.PROGRAM_ID, - Obligation.State( - obligor = MINI_CORP, - quantity = 1000.DOLLARS.quantity, - beneficiary = CHARLIE, - template = megaCorpDollarSettlement)) + Obligation.State( + obligor = MINI_CORP, + quantity = 1000.DOLLARS.quantity, + beneficiary = CHARLIE, + template = megaCorpDollarSettlement)) command(MINI_CORP_PUBKEY, Obligation.Commands.Issue()) this.verifies() } @@ -506,10 +506,10 @@ class ObligationTests { ledgerServices.ledger(DUMMY_NOTARY) { cashObligationTestRoots(this) transaction("Settlement") { - attachments(Obligation.PROGRAM_ID) + attachments(Obligation.PROGRAM_ID, Cash.PROGRAM_ID) input("Alice's $1,000,000 obligation to Bob") input("Alice's $1,000,000") - output(Obligation.PROGRAM_ID, "Bob's $1,000,000", 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB) + output(Cash.PROGRAM_ID, "Bob's $1,000,000", 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB) command(ALICE_PUBKEY, Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.amount.token))) command(ALICE_PUBKEY, Cash.Commands.Move(Obligation::class.java)) attachment(attachment(cashContractBytes.inputStream())) @@ -525,7 +525,7 @@ class ObligationTests { input(Obligation.PROGRAM_ID, oneMillionDollars.OBLIGATION between Pair(ALICE, BOB)) input(Cash.PROGRAM_ID, 500000.DOLLARS.CASH issuedBy defaultIssuer ownedBy ALICE) output(Obligation.PROGRAM_ID, "Alice's $500,000 obligation to Bob", halfAMillionDollars.OBLIGATION between Pair(ALICE, BOB)) - output(Obligation.PROGRAM_ID, "Bob's $500,000", 500000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB) + output(Cash.PROGRAM_ID, "Bob's $500,000", 500000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB) command(ALICE_PUBKEY, Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.amount.token))) command(ALICE_PUBKEY, Cash.Commands.Move(Obligation::class.java)) attachment(attachment(cashContractBytes.inputStream())) @@ -540,7 +540,7 @@ class ObligationTests { attachments(Obligation.PROGRAM_ID, Cash.PROGRAM_ID) input(Obligation.PROGRAM_ID, defaultedObligation) // Alice's defaulted $1,000,000 obligation to Bob input(Cash.PROGRAM_ID, 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy ALICE) - output(Obligation.PROGRAM_ID, "Bob's $1,000,000", 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB) + output(Cash.PROGRAM_ID, "Bob's $1,000,000", 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB) command(ALICE_PUBKEY, Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.amount.token))) command(ALICE_PUBKEY, Cash.Commands.Move(Obligation::class.java)) this `fails with` "all inputs are in the normal state" @@ -554,7 +554,7 @@ class ObligationTests { attachments(Obligation.PROGRAM_ID) input("Alice's $1,000,000 obligation to Bob") input("Alice's $1,000,000") - output(Obligation.PROGRAM_ID, "Bob's $1,000,000", 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB) + output(Cash.PROGRAM_ID, "Bob's $1,000,000", 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB) command(ALICE_PUBKEY, Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.amount.token))) command(ALICE_PUBKEY, Cash.Commands.Move(Obligation::class.java)) attachment(attachment(cashContractBytes.inputStream())) @@ -701,10 +701,10 @@ class ObligationTests { attachments(Obligation.PROGRAM_ID) input(Obligation.PROGRAM_ID, inState) input(Obligation.PROGRAM_ID, - inState.copy( - quantity = 15000, - template = megaCorpPoundSettlement, - beneficiary = AnonymousParty(BOB_PUBKEY))) + inState.copy( + quantity = 15000, + template = megaCorpPoundSettlement, + beneficiary = AnonymousParty(BOB_PUBKEY))) output(Obligation.PROGRAM_ID, outState.copy(quantity = 115000)) command(MINI_CORP_PUBKEY, Obligation.Commands.Move()) this `fails with` "the amounts balance" @@ -962,4 +962,4 @@ class ObligationTests { get() = Obligation.Terms(NonEmptySet.of(cashContractBytes.sha256() as SecureHash), NonEmptySet.of(this), TEST_TX_TIME) private val Amount>.OBLIGATION: Obligation.State get() = Obligation.State(Obligation.Lifecycle.NORMAL, DUMMY_OBLIGATION_ISSUER, token.OBLIGATION_DEF, quantity, NULL_PARTY) -} +} \ No newline at end of file From 32f279a24372e31b07cfddac53edf805175fc971 Mon Sep 17 00:00:00 2001 From: Tudor Malene Date: Fri, 14 Sep 2018 11:25:16 +0100 Subject: [PATCH 2/6] ENT-2506 Changes signers field type ENT-2506 Clean up some docs ENT-2506 Fix tests and api ENT-2506 Fix compilation error ENT-2506 Fix compilation error --- .ci/api-current.txt | 4 +-- .../common/MockContractAttachment.kt | 4 +-- .../deterministic/contracts/AttachmentTest.kt | 4 +-- .../net/corda/core/contracts/Attachment.kt | 5 +-- .../core/contracts/AttachmentConstraint.kt | 2 +- .../corda/core/internal/AbstractAttachment.kt | 6 ++-- .../core/internal/JarSignatureCollector.kt | 15 ++++---- .../core/transactions/TransactionBuilder.kt | 4 +-- .../internal/JarSignatureCollectorTest.kt | 35 +++++++++++-------- .../transactions/TransactionBuilderTest.kt | 5 +-- 10 files changed, 47 insertions(+), 37 deletions(-) diff --git a/.ci/api-current.txt b/.ci/api-current.txt index e30e90cdcf..d3363ad577 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -429,7 +429,7 @@ public static final class net.corda.core.contracts.AmountTransfer$Companion exte public interface net.corda.core.contracts.Attachment extends net.corda.core.contracts.NamedByHash public void extractFile(String, java.io.OutputStream) @NotNull - public abstract java.util.List getSigners() + public abstract java.util.List getSigners() public abstract int getSize() @NotNull public abstract java.io.InputStream open() @@ -540,7 +540,7 @@ public final class net.corda.core.contracts.ContractAttachment extends java.lang @NotNull public net.corda.core.crypto.SecureHash getId() @NotNull - public java.util.List getSigners() + public java.util.List getSigners() public int getSize() @Nullable public final String getUploader() diff --git a/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/MockContractAttachment.kt b/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/MockContractAttachment.kt index 1526825683..a4b3b8a21e 100644 --- a/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/MockContractAttachment.kt +++ b/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/MockContractAttachment.kt @@ -3,13 +3,13 @@ package net.corda.deterministic.common import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractClassName import net.corda.core.crypto.SecureHash -import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable import java.io.ByteArrayInputStream import java.io.InputStream +import java.security.PublicKey @CordaSerializable -class MockContractAttachment(override val id: SecureHash = SecureHash.zeroHash, val contract: ContractClassName, override val signers: List = ArrayList()) : Attachment { +class MockContractAttachment(override val id: SecureHash = SecureHash.zeroHash, val contract: ContractClassName, override val signers: List = emptyList()) : Attachment { override fun open(): InputStream = ByteArrayInputStream(id.bytes) override val size = id.size } diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/contracts/AttachmentTest.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/contracts/AttachmentTest.kt index 21ad6a3e45..01f7751905 100644 --- a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/contracts/AttachmentTest.kt +++ b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/contracts/AttachmentTest.kt @@ -42,8 +42,8 @@ class AttachmentTest { attachment = object : Attachment { override val id: SecureHash get() = SecureHash.allOnesHash - override val signers: List - get() = listOf(ALICE) + override val signers: List + get() = listOf(ALICE_KEY) override val size: Int get() = jarData.size diff --git a/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt b/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt index 8cf0ed5839..d17d053e1f 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt @@ -7,6 +7,7 @@ import net.corda.core.serialization.CordaSerializable import java.io.FileNotFoundException import java.io.InputStream import java.io.OutputStream +import java.security.PublicKey import java.util.jar.JarInputStream /** @@ -51,10 +52,10 @@ interface Attachment : NamedByHash { fun extractFile(path: String, outputTo: OutputStream) = openAsJAR().use { it.extractFile(path, outputTo) } /** - * The parties that have correctly signed the whole attachment. + * The keys that have correctly signed the whole attachment. * Can be empty, for example non-contract attachments won't be necessarily be signed. */ - val signers: List + val signers: List /** * Attachment size in bytes. diff --git a/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt b/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt index b4fbe24ef4..76ffc8a866 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt @@ -82,5 +82,5 @@ data class SignatureAttachmentConstraint( val key: PublicKey ) : AttachmentConstraint { override fun isSatisfiedBy(attachment: Attachment): Boolean = - key.isFulfilledBy(attachment.signers.map { it.owningKey }) + key.isFulfilledBy(attachment.signers.map { it }) } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt b/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt index 3c72e5d0ef..c509abdd4f 100644 --- a/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt +++ b/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt @@ -1,4 +1,5 @@ @file:KeepForDJVM + package net.corda.core.internal import net.corda.core.DeleteForDJVM @@ -11,6 +12,7 @@ import java.io.FileNotFoundException import java.io.IOException import java.io.InputStream import java.io.OutputStream +import java.security.PublicKey import java.util.jar.JarInputStream const val DEPLOYED_CORDAPP_UPLOADER = "app" @@ -40,8 +42,8 @@ abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment { override val size: Int get() = attachmentData.size override fun open(): InputStream = attachmentData.inputStream() - override val signers by lazy { - openAsJAR().use(JarSignatureCollector::collectSigningParties) + override val signers: List by lazy { + openAsJAR().use(JarSignatureCollector::collectSigners) } override fun equals(other: Any?) = other === this || other is Attachment && other.id == this.id diff --git a/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt b/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt index 70d8c84873..ff8970461a 100644 --- a/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt +++ b/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt @@ -2,6 +2,7 @@ package net.corda.core.internal import net.corda.core.identity.Party import java.security.CodeSigner +import java.security.PublicKey import java.security.cert.X509Certificate import java.util.jar.JarEntry import java.util.jar.JarInputStream @@ -20,7 +21,7 @@ object JarSignatureCollector { * @param jar The open [JarInputStream] to collect signing parties from. * @throws InvalidJarSignersException If the signer sets for any two signable items are different from each other. */ - fun collectSigningParties(jar: JarInputStream): List { + fun collectSigners(jar: JarInputStream): List { val signerSets = jar.fileSignerSets if (signerSets.isEmpty()) return emptyList() @@ -28,14 +29,14 @@ object JarSignatureCollector { for ((otherFile, otherSignerSet) in signerSets.subList(1, signerSets.size)) { if (otherSignerSet != firstSignerSet) throw InvalidJarSignersException( """ - Mismatch between signers ${firstSignerSet.toPartiesOrderedByName()} for file $firstFile - and signers ${otherSignerSet.toPartiesOrderedByName()} for file ${otherFile}. + Mismatch between signers ${firstSignerSet.toOrderedPublicKeys()} for file $firstFile + and signers ${otherSignerSet.toOrderedPublicKeys()} for file ${otherFile}. See https://docs.corda.net/design/data-model-upgrades/signature-constraints.html for details of the constraints applied to attachment signatures. """.trimIndent().replace('\n', ' ')) } - return firstSignerSet.toPartiesOrderedByName() + return firstSignerSet.toOrderedPublicKeys() } private val JarInputStream.fileSignerSets: List>> get() = @@ -56,9 +57,9 @@ object JarSignatureCollector { private fun Sequence.toFileSignerSet(): Sequence>> = map { entry -> entry.name to (entry.codeSigners?.toSet() ?: emptySet()) } - private fun Set.toPartiesOrderedByName(): List = map { - Party(it.signerCertPath.certificates[0] as X509Certificate) - }.sortedBy { it.name.toString() } // Sorted for determinism. + private fun Set.toOrderedPublicKeys(): List = map { + (it.signerCertPath.certificates[0] as X509Certificate).publicKey + }.sortedBy { it.hash} // Sorted for determinism. private val JarInputStream.entries get(): Sequence = generateSequence(nextJarEntry) { nextJarEntry } } diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index bcfd8f3bca..7de546016a 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -163,8 +163,8 @@ open class TransactionBuilder @JvmOverloads constructor( } } - private fun makeSignatureAttachmentConstraint(attachmentSigners: List) = - SignatureAttachmentConstraint(CompositeKey.Builder().addKeys(attachmentSigners.map { it.owningKey }).build()) + private fun makeSignatureAttachmentConstraint(attachmentSigners: List) = + SignatureAttachmentConstraint(CompositeKey.Builder().addKeys(attachmentSigners.map { it }).build()) private fun useWhitelistedByZoneAttachmentConstraint(contractClassName: ContractClassName, networkParameters: NetworkParameters) = contractClassName in networkParameters.whitelistedContractImplementations.keys diff --git a/core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt b/core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt index b8620082d8..95f0b187e6 100644 --- a/core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt +++ b/core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt @@ -2,6 +2,7 @@ package net.corda.core.internal import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party +import net.corda.nodeapi.internal.crypto.loadKeyStore import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import org.assertj.core.api.Assertions.assertThat @@ -13,6 +14,7 @@ import java.io.FileInputStream import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths +import java.security.PublicKey import java.util.jar.JarInputStream import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -60,7 +62,7 @@ class JarSignatureCollectorTest { } } - private val List.names get() = map { it.name } + private val List.keys get() = map { it.owningKey } @After fun tearDown() { @@ -92,33 +94,33 @@ class JarSignatureCollectorTest { @Test fun `one signer`() { createJar("_signable1", "_signable2") - signAsAlice() - assertEquals(listOf(ALICE_NAME), getJarSigners().names) // We only reused ALICE's distinguished name, so the keys will be different. + val key = signAsAlice() + assertEquals(listOf(key), getJarSigners()) (dir / "my-dir").createDirectory() updateJar("my-dir") - assertEquals(listOf(ALICE_NAME), getJarSigners().names) // Unsigned directory is irrelevant. + assertEquals(listOf(key), getJarSigners()) // Unsigned directory is irrelevant. } @Test fun `two signers`() { createJar("_signable1", "_signable2") - signAsAlice() - signAsBob() + val key1 = signAsAlice() + val key2 = signAsBob() - assertEquals(listOf(ALICE_NAME, BOB_NAME), getJarSigners().names) + assertEquals(setOf(key1, key2), getJarSigners().toSet()) } @Test fun `all files must be signed by the same set of signers`() { createJar("_signable1") - signAsAlice() - assertEquals(listOf(ALICE_NAME), getJarSigners().names) + val key1 = signAsAlice() + assertEquals(listOf(key1), getJarSigners()) updateJar("_signable2") signAsBob() assertFailsWith( - """ + """ Mismatch between signers [O=Alice Corp, L=Madrid, C=ES, O=Bob Plc, L=Rome, C=IT] for file _signable1 and signers [O=Bob Plc, L=Rome, C=IT] for file _signable2. See https://docs.corda.net/design/data-model-upgrades/signature-constraints.html for details of the @@ -131,8 +133,8 @@ class JarSignatureCollectorTest { fun `bad signature is caught even if the party would not qualify as a signer`() { (dir / "volatile").writeLines(listOf("volatile")) createJar("volatile") - signAsAlice() - assertEquals(listOf(ALICE_NAME), getJarSigners().names) + val key1 = signAsAlice() + assertEquals(listOf(key1), getJarSigners()) (dir / "volatile").writeLines(listOf("garbage")) updateJar("volatile", "_signable1") // ALICE's signature on volatile is now bad. @@ -148,14 +150,17 @@ class JarSignatureCollectorTest { private fun updateJar(vararg contents: String) = execute(*(arrayOf("jar", "uvf", FILENAME) + contents)) - private fun signJar(alias: String, password: String) = - execute("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", password, FILENAME, alias) + private fun signJar(alias: String, password: String): PublicKey { + execute("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", password, FILENAME, alias) + val ks = loadKeyStore(dir.resolve("_teststore"), "storepass") + return ks.getCertificate(alias).publicKey + } private fun signAsAlice() = signJar(ALICE, ALICE_PASS) private fun signAsBob() = signJar(BOB, BOB_PASS) private fun getJarSigners() = - JarInputStream(FileInputStream((dir / FILENAME).toFile())).use(JarSignatureCollector::collectSigningParties) + JarInputStream(FileInputStream((dir / FILENAME).toFile())).use(JarSignatureCollector::collectSigners) //endregion } diff --git a/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt b/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt index f779c85678..aa1d6240d0 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt @@ -23,6 +23,7 @@ import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test +import java.security.PublicKey class TransactionBuilderTest { @Rule @@ -119,12 +120,12 @@ class TransactionBuilderTest { private val unsignedAttachment = object : AbstractAttachment({ byteArrayOf() }) { override val id: SecureHash get() = throw UnsupportedOperationException() - override val signers: List get() = emptyList() + override val signers: List get() = emptyList() } private fun signedAttachment(vararg parties: Party) = object : AbstractAttachment({ byteArrayOf() }) { override val id: SecureHash get() = throw UnsupportedOperationException() - override val signers: List get() = parties.toList() + override val signers: List get() = parties.map { it.owningKey } } } From ab98c03d1ab15479c106b89f8b85bec185a7f9fa Mon Sep 17 00:00:00 2001 From: Tudor Malene Date: Wed, 26 Sep 2018 16:05:57 +0100 Subject: [PATCH 3/6] Upgrade hibernate and fix tests CORDA-1947 Address code review changes CORDA-1947 Address code review changes --- build.gradle | 2 +- .../persistence/HibernateStatistics.kt | 27 ++++++++++++++----- .../node/services/vault/NodeVaultService.kt | 3 ++- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index 12d70e0312..ae3ed9e67c 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,7 @@ buildscript { ext.hamkrest_version = '1.4.2.2' ext.jopt_simple_version = '5.0.2' ext.jansi_version = '1.14' - ext.hibernate_version = '5.2.6.Final' + ext.hibernate_version = '5.3.6.Final' ext.h2_version = '1.4.197' // Update docs if renamed or removed. ext.postgresql_version = '42.1.4' ext.rxjava_version = '1.2.4' diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateStatistics.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateStatistics.kt index 2c08d3e77f..c902f57f27 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateStatistics.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateStatistics.kt @@ -1,14 +1,8 @@ package net.corda.nodeapi.internal.persistence +import org.hibernate.stat.* import javax.management.MXBean -import org.hibernate.stat.Statistics -import org.hibernate.stat.SecondLevelCacheStatistics -import org.hibernate.stat.QueryStatistics -import org.hibernate.stat.NaturalIdCacheStatistics -import org.hibernate.stat.EntityStatistics -import org.hibernate.stat.CollectionStatistics - /** * Exposes Hibernate [Statistics] contract as JMX resource. */ @@ -20,6 +14,25 @@ interface StatisticsService : Statistics * session factory. */ class DelegatingStatisticsService(private val delegate: Statistics) : StatisticsService { + override fun getNaturalIdStatistics(entityName: String?): NaturalIdStatistics { + return delegate.getNaturalIdStatistics(entityName) + } + + override fun getDomainDataRegionStatistics(regionName: String?): CacheRegionStatistics { + return delegate.getDomainDataRegionStatistics(regionName) + } + + override fun getQueryRegionStatistics(regionName: String?): CacheRegionStatistics { + return delegate.getQueryRegionStatistics(regionName) + } + + override fun getNaturalIdQueryExecutionMaxTimeEntity(): String { + return delegate.getNaturalIdQueryExecutionMaxTimeEntity() + } + + override fun getCacheRegionStatistics(regionName: String?): CacheRegionStatistics { + return delegate.getCacheRegionStatistics(regionName) + } override fun clear() { delegate.clear() diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 5c59c1e28d..a815b34a0a 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -480,7 +480,8 @@ class NodeVaultService( // Even if we set the default pageNumber to be 1 instead, that may not cover the non-default cases. // So the floor may be necessary anyway. query.firstResult = maxOf(0, (paging.pageNumber - 1) * paging.pageSize) - query.maxResults = paging.pageSize + 1 // detection too many results + val pageSize = paging.pageSize + 1 + query.maxResults = if (pageSize > 0) pageSize else Integer.MAX_VALUE // detection too many results, protected against overflow // execution val results = query.resultList From f96a59932cc9265b6df16bb0ad00f8332c1ca3cd Mon Sep 17 00:00:00 2001 From: Tudor Malene Date: Wed, 15 Aug 2018 18:38:35 +0100 Subject: [PATCH 4/6] ENT-2222 Constraints propagation ENT-2222 Fix tests ENT-2222 Fix tests ENT-2222 Add ledger transaction verification logic ENT-2222 Fixed IRS test ENT-2222 Fixed IRS test ENT-2222 Fixed unit test ENT-2222 Better kdocs ENT-2222 Support for reference states ENT-2222 Fix support for reference states ENT-2222 Revert wrong change ENT-2222 Fix Kdoc ENT-2222 Fix Kdoc ENT-2222 Better docs ENT-2222 Address code review comments ENT-2222 Fix test ENT-2222 Fix rebase ENT-2222 Add documentation around constraint propagation ENT-2222 Add tests for contract propagation ENT-2222 Add Signature Constraints propagation - first draft ENT-2222 fix tests ENT-2222 more tests ENT-2222 unseal the TransactionVerificationException ENT-2222 unseal the TransactionVerificationException ENT-2222 more docs ENT-2222 address code review comments ENT-2222 address code review comments ENT-2222 re-implement transition logic ENT-2222 better comments and checks ENT-2222 Fix tests ENT-2222 merge fixes --- .../client/jackson/JacksonSupportTest.kt | 9 + .../core/contracts/AttachmentConstraint.kt | 126 +++++++- .../corda/core/contracts/TransactionState.kt | 3 +- .../TransactionVerificationException.kt | 15 +- .../corda/core/flows/SendTransactionFlow.kt | 2 +- .../core/transactions/LedgerTransaction.kt | 66 +++- .../core/transactions/TransactionBuilder.kt | 283 +++++++++++++++--- .../net/corda/core/utilities/KotlinUtils.kt | 9 +- .../test/kotlin/net/corda/core/UtilsTest.kt | 25 ++ .../contracts/ConstraintsPropagationTests.kt | 277 +++++++++++++++++ .../net/corda/core/node/VaultUpdateTests.kt | 10 +- .../TransactionSerializationTests.kt | 2 +- .../LedgerTransactionQueryTests.kt | 2 +- .../transactions/ReferenceInputStateTests.kt | 2 +- .../transactions/TransactionBuilderTest.kt | 21 +- docs/source/api-contract-constraints.rst | 125 +++++--- .../finance/contracts/asset/CashTests.kt | 2 +- .../contracts/asset/ObligationTests.kt | 2 +- ...tachmentsClassLoaderStaticContractTests.kt | 11 +- .../node/services/RaftNotaryServiceTests.kt | 2 +- .../cordapp/JarScanningCordappLoader.kt | 13 +- .../schema/PersistentStateServiceTests.kt | 7 +- .../ValidatingNotaryServiceTests.kt | 2 +- .../main/kotlin/net/corda/irs/contract/IRS.kt | 31 +- .../kotlin/net/corda/testing/dsl/TestDSL.kt | 8 +- .../testing/dsl/TransactionDSLInterpreter.kt | 32 +- .../testing/internal/MockCordappProvider.kt | 20 +- .../testing/services/MockAttachmentStorage.kt | 13 +- 28 files changed, 925 insertions(+), 195 deletions(-) create mode 100644 core/src/test/kotlin/net/corda/core/contracts/ConstraintsPropagationTests.kt diff --git a/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt index ae64ab4004..9b01e94d40 100644 --- a/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt +++ b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt @@ -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() + doReturn(attachmentStorage).whenever(services).attachments + val attachment = rigorousMock() + doReturn(attachment).whenever(attachmentStorage).openAttachment(attachmentId) + doReturn(attachmentId).whenever(attachment).id + doReturn(emptyList()).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)), diff --git a/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt b/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt index 76ffc8a866..f3022c319f 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt @@ -3,20 +3,80 @@ package net.corda.core.contracts import net.corda.core.DoNotImplement import net.corda.core.KeepForDJVM import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint.isSatisfiedBy -import net.corda.core.crypto.CompositeKey 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. */ @@ -48,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 } } -/** - * 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]. - * - * 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]. - */ @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") + throw UnsupportedOperationException("Contracts cannot be satisfied by an AutomaticHashConstraint placeholder.") + } +} + +/** + * 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. + * + * 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 AutomaticPlaceholderConstraint : AttachmentConstraint { + override fun isSatisfiedBy(attachment: Attachment): Boolean { + 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.") } } diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionState.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionState.kt index 862c457192..dadda29255 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionState.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionState.kt @@ -58,7 +58,8 @@ data class TransactionState @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>() } diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt index 29db493d86..e0dab5381f 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt @@ -36,7 +36,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) { /** @@ -50,6 +50,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. diff --git a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt index d84154eb26..33034efa07 100644 --- a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt @@ -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() \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index 7492641938..e326ddf7a0 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -61,6 +61,9 @@ data class LedgerTransaction @JvmOverloads constructor( val inputStates: List get() = inputs.map { it.state.data } val referenceStates: List get() = references.map { it.state.data } + private val inputAndOutputStates = inputs.asSequence().map { it.state } + outputs.asSequence() + private val allStates = inputAndOutputStates + references.asSequence().map { it.state } + /** * Returns the typed input StateAndRef at the specified index * @param index The index into the inputs. @@ -75,20 +78,22 @@ data class LedgerTransaction @JvmOverloads constructor( */ @Throws(TransactionVerificationException::class) fun verify() { + val contractAttachmentsByContract: Map = getUniqueContractAttachmentsByContract() + + // TODO - verify for version downgrade validateStatesAgainstContract() - verifyConstraints() + verifyConstraintsValidity(contractAttachmentsByContract) + verifyConstraints(contractAttachmentsByContract) verifyContracts() } - private fun allStates() = inputs.asSequence().map { it.state } + outputs.asSequence() - /** * For all input and output [TransactionState]s, validates that the wrapped [ContractState] matches up with the * wrapped [Contract], as declared by the [BelongsToContract] annotation on the [ContractState]'s class. * * A warning will be written to the log if any mismatch is detected. */ - private fun validateStatesAgainstContract() = allStates().forEach(::validateStateAgainstContract) + private fun validateStatesAgainstContract() = allStates.forEach(::validateStateAgainstContract) private fun validateStateAgainstContract(state: TransactionState) { state.data.requiredContractClassName?.let { requiredContractClassName -> @@ -101,20 +106,50 @@ data class LedgerTransaction @JvmOverloads constructor( } /** - * Verify that all contract constraints are valid for each state before running any contract code + * Enforces the validity of the actual constraints. + * * Constraints should be one of the valid supported ones. + * * Constraints should propagate correctly if not marked otherwise. + */ + private fun verifyConstraintsValidity(contractAttachmentsByContract: Map) { + // First check that the constraints are valid. + for (state in allStates) { + checkConstraintValidity(state) + } + + // Group the inputs and outputs by contract, and for each contract verify the constraints propagation logic. + // This is not required for reference states as there is nothing to propagate. + val inputContractGroups = inputs.groupBy { it.state.contract } + val outputContractGroups = outputs.groupBy { it.contract } + + for (contractClassName in (inputContractGroups.keys + outputContractGroups.keys)) { + if (contractClassName.contractHasAutomaticConstraintPropagation()) { + // Verify that the constraints of output states have at least the same level of restriction as the constraints of the corresponding input states. + val inputConstraints = inputContractGroups[contractClassName]?.map { it.state.constraint }?.toSet() + val outputConstraints = outputContractGroups[contractClassName]?.map { it.constraint }?.toSet() + outputConstraints?.forEach { outputConstraint -> + inputConstraints?.forEach { inputConstraint -> + if (!(outputConstraint.canBeTransitionedFrom(inputConstraint, contractAttachmentsByContract[contractClassName]!! ))) { + throw TransactionVerificationException.ConstraintPropagationRejection(id, contractClassName, inputConstraint, outputConstraint) + } + } + } + } else { + contractClassName.warnContractWithoutConstraintPropagation() + } + } + } + + /** + * Verify that all contract constraints are passing before running any contract code. * - * In case the transaction was created on this node then the attachments will contain the hash of the current cordapp jars. - * In case this verifies an older transaction or one originated on a different node, then this verifies that the attachments - * are valid. + * This check is running the [AttachmentConstraint.isSatisfiedBy] method for each corresponding [ContractAttachment]. * * @throws TransactionVerificationException if the constraints fail to verify */ - private fun verifyConstraints() { - val contractAttachmentsByContract = getUniqueContractAttachmentsByContract() - - for (state in allStates()) { - val contractAttachment = contractAttachmentsByContract[state.contract] ?: - throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract) + private fun verifyConstraints(contractAttachmentsByContract: Map) { + for (state in allStates) { + val contractAttachment = contractAttachmentsByContract[state.contract] + ?: throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract) val constraintAttachment = AttachmentWithContext(contractAttachment, state.contract, networkParameters?.whitelistedContractImplementations) @@ -152,7 +187,7 @@ data class LedgerTransaction @JvmOverloads constructor( * 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. */ - private fun verifyContracts() = allStates().forEach { ts -> + private fun verifyContracts() = inputAndOutputStates.forEach { ts -> val contractClass = getContractClass(ts) val contract = createContractInstance(contractClass) @@ -181,7 +216,6 @@ data class LedgerTransaction @JvmOverloads constructor( throw TransactionVerificationException.ContractCreationError(id, contractClass.name, e) } - /** * Make sure the notary has stayed the same. As we can't tell how inputs and outputs connect, if there * are any inputs or reference inputs, all outputs must have the same notary. diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index 7de546016a..8475afaf52 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -4,11 +4,12 @@ import co.paralleluniverse.strands.Strand import net.corda.core.CordaInternal import net.corda.core.DeleteForDJVM import net.corda.core.contracts.* -import net.corda.core.cordapp.CordappProvider import net.corda.core.crypto.* import net.corda.core.identity.Party +import net.corda.core.internal.AttachmentWithContext import net.corda.core.internal.FlowStateMachine import net.corda.core.internal.ensureMinimumPlatformVersion +import net.corda.core.internal.isUploaderTrusted import net.corda.core.node.NetworkParameters import net.corda.core.node.ServiceHub import net.corda.core.node.ServicesForResolution @@ -97,8 +98,7 @@ open class TransactionBuilder @JvmOverloads constructor( // DOCEND 1 /** - * Generates a [WireTransaction] from this builder and resolves any [AutomaticHashConstraint] on contracts to - * [HashAttachmentConstraint]. + * Generates a [WireTransaction] from this builder, resolves any [AutomaticPlaceholderConstraint], and selects the attachments to use for this transaction. * * @returns A new [WireTransaction] that will be unaffected by further changes to this [TransactionBuilder]. * @@ -114,21 +114,11 @@ open class TransactionBuilder @JvmOverloads constructor( services.ensureMinimumPlatformVersion(4, "Reference states") } - /** - * Resolves the [AutomaticHashConstraint]s to [HashAttachmentConstraint]s, - * [WhitelistedByZoneAttachmentConstraint]s or [SignatureAttachmentConstraint]s based on a global parameter. - * - * The [AutomaticHashConstraint] allows for less boiler plate when constructing transactions since for the - * typical case the named contract will be available when building the transaction. In exceptional cases the - * [TransactionStates] must be created with an explicit [AttachmentConstraint] - */ - val resolvedOutputs = outputs.map { state -> - state.withConstraint(when { - state.constraint !== AutomaticHashConstraint -> state.constraint - useWhitelistedByZoneAttachmentConstraint(state.contract, services.networkParameters) -> - WhitelistedByZoneAttachmentConstraint - else -> makeAttachmentConstraint(services, state) - }) + val (allContractAttachments: Collection, resolvedOutputs: List>) = selectContractAttachmentsAndOutputStateConstraints(services, serializationContext) + + // Final sanity check that all states have the correct constraints. + for (state in (inputsWithTransactionState + resolvedOutputs)) { + checkConstraintValidity(state) } return SerializationFactory.defaultFactory.withCurrentContext(serializationContext) { @@ -137,51 +127,250 @@ open class TransactionBuilder @JvmOverloads constructor( inputStates(), resolvedOutputs, commands, - attachments + makeContractAttachments(services.cordappProvider), + (allContractAttachments + attachments).toSortedSet().toList(), // Sort the attachments to ensure transaction builds are stable. notary, window, - referenceStates - ), + referenceStates), privacySalt ) } } - private fun TransactionState.withConstraint(newConstraint: AttachmentConstraint) = - if (newConstraint == constraint) this else copy(constraint = newConstraint) + /** + * This method is responsible for selecting the contract versions to be used for the current transaction and resolve the output state [AutomaticPlaceholderConstraint]s. + * The contract attachments are used to create a deterministic Classloader to deserialise the transaction and to run the contract verification. + * + * The selection logic depends on the Attachment Constraints of the input, output and reference states, also on the explicitly set attachments. + * TODO also on the versions of the attachments of the transactions generating the input states. ( after we add versioning) + */ + private fun selectContractAttachmentsAndOutputStateConstraints( + services: ServicesForResolution, serializationContext: SerializationContext?): Pair, List>> { - private fun makeAttachmentConstraint(services: ServicesForResolution, state: TransactionState): AttachmentConstraint { - val attachmentId = services.cordappProvider.getContractAttachmentID(state.contract) - ?: throw MissingContractAttachments(listOf(state)) + // Determine the explicitly set contract attachments. + val explicitAttachmentContracts: List> = this.attachments + .map(services.attachments::openAttachment) + .mapNotNull { it as? ContractAttachment } + .flatMap { attch -> + attch.allContracts.map { it to attch.id } + } - val attachmentSigners = services.attachments.openAttachment(attachmentId)?.signers - ?: throw MissingContractAttachments(listOf(state)) + // And fail early if there's more than 1 for a contract. + require(explicitAttachmentContracts.isEmpty() || explicitAttachmentContracts.groupBy { (ctr, _) -> ctr }.all { (_, groups) -> groups.size == 1 }) { "Multiple attachments set for the same contract." } - return when { - attachmentSigners.isEmpty() -> HashAttachmentConstraint(attachmentId) - else -> makeSignatureAttachmentConstraint(attachmentSigners) + val explicitAttachmentContractsMap: Map = explicitAttachmentContracts.toMap() + + val inputContractGroups: Map>> = inputsWithTransactionState.groupBy { it.contract } + val outputContractGroups: Map>> = outputs.groupBy { it.contract } + + val allContracts: Set = inputContractGroups.keys + outputContractGroups.keys + + // Handle reference states. + // Filter out all contracts that might have been already used by 'normal' input or output states. + val referenceStateGroups: Map>> = referencesWithTransactionState.groupBy { it.contract } + val refStateContractAttachments: List = referenceStateGroups + .filterNot { it.key in allContracts } + .map { refStateEntry -> + selectAttachmentThatSatisfiesConstraints(true, refStateEntry.key, refStateEntry.value.map { it.constraint }, services) + } + + // For each contract, resolve the AutomaticPlaceholderConstraint, and select the attachment. + val contractAttachmentsAndResolvedOutputStates: List>?>> = allContracts.toSet().map { ctr -> + handleContract(ctr, inputContractGroups[ctr], outputContractGroups[ctr], explicitAttachmentContractsMap[ctr], serializationContext, services) } + + val resolvedStates: List> = contractAttachmentsAndResolvedOutputStates.mapNotNull { it.second }.flatten() + + // The output states need to preserve the order in which they were added. + val resolvedOutputStatesInTheOriginalOrder: List> = outputStates().map { os -> resolvedStates.find { rs -> rs.data == os.data }!! } + + val attachments: Collection = contractAttachmentsAndResolvedOutputStates.map { it.first } + refStateContractAttachments + + return Pair(attachments, resolvedOutputStatesInTheOriginalOrder) + } + + private val automaticConstraints = setOf(AutomaticPlaceholderConstraint, AutomaticHashConstraint) + + /** + * Selects an attachment and resolves the constraints for the output states with [AutomaticPlaceholderConstraint]. + * + * This is the place where the complex logic of the upgradability of contracts and constraint propagation is handled. + * + * * For contracts that *are not* annotated with @[NoConstraintPropagation], this will attempt to determine a constraint for the output states + * that is a valid transition from all the constraints of the input states. + * + * * For contracts that *are* annotated with @[NoConstraintPropagation], this enforces setting an explicit output constraint. + * + * * For states with the [HashAttachmentConstraint], if an attachment with that hash is installed on the current node, then it will be inherited by the output states and selected for the transaction. + * Otherwise a [MissingContractAttachments] is thrown. + * + * * For input states with [WhitelistedByZoneAttachmentConstraint] or a [AlwaysAcceptAttachmentConstraint] implementations, then the currently installed cordapp version is used. + */ + private fun handleContract( + contractClassName: ContractClassName, + inputStates: List>?, + outputStates: List>?, + explicitContractAttachment: AttachmentId?, + serializationContext: SerializationContext?, + services: ServicesForResolution + ): Pair>?> { + val inputsAndOutputs = (inputStates ?: emptyList()) + (outputStates ?: emptyList()) + + // Determine if there are any HashConstraints that pin the version of a contract. If there are, check if we trust them. + val hashAttachments = inputsAndOutputs + .filter { it.constraint is HashAttachmentConstraint } + .map { state -> + val attachment = services.attachments.openAttachment((state.constraint as HashAttachmentConstraint).attachmentId) + if (attachment == null || attachment !is ContractAttachment || !isUploaderTrusted(attachment.uploader)) { + // This should never happen because these are input states that should have been validated already. + throw MissingContractAttachments(listOf(state)) + } + attachment + }.toSet() + + // Check that states with the HashConstraint don't conflict between themselves or with an explicitly set attachment. + require(hashAttachments.size <= 1) { + "Transaction was built with $contractClassName states with multiple HashConstraints. This is illegal, because it makes it impossible to validate with a single version of the contract code." + } + if (explicitContractAttachment != null && hashAttachments.singleOrNull() != null) { + require(explicitContractAttachment == (hashAttachments.single() as ContractAttachment).attachment.id) { + "An attachment has been explicitly set for contract $contractClassName in the transaction builder which conflicts with the HashConstraint of a state." + } + } + + // This will contain the hash of the JAR that *has* to be used by this Transaction, because it is explicit. Or null if none. + val forcedAttachmentId = explicitContractAttachment ?: hashAttachments.singleOrNull()?.id + + fun selectAttachment() = selectAttachmentThatSatisfiesConstraints( + false, + contractClassName, + inputsAndOutputs.map { it.constraint }.toSet().filterNot { it in automaticConstraints }, + services) + + // This will contain the hash of the JAR that will be used by this Transaction. + val selectedAttachmentId = forcedAttachmentId ?: selectAttachment() + + val attachmentToUse = services.attachments.openAttachment(selectedAttachmentId)?.let { it as ContractAttachment } + ?: throw IllegalArgumentException("Contract attachment $selectedAttachmentId for $contractClassName is missing.") + + // For Exit transactions (no output states) there is no need to resolve the output constraints. + if (outputStates == null) { + return Pair(selectedAttachmentId, null) + } + + // If there are no automatic constraints, there is nothing to resolve. + if (outputStates.none { it.constraint in automaticConstraints }) { + return Pair(selectedAttachmentId, outputStates) + } + + // The final step is to resolve AutomaticPlaceholderConstraint. + val automaticConstraintPropagation = contractClassName.contractHasAutomaticConstraintPropagation(serializationContext?.deserializationClassLoader) + + // When automaticConstraintPropagation is disabled for a contract, output states must an explicit Constraint. + require(automaticConstraintPropagation) { "Contract $contractClassName was marked with @NoConstraintPropagation, which means the constraint of the output states has to be set explicitly." } + + // This is the logic to determine the constraint which will replace the AutomaticPlaceholderConstraint. + val defaultOutputConstraint = selectAttachmentConstraint(contractClassName, inputStates, attachmentToUse, services) + + // Sanity check that the selected attachment actually passes. + val constraintAttachment = AttachmentWithContext(attachmentToUse, contractClassName, services.networkParameters.whitelistedContractImplementations) + require(defaultOutputConstraint.isSatisfiedBy(constraintAttachment)) { "Selected output constraint: $defaultOutputConstraint not satisfying $selectedAttachmentId" } + + val resolvedOutputStates = outputStates.map { + val outputConstraint = it.constraint + if (outputConstraint in automaticConstraints) { + it.copy(constraint = defaultOutputConstraint) + } else { + // If the constraint on the output state is already set, and is not a valid transition or can't be transitioned, then fail early. + inputStates?.forEach { input -> + require(outputConstraint.canBeTransitionedFrom(input.constraint, attachmentToUse)) { "Output state constraint $outputConstraint cannot be transitions from ${input.constraint}" } + } + require(outputConstraint.isSatisfiedBy(constraintAttachment)) { "Output state constraint check fails. $outputConstraint" } + it + } + } + + return Pair(selectedAttachmentId, resolvedOutputStates) + } + + /** + * If there are multiple input states with different constraints then run the constraint intersection logic to determine the resulting output constraint. + * For issuing transactions where the attachmentToUse is JarSigned, then default to the SignatureConstraint with all the signatures. + * TODO - in the future this step can actually create a new ContractAttachment by merging 2 signed jars of the same version. + */ + private fun selectAttachmentConstraint( + contractClassName: ContractClassName, + inputStates: List>?, + attachmentToUse: ContractAttachment, + services: ServicesForResolution): AttachmentConstraint = when { + inputStates != null -> attachmentConstraintsTransition(inputStates.groupBy { it.constraint }.keys, attachmentToUse) + useWhitelistedByZoneAttachmentConstraint(contractClassName, services.networkParameters) -> WhitelistedByZoneAttachmentConstraint + attachmentToUse.signers.isNotEmpty() -> makeSignatureAttachmentConstraint(attachmentToUse.signers) + else -> HashAttachmentConstraint(attachmentToUse.id) + } + + /** + * Given a set of [AttachmentConstraint]s, this function implements the rules on how constraints can evolve. + * + * This should be an exhaustive check, and should mirror [AttachmentConstraint.canBeTransitionedFrom]. + * + * TODO - once support for third party signing is added, it should be implemented here. ( a constraint with 2 signatures is less restrictive than a constraint with 1 more signature) + */ + private fun attachmentConstraintsTransition(constraints: Set, attachmentToUse: ContractAttachment): AttachmentConstraint = when { + + // Sanity check. + constraints.isEmpty() -> throw IllegalArgumentException("Cannot transition from no constraints.") + + // When all input states have the same constraint. + constraints.size == 1 -> constraints.single() + + // Fail when combining the insecure AlwaysAcceptAttachmentConstraint with something else. The size must be at least 2 at this point. + constraints.any { it is AlwaysAcceptAttachmentConstraint } -> + throw IllegalArgumentException("Can't mix the AlwaysAcceptAttachmentConstraint with a secure constraint in the same transaction. This can be used to hide insecure transitions.") + + // Multiple states with Hash constraints with different hashes. This should not happen as we checked already. + constraints.all { it is HashAttachmentConstraint } -> + throw IllegalArgumentException("Cannot mix HashConstraints with different hashes in the same transaction.") + + // The HashAttachmentConstraint is the strongest constraint, so it wins when mixed with anything. As long as the actual constraints pass. + // TODO - this could change if we decide to introduce a way to gracefully migrate from the Hash Constraint to the Signature Constraint. + constraints.any { it is HashAttachmentConstraint } -> constraints.find { it is HashAttachmentConstraint }!! + + // TODO, we don't currently support mixing signature constraints with different signers. This will change once we introduce third party signers. + constraints.all { it is SignatureAttachmentConstraint } -> + throw IllegalArgumentException("Cannot mix SignatureAttachmentConstraints signed by different parties in the same transaction.") + + // This ensures a smooth migration from the Whitelist Constraint, given that for the transaction to be valid it still has to pass both constraints. + // The transition is possible only when the SignatureConstraint contains ALL signers from the attachment. + constraints.any { it is SignatureAttachmentConstraint } && constraints.any { it is WhitelistedByZoneAttachmentConstraint } -> { + val signatureConstraint = constraints.mapNotNull { it as? SignatureAttachmentConstraint }.single() + when { + attachmentToUse.signers.isEmpty() -> throw IllegalArgumentException("Cannot mix a state with the WhitelistedByZoneAttachmentConstraint and a state with the SignatureAttachmentConstraint, when the latest attachment is not signed. Please contact your Zone operator.") + signatureConstraint.key.keys.containsAll(attachmentToUse.signers) -> signatureConstraint + else -> throw IllegalArgumentException("Attempting to transition a WhitelistedByZoneAttachmentConstraint state backed by an attachment signed by multiple parties to a weaker SignatureConstraint that does not require all those signatures. Please contact your Zone operator.") + } + } + + else -> throw IllegalArgumentException("Unexpected constraints $constraints.") } private fun makeSignatureAttachmentConstraint(attachmentSigners: List) = SignatureAttachmentConstraint(CompositeKey.Builder().addKeys(attachmentSigners.map { it }).build()) - private fun useWhitelistedByZoneAttachmentConstraint(contractClassName: ContractClassName, networkParameters: NetworkParameters) = - contractClassName in networkParameters.whitelistedContractImplementations.keys - /** - * The attachments added to the current transaction contain only the hashes of the current cordapps. - * NOT the hashes of the cordapps that were used when the input states were created ( in case they changed in the meantime) - * TODO - review this logic + * This method should only be called for upgradeable contracts. + * + * For now we use the currently installed CorDapp version. + * TODO - When the SignatureConstraint and contract version logic is in, this will need to query the attachments table and find the latest one that satisfies all constraints. + * TODO - select a version of the contract that is no older than the one from the previous transactions. */ - private fun makeContractAttachments(cordappProvider: CordappProvider): List { - // Reference inputs not included as it is not necessary to verify them. - return (inputsWithTransactionState + outputs).map { state -> - cordappProvider.getContractAttachmentID(state.contract) - ?: throw MissingContractAttachments(listOf(state)) - }.distinct() + private fun selectAttachmentThatSatisfiesConstraints(isReference: Boolean, contractClassName: String, constraints: List, services: ServicesForResolution): AttachmentId { + require(constraints.none { it in automaticConstraints }) + require(isReference || constraints.none { it is HashAttachmentConstraint }) + return services.cordappProvider.getContractAttachmentID(contractClassName)!! } + private fun useWhitelistedByZoneAttachmentConstraint(contractClassName: ContractClassName, networkParameters: NetworkParameters) = contractClassName in networkParameters.whitelistedContractImplementations.keys + @Throws(AttachmentResolutionException::class, TransactionResolutionException::class) fun toLedgerTransaction(services: ServiceHub) = toWireTransaction(services).toLedgerTransaction(services) @@ -272,10 +461,8 @@ with @BelongsToContract, or supply an explicit contract parameter to addOutputSt """.trimIndent().replace('\n', ' ') }, notary: Party, encumbrance: Int? = null, - constraint: AttachmentConstraint = AutomaticHashConstraint - ): TransactionBuilder { - return addOutputState(TransactionState(state, contract, notary, encumbrance, constraint)) - } + constraint: AttachmentConstraint = AutomaticPlaceholderConstraint + ) = addOutputState(TransactionState(state, contract, notary, encumbrance, constraint)) /** A default notary must be specified during builder construction to use this method */ @JvmOverloads @@ -289,7 +476,7 @@ Unable to infer Contract class name because state class ${state::class.java.name with @BelongsToContract, or supply an explicit contract parameter to addOutputState(). """.trimIndent().replace('\n', ' ') }, - constraint: AttachmentConstraint = AutomaticHashConstraint + constraint: AttachmentConstraint = AutomaticPlaceholderConstraint ) = apply { checkNotNull(notary) { "Need to specify a notary for the state, or set a default one on TransactionBuilder initialisation" diff --git a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt index 849c937b8e..26d4fbce11 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt @@ -9,6 +9,7 @@ import net.corda.core.serialization.CordaSerializable import org.slf4j.Logger import org.slf4j.LoggerFactory import java.time.Duration +import java.util.* import java.util.concurrent.ExecutionException import java.util.concurrent.Future import kotlin.reflect.KProperty @@ -135,12 +136,16 @@ fun Future.getOrThrow(timeout: Duration? = null): V = try { throw e.cause!! } -private val warnings = mutableSetOf() +private const val MAX_SIZE = 100 +private val warnings = Collections.newSetFromMap(object : LinkedHashMap() { + override fun removeEldestEntry(eldest: MutableMap.MutableEntry?) = size > MAX_SIZE +}) /** * Utility to help log a warning message only once. - * It's not thread safe, as in the worst case the warning will be logged twice. + * It implements an ad hoc Fifo cache because there's none available in the standard libraries. */ +@Synchronized fun Logger.warnOnce(warning: String) { if (warning !in warnings) { warnings.add(warning) diff --git a/core/src/test/kotlin/net/corda/core/UtilsTest.kt b/core/src/test/kotlin/net/corda/core/UtilsTest.kt index 717356f4d4..064fd0c016 100644 --- a/core/src/test/kotlin/net/corda/core/UtilsTest.kt +++ b/core/src/test/kotlin/net/corda/core/UtilsTest.kt @@ -1,8 +1,13 @@ package net.corda.core +import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockito_kotlin.times +import com.nhaarman.mockito_kotlin.verify import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.warnOnce import org.assertj.core.api.Assertions.* import org.junit.Test +import org.slf4j.Logger import rx.subjects.PublishSubject import java.util.* import java.util.concurrent.CancellationException @@ -58,4 +63,24 @@ class UtilsTest { future.get() } } + + @Test + fun `warnOnce works, but the backing cache grows only to a maximum size`() { + val MAX_SIZE = 100 + + val logger = mock() + logger.warnOnce("a") + logger.warnOnce("b") + logger.warnOnce("b") + + // This should cause the eviction of "a". + (1..MAX_SIZE).forEach { logger.warnOnce("$it") } + logger.warnOnce("a") + + // "a" should be logged twice because it was evicted. + verify(logger, times(2)).warn("a") + + // "b" should be logged only once because there was no eviction. + verify(logger, times(1)).warn("b") + } } diff --git a/core/src/test/kotlin/net/corda/core/contracts/ConstraintsPropagationTests.kt b/core/src/test/kotlin/net/corda/core/contracts/ConstraintsPropagationTests.kt new file mode 100644 index 0000000000..9417f14caa --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/contracts/ConstraintsPropagationTests.kt @@ -0,0 +1,277 @@ +package net.corda.core.contracts + +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockito_kotlin.whenever +import net.corda.core.crypto.SecureHash +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.CordaX500Name +import net.corda.core.transactions.LedgerTransaction +import net.corda.core.transactions.MissingContractAttachments +import net.corda.finance.POUNDS +import net.corda.finance.`issued by` +import net.corda.finance.contracts.asset.Cash +import net.corda.node.services.api.IdentityServiceInternal +import net.corda.testing.common.internal.testNetworkParameters +import net.corda.testing.core.DUMMY_NOTARY_NAME +import net.corda.testing.core.SerializationEnvironmentRule +import net.corda.testing.core.TestIdentity +import net.corda.testing.internal.rigorousMock +import net.corda.testing.node.MockServices +import net.corda.testing.node.ledger +import org.junit.Rule +import org.junit.Test +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class ConstraintsPropagationTests { + + private companion object { + val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party + val ALICE = TestIdentity(CordaX500Name("ALICE", "London", "GB")) + val ALICE_PARTY get() = ALICE.party + val ALICE_PUBKEY get() = ALICE.publicKey + val BOB = TestIdentity(CordaX500Name("BOB", "London", "GB")) + val BOB_PARTY get() = BOB.party + val BOB_PUBKEY get() = BOB.publicKey + val noPropagationContractClassName = "net.corda.core.contracts.NoPropagationContract" + } + + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule() + + private val ledgerServices = MockServices( + cordappPackages = listOf("net.corda.finance.contracts.asset"), + initialIdentity = ALICE, + identityService = rigorousMock().also { + doReturn(ALICE_PARTY).whenever(it).partyFromKey(ALICE_PUBKEY) + doReturn(BOB_PARTY).whenever(it).partyFromKey(BOB_PUBKEY) + }, + networkParameters = testNetworkParameters(minimumPlatformVersion = 4) + .copy(whitelistedContractImplementations = mapOf( + Cash.PROGRAM_ID to listOf(SecureHash.zeroHash, SecureHash.allOnesHash), + noPropagationContractClassName to listOf(SecureHash.zeroHash))) + ) + + @Test + fun `Happy path with the HashConstraint`() { + ledgerServices.ledger(DUMMY_NOTARY) { + transaction { + attachment(Cash.PROGRAM_ID, SecureHash.allOnesHash) + output(Cash.PROGRAM_ID, "c1", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.allOnesHash), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), ALICE_PARTY)) + command(ALICE_PUBKEY, Cash.Commands.Issue()) + verifies() + } + transaction { + attachment(Cash.PROGRAM_ID, SecureHash.allOnesHash) + input("c1") + output(Cash.PROGRAM_ID, "c2", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.allOnesHash), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY)) + command(ALICE_PUBKEY, Cash.Commands.Move()) + verifies() + } + } + } + + @Test + fun `Fail early in the TransactionBuilder when attempting to change the hash of the HashConstraint on the spending transaction`() { + ledgerServices.ledger(DUMMY_NOTARY) { + transaction { + attachment(Cash.PROGRAM_ID, SecureHash.zeroHash) + output(Cash.PROGRAM_ID, "c1", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.zeroHash), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), ALICE_PARTY)) + command(ALICE_PUBKEY, Cash.Commands.Issue()) + verifies() + } + assertFailsWith { + transaction { + attachment(Cash.PROGRAM_ID, SecureHash.allOnesHash) + input("c1") + output(Cash.PROGRAM_ID, "c2", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.allOnesHash), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY)) + command(ALICE_PUBKEY, Cash.Commands.Move()) + verifies() + } + } + } + } + + @Test + fun `Transaction validation fails, when constraints do not propagate correctly`() { + ledgerServices.ledger(DUMMY_NOTARY) { + transaction { + attachment(Cash.PROGRAM_ID, SecureHash.zeroHash) + output(Cash.PROGRAM_ID, "c1", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.zeroHash), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), ALICE_PARTY)) + command(ALICE_PUBKEY, Cash.Commands.Issue()) + verifies() + } + transaction { + attachment(Cash.PROGRAM_ID, SecureHash.zeroHash) + input("c1") + output(Cash.PROGRAM_ID, "c2", DUMMY_NOTARY, null, WhitelistedByZoneAttachmentConstraint, Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY)) + command(ALICE_PUBKEY, Cash.Commands.Move()) + failsWith("are not propagated correctly") + } + transaction { + attachment(Cash.PROGRAM_ID, SecureHash.zeroHash) + input("c1") + output(Cash.PROGRAM_ID, "c3", DUMMY_NOTARY, null, SignatureAttachmentConstraint(ALICE_PUBKEY), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY)) + command(ALICE_PUBKEY, Cash.Commands.Move()) + failsWith("are not propagated correctly") + } + transaction { + attachment(Cash.PROGRAM_ID, SecureHash.zeroHash) + input("c1") + output(Cash.PROGRAM_ID, "c4", DUMMY_NOTARY, null, AlwaysAcceptAttachmentConstraint, Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY)) + command(ALICE_PUBKEY, Cash.Commands.Move()) + failsWith("are not propagated correctly") + } + } + } + + @Test + fun `When the constraint of the output state is a valid transition from the input state, transaction validation works`() { + ledgerServices.ledger(DUMMY_NOTARY) { + transaction { + attachment(Cash.PROGRAM_ID, SecureHash.zeroHash) + output(Cash.PROGRAM_ID, "c1", DUMMY_NOTARY, null, WhitelistedByZoneAttachmentConstraint, Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), ALICE_PARTY)) + command(ALICE_PUBKEY, Cash.Commands.Issue()) + verifies() + } + transaction { + attachment(Cash.PROGRAM_ID, SecureHash.zeroHash) + input("c1") + output(Cash.PROGRAM_ID, "c2", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.zeroHash), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY)) + command(ALICE_PUBKEY, Cash.Commands.Move()) + verifies() + } + } + } + + @Test + fun `Switching from the WhitelistConstraint to the Signature Constraint is possible if the attachment satisfies both constraints, and the signature constraint inherits all jar signatures`() { + + ledgerServices.ledger(DUMMY_NOTARY) { + transaction { + attachment(Cash.PROGRAM_ID, SecureHash.zeroHash) + output(Cash.PROGRAM_ID, "w1", DUMMY_NOTARY, null, WhitelistedByZoneAttachmentConstraint, Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), ALICE_PARTY)) + command(ALICE_PUBKEY, Cash.Commands.Issue()) + verifies() + } + + // the attachment is signed + transaction { + attachment(Cash.PROGRAM_ID, SecureHash.allOnesHash, listOf(ALICE_PARTY.owningKey)) + input("w1") + output(Cash.PROGRAM_ID, "w2", DUMMY_NOTARY, null, SignatureAttachmentConstraint(ALICE_PUBKEY), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY)) + command(ALICE_PUBKEY, Cash.Commands.Move()) + verifies() + } + } + } + + @Test + fun `Switching from the WhitelistConstraint to the Signature Constraint fails if the signature constraint does not inherit all jar signatures`() { + ledgerServices.ledger(DUMMY_NOTARY) { + transaction { + attachment(Cash.PROGRAM_ID, SecureHash.zeroHash) + output(Cash.PROGRAM_ID, "w1", DUMMY_NOTARY, null, WhitelistedByZoneAttachmentConstraint, Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), ALICE_PARTY)) + command(ALICE_PUBKEY, Cash.Commands.Issue()) + verifies() + } + + // the attachment is not signed + transaction { + attachment(Cash.PROGRAM_ID, SecureHash.zeroHash) + input("w1") + output(Cash.PROGRAM_ID, "w2", DUMMY_NOTARY, null, SignatureAttachmentConstraint(ALICE_PUBKEY), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY)) + command(ALICE_PUBKEY, Cash.Commands.Move()) + // Note that it fails after the constraints propagation check, because the attachment is not signed. + failsWith("are not propagated correctly") + } + } + } + + @Test + fun `On contract annotated with NoConstraintPropagation there is no platform check for propagation, but the transaction builder can't use the AutomaticPlaceholderConstraint`() { + ledgerServices.ledger(DUMMY_NOTARY) { + transaction { + attachment(noPropagationContractClassName, SecureHash.zeroHash) + output(noPropagationContractClassName, "c1", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.zeroHash), NoPropagationContractState()) + command(ALICE_PUBKEY, NoPropagationContract.Create()) + verifies() + } + transaction { + attachment(noPropagationContractClassName, SecureHash.zeroHash) + input("c1") + output(noPropagationContractClassName, "c2", DUMMY_NOTARY, null, WhitelistedByZoneAttachmentConstraint, NoPropagationContractState()) + command(ALICE_PUBKEY, NoPropagationContract.Create()) + verifies() + } + assertFailsWith { + transaction { + attachment(noPropagationContractClassName, SecureHash.zeroHash) + input("c1") + output(noPropagationContractClassName, "c3", DUMMY_NOTARY, null, AutomaticPlaceholderConstraint, NoPropagationContractState()) + command(ALICE_PUBKEY, NoPropagationContract.Create()) + verifies() + } + } + } + } + + @Test + fun `Attachment canBeTransitionedFrom behaves as expected`() { + + val attachment = mock() + whenever(attachment.signers).thenReturn(listOf(ALICE_PARTY.owningKey)) + + // Exhaustive positive check + assertTrue(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment)) + assertTrue(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment)) + + assertTrue(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment)) + assertTrue(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment)) + + assertTrue(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment)) + + assertTrue(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment)) + + // Exhaustive negative check + assertFalse(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment)) + assertFalse(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment)) + assertFalse(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment)) + + assertFalse(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment)) + + assertFalse(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment)) + assertFalse(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment)) + + assertFalse(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment)) + assertFalse(SignatureAttachmentConstraint(BOB_PUBKEY).canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment)) + assertFalse(SignatureAttachmentConstraint(BOB_PUBKEY).canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment)) + + assertFalse(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment)) + assertFalse(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment)) + assertFalse(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment)) + + // Fail when encounter a AutomaticPlaceholderConstraint + assertFailsWith { HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(AutomaticPlaceholderConstraint, attachment) } + assertFailsWith { AutomaticPlaceholderConstraint.canBeTransitionedFrom(AutomaticPlaceholderConstraint, attachment) } + } +} + +@BelongsToContract(NoPropagationContract::class) +class NoPropagationContractState : ContractState { + override val participants: List + get() = emptyList() +} + +@NoConstraintPropagation +class NoPropagationContract : Contract { + interface Commands : CommandData + class Create : Commands + + override fun verify(tx: LedgerTransaction) { + //do nothing + } +} diff --git a/core/src/test/kotlin/net/corda/core/node/VaultUpdateTests.kt b/core/src/test/kotlin/net/corda/core/node/VaultUpdateTests.kt index 0b9db73064..750a76af04 100644 --- a/core/src/test/kotlin/net/corda/core/node/VaultUpdateTests.kt +++ b/core/src/test/kotlin/net/corda/core/node/VaultUpdateTests.kt @@ -35,11 +35,11 @@ class VaultUpdateTests { private val stateRef3 = StateRef(SecureHash.randomSHA256(), 3) private val stateRef4 = StateRef(SecureHash.randomSHA256(), 4) - private val stateAndRef0 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY), stateRef0) - private val stateAndRef1 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY), stateRef1) - private val stateAndRef2 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY), stateRef2) - private val stateAndRef3 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY), stateRef3) - private val stateAndRef4 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY), stateRef4) + private val stateAndRef0 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), stateRef0) + private val stateAndRef1 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), stateRef1) + private val stateAndRef2 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), stateRef2) + private val stateAndRef3 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), stateRef3) + private val stateAndRef4 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), stateRef4) @Test fun `nothing plus nothing is nothing`() { diff --git a/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt b/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt index fa50d3f273..6504ae07cb 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt @@ -63,7 +63,7 @@ class TransactionSerializationTests { // It refers to a fake TX/state that we don't bother creating here. val depositRef = MINI_CORP.ref(1) val fakeStateRef = generateStateRef() - val inputState = StateAndRef(TransactionState(TestCash.State(depositRef, 100.POUNDS, MEGA_CORP), TEST_CASH_PROGRAM_ID, DUMMY_NOTARY), fakeStateRef) + val inputState = StateAndRef(TransactionState(TestCash.State(depositRef, 100.POUNDS, MEGA_CORP), TEST_CASH_PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), fakeStateRef ) val outputState = TransactionState(TestCash.State(depositRef, 600.POUNDS, MEGA_CORP), TEST_CASH_PROGRAM_ID, DUMMY_NOTARY) val changeState = TransactionState(TestCash.State(depositRef, 400.POUNDS, MEGA_CORP), TEST_CASH_PROGRAM_ID, DUMMY_NOTARY) val megaCorpServices = MockServices(listOf("net.corda.core.serialization"), MEGA_CORP.name, rigorousMock(), MEGA_CORP_KEY) diff --git a/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt b/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt index 67a04ac5f3..45bf4e4c5d 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt @@ -73,7 +73,7 @@ class LedgerTransactionQueryTests { ) services.recordTransactions(fakeIssueTx) val dummyStateRef = StateRef(fakeIssueTx.id, 0) - return StateAndRef(TransactionState(dummyState, DummyContract.PROGRAM_ID, DUMMY_NOTARY, null), dummyStateRef) + return StateAndRef(TransactionState(dummyState, DummyContract.PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), dummyStateRef) } private fun makeDummyTransaction(): LedgerTransaction { diff --git a/core/src/test/kotlin/net/corda/core/transactions/ReferenceInputStateTests.kt b/core/src/test/kotlin/net/corda/core/transactions/ReferenceInputStateTests.kt index 8b09b574ca..db120fa2e1 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/ReferenceInputStateTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/ReferenceInputStateTests.kt @@ -187,7 +187,7 @@ class ReferenceStateTests { @Test fun `state ref cannot be a reference input and regular input in the same transaction`() { val state = ExampleState(ALICE_PARTY, "HELLO CORDA") - val stateAndRef = StateAndRef(TransactionState(state, CONTRACT_ID, DUMMY_NOTARY), StateRef(SecureHash.zeroHash, 0)) + val stateAndRef = StateAndRef(TransactionState(state, CONTRACT_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), StateRef(SecureHash.zeroHash, 0)) assertFailsWith(IllegalArgumentException::class, "A StateRef cannot be both an input and a reference input in the same transaction.") { @Suppress("DEPRECATION") // To be removed when feature is finalised. TransactionBuilder(notary = DUMMY_NOTARY).addInputState(stateAndRef).addReferenceState(stateAndRef.referenced()) diff --git a/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt b/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt index aa1d6240d0..ec8cfe1178 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt @@ -41,7 +41,15 @@ class TransactionBuilderTest { doReturn(cordappProvider).whenever(services).cordappProvider doReturn(contractAttachmentId).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID) doReturn(testNetworkParameters()).whenever(services).networkParameters - doReturn(attachments).whenever(services).attachments + + val attachmentStorage = rigorousMock() + doReturn(attachmentStorage).whenever(services).attachments + val attachment = rigorousMock() + doReturn(attachment).whenever(attachmentStorage).openAttachment(contractAttachmentId) + doReturn(contractAttachmentId).whenever(attachment).id + doReturn(setOf(DummyContract.PROGRAM_ID)).whenever(attachment).allContracts + doReturn("app").whenever(attachment).uploader + doReturn(emptyList()).whenever(attachment).signers } @Test @@ -104,6 +112,7 @@ class TransactionBuilderTest { assertTrue(expectedConstraint.isSatisfiedBy(signedAttachment)) assertFalse(expectedConstraint.isSatisfiedBy(unsignedAttachment)) + doReturn(attachments).whenever(services).attachments doReturn(signedAttachment).whenever(attachments).openAttachment(contractAttachmentId) val outputState = TransactionState(data = DummyState(), contract = DummyContract.PROGRAM_ID, notary = notary) @@ -113,19 +122,17 @@ class TransactionBuilderTest { val wtx = builder.toWireTransaction(services) assertThat(wtx.outputs).containsOnly(outputState.copy(constraint = expectedConstraint)) - } - - private val unsignedAttachment = object : AbstractAttachment({ byteArrayOf() }) { + private val unsignedAttachment = ContractAttachment(object : AbstractAttachment({ byteArrayOf() }) { override val id: SecureHash get() = throw UnsupportedOperationException() override val signers: List get() = emptyList() - } + }, DummyContract.PROGRAM_ID) - private fun signedAttachment(vararg parties: Party) = object : AbstractAttachment({ byteArrayOf() }) { + private fun signedAttachment(vararg parties: Party) = ContractAttachment(object : AbstractAttachment({ byteArrayOf() }) { override val id: SecureHash get() = throw UnsupportedOperationException() override val signers: List get() = parties.map { it.owningKey } - } + }, DummyContract.PROGRAM_ID) } diff --git a/docs/source/api-contract-constraints.rst b/docs/source/api-contract-constraints.rst index 3863926981..4387de90c4 100644 --- a/docs/source/api-contract-constraints.rst +++ b/docs/source/api-contract-constraints.rst @@ -19,22 +19,31 @@ isn't allowed unless you're a cash issuer - otherwise you could print money for For a transaction to be valid, the ``verify`` function associated with each state must run successfully. However, for this to be secure, it is not sufficient to specify the ``verify`` function by name as there may exist multiple different implementations with the same method signature and enclosing class. This normally will happen as applications -evolve, but could also happen maliciously. +evolve, but could also happen maliciously as anyone can create a JAR with a class of that name. -Contract constraints solve this problem by allowing a contract developer to constrain which ``verify`` functions out of -the universe of implementations can be used (i.e. the universe is everything that matches the signature and contract +Contract constraints solve this problem by allowing a state creator to constrain which ``verify`` functions out of +the universe of implementations can be used (i.e. the universe is everything that matches the class name and contract constraints restrict this universe to a subset). Constraints are satisfied by attachments (JARs). You are not allowed to attach two JARs that both define the same application due to the *no overlap rule*. This rule specifies that two attachment JARs may not provide the same file path. If they do, the transaction is considered invalid. Because each state specifies both a constraint over attachments *and* a Contract class name to use, the specified class must appear in only one attachment. -So who picks the attachment to use? It is chosen by the creator of the transaction that has to satisfy input constraints. -The transaction creator also gets to pick the constraints used by any output states, but the contract logic itself may -have opinions about what those constraints are - a typical contract would require that the constraints are propagated, -that is, the contract will not just enforce the validity of the next transaction that uses a state, but *all successive -transactions as well*. The constraints mechanism creates a balance of power between the creator of data on -the ledger and the user who is trying to edit it, which can be very useful when managing upgrades to Corda applications. +Recap: A corda transaction transitions input states to output states. Each state is composed of data, the name of the class that verifies the transition(contract), and +the contract constraint. The transaction also contains a list of attachments (normal JARs) from where these classes will be loaded. There must be only one JAR containing each contract. +The contract constraints are responsible to ensure the attachment JARs are following the rules set by the creators of the input states (in a continuous chain to the issue). +This way, we have both valid data and valid code that checks the transition packed into the transaction. + +So who picks the attachment to use? It is chosen by the creator of the transaction but has to satisfy the constraints of the input states. +This is because any node doing transaction resolution will actually verify the selected attachment against all constraints, +so the transaction will only be valid if it passes those checks. +For example, when the input state is constrained by the ``HashAttachmentConstraint``, can only attach the JAR with that hash to the transaction. + +The transaction creator also gets to pick the constraints used by any output states. +When building a transaction, the default constraint on output states is ``AutomaticPlaceholderConstraint``, which means that corda will select the appropriate constraint. +Unless specified otherwise, attachment constraints will propagate from input to output states. (The rules are described below) +Constraint propagation is also enforced during transaction verification, where for normal transactions (not explicit upgrades, or notary changes), +the constraints of the output states are required to "inherit" the constraint of the input states. ( See below for details) There are two ways of handling upgrades to a smart contract in Corda: @@ -84,18 +93,19 @@ time effectively stop being a part of the network. signed by a specified identity, via the regular Java ``jarsigner`` tool. This will be the most flexible type and the smoothest to deploy: no restarts or contract upgrade transactions are needed. -**Defaults.** The default constraint type is either a zone constraint, if the network parameters in effect when the -transaction is built contain an entry for that contract class, or a hash constraint if not. +**Defaults.** Currently, the default constraint type is either a zone constraint, if the network parameters in effect when the +transaction is built contain an entry for that contract class, or a hash constraint if not. Once the Signature Constraints are introduced, +the default constraint will be the Signature Constraint if the jar is signed. A ``TransactionState`` has a ``constraint`` field that represents that state's attachment constraint. When a party constructs a ``TransactionState``, or adds a state using ``TransactionBuilder.addOutput(ContractState)`` without -specifying the constraint parameter, a default value (``AutomaticHashConstraint``) is used. This default will be +specifying the constraint parameter, a default value (``AutomaticPlaceholderConstraint``) is used. This default will be automatically resolved to a specific ``HashAttachmentConstraint`` or a ``WhitelistedByZoneAttachmentConstraint``. This automatic resolution occurs when a ``TransactionBuilder`` is converted to a ``WireTransaction``. This reduces the boilerplate that would otherwise be involved. Finally, an ``AlwaysAcceptAttachmentConstraint`` can be used which accepts anything, though this is intended for -testing only. +testing only, and a warning will be shown if used by a contract. Please note that the ``AttachmentConstraint`` interface is marked as ``@DoNotImplement``. You are not allowed to write new constraint types. Only the platform may implement this interface. If you tried, other nodes would not understand @@ -116,48 +126,38 @@ a flow: TransactionBuilder tx = new TransactionBuilder(); Party notaryParty = ... // a notary party + + tx.addInputState(...) + tx.addInputState(...) + DummyState contractState = new DummyState(); - SecureHash myAttachmentHash = SecureHash.parse("2b4042aed7e0e39d312c4c477dca1d96ec5a878ddcfd5583251a8367edbd4a5f"); - TransactionState transactionState = new TransactionState(contractState, DummyContract.Companion.getPROGRAMID(), notaryParty, new AttachmentHashConstraint(myAttachmentHash)); - + TransactionState transactionState = new TransactionState(contractState, DummyContract.Companion.getPROGRAMID(), notaryParty, null, HashAttachmentConstraint(myhash)); tx.addOutputState(transactionState); WireTransaction wtx = tx.toWireTransaction(serviceHub); // This is where an automatic constraint would be resolved. LedgerTransaction ltx = wtx.toLedgerTransaction(serviceHub); ltx.verify(); // Verifies both the attachment constraints and contracts -Hard-coding the hash of your app in the code itself can be pretty awkward, so the API also offers the ``AutomaticHashConstraint``. -This isn't a real constraint that will appear in a transaction: it acts as a marker to the ``TransactionBuilder`` that -you require the hash of the node's installed app which supplies the specified contract to be used. In practice, when using -hash constraints, you almost always want "whatever the current code is" and not a hard-coded hash. So this automatic -constraint placeholder is useful. -FinalityFlow ------------- +Issues when using the HashAttachmentConstraint +---------------------------------------------- -It's possible to encounter contract constraint issues when notarising transactions with the ``FinalityFlow`` on a network -containing multiple versions of the same CorDapp. This will happen when using hash constraints or with zone constraints -if the zone whitelist has missing CorDapp versions. If a participating party fails to validate the **notarised** transaction -then we have a scenario where the members of the network do not have a consistent view of the ledger. +When setting up a new network, it is possible to encounter errors when states are issued with the ``HashAttachmentConstraint``, +but not all nodes have that same version of the CorDapp installed locally. -Therefore, if the finality handler flow (which is run on the counter-party) errors for any reason it will always be sent to -the flow hospital. From there it's suspended waiting to be retried on node restart. This gives the node operator the opportunity -to recover from those errors, which in the case of contract constraint violations means either updating the CorDapp or -adding its hash to the zone whitelist. +In this case, flows will fail with a ``ContractConstraintRejection``, and the failed flow will be sent to the flow hospital. +From there it's suspended waiting to be retried on node restart. +This gives the node operator the opportunity to recover from those errors, which in the case of constraint violations means +adding the right cordapp jar to the ``cordapps`` folder. -.. note:: This is a temporary issue in the current version of Corda, until we implement some missing features which will - enable a seamless handling of differences in CorDapp versions. CorDapps as attachments ----------------------- -CorDapp JARs (see :doc:`cordapp-overview`) that are installed to the node and contain classes implementing the ``Contract`` -interface are automatically loaded into the ``AttachmentStorage`` of a node at startup. - -After CorDapps are loaded into the attachment store the node creates a link between contract classes and the attachment -that they were loaded from. This makes it possible to find the attachment for any given contract. This is how the -automatic resolution of attachments is done by the ``TransactionBuilder`` and how, when verifying the constraints and -contracts, attachments are associated with their respective contracts. +CorDapp JARs (see :doc:`cordapp-overview`) that contain classes implementing the ``Contract`` interface are automatically +loaded into the ``AttachmentStorage`` of a node, and made available as ``ContractAttachments``. +They are retrievable by hash using ``AttachmentStorage.openAttachment``. +These JARs can either be installed on the node or fetched from the network using the ``FetchAttachmentsFlow``. .. note:: The obvious way to write a CorDapp is to put all you states, contracts, flows and support code into a single Java module. This will work but it will effectively publish your entire app onto the ledger. That has two problems: @@ -166,6 +166,51 @@ contracts, attachments are associated with their respective contracts. app into multiple modules: one which contains just states, contracts and core data types. And another which contains the rest of the app. See :ref:`cordapp-structure`. + +Constraints propagation +----------------------- + +As was mentioned above, the TransactionBuilder API gives the CorDapp developer or even malicious node owner the possibility +to construct output states with a constraint of his choosing. +Also, as listed above, some constraints are more restrictive then others. +For example, the ``HashAttachmentConstraint`` is the most restrictive, basically reducing the universe of possible attachments +to 1, while the ``AlwaysAcceptAttachmentConstraint`` allows any attachment to be selected. + +For the ledger to remain in a consistent state, the expected behavior is for output state to inherit the constraints of input states. +This guarantees that for example, a transaction can't output a state with the ``AlwaysAcceptAttachmentConstraint`` when the +corresponding input state was the ``HashAttachmentConstraint``. Translated, this means that if this rule is enforced, it ensures +that the output state will be spent under similar conditions as it was created. + +Before version 4, the constraint propagation logic was expected to be enforced in the contract verify code, as it has access to the entire Transaction. + +Starting with version 4 of Corda, the constraint propagation logic has been implemented and enforced directly by the platform, +unless disabled using ``@NoConstraintPropagation`` - which reverts to the previous behavior. + +For Contracts that are not annotated with ``@NoConstraintPropagation``, the platform implements a fairly simple constraint transition policy +to ensure security and also allow the possibility to transition to the new SignatureAttachmentConstraint. + +During transaction building the ``AutomaticPlaceholderConstraint`` for output states will be resolved and the best contract attachment versions +will be selected based on a variety of factors so that the above holds true. +If it can't find attachments in storage or there are no possible constraints, the Transaction Builder will fail early. + +For example: +- In the simple case, if a ``MyContract`` input state is constrained by the ``HashAttachmentConstraint``, then the constraints of all output states of that type will be resolved +to the ``HashAttachmentConstraint`` with the same hash, and the attachment with that hash will be selected. +- For upgradeable constraints like the ``WhitelistedByZoneAttachmentConstraint``, the output states will inherit the same, +and the selected attachment will be the latest version installed on the node. +- A more complex case is when for ``MyContract``, one input state is constrained by the ``HashAttachmentConstraint``, while another +state by the ``WhitelistedByZoneAttachmentConstraint``. To respect the rule from above, if the hash of the ``HashAttachmentConstraint`` +is whitelisted by the network, then the output states will inherit the ``HashAttachmentConstraint``, as it is more restrictive. +If the hash was not whitelisted, then the builder will fail as it is unable to select a correct constraint. +- The ``SignatureAttachmentConstraint`` is an upgradeable constraint, same as the ``WhitelistedByZoneAttachmentConstraint``. +By convention we allow states to transition to the ``SignatureAttachmentConstraint`` from the ``WhitelistedByZoneAttachmentConstraint`` as long as the Signatures +from new constraints are all the jarsigners from the whitelisted attachment. + + +For Contracts that are annotated with ``@NoConstraintPropagation``, the platform requires that the Transaction Builder specifies +an actual constraint for the output states (the ``AutomaticPlaceholderConstraint`` can't be used) . + + Testing ------- diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt index 6ae37494af..a271d13af2 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt @@ -501,7 +501,7 @@ class CashTests { private fun makeCash(amount: Amount, issuer: AbstractParty, depositRef: Byte = 1) = StateAndRef( - TransactionState(Cash.State(amount `issued by` issuer.ref(depositRef), ourIdentity), Cash.PROGRAM_ID, dummyNotary.party), + TransactionState(Cash.State(amount `issued by` issuer.ref(depositRef), ourIdentity), Cash.PROGRAM_ID, dummyNotary.party, constraint = AlwaysAcceptAttachmentConstraint), StateRef(SecureHash.randomSHA256(), Random().nextInt(32)) ) diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt index 6d31af0ea5..e9e1ebf0d4 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt @@ -314,7 +314,7 @@ class ObligationTests { } private inline fun getStateAndRef(state: T, contractClassName: ContractClassName): StateAndRef { - val txState = TransactionState(state, contractClassName, DUMMY_NOTARY) + val txState = TransactionState(state, contractClassName, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint) return StateAndRef(txState, StateRef(SecureHash.randomSHA256(), 0)) } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt index 0909707ac4..1460113a47 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt @@ -47,7 +47,7 @@ class AttachmentsClassLoaderStaticContractTests { class AttachmentDummyContract : Contract { companion object { - private const val ATTACHMENT_PROGRAM_ID = "net.corda.nodeapi.internal.AttachmentsClassLoaderStaticContractTests\$AttachmentDummyContract" + const val ATTACHMENT_PROGRAM_ID = "net.corda.nodeapi.internal.AttachmentsClassLoaderStaticContractTests\$AttachmentDummyContract" } data class State(val magicNumber: Int = 0) : ContractState { @@ -83,7 +83,14 @@ class AttachmentsClassLoaderStaticContractTests { cordappProviderImpl.start(testNetworkParameters().whitelistedContractImplementations) doReturn(cordappProviderImpl).whenever(it).cordappProvider doReturn(testNetworkParameters()).whenever(it).networkParameters - doReturn(attachments).whenever(it).attachments + val attachmentStorage = rigorousMock() + doReturn(attachmentStorage).whenever(it).attachments + val attachment = rigorousMock() + doReturn(attachment).whenever(attachmentStorage).openAttachment(any()) + doReturn(it.cordappProvider.getContractAttachmentID(AttachmentDummyContract.ATTACHMENT_PROGRAM_ID)).whenever(attachment).id + doReturn(setOf(AttachmentDummyContract.ATTACHMENT_PROGRAM_ID)).whenever(attachment).allContracts + doReturn("app").whenever(attachment).uploader + doReturn(emptyList()).whenever(attachment).signers } @Test diff --git a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt index 43090937cc..895715d881 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt @@ -81,6 +81,6 @@ class RaftNotaryServiceTests { val builder = DummyContract.generateInitial(Random().nextInt(), notary, nodeHandle.services.myInfo.singleIdentity().ref(0)) val stx = nodeHandle.services.signInitialTransaction(builder) nodeHandle.services.recordTransactions(stx) - return StateAndRef(builder.outputStates().first(), StateRef(stx.id, 0)) + return StateAndRef(stx.coreTransaction.outputs.first(), StateRef(stx.id, 0)) } } diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt index 94dc45a0fb..a351d05fe1 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt @@ -2,6 +2,7 @@ package net.corda.node.internal.cordapp import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult +import net.corda.core.contracts.warnContractWithoutConstraintPropagation import net.corda.core.cordapp.Cordapp import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 @@ -67,7 +68,7 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: */ fun fromJarUrls(scanJars: List) = JarScanningCordappLoader(scanJars.map { it.restricted() }) - private fun URL.restricted(rootPackageName: String? = null) = RestrictedURL(this, rootPackageName) + private fun URL.restricted(rootPackageName: String? = null) = RestrictedURL(this, rootPackageName) private fun jarUrlsInDirectory(directory: Path): List { @@ -169,7 +170,11 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: } private fun findContractClassNames(scanResult: RestrictedScanResult): List { - return coreContractClasses.flatMap { scanResult.getNamesOfClassesImplementing(it) }.distinct() + val contractClasses = coreContractClasses.flatMap { scanResult.getNamesOfClassesImplementing(it) }.distinct() + for (contractClass in contractClasses) { + contractClass.warnContractWithoutConstraintPropagation(appClassLoader) + } + return contractClasses } private fun findPlugins(cordappJarPath: RestrictedURL): List { @@ -273,7 +278,9 @@ abstract class CordappLoaderTemplate : CordappLoader { cordapps.flatMap { corDapp -> corDapp.allFlows.map { flow -> flow to corDapp } } .groupBy { it.first } .mapValues { - if(it.value.size > 1) { throw MultipleCordappsForFlowException("There are multiple CorDapp JARs on the classpath for flow ${it.value.first().first.name}: [ ${it.value.joinToString { it.second.name }} ].") } + if (it.value.size > 1) { + throw MultipleCordappsForFlowException("There are multiple CorDapp JARs on the classpath for flow ${it.value.first().first.name}: [ ${it.value.joinToString { it.second.name }} ].") + } it.value.single().second } } diff --git a/node/src/test/kotlin/net/corda/node/services/schema/PersistentStateServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/schema/PersistentStateServiceTests.kt index f1abf5e618..7066435e3c 100644 --- a/node/src/test/kotlin/net/corda/node/services/schema/PersistentStateServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/schema/PersistentStateServiceTests.kt @@ -1,9 +1,6 @@ package net.corda.node.services.schema -import net.corda.core.contracts.ContractState -import net.corda.core.contracts.StateAndRef -import net.corda.core.contracts.StateRef -import net.corda.core.contracts.TransactionState +import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name @@ -70,7 +67,7 @@ class PersistentStateServiceTests { val persistentStateService = PersistentStateService(schemaService) database.transaction { val MEGA_CORP = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party - persistentStateService.persist(setOf(StateAndRef(TransactionState(TestState(), DummyContract.PROGRAM_ID, MEGA_CORP), StateRef(SecureHash.sha256("dummy"), 0)))) + persistentStateService.persist(setOf(StateAndRef(TransactionState(TestState(), DummyContract.PROGRAM_ID, MEGA_CORP, constraint = AlwaysAcceptAttachmentConstraint), StateRef(SecureHash.sha256("dummy"), 0)))) currentDBSession().flush() val parentRowCountResult = connection.prepareStatement("select count(*) from Parents").executeQuery() parentRowCountResult.next() diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt index 4815ace9d8..a9b27b5b51 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt @@ -332,6 +332,6 @@ class ValidatingNotaryServiceTests { val signedByNode = serviceHub.signInitialTransaction(tx) val stx = notaryNode.services.addSignature(signedByNode, notary.owningKey) serviceHub.recordTransactions(stx) - return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0)) + return StateAndRef(stx.coreTransaction.outputs.first(), StateRef(stx.id, 0)) } } diff --git a/samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/contract/IRS.kt b/samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/contract/IRS.kt index ba307993e3..fb999ba542 100644 --- a/samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/contract/IRS.kt +++ b/samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/contract/IRS.kt @@ -1,21 +1,7 @@ package net.corda.irs.contract import com.fasterxml.jackson.annotation.JsonIgnoreProperties -import net.corda.core.contracts.Amount -import net.corda.core.contracts.Command -import net.corda.core.contracts.CommandData -import net.corda.core.contracts.CommandWithParties -import net.corda.core.contracts.Contract -import net.corda.core.contracts.SchedulableState -import net.corda.core.contracts.ScheduledActivity -import net.corda.core.contracts.StateAndContract -import net.corda.core.contracts.StateAndRef -import net.corda.core.contracts.StateRef -import net.corda.core.contracts.TransactionState -import net.corda.core.contracts.TypeOnlyCommandData -import net.corda.core.contracts.UniqueIdentifier -import net.corda.core.contracts.requireThat -import net.corda.core.contracts.select +import net.corda.core.contracts.* import net.corda.core.flows.FlowLogicRefFactory import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party @@ -254,8 +240,7 @@ class InterestRateSwap : Contract { * @return LocalDate or null if no more fixings. */ fun nextFixingDate(): LocalDate? { - return floatingLegPaymentSchedule. - filter { it.value.rate is ReferenceRate }.// TODO - a better way to determine what fixings remain to be fixed + return floatingLegPaymentSchedule.filter { it.value.rate is ReferenceRate }.// TODO - a better way to determine what fixings remain to be fixed minBy { it.value.fixingDate.toEpochDay() }?.value?.fixingDate } @@ -650,7 +635,7 @@ class InterestRateSwap : Contract { } override fun generateFix(ptx: TransactionBuilder, oldState: StateAndRef<*>, fix: Fix) { - InterestRateSwap().generateFix(ptx, StateAndRef(TransactionState(this, IRS_PROGRAM_ID, oldState.state.notary), oldState.ref), fix) + InterestRateSwap().generateFix(ptx, StateAndRef(TransactionState(this, IRS_PROGRAM_ID, oldState.state.notary, constraint = AlwaysAcceptAttachmentConstraint), oldState.ref), fix) } override fun nextFixingOf(): FixOf? { @@ -748,10 +733,9 @@ class InterestRateSwap : Contract { // Put all the above into a new State object. val state = State(fixedLeg, floatingLeg, newCalculation, common, oracle) - return TransactionBuilder(notary).withItems( - StateAndContract(state, IRS_PROGRAM_ID), - Command(Commands.Agree(), listOf(state.floatingLeg.floatingRatePayer.owningKey, state.fixedLeg.fixedRatePayer.owningKey)) - ) + return TransactionBuilder(notary) + .addCommand(Command(Commands.Agree(), listOf(state.floatingLeg.floatingRatePayer.owningKey, state.fixedLeg.fixedRatePayer.owningKey))) + .addOutputState(TransactionState(state, IRS_PROGRAM_ID, notary, null, AlwaysAcceptAttachmentConstraint)) } private fun calcFixingDate(date: LocalDate, fixingPeriodOffset: Int, calendar: BusinessCalendar): LocalDate { @@ -767,7 +751,8 @@ class InterestRateSwap : Contract { tx.addOutputState( irs.state.data.copy(calculation = irs.state.data.calculation.applyFixing(fixing.of.forDay, fixedRate)), irs.state.contract, - irs.state.notary + irs.state.notary, + constraint = AlwaysAcceptAttachmentConstraint ) tx.addCommand(Commands.Refix(fixing), listOf(irs.state.data.floatingLeg.floatingRatePayer.owningKey, irs.state.data.fixedLeg.fixedRatePayer.owningKey)) } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt index 19f282dac1..c98a026365 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt @@ -11,6 +11,7 @@ import net.corda.core.internal.UNKNOWN_UPLOADER import net.corda.core.internal.uncheckedCast import net.corda.core.node.ServiceHub import net.corda.core.node.ServicesForResolution +import net.corda.core.node.services.AttachmentId import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction @@ -147,8 +148,13 @@ data class TestTransactionDSLInterpreter private constructor( override fun _tweak(dsl: TransactionDSLInterpreter.() -> EnforceVerifyOrFail) = copy().dsl() override fun _attachment(contractClassName: ContractClassName) { - (services.cordappProvider as MockCordappProvider).addMockCordapp(contractClassName, services.attachments as MockAttachmentStorage) + attachment((services.cordappProvider as MockCordappProvider).addMockCordapp(contractClassName, services.attachments as MockAttachmentStorage)) } + + override fun _attachment(contractClassName: ContractClassName, attachmentId: AttachmentId, signers: List){ + attachment((services.cordappProvider as MockCordappProvider).addMockCordapp(contractClassName, services.attachments as MockAttachmentStorage, attachmentId, signers)) + } + } data class TestLedgerDSLInterpreter private constructor( diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TransactionDSLInterpreter.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TransactionDSLInterpreter.kt index ecee16308c..e47a8037e5 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TransactionDSLInterpreter.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TransactionDSLInterpreter.kt @@ -1,9 +1,19 @@ package net.corda.testing.dsl import net.corda.core.DoNotImplement +import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint +import net.corda.core.contracts.Attachment +import net.corda.core.contracts.AttachmentConstraint +import net.corda.core.contracts.AutomaticPlaceholderConstraint +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.ContractClassName +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TimeWindow import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.identity.Party +import net.corda.core.node.services.AttachmentId import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.seconds import java.security.PublicKey @@ -80,6 +90,13 @@ interface TransactionDSLInterpreter : Verifies, OutputStateLookup { * @param contractClassName The contract class to attach */ fun _attachment(contractClassName: ContractClassName) + + /** + * Attaches an attachment containing the named contract to the transaction + * @param contractClassName The contract class to attach + * @param attachmentId The attachment + */ + fun _attachment(contractClassName: ContractClassName, attachmentId: AttachmentId, signers: List) } /** @@ -131,37 +148,37 @@ class TransactionDSL(interpreter: T, private * Adds a labelled output to the transaction. */ fun output(contractClassName: ContractClassName, label: String, notary: Party, contractState: ContractState) = - output(contractClassName, label, notary, null, AutomaticHashConstraint, contractState) + output(contractClassName, label, notary, null, AutomaticPlaceholderConstraint, contractState) /** * Adds a labelled output to the transaction. */ fun output(contractClassName: ContractClassName, label: String, encumbrance: Int, contractState: ContractState) = - output(contractClassName, label, notary, encumbrance, AutomaticHashConstraint, contractState) + output(contractClassName, label, notary, encumbrance, AutomaticPlaceholderConstraint, contractState) /** * Adds a labelled output to the transaction. */ fun output(contractClassName: ContractClassName, label: String, contractState: ContractState) = - output(contractClassName, label, notary, null, AutomaticHashConstraint, contractState) + output(contractClassName, label, notary, null, AutomaticPlaceholderConstraint, contractState) /** * Adds an output to the transaction. */ fun output(contractClassName: ContractClassName, notary: Party, contractState: ContractState) = - output(contractClassName, null, notary, null, AutomaticHashConstraint, contractState) + output(contractClassName, null, notary, null, AutomaticPlaceholderConstraint, contractState) /** * Adds an output to the transaction. */ fun output(contractClassName: ContractClassName, encumbrance: Int, contractState: ContractState) = - output(contractClassName, null, notary, encumbrance, AutomaticHashConstraint, contractState) + output(contractClassName, null, notary, encumbrance, AutomaticPlaceholderConstraint, contractState) /** * Adds an output to the transaction. */ fun output(contractClassName: ContractClassName, contractState: ContractState) = - output(contractClassName, null, notary, null, AutomaticHashConstraint, contractState) + output(contractClassName, null, notary, null, AutomaticPlaceholderConstraint, contractState) /** * Adds a command to the transaction. @@ -186,5 +203,8 @@ class TransactionDSL(interpreter: T, private */ fun attachment(contractClassName: ContractClassName) = _attachment(contractClassName) + fun attachment(contractClassName: ContractClassName, attachmentId: AttachmentId, signers: List) = _attachment(contractClassName, attachmentId, signers) + fun attachment(contractClassName: ContractClassName, attachmentId: AttachmentId) = _attachment(contractClassName, attachmentId, emptyList()) + fun attachments(vararg contractClassNames: ContractClassName) = contractClassNames.forEach { attachment(it) } } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt index 508ace7fb4..317c8dbfcb 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt @@ -3,6 +3,7 @@ package net.corda.testing.internal import net.corda.core.contracts.ContractClassName import net.corda.core.cordapp.Cordapp import net.corda.core.crypto.SecureHash +import net.corda.core.identity.Party import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.node.services.AttachmentId @@ -11,6 +12,7 @@ import net.corda.node.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.testing.services.MockAttachmentStorage import java.nio.file.Paths +import java.security.PublicKey import java.util.* class MockCordappProvider( @@ -21,7 +23,7 @@ class MockCordappProvider( private val cordappRegistry = mutableListOf>() - fun addMockCordapp(contractClassName: ContractClassName, attachments: MockAttachmentStorage) { + fun addMockCordapp(contractClassName: ContractClassName, attachments: MockAttachmentStorage, contractHash: AttachmentId? = null, signers: List = emptyList()): AttachmentId { val cordapp = CordappImpl( contractClassNames = listOf(contractClassName), initiatedFlows = emptyList(), @@ -35,21 +37,23 @@ class MockCordappProvider( jarPath = Paths.get("").toUri().toURL(), allFlows = emptyList(), jarHash = SecureHash.allOnesHash) - if (cordappRegistry.none { it.first.contractClassNames.contains(contractClassName) }) { - cordappRegistry.add(Pair(cordapp, findOrImportAttachment(listOf(contractClassName), contractClassName.toByteArray(), attachments))) + if (cordappRegistry.none { it.first.contractClassNames.contains(contractClassName) && it.second == contractHash }) { + cordappRegistry.add(Pair(cordapp, findOrImportAttachment(listOf(contractClassName), contractClassName.toByteArray(), attachments, contractHash, signers))) } + return cordappRegistry.findLast { contractClassName in it.first.contractClassNames }?.second!! } - override fun getContractAttachmentID(contractClassName: ContractClassName): AttachmentId? = cordappRegistry.find { it.first.contractClassNames.contains(contractClassName) }?.second ?: super.getContractAttachmentID(contractClassName) + override fun getContractAttachmentID(contractClassName: ContractClassName): AttachmentId? = cordappRegistry.find { it.first.contractClassNames.contains(contractClassName) }?.second + ?: super.getContractAttachmentID(contractClassName) - private fun findOrImportAttachment(contractClassNames: List, data: ByteArray, attachments: MockAttachmentStorage): AttachmentId { - val existingAttachment = attachments.files.filter { - Arrays.equals(it.value.second, data) + private fun findOrImportAttachment(contractClassNames: List, data: ByteArray, attachments: MockAttachmentStorage, contractHash: AttachmentId?, signers: List): AttachmentId { + val existingAttachment = attachments.files.filter { (attachmentId, content) -> + contractHash == attachmentId } return if (!existingAttachment.isEmpty()) { existingAttachment.keys.first() } else { - attachments.importContractAttachment(contractClassNames, DEPLOYED_CORDAPP_UPLOADER, data.inputStream()) + attachments.importContractAttachment(contractClassNames, DEPLOYED_CORDAPP_UPLOADER, data.inputStream(), contractHash, signers) } } } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt index b113dda1b8..4be209c3c0 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt @@ -5,6 +5,7 @@ import net.corda.core.contracts.ContractAttachment import net.corda.core.contracts.ContractClassName import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 +import net.corda.core.identity.Party import net.corda.core.internal.AbstractAttachment import net.corda.core.internal.UNKNOWN_UPLOADER import net.corda.core.internal.readFully @@ -15,6 +16,7 @@ import net.corda.core.node.services.vault.AttachmentSort import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.nodeapi.internal.withContractsInJar import java.io.InputStream +import java.security.PublicKey import java.util.* import java.util.jar.JarInputStream @@ -53,21 +55,22 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() { } } - fun importContractAttachment(contractClassNames: List, uploader: String, jar: InputStream): AttachmentId = importAttachmentInternal(jar, uploader, contractClassNames) + @JvmOverloads + fun importContractAttachment(contractClassNames: List, uploader: String, jar: InputStream, attachmentId: AttachmentId? = null, signers: List = emptyList()): AttachmentId = importAttachmentInternal(jar, uploader, contractClassNames, attachmentId, signers) fun getAttachmentIdAndBytes(jar: InputStream): Pair = jar.readFully().let { bytes -> Pair(bytes.sha256(), bytes) } - private class MockAttachment(dataLoader: () -> ByteArray, override val id: SecureHash) : AbstractAttachment(dataLoader) + private class MockAttachment(dataLoader: () -> ByteArray, override val id: SecureHash, override val signers: List) : AbstractAttachment(dataLoader) - private fun importAttachmentInternal(jar: InputStream, uploader: String, contractClassNames: List? = null): AttachmentId { + private fun importAttachmentInternal(jar: InputStream, uploader: String, contractClassNames: List? = null, attachmentId: AttachmentId? = null, signers: List = emptyList()): AttachmentId { // JIS makes read()/readBytes() return bytes of the current file, but we want to hash the entire container here. require(jar !is JarInputStream) val bytes = jar.readFully() - val sha256 = bytes.sha256() + val sha256 = attachmentId ?: bytes.sha256() if (sha256 !in files.keys) { - val baseAttachment = MockAttachment({ bytes }, sha256) + val baseAttachment = MockAttachment({ bytes }, sha256, signers) val attachment = if (contractClassNames == null || contractClassNames.isEmpty()) baseAttachment else ContractAttachment(baseAttachment, contractClassNames.first(), contractClassNames.toSet(), uploader) _files[sha256] = Pair(attachment, bytes) } From 86bc0d9606922d48a30d395af2a21d6ce7dfc03b Mon Sep 17 00:00:00 2001 From: Tudor Malene Date: Mon, 10 Sep 2018 18:38:42 +0100 Subject: [PATCH 5/6] CORDA-1947 added packageOwnership parameter CORDA-1947 add signers field to DbAttachment. Add check when importing attachments CORDA-1947 add signers field to DbAttachment. Add check when importing attachments CORDA-1947 add tests CORDA-1947 fix comment CORDA-1947 Fix test CORDA-1947 fix serialiser CORDA-1947 fix tests CORDA-1947 fix tests CORDA-1947 fix serialiser CORDA-1947 Address code review changes CORDA-1947 Address code review changes CORDA-1947 Revert test fixes CORDA-1947 address code review comments CORDA-1947 move verification logic to LedgerTransaction.verify CORDA-1947 fix test CORDA-1947 fix tests CORDA-1947 fix tests CORDA-1947 address code review comments CORDA-1947 address code review comments --- .../client/jfx/model/NodeMonitorModel.kt | 8 +- .../core/contracts/ContractAttachment.kt | 8 +- .../TransactionVerificationException.kt | 9 ++ .../net/corda/core/node/NetworkParameters.kt | 107 +++++++++++++++--- .../ContractUpgradeTransactions.kt | 2 +- .../core/transactions/LedgerTransaction.kt | 26 +++++ .../net/corda/core/JarSignatureTestUtils.kt | 45 ++++++++ .../PackageOwnershipVerificationTests.kt | 95 ++++++++++++++++ .../internal/JarSignatureCollectorTest.kt | 94 +++++---------- .../transactions/TransactionBuilderTest.kt | 2 +- .../core/transactions/TransactionTests.kt | 4 +- docs/source/network-map.rst | 9 +- .../net/corda/node/internal/AbstractNode.kt | 6 +- .../kryo/DefaultKryoCustomizer.kt | 10 +- .../persistence/NodeAttachmentService.kt | 34 +++++- .../persistence/PublicKeyToTextConverter.kt | 18 +++ .../migration/node-core.changelog-v8.xml | 13 +++ .../node/internal/NetworkParametersTest.kt | 62 +++++++++- .../persistence/NodeAttachmentServiceTest.kt | 107 ++++++++++++++++-- .../custom/ContractAttachmentSerializer.kt | 7 +- .../net/corda/testing/contracts/DummyState.kt | 2 + .../testing/services/MockAttachmentStorage.kt | 3 +- 22 files changed, 561 insertions(+), 110 deletions(-) create mode 100644 core/src/test/kotlin/net/corda/core/JarSignatureTestUtils.kt create mode 100644 core/src/test/kotlin/net/corda/core/contracts/PackageOwnershipVerificationTests.kt create mode 100644 node/src/main/kotlin/net/corda/node/services/persistence/PublicKeyToTextConverter.kt diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt index eb8e78898f..550344ae35 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt @@ -191,10 +191,10 @@ class NodeMonitorModel : AutoCloseable { val retryInterval = 5.seconds val client = CordaRPCClient( - nodeHostAndPort, - CordaRPCClientConfiguration.DEFAULT.copy( - connectionMaxRetryInterval = retryInterval - ) + nodeHostAndPort, + CordaRPCClientConfiguration.DEFAULT.copy( + connectionMaxRetryInterval = retryInterval + ) ) do { val connection = try { diff --git a/core/src/main/kotlin/net/corda/core/contracts/ContractAttachment.kt b/core/src/main/kotlin/net/corda/core/contracts/ContractAttachment.kt index 7a19128c32..4b1c25fed8 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/ContractAttachment.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/ContractAttachment.kt @@ -2,6 +2,7 @@ package net.corda.core.contracts import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializable +import java.security.PublicKey /** * Wrap an attachment in this if it is to be used as an executable contract attachment @@ -12,7 +13,12 @@ import net.corda.core.serialization.CordaSerializable */ @KeepForDJVM @CordaSerializable -class ContractAttachment @JvmOverloads constructor(val attachment: Attachment, val contract: ContractClassName, val additionalContracts: Set = emptySet(), val uploader: String? = null) : Attachment by attachment { +class ContractAttachment @JvmOverloads constructor( + val attachment: Attachment, + val contract: ContractClassName, + val additionalContracts: Set = emptySet(), + val uploader: String? = null, + override val signers: List = emptyList()) : Attachment by attachment { val allContracts: Set get() = additionalContracts + contract diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt index e0dab5381f..02e4404607 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt @@ -5,6 +5,7 @@ import net.corda.core.KeepForDJVM import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowException import net.corda.core.identity.Party +import net.corda.core.node.services.AttachmentId import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.NonEmptySet import java.security.PublicKey @@ -163,4 +164,12 @@ abstract class TransactionVerificationException(val txId: SecureHash, message: S @DeleteForDJVM class InvalidNotaryChange(txId: SecureHash) : TransactionVerificationException(txId, "Detected a notary change. Outputs must use the same notary as inputs", null) + + /** + * Thrown to indicate that a contract attachment is not signed by the network-wide package owner. + */ + class ContractAttachmentNotSignedByPackageOwnerException(txId: SecureHash, val attachmentHash: AttachmentId, val contractClass: String) : TransactionVerificationException(txId, + """The Contract attachment JAR: $attachmentHash containing the contract: $contractClass is not signed by the owner specified in the network parameters. + Please check the source of this attachment and if it is malicious contact your zone operator to report this incident. + For details see: https://docs.corda.net/network-map.html#network-parameters""".trimIndent(), null) } diff --git a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt index 7127177c71..f707a8e308 100644 --- a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt +++ b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt @@ -7,6 +7,7 @@ import net.corda.core.node.services.AttachmentId import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.DeprecatedConstructorForDeserialization import net.corda.core.utilities.days +import java.security.PublicKey import java.time.Duration import java.time.Instant @@ -22,6 +23,7 @@ import java.time.Instant * of parameters. * @property whitelistedContractImplementations List of whitelisted jars containing contract code for each contract class. * This will be used by [net.corda.core.contracts.WhitelistedByZoneAttachmentConstraint]. [You can learn more about contract constraints here](https://docs.corda.net/api-contract-constraints.html). + * @property packageOwnership List of the network-wide java packages that were successfully claimed by their owners. Any CorDapp JAR that offers contracts and states in any of these packages must be signed by the owner. * @property eventHorizon Time after which nodes will be removed from the network map if they have not been seen * during this period */ @@ -35,7 +37,8 @@ data class NetworkParameters( val modifiedTime: Instant, val epoch: Int, val whitelistedContractImplementations: Map>, - val eventHorizon: Duration + val eventHorizon: Duration, + val packageOwnership: Map ) { @DeprecatedConstructorForDeserialization(1) constructor (minimumPlatformVersion: Int, @@ -52,7 +55,28 @@ data class NetworkParameters( modifiedTime, epoch, whitelistedContractImplementations, - Int.MAX_VALUE.days + Int.MAX_VALUE.days, + emptyMap() + ) + + @DeprecatedConstructorForDeserialization(2) + constructor (minimumPlatformVersion: Int, + notaries: List, + maxMessageSize: Int, + maxTransactionSize: Int, + modifiedTime: Instant, + epoch: Int, + whitelistedContractImplementations: Map>, + eventHorizon: Duration + ) : this(minimumPlatformVersion, + notaries, + maxMessageSize, + maxTransactionSize, + modifiedTime, + epoch, + whitelistedContractImplementations, + eventHorizon, + emptyMap() ) init { @@ -63,6 +87,7 @@ data class NetworkParameters( require(maxTransactionSize > 0) { "maxTransactionSize must be at least 1" } require(maxTransactionSize <= maxMessageSize) { "maxTransactionSize cannot be bigger than maxMessageSize" } require(!eventHorizon.isNegative) { "eventHorizon must be positive value" } + require(noOverlap(packageOwnership.keys)) { "multiple packages added to the packageOwnership overlap." } } fun copy(minimumPlatformVersion: Int, @@ -83,20 +108,47 @@ data class NetworkParameters( eventHorizon = eventHorizon) } + fun copy(minimumPlatformVersion: Int, + notaries: List, + maxMessageSize: Int, + maxTransactionSize: Int, + modifiedTime: Instant, + epoch: Int, + whitelistedContractImplementations: Map>, + eventHorizon: Duration + ): NetworkParameters { + return copy(minimumPlatformVersion = minimumPlatformVersion, + notaries = notaries, + maxMessageSize = maxMessageSize, + maxTransactionSize = maxTransactionSize, + modifiedTime = modifiedTime, + epoch = epoch, + whitelistedContractImplementations = whitelistedContractImplementations, + eventHorizon = eventHorizon) + } + override fun toString(): String { return """NetworkParameters { - minimumPlatformVersion=$minimumPlatformVersion - notaries=$notaries - maxMessageSize=$maxMessageSize - maxTransactionSize=$maxTransactionSize - whitelistedContractImplementations { - ${whitelistedContractImplementations.entries.joinToString("\n ")} - } - eventHorizon=$eventHorizon - modifiedTime=$modifiedTime - epoch=$epoch -}""" + minimumPlatformVersion=$minimumPlatformVersion + notaries=$notaries + maxMessageSize=$maxMessageSize + maxTransactionSize=$maxTransactionSize + whitelistedContractImplementations { + ${whitelistedContractImplementations.entries.joinToString("\n ")} + } + eventHorizon=$eventHorizon + modifiedTime=$modifiedTime + epoch=$epoch, + packageOwnership= { + ${packageOwnership.keys.joinToString()}} + } + }""" } + + /** + * Returns the public key of the package owner of the [contractClassName], or null if not owned. + */ + fun getOwnerOf(contractClassName: String): PublicKey? = this.packageOwnership.filterKeys { it.owns(contractClassName) }.values.singleOrNull() } /** @@ -113,3 +165,32 @@ data class NotaryInfo(val identity: Party, val validating: Boolean) * version. */ class ZoneVersionTooLowException(message: String) : CordaRuntimeException(message) + +/** + * A wrapper for a legal java package. Used by the network parameters to store package ownership. + */ +@CordaSerializable +data class JavaPackageName(val name: String) { + init { + require(isPackageValid(name)) { "Attempting to whitelist illegal java package: $name" } + } + + /** + * Returns true if the [fullClassName] is in a subpackage of the current package. + * E.g.: "com.megacorp" owns "com.megacorp.tokens.MegaToken" + * + * Note: The ownership check is ignoring case to prevent people from just releasing a jar with: "com.megaCorp.megatoken" and pretend they are MegaCorp. + * By making the check case insensitive, the node will require that the jar is signed by MegaCorp, so the attack fails. + */ + fun owns(fullClassName: String) = fullClassName.startsWith("${name}.", ignoreCase = true) +} + +// Check if a string is a legal Java package name. +private fun isPackageValid(packageName: String): Boolean = packageName.isNotEmpty() && !packageName.endsWith(".") && packageName.split(".").all { token -> + Character.isJavaIdentifierStart(token[0]) && token.toCharArray().drop(1).all { Character.isJavaIdentifierPart(it) } +} + +// Make sure that packages don't overlap so that ownership is clear. +private fun noOverlap(packages: Collection) = packages.all { currentPackage -> + packages.none { otherPackage -> otherPackage != currentPackage && otherPackage.name.startsWith("${currentPackage.name}.") } +} diff --git a/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt index 55be2f424f..ddadaefc0f 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt @@ -197,7 +197,7 @@ data class ContractUpgradeLedgerTransaction( private fun verifyConstraints() { val attachmentForConstraintVerification = AttachmentWithContext( legacyContractAttachment as? ContractAttachment - ?: ContractAttachment(legacyContractAttachment, legacyContractClassName), + ?: ContractAttachment(legacyContractAttachment, legacyContractClassName, signers = legacyContractAttachment.signers), upgradedContract.legacyContract, networkParameters.whitelistedContractImplementations ) diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index e326ddf7a0..f241c067f9 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -3,6 +3,7 @@ package net.corda.core.transactions import net.corda.core.KeepForDJVM import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.isFulfilledBy import net.corda.core.identity.Party import net.corda.core.internal.AttachmentWithContext import net.corda.core.internal.castIfPossible @@ -81,6 +82,7 @@ data class LedgerTransaction @JvmOverloads constructor( val contractAttachmentsByContract: Map = getUniqueContractAttachmentsByContract() // TODO - verify for version downgrade + validatePackageOwnership(contractAttachmentsByContract) validateStatesAgainstContract() verifyConstraintsValidity(contractAttachmentsByContract) verifyConstraints(contractAttachmentsByContract) @@ -105,6 +107,30 @@ data class LedgerTransaction @JvmOverloads constructor( } } + /** + * Verify that for each contract the network wide package owner is respected. + * + * TODO - revisit once transaction contains network parameters. + */ + private fun validatePackageOwnership(contractAttachmentsByContract: Map) { + // This should never happen once we have network parameters in the transaction. + if (networkParameters == null) { + return + } + + val contractsAndOwners = allStates.mapNotNull { transactionState -> + val contractClassName = transactionState.contract + networkParameters.getOwnerOf(contractClassName)?.let { contractClassName to it } + }.toMap() + + contractsAndOwners.forEach { contract, owner -> + val attachment = contractAttachmentsByContract[contract]!! + if (!owner.isFulfilledBy(attachment.signers)) { + throw TransactionVerificationException.ContractAttachmentNotSignedByPackageOwnerException(this.id, id, contract) + } + } + } + /** * Enforces the validity of the actual constraints. * * Constraints should be one of the valid supported ones. diff --git a/core/src/test/kotlin/net/corda/core/JarSignatureTestUtils.kt b/core/src/test/kotlin/net/corda/core/JarSignatureTestUtils.kt new file mode 100644 index 0000000000..d5d9bed118 --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/JarSignatureTestUtils.kt @@ -0,0 +1,45 @@ +package net.corda.core + +import net.corda.core.internal.JarSignatureCollector +import net.corda.core.internal.div +import net.corda.nodeapi.internal.crypto.loadKeyStore +import java.io.FileInputStream +import java.nio.file.Path +import java.nio.file.Paths +import java.security.PublicKey +import java.util.jar.JarInputStream +import kotlin.test.assertEquals + +object JarSignatureTestUtils { + val bin = Paths.get(System.getProperty("java.home")).let { if (it.endsWith("jre")) it.parent else it } / "bin" + + fun Path.executeProcess(vararg command: String) { + val shredder = (this / "_shredder").toFile() // No need to delete after each test. + assertEquals(0, ProcessBuilder() + .inheritIO() + .redirectOutput(shredder) + .redirectError(shredder) + .directory(this.toFile()) + .command((bin / command[0]).toString(), *command.sliceArray(1 until command.size)) + .start() + .waitFor()) + } + + fun Path.generateKey(alias: String, password: String, name: String) = + executeProcess("keytool", "-genkey", "-keystore", "_teststore", "-storepass", "storepass", "-keyalg", "RSA", "-alias", alias, "-keypass", password, "-dname", name) + + fun Path.createJar(fileName: String, vararg contents: String) = + executeProcess(*(arrayOf("jar", "cvf", fileName) + contents)) + + fun Path.updateJar(fileName: String, vararg contents: String) = + executeProcess(*(arrayOf("jar", "uvf", fileName) + contents)) + + fun Path.signJar(fileName: String, alias: String, password: String): PublicKey { + executeProcess("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", password, fileName, alias) + val ks = loadKeyStore(this.resolve("_teststore"), "storepass") + return ks.getCertificate(alias).publicKey + } + + fun Path.getJarSigners(fileName: String) = + JarInputStream(FileInputStream((this / fileName).toFile())).use(JarSignatureCollector::collectSigners) +} diff --git a/core/src/test/kotlin/net/corda/core/contracts/PackageOwnershipVerificationTests.kt b/core/src/test/kotlin/net/corda/core/contracts/PackageOwnershipVerificationTests.kt new file mode 100644 index 0000000000..ecbb8f8374 --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/contracts/PackageOwnershipVerificationTests.kt @@ -0,0 +1,95 @@ +package net.corda.core.contracts + +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.SecureHash +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.CordaX500Name +import net.corda.core.node.JavaPackageName +import net.corda.core.transactions.LedgerTransaction +import net.corda.finance.POUNDS +import net.corda.finance.`issued by` +import net.corda.finance.contracts.asset.Cash +import net.corda.node.services.api.IdentityServiceInternal +import net.corda.testing.common.internal.testNetworkParameters +import net.corda.testing.core.DUMMY_NOTARY_NAME +import net.corda.testing.core.SerializationEnvironmentRule +import net.corda.testing.core.TestIdentity +import net.corda.testing.internal.rigorousMock +import net.corda.testing.node.MockServices +import net.corda.testing.node.ledger +import org.junit.Rule +import org.junit.Test + +class PackageOwnershipVerificationTests { + + private companion object { + val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party + val ALICE = TestIdentity(CordaX500Name("ALICE", "London", "GB")) + val ALICE_PARTY get() = ALICE.party + val ALICE_PUBKEY get() = ALICE.publicKey + val BOB = TestIdentity(CordaX500Name("BOB", "London", "GB")) + val BOB_PARTY get() = BOB.party + val BOB_PUBKEY get() = BOB.publicKey + val dummyContract = "net.corda.core.contracts.DummyContract" + val OWNER_KEY_PAIR = Crypto.generateKeyPair() + } + + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule() + + private val ledgerServices = MockServices( + cordappPackages = listOf("net.corda.finance.contracts.asset"), + initialIdentity = ALICE, + identityService = rigorousMock().also { + doReturn(ALICE_PARTY).whenever(it).partyFromKey(ALICE_PUBKEY) + doReturn(BOB_PARTY).whenever(it).partyFromKey(BOB_PUBKEY) + }, + networkParameters = testNetworkParameters() + .copy(packageOwnership = mapOf(JavaPackageName("net.corda.core.contracts") to OWNER_KEY_PAIR.public)) + ) + + @Test + fun `Happy path - Transaction validates when package signed by owner`() { + ledgerServices.ledger(DUMMY_NOTARY) { + transaction { + attachment(dummyContract, SecureHash.allOnesHash, listOf(OWNER_KEY_PAIR.public)) + output(dummyContract, "c1", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.allOnesHash), DummyContractState()) + command(ALICE_PUBKEY, DummyIssue()) + verifies() + } + } + } + + @Test + fun `Transaction validation fails when the selected attachment is not signed by the owner`() { + ledgerServices.ledger(DUMMY_NOTARY) { + transaction { + attachment(dummyContract, SecureHash.allOnesHash, listOf(ALICE_PUBKEY)) + output(dummyContract, "c1", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.allOnesHash), DummyContractState()) + command(ALICE_PUBKEY, DummyIssue()) + failsWith("is not signed by the owner specified in the network parameters") + } + } + } + +} + +@BelongsToContract(DummyContract::class) +class DummyContractState : ContractState { + override val participants: List + get() = emptyList() +} + +class DummyContract : Contract { + interface Commands : CommandData + class Create : Commands + + override fun verify(tx: LedgerTransaction) { + //do nothing + } +} + +class DummyIssue : TypeOnlyCommandData() \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt b/core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt index 95f0b187e6..e03b198340 100644 --- a/core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt +++ b/core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt @@ -1,8 +1,11 @@ package net.corda.core.internal -import net.corda.core.identity.CordaX500Name +import net.corda.core.JarSignatureTestUtils.createJar +import net.corda.core.JarSignatureTestUtils.generateKey +import net.corda.core.JarSignatureTestUtils.getJarSigners +import net.corda.core.JarSignatureTestUtils.signJar +import net.corda.core.JarSignatureTestUtils.updateJar import net.corda.core.identity.Party -import net.corda.nodeapi.internal.crypto.loadKeyStore import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import org.assertj.core.api.Assertions.assertThat @@ -10,30 +13,14 @@ import org.junit.After import org.junit.AfterClass import org.junit.BeforeClass import org.junit.Test -import java.io.FileInputStream import java.nio.file.Files import java.nio.file.Path -import java.nio.file.Paths -import java.security.PublicKey -import java.util.jar.JarInputStream import kotlin.test.assertEquals import kotlin.test.assertFailsWith class JarSignatureCollectorTest { companion object { private val dir = Files.createTempDirectory(JarSignatureCollectorTest::class.simpleName) - private val bin = Paths.get(System.getProperty("java.home")).let { if (it.endsWith("jre")) it.parent else it } / "bin" - private val shredder = (dir / "_shredder").toFile() // No need to delete after each test. - - fun execute(vararg command: String) { - assertEquals(0, ProcessBuilder() - .inheritIO() - .redirectOutput(shredder) - .directory(dir.toFile()) - .command((bin / command[0]).toString(), *command.sliceArray(1 until command.size)) - .start() - .waitFor()) - } private const val FILENAME = "attachment.jar" private const val ALICE = "alice" @@ -41,14 +28,11 @@ class JarSignatureCollectorTest { private const val BOB = "bob" private const val BOB_PASS = "bobpass" - private fun generateKey(alias: String, password: String, name: CordaX500Name) = - execute("keytool", "-genkey", "-keystore", "_teststore", "-storepass", "storepass", "-keyalg", "RSA", "-alias", alias, "-keypass", password, "-dname", name.toString()) - @BeforeClass @JvmStatic fun beforeClass() { - generateKey(ALICE, ALICE_PASS, ALICE_NAME) - generateKey(BOB, BOB_PASS, BOB_NAME) + dir.generateKey(ALICE, ALICE_PASS, ALICE_NAME.toString()) + dir.generateKey(BOB, BOB_PASS, BOB_NAME.toString()) (dir / "_signable1").writeLines(listOf("signable1")) (dir / "_signable2").writeLines(listOf("signable2")) @@ -75,49 +59,49 @@ class JarSignatureCollectorTest { @Test fun `empty jar has no signers`() { (dir / "META-INF").createDirectory() // At least one arg is required, and jar cvf conveniently ignores this. - createJar("META-INF") - assertEquals(emptyList(), getJarSigners()) + dir.createJar(FILENAME, "META-INF") + assertEquals(emptyList(), dir.getJarSigners(FILENAME)) signAsAlice() - assertEquals(emptyList(), getJarSigners()) // There needs to have been a file for ALICE to sign. + assertEquals(emptyList(), dir.getJarSigners(FILENAME)) // There needs to have been a file for ALICE to sign. } @Test fun `unsigned jar has no signers`() { - createJar("_signable1") - assertEquals(emptyList(), getJarSigners()) + dir.createJar(FILENAME, "_signable1") + assertEquals(emptyList(), dir.getJarSigners(FILENAME)) - updateJar("_signable2") - assertEquals(emptyList(), getJarSigners()) + dir.updateJar(FILENAME, "_signable2") + assertEquals(emptyList(), dir.getJarSigners(FILENAME)) } @Test fun `one signer`() { - createJar("_signable1", "_signable2") + dir.createJar(FILENAME, "_signable1", "_signable2") val key = signAsAlice() - assertEquals(listOf(key), getJarSigners()) + assertEquals(listOf(key), dir.getJarSigners(FILENAME)) (dir / "my-dir").createDirectory() - updateJar("my-dir") - assertEquals(listOf(key), getJarSigners()) // Unsigned directory is irrelevant. + dir.updateJar(FILENAME, "my-dir") + assertEquals(listOf(key), dir.getJarSigners(FILENAME)) // Unsigned directory is irrelevant. } @Test fun `two signers`() { - createJar("_signable1", "_signable2") + dir.createJar(FILENAME, "_signable1", "_signable2") val key1 = signAsAlice() val key2 = signAsBob() - assertEquals(setOf(key1, key2), getJarSigners().toSet()) + assertEquals(setOf(key1, key2), dir.getJarSigners(FILENAME).toSet()) } @Test fun `all files must be signed by the same set of signers`() { - createJar("_signable1") + dir.createJar(FILENAME, "_signable1") val key1 = signAsAlice() - assertEquals(listOf(key1), getJarSigners()) + assertEquals(listOf(key1), dir.getJarSigners(FILENAME)) - updateJar("_signable2") + dir.updateJar(FILENAME, "_signable2") signAsBob() assertFailsWith( """ @@ -126,41 +110,23 @@ class JarSignatureCollectorTest { See https://docs.corda.net/design/data-model-upgrades/signature-constraints.html for details of the constraints applied to attachment signatures. """.trimIndent().replace('\n', ' ') - ) { getJarSigners() } + ) { dir.getJarSigners(FILENAME) } } @Test fun `bad signature is caught even if the party would not qualify as a signer`() { (dir / "volatile").writeLines(listOf("volatile")) - createJar("volatile") + dir.createJar(FILENAME, "volatile") val key1 = signAsAlice() - assertEquals(listOf(key1), getJarSigners()) + assertEquals(listOf(key1), dir.getJarSigners(FILENAME)) (dir / "volatile").writeLines(listOf("garbage")) - updateJar("volatile", "_signable1") // ALICE's signature on volatile is now bad. + dir.updateJar(FILENAME, "volatile", "_signable1") // ALICE's signature on volatile is now bad. signAsBob() // The JDK doesn't care that BOB has correctly signed the whole thing, it won't let us process the entry with ALICE's bad signature: - assertFailsWith { getJarSigners() } + assertFailsWith { dir.getJarSigners(FILENAME) } } - //region Helper functions - private fun createJar(vararg contents: String) = - execute(*(arrayOf("jar", "cvf", FILENAME) + contents)) - - private fun updateJar(vararg contents: String) = - execute(*(arrayOf("jar", "uvf", FILENAME) + contents)) - - private fun signJar(alias: String, password: String): PublicKey { - execute("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", password, FILENAME, alias) - val ks = loadKeyStore(dir.resolve("_teststore"), "storepass") - return ks.getCertificate(alias).publicKey - } - - private fun signAsAlice() = signJar(ALICE, ALICE_PASS) - private fun signAsBob() = signJar(BOB, BOB_PASS) - - private fun getJarSigners() = - JarInputStream(FileInputStream((dir / FILENAME).toFile())).use(JarSignatureCollector::collectSigners) - //endregion - + private fun signAsAlice() = dir.signJar(FILENAME, ALICE, ALICE_PASS) + private fun signAsBob() = dir.signJar(FILENAME, BOB, BOB_PASS) } diff --git a/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt b/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt index ec8cfe1178..cd9456cdc3 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/TransactionBuilderTest.kt @@ -134,5 +134,5 @@ class TransactionBuilderTest { override val id: SecureHash get() = throw UnsupportedOperationException() override val signers: List get() = parties.map { it.owningKey } - }, DummyContract.PROGRAM_ID) + }, DummyContract.PROGRAM_ID, signers = parties.map { it.owningKey }) } diff --git a/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt b/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt index 3f6f40bf51..32f9105a1c 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt @@ -6,6 +6,7 @@ import net.corda.core.contracts.* import net.corda.core.crypto.* import net.corda.core.crypto.CompositeKey import net.corda.core.identity.Party +import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.contracts.DummyContract import net.corda.testing.core.* import net.corda.testing.internal.createWireTransaction @@ -129,7 +130,8 @@ class TransactionTests { id, null, timeWindow, - privacySalt + privacySalt, + testNetworkParameters() ) transaction.verify() diff --git a/docs/source/network-map.rst b/docs/source/network-map.rst index 3b5e4e5893..3af6b5d6ce 100644 --- a/docs/source/network-map.rst +++ b/docs/source/network-map.rst @@ -125,7 +125,14 @@ The current set of network parameters: :eventHorizon: Time after which nodes are considered to be unresponsive and removed from network map. Nodes republish their ``NodeInfo`` on a regular interval. Network map treats that as a heartbeat from the node. -More parameters will be added in future releases to regulate things like allowed port numbers, whether or not IPv6 +:packageOwnership: List of the network-wide java packages that were successfully claimed by their owners. + Any CorDapp JAR that offers contracts and states in any of these packages must be signed by the owner. + This ensures that when a node encounters an owned contract it can uniquely identify it and knows that all other nodes can do the same. + Encountering an owned contract in a JAR that is not signed by the rightful owner is most likely a sign of malicious behaviour, and should be reported. + The transaction verification logic will throw an exception when this happens. + Read more about *Package ownership* here :doc:`design/data-model-upgrades/package-namespace-ownership`. + +More parameters will be added in future releases to regulate things like allowed port numbers, whether or not IPv6 connectivity is required for zone members, required cryptographic algorithms and roll-out schedules (e.g. for moving to post quantum cryptography), parameters related to SGX and so on. Network parameters update process diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 2391cf7fca..809735c75a 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -164,7 +164,9 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(), attachments).tokenize() @Suppress("LeakingThis") val keyManagementService = makeKeyManagementService(identityService).tokenize() - val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, transactionStorage) + val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, transactionStorage).also { + attachments.servicesForResolution = it + } @Suppress("LeakingThis") val vaultService = makeVaultService(keyManagementService, servicesForResolution, database).tokenize() val nodeProperties = NodePropertiesPersistentStore(StubbedNodeUniqueIdProvider::value, database) @@ -1052,7 +1054,7 @@ fun createCordaPersistence(databaseConfig: DatabaseConfig, // so we end up providing both descriptor and converter. We should re-examine this in later versions to see if // either Hibernate can be convinced to stop warning, use the descriptor by default, or something else. JavaTypeDescriptorRegistry.INSTANCE.addDescriptor(AbstractPartyDescriptor(wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous)) - val attributeConverters = listOf(AbstractPartyToX500NameAsStringConverter(wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous)) + val attributeConverters = listOf(PublicKeyToTextConverter(), AbstractPartyToX500NameAsStringConverter(wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous)) val jdbcUrl = hikariProperties.getProperty("dataSource.url", "") return CordaPersistence(databaseConfig, schemaService.schemaOptions.keys, jdbcUrl, attributeConverters) } diff --git a/node/src/main/kotlin/net/corda/node/serialization/kryo/DefaultKryoCustomizer.kt b/node/src/main/kotlin/net/corda/node/serialization/kryo/DefaultKryoCustomizer.kt index 0fd8fa7432..37bd1acd33 100644 --- a/node/src/main/kotlin/net/corda/node/serialization/kryo/DefaultKryoCustomizer.kt +++ b/node/src/main/kotlin/net/corda/node/serialization/kryo/DefaultKryoCustomizer.kt @@ -209,15 +209,17 @@ object DefaultKryoCustomizer { output.writeString(obj.contract) kryo.writeClassAndObject(output, obj.additionalContracts) output.writeString(obj.uploader) + kryo.writeClassAndObject(output, obj.signers) } + @Suppress("UNCHECKED_CAST") override fun read(kryo: Kryo, input: Input, type: Class): ContractAttachment { if (kryo.serializationContext() != null) { val attachmentHash = SecureHash.SHA256(input.readBytes(32)) val contract = input.readString() - @Suppress("UNCHECKED_CAST") val additionalContracts = kryo.readClassAndObject(input) as Set val uploader = input.readString() + val signers = kryo.readClassAndObject(input) as List val context = kryo.serializationContext()!! val attachmentStorage = context.serviceHub.attachments @@ -229,14 +231,14 @@ object DefaultKryoCustomizer { override val id = attachmentHash } - return ContractAttachment(lazyAttachment, contract, additionalContracts, uploader) + return ContractAttachment(lazyAttachment, contract, additionalContracts, uploader, signers) } else { val attachment = GeneratedAttachment(input.readBytesWithLength()) val contract = input.readString() - @Suppress("UNCHECKED_CAST") val additionalContracts = kryo.readClassAndObject(input) as Set val uploader = input.readString() - return ContractAttachment(attachment, contract, additionalContracts, uploader) + val signers = kryo.readClassAndObject(input) as List + return ContractAttachment(attachment, contract, additionalContracts, uploader, signers) } } } diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt index ca96ef1ad5..ba0b1d98ca 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt @@ -6,13 +6,16 @@ import com.google.common.hash.HashCode import com.google.common.hash.Hashing import com.google.common.hash.HashingInputStream import com.google.common.io.CountingInputStream +import net.corda.core.ClientRelevantError import net.corda.core.CordaRuntimeException import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractAttachment import net.corda.core.contracts.ContractClassName import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.isFulfilledBy import net.corda.core.crypto.sha256 import net.corda.core.internal.* +import net.corda.core.node.ServicesForResolution import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.vault.AttachmentQueryCriteria import net.corda.core.node.services.vault.AttachmentSort @@ -31,6 +34,7 @@ import java.io.FilterInputStream import java.io.IOException import java.io.InputStream import java.nio.file.Paths +import java.security.PublicKey import java.time.Instant import java.util.* import java.util.jar.JarInputStream @@ -46,6 +50,10 @@ class NodeAttachmentService( cacheFactory: NamedCacheFactory, private val database: CordaPersistence ) : AttachmentStorageInternal, SingletonSerializeAsToken() { + + // This is to break the circular dependency. + lateinit var servicesForResolution: ServicesForResolution + companion object { private val log = contextLogger() @@ -95,7 +103,13 @@ class NodeAttachmentService( @Column(name = "contract_class_name", nullable = false) @CollectionTable(name = "${NODE_DATABASE_PREFIX}attachments_contracts", joinColumns = [(JoinColumn(name = "att_id", referencedColumnName = "att_id"))], foreignKey = ForeignKey(name = "FK__ctr_class__attachments")) - var contractClassNames: List? = null + var contractClassNames: List? = null, + + @ElementCollection(targetClass = PublicKey::class, fetch = FetchType.EAGER) + @Column(name = "signer", nullable = false) + @CollectionTable(name = "${NODE_DATABASE_PREFIX}attachments_signers", joinColumns = [(JoinColumn(name = "att_id", referencedColumnName = "att_id"))], + foreignKey = ForeignKey(name = "FK__signers__attachments")) + var signers: List? = null ) @VisibleForTesting @@ -213,11 +227,13 @@ class NodeAttachmentService( private fun loadAttachmentContent(id: SecureHash): Pair? { return database.transaction { - val attachment = currentDBSession().get(NodeAttachmentService.DBAttachment::class.java, id.toString()) ?: return@transaction null + val attachment = currentDBSession().get(NodeAttachmentService.DBAttachment::class.java, id.toString()) + ?: return@transaction null val attachmentImpl = AttachmentImpl(id, { attachment.content }, checkAttachmentsOnLoad).let { val contracts = attachment.contractClassNames if (contracts != null && contracts.isNotEmpty()) { - ContractAttachment(it, contracts.first(), contracts.drop(1).toSet(), attachment.uploader) + ContractAttachment(it, contracts.first(), contracts.drop(1).toSet(), attachment.uploader, attachment.signers + ?: emptyList()) } else { it } @@ -291,14 +307,19 @@ class NodeAttachmentService( val id = bytes.sha256() if (!hasAttachment(id)) { checkIsAValidJAR(bytes.inputStream()) + + val jarSigners = getSigners(bytes) + val session = currentDBSession() val attachment = NodeAttachmentService.DBAttachment( attId = id.toString(), content = bytes, uploader = uploader, filename = filename, - contractClassNames = contractClassNames + contractClassNames = contractClassNames, + signers = jarSigners ) + session.save(attachment) attachmentCount.inc() log.info("Stored new attachment $id") @@ -310,6 +331,9 @@ class NodeAttachmentService( } } + private fun getSigners(attachmentBytes: ByteArray) = + JarSignatureCollector.collectSigners(JarInputStream(attachmentBytes.inputStream())) + @Suppress("OverridingDeprecatedMember") override fun importOrGetAttachment(jar: InputStream): AttachmentId { return try { @@ -340,4 +364,4 @@ class NodeAttachmentService( query.resultList.map { AttachmentId.parse(it.attId) } } } -} +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/PublicKeyToTextConverter.kt b/node/src/main/kotlin/net/corda/node/services/persistence/PublicKeyToTextConverter.kt new file mode 100644 index 0000000000..4ee5a15e57 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/persistence/PublicKeyToTextConverter.kt @@ -0,0 +1,18 @@ +package net.corda.node.services.persistence + +import net.corda.core.crypto.Crypto +import net.corda.core.utilities.hexToByteArray +import net.corda.core.utilities.toHex +import java.security.PublicKey +import javax.persistence.AttributeConverter +import javax.persistence.Converter + +/** + * Converts to and from a Public key into a hex encoded string. + * Used by JPA to automatically map a [PublicKey] to a text column + */ +@Converter(autoApply = true) +class PublicKeyToTextConverter : AttributeConverter { + override fun convertToDatabaseColumn(key: PublicKey?): String? = key?.encoded?.toHex() + override fun convertToEntityAttribute(text: String?): PublicKey? = text?.let { Crypto.decodePublicKey(it.hexToByteArray()) } +} \ No newline at end of file diff --git a/node/src/main/resources/migration/node-core.changelog-v8.xml b/node/src/main/resources/migration/node-core.changelog-v8.xml index 380faf518d..11c55db5ee 100644 --- a/node/src/main/resources/migration/node-core.changelog-v8.xml +++ b/node/src/main/resources/migration/node-core.changelog-v8.xml @@ -14,4 +14,17 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt b/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt index b5b14d93ad..0716d4e394 100644 --- a/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt @@ -2,6 +2,8 @@ package net.corda.node.internal import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.whenever +import net.corda.core.crypto.generateKeyPair +import net.corda.core.node.JavaPackageName import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.finance.DOLLARS @@ -10,6 +12,7 @@ import net.corda.node.services.config.NotaryConfig import net.corda.core.node.NetworkParameters import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.core.node.NotaryInfo +import net.corda.core.utilities.days import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME @@ -65,7 +68,8 @@ class NetworkParametersTest { fun `choosing notary not specified in network parameters will fail`() { val fakeNotary = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME, configOverrides = { val notary = NotaryConfig(false) - doReturn(notary).whenever(it).notary})) + doReturn(notary).whenever(it).notary + })) val fakeNotaryId = fakeNotary.info.singleIdentity() val alice = mockNet.createPartyNode(ALICE_NAME) assertThat(alice.services.networkMapCache.notaryIdentities).doesNotContain(fakeNotaryId) @@ -87,6 +91,62 @@ class NetworkParametersTest { }.withMessage("maxTransactionSize cannot be bigger than maxMessageSize") } + @Test + fun `package ownership checks are correct`() { + val key1 = generateKeyPair().public + val key2 = generateKeyPair().public + + assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy { + NetworkParameters(1, + emptyList(), + 2001, + 2000, + Instant.now(), + 1, + emptyMap(), + Int.MAX_VALUE.days, + mapOf( + JavaPackageName("com.!example.stuff") to key2 + ) + ) + }.withMessageContaining("Attempting to whitelist illegal java package") + + assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy { + NetworkParameters(1, + emptyList(), + 2001, + 2000, + Instant.now(), + 1, + emptyMap(), + Int.MAX_VALUE.days, + mapOf( + JavaPackageName("com.example") to key1, + JavaPackageName("com.example.stuff") to key2 + ) + ) + }.withMessage("multiple packages added to the packageOwnership overlap.") + + NetworkParameters(1, + emptyList(), + 2001, + 2000, + Instant.now(), + 1, + emptyMap(), + Int.MAX_VALUE.days, + mapOf( + JavaPackageName("com.example") to key1, + JavaPackageName("com.examplestuff") to key2 + ) + ) + + assert(JavaPackageName("com.example").owns("com.example.something.MyClass")) + assert(!JavaPackageName("com.example").owns("com.examplesomething.MyClass")) + assert(!JavaPackageName("com.exam").owns("com.example.something.MyClass")) + + } + // Helpers private fun dropParametersToDir(dir: Path, params: NetworkParameters) { NetworkParametersCopier(params).install(dir) diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt index 70c827b496..5730645493 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt @@ -4,10 +4,16 @@ import co.paralleluniverse.fibers.Suspendable import com.codahale.metrics.MetricRegistry import com.google.common.jimfs.Configuration import com.google.common.jimfs.Jimfs +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever +import net.corda.core.JarSignatureTestUtils.createJar +import net.corda.core.JarSignatureTestUtils.generateKey +import net.corda.core.JarSignatureTestUtils.signJar import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 import net.corda.core.flows.FlowLogic import net.corda.core.internal.* +import net.corda.core.node.ServicesForResolution import net.corda.core.node.services.vault.AttachmentQueryCriteria import net.corda.core.node.services.vault.AttachmentSort import net.corda.core.node.services.vault.Builder @@ -18,32 +24,39 @@ import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.node.utilities.TestingNamedCacheFactory import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig +import net.corda.testing.common.internal.testNetworkParameters +import net.corda.testing.core.ALICE_NAME import net.corda.testing.internal.LogHelper +import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.startFlow import org.assertj.core.api.Assertions.assertThatIllegalArgumentException -import org.junit.After -import org.junit.Before -import org.junit.Ignore -import org.junit.Test +import org.junit.* import java.io.ByteArrayOutputStream import java.io.OutputStream +import java.net.URI import java.nio.charset.StandardCharsets -import java.nio.file.FileAlreadyExistsException -import java.nio.file.FileSystem -import java.nio.file.Path +import java.nio.file.* +import java.security.PublicKey import java.util.jar.JarEntry import java.util.jar.JarOutputStream +import javax.tools.JavaFileObject +import javax.tools.SimpleJavaFileObject +import javax.tools.StandardLocation +import javax.tools.ToolProvider import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertNull + class NodeAttachmentServiceTest { + // Use an in memory file system for testing attachment storage. private lateinit var fs: FileSystem private lateinit var database: CordaPersistence private lateinit var storage: NodeAttachmentService + private val services = rigorousMock() @Before fun setUp() { @@ -52,18 +65,40 @@ class NodeAttachmentServiceTest { val dataSourceProperties = makeTestDataSourceProperties() database = configureDatabase(dataSourceProperties, DatabaseConfig(), { null }, { null }) fs = Jimfs.newFileSystem(Configuration.unix()) + + doReturn(testNetworkParameters()).whenever(services).networkParameters + storage = NodeAttachmentService(MetricRegistry(), TestingNamedCacheFactory(), database).also { database.transaction { it.start() } } + storage.servicesForResolution = services } @After fun tearDown() { + dir.list { subdir -> + subdir.forEach(Path::deleteRecursively) + } database.close() } + @Test + fun `importing a signed jar saves the signers to the storage`() { + val jarAndSigner = makeTestSignedContractJar("com.example.MyContract") + val signedJar = jarAndSigner.first + val attachmentId = storage.importAttachment(signedJar.inputStream(), "test", null) + assertEquals(listOf(jarAndSigner.second.hash), storage.openAttachment(attachmentId)!!.signers.map { it.hash }) + } + + @Test + fun `importing a non-signed jar will save no signers`() { + val jarName = makeTestContractJar("com.example.MyContract") + val attachmentId = storage.importAttachment(dir.resolve(jarName).inputStream(), "test", null) + assertEquals(0, storage.openAttachment(attachmentId)!!.signers.size) + } + @Test fun `insert and retrieve`() { val (testJar, expectedHash) = makeTestJar() @@ -289,7 +324,20 @@ class NodeAttachmentServiceTest { return Pair(file, file.readAll().sha256()) } - private companion object { + companion object { + private val dir = Files.createTempDirectory(NodeAttachmentServiceTest::class.simpleName) + + @BeforeClass + @JvmStatic + fun beforeClass() { + } + + @AfterClass + @JvmStatic + fun afterClass() { + dir.deleteRecursively() + } + private fun makeTestJar(output: OutputStream, extraEntries: List> = emptyList()) { output.use { val jar = JarOutputStream(it) @@ -305,5 +353,48 @@ class NodeAttachmentServiceTest { jar.closeEntry() } } + + private fun makeTestSignedContractJar(contractName: String): Pair { + val alias = "testAlias" + val pwd = "testPassword" + dir.generateKey(alias, pwd, ALICE_NAME.toString()) + val jarName = makeTestContractJar(contractName) + val signer = dir.signJar(jarName, alias, pwd) + return dir.resolve(jarName) to signer + } + + private fun makeTestContractJar(contractName: String): String { + val packages = contractName.split(".") + val jarName = "testattachment.jar" + val className = packages.last() + createTestClass(className, packages.subList(0, packages.size - 1)) + dir.createJar(jarName, "${contractName.replace(".", "/")}.class") + return jarName + } + + private fun createTestClass(className: String, packages: List): Path { + val newClass = """package ${packages.joinToString(".")}; + import net.corda.core.contracts.*; + import net.corda.core.transactions.*; + + public class $className implements Contract { + @Override + public void verify(LedgerTransaction tx) throws IllegalArgumentException { + } + } + """.trimIndent() + val compiler = ToolProvider.getSystemJavaCompiler() + val source = object : SimpleJavaFileObject(URI.create("string:///${packages.joinToString("/")}/${className}.java"), JavaFileObject.Kind.SOURCE) { + override fun getCharContent(ignoreEncodingErrors: Boolean): CharSequence { + return newClass + } + } + val fileManager = compiler.getStandardFileManager(null, null, null) + fileManager.setLocation(StandardLocation.CLASS_OUTPUT, listOf(dir.toFile())) + + val compile = compiler.getTask(System.out.writer(), fileManager, null, null, null, listOf(source)).call() + return Paths.get(fileManager.list(StandardLocation.CLASS_OUTPUT, "", setOf(JavaFileObject.Kind.CLASS), true).single().name) + } } + } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ContractAttachmentSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ContractAttachmentSerializer.kt index 05fe559ac7..f487840f25 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ContractAttachmentSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ContractAttachmentSerializer.kt @@ -9,6 +9,7 @@ import net.corda.core.serialization.MissingAttachmentsException import net.corda.serialization.internal.GeneratedAttachment import net.corda.serialization.internal.amqp.CustomSerializer import net.corda.serialization.internal.amqp.SerializerFactory +import java.security.PublicKey /** * A serializer for [ContractAttachment] that uses a proxy object to write out the full attachment eagerly. @@ -23,13 +24,13 @@ class ContractAttachmentSerializer(factory: SerializerFactory) : CustomSerialize } catch (e: Exception) { throw MissingAttachmentsException(listOf(obj.id)) } - return ContractAttachmentProxy(GeneratedAttachment(bytes), obj.contract, obj.additionalContracts, obj.uploader) + return ContractAttachmentProxy(GeneratedAttachment(bytes), obj.contract, obj.additionalContracts, obj.uploader, obj.signers) } override fun fromProxy(proxy: ContractAttachmentProxy): ContractAttachment { - return ContractAttachment(proxy.attachment, proxy.contract, proxy.contracts, proxy.uploader) + return ContractAttachment(proxy.attachment, proxy.contract, proxy.contracts, proxy.uploader, proxy.signers) } @KeepForDJVM - data class ContractAttachmentProxy(val attachment: Attachment, val contract: ContractClassName, val contracts: Set, val uploader: String?) + data class ContractAttachmentProxy(val attachment: Attachment, val contract: ContractClassName, val contracts: Set, val uploader: String?, val signers: List) } \ No newline at end of file diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyState.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyState.kt index a358427b08..bb3b451aad 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyState.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyState.kt @@ -1,11 +1,13 @@ package net.corda.testing.contracts +import net.corda.core.contracts.BelongsToContract import net.corda.core.contracts.ContractState import net.corda.core.identity.AbstractParty /** * Dummy state for use in testing. Not part of any contract, not even the [DummyContract]. */ +@BelongsToContract(DummyContract::class) data class DummyState @JvmOverloads constructor ( /** Some information that the state represents for test purposes. **/ val magicNumber: Int = 0, diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt index 4be209c3c0..727ca1fd48 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt @@ -7,6 +7,7 @@ import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 import net.corda.core.identity.Party import net.corda.core.internal.AbstractAttachment +import net.corda.core.internal.JarSignatureCollector import net.corda.core.internal.UNKNOWN_UPLOADER import net.corda.core.internal.readFully import net.corda.core.node.services.AttachmentId @@ -71,7 +72,7 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() { val sha256 = attachmentId ?: bytes.sha256() if (sha256 !in files.keys) { val baseAttachment = MockAttachment({ bytes }, sha256, signers) - val attachment = if (contractClassNames == null || contractClassNames.isEmpty()) baseAttachment else ContractAttachment(baseAttachment, contractClassNames.first(), contractClassNames.toSet(), uploader) + val attachment = if (contractClassNames == null || contractClassNames.isEmpty()) baseAttachment else ContractAttachment(baseAttachment, contractClassNames.first(), contractClassNames.toSet(), uploader, signers) _files[sha256] = Pair(attachment, bytes) } return sha256 From 469ffe473fe480b631b0e5ed8da1fd24ac210e89 Mon Sep 17 00:00:00 2001 From: Tudor Malene Date: Wed, 24 Oct 2018 17:36:14 +0100 Subject: [PATCH 6/6] Fix merge Fix merge Fix merge --- .../core/transactions/LedgerTransaction.kt | 26 ++----------------- .../core/transactions/TransactionBuilder.kt | 6 ++--- .../cordapp/JarScanningCordappLoader.kt | 1 + .../node/services/vault/VaultQueryTests.kt | 19 +++++++------- .../kotlin/net/corda/testing/dsl/TestDSL.kt | 4 --- .../testing/internal/vault/VaultFiller.kt | 14 +++++----- 6 files changed, 23 insertions(+), 47 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index 880f78b0b2..d0ee49a524 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -10,6 +10,7 @@ import net.corda.core.internal.castIfPossible import net.corda.core.internal.uncheckedCast import net.corda.core.node.NetworkParameters import net.corda.core.serialization.CordaSerializable +import net.corda.core.utilities.Try import net.corda.core.utilities.loggerFor import java.util.* import java.util.function.Predicate @@ -56,6 +57,7 @@ data class LedgerTransaction @JvmOverloads constructor( } private companion object { + val logger = loggerFor() private fun contractClassFor(className: ContractClassName, classLoader: ClassLoader?): Try> { return Try.on { (classLoader ?: this::class.java.classLoader) @@ -141,30 +143,6 @@ data class LedgerTransaction @JvmOverloads constructor( } } - /** - * Verify that for each contract the network wide package owner is respected. - * - * TODO - revisit once transaction contains network parameters. - */ - private fun validatePackageOwnership(contractAttachmentsByContract: Map) { - // This should never happen once we have network parameters in the transaction. - if (networkParameters == null) { - return - } - - val contractsAndOwners = allStates.mapNotNull { transactionState -> - val contractClassName = transactionState.contract - networkParameters.getOwnerOf(contractClassName)?.let { contractClassName to it } - }.toMap() - - contractsAndOwners.forEach { contract, owner -> - val attachment = contractAttachmentsByContract[contract]!! - if (!owner.isFulfilledBy(attachment.signers)) { - throw TransactionVerificationException.ContractAttachmentNotSignedByPackageOwnerException(this.id, id, contract) - } - } - } - /** * Enforces the validity of the actual constraints. * * Constraints should be one of the valid supported ones. diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index 8475afaf52..5f747f2df3 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -181,7 +181,7 @@ open class TransactionBuilder @JvmOverloads constructor( val resolvedStates: List> = contractAttachmentsAndResolvedOutputStates.mapNotNull { it.second }.flatten() // The output states need to preserve the order in which they were added. - val resolvedOutputStatesInTheOriginalOrder: List> = outputStates().map { os -> resolvedStates.find { rs -> rs.data == os.data }!! } + val resolvedOutputStatesInTheOriginalOrder: List> = outputStates().map { os -> resolvedStates.find { rs -> rs.data == os.data && rs.encumbrance == os.encumbrance}!! } val attachments: Collection = contractAttachmentsAndResolvedOutputStates.map { it.first } + refStateContractAttachments @@ -454,7 +454,7 @@ open class TransactionBuilder @JvmOverloads constructor( state: ContractState, contract: ContractClassName = requireNotNull(state.requiredContractClassName) { //TODO: add link to docsite page, when there is one. -""" + """ Unable to infer Contract class name because state class ${state::class.java.name} is not annotated with @BelongsToContract, and does not have an enclosing class which implements Contract. Either annotate ${state::class.java.name} with @BelongsToContract, or supply an explicit contract parameter to addOutputState(). @@ -470,7 +470,7 @@ with @BelongsToContract, or supply an explicit contract parameter to addOutputSt state: ContractState, contract: ContractClassName = requireNotNull(state.requiredContractClassName) { //TODO: add link to docsite page, when there is one. -""" + """ Unable to infer Contract class name because state class ${state::class.java.name} is not annotated with @BelongsToContract, and does not have an enclosing class which implements Contract. Either annotate ${state::class.java.name} with @BelongsToContract, or supply an explicit contract parameter to addOutputState(). diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt index 3827957bec..3fbe766a6e 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt @@ -2,6 +2,7 @@ package net.corda.node.internal.cordapp import io.github.classgraph.ClassGraph import io.github.classgraph.ScanResult +import net.corda.core.contracts.warnContractWithoutConstraintPropagation import net.corda.core.cordapp.Cordapp import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index 7f55504ee9..70dbfe79fe 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -7,9 +7,9 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.packageName import net.corda.core.node.services.* +import net.corda.core.node.services.Vault.ConstraintInfo.Type.* import net.corda.core.node.services.vault.* import net.corda.core.node.services.vault.QueryCriteria.* -import net.corda.core.node.services.Vault.ConstraintInfo.Type.* import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.* @@ -481,7 +481,7 @@ abstract class VaultQueryTestsBase : VaultQueryParties { vaultFiller.fillWithSomeTestLinearStates(1, constraint = AlwaysAcceptAttachmentConstraint).states.first().state.constraint vaultFiller.fillWithSomeTestLinearStates(1, constraint = WhitelistedByZoneAttachmentConstraint).states.first().state.constraint // hash constraint - val linearStateHash = vaultFiller.fillWithSomeTestLinearStates(1, constraint = HashAttachmentConstraint(SecureHash.randomSHA256())) + val linearStateHash = vaultFiller.fillWithSomeTestLinearStates(1, constraint = AutomaticPlaceholderConstraint) // defaults to the HashConstraint val constraintHash = linearStateHash.states.first().state.constraint as HashAttachmentConstraint // signature constraint (single key) val linearStateSignature = vaultFiller.fillWithSomeTestLinearStates(1, constraint = SignatureAttachmentConstraint(alice.publicKey)) @@ -504,7 +504,7 @@ abstract class VaultQueryTestsBase : VaultQueryParties { val constraintTypeCriteria2 = VaultQueryCriteria(constraintTypes = setOf(HASH)) val constraintResults2 = vaultService.queryBy(constraintTypeCriteria2) assertThat(constraintResults2.states).hasSize(2) - assertThat(constraintResults2.states.map { it.state.constraint }).containsOnlyOnce(constraintHash) + assertThat(constraintResults2.states.map { it.state.constraint }.toSet()).isEqualTo(setOf(constraintHash)) // search for states with [Vault.ConstraintInfo.Type] either HASH or CZ_WHITELISED // DOCSTART VaultQueryExample30 @@ -536,7 +536,7 @@ abstract class VaultQueryTestsBase : VaultQueryParties { val alwaysAcceptConstraint = vaultFiller.fillWithSomeTestLinearStates(1, constraint = AlwaysAcceptAttachmentConstraint).states.first().state.constraint vaultFiller.fillWithSomeTestLinearStates(1, constraint = WhitelistedByZoneAttachmentConstraint) // hash constraint - val linearStateHash = vaultFiller.fillWithSomeTestLinearStates(1, constraint = HashAttachmentConstraint(SecureHash.randomSHA256())) + val linearStateHash = vaultFiller.fillWithSomeTestLinearStates(1, constraint = AutomaticPlaceholderConstraint) // defaults to the hash constraint. val constraintHash = linearStateHash.states.first().state.constraint as HashAttachmentConstraint // signature constraint (single key) val linearStateSignature = vaultFiller.fillWithSomeTestLinearStates(1, constraint = SignatureAttachmentConstraint(alice.publicKey)) @@ -559,7 +559,7 @@ abstract class VaultQueryTestsBase : VaultQueryParties { // search for states for a specific HashAttachmentConstraint val constraintsCriteria2 = VaultQueryCriteria(constraints = setOf(Vault.ConstraintInfo(constraintHash))) val constraintResults2 = vaultService.queryBy(constraintsCriteria2) - assertThat(constraintResults2.states).hasSize(1) + assertThat(constraintResults2.states).hasSize(2) assertThat(constraintResults2.states.first().state.constraint).isEqualTo(constraintHash) // search for states with a specific SignatureAttachmentConstraint constraint @@ -574,7 +574,7 @@ abstract class VaultQueryTestsBase : VaultQueryParties { Vault.ConstraintInfo(constraintSignatureCompositeKey), Vault.ConstraintInfo(constraintHash))) val constraintResults = vaultService.queryBy(constraintCriteria) // DOCEND VaultQueryExample31 - assertThat(constraintResults.states).hasSize(3) + assertThat(constraintResults.states).hasSize(4) assertThat(constraintResults.states.map { it.state.constraint }).containsAll(listOf(constraintHash, constraintSignature, constraintSignatureCompositeKey)) // exercise enriched query @@ -1474,8 +1474,9 @@ abstract class VaultQueryTestsBase : VaultQueryParties { vaultFiller.fillWithSomeTestLinearStates(1, linearNumber = it.toLong(), linearString = it.toString()) } val max = builder { DummyLinearStateSchemaV1.PersistentDummyLinearState::linearTimestamp.max( - groupByColumns = listOf(DummyLinearStateSchemaV1.PersistentDummyLinearState::linearNumber) - ) + groupByColumns = listOf(DummyLinearStateSchemaV1.PersistentDummyLinearState::linearNumber), + orderBy = Sort.Direction.ASC + ) } val maxCriteria = VaultCustomQueryCriteria(max) val pageSpec = PageSpecification(DEFAULT_PAGE_NUM, MAX_PAGE_SIZE) @@ -2336,7 +2337,7 @@ abstract class VaultQueryTestsBase : VaultQueryParties { database.transaction { vaultFiller.fillWithSomeTestLinearStates(1, constraint = WhitelistedByZoneAttachmentConstraint) vaultFiller.fillWithSomeTestLinearStates(1, constraint = SignatureAttachmentConstraint(alice.publicKey)) - vaultFiller.fillWithSomeTestLinearStates(1, constraint = HashAttachmentConstraint( SecureHash.randomSHA256())) + vaultFiller.fillWithSomeTestLinearStates(1, constraint = AutomaticPlaceholderConstraint) // this defaults to the HashConstraint vaultFiller.fillWithSomeTestLinearStates(1, constraint = AlwaysAcceptAttachmentConstraint) // Base criteria diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt index 9cc8cff843..c98a026365 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt @@ -155,10 +155,6 @@ data class TestTransactionDSLInterpreter private constructor( attachment((services.cordappProvider as MockCordappProvider).addMockCordapp(contractClassName, services.attachments as MockAttachmentStorage, attachmentId, signers)) } - override fun _attachment(contractClassName: ContractClassName, attachmentId: AttachmentId, signers: List){ - attachment((services.cordappProvider as MockCordappProvider).addMockCordapp(contractClassName, services.attachments as MockAttachmentStorage, attachmentId, signers)) - } - } data class TestLedgerDSLInterpreter private constructor( diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt index eb46690305..baba1b0cfe 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt @@ -103,7 +103,7 @@ class VaultFiller @JvmOverloads constructor( linearNumber: Long = 0L, linearBoolean: Boolean = false, linearTimestamp: Instant = now(), - constraint: AttachmentConstraint = AutomaticHashConstraint): Vault { + constraint: AttachmentConstraint = AutomaticPlaceholderConstraint): Vault { val myKey: PublicKey = services.myInfo.chooseIdentity().owningKey val me = AnonymousParty(myKey) val issuerKey = defaultNotary.keyPair @@ -134,12 +134,12 @@ class VaultFiller @JvmOverloads constructor( @JvmOverloads fun fillWithSomeTestLinearAndDealStates(numberToCreate: Int, - externalId: String? = null, - participants: List = emptyList(), - linearString: String = "", - linearNumber: Long = 0L, - linearBoolean: Boolean = false, - linearTimestamp: Instant = now()): Vault { + externalId: String? = null, + participants: List = emptyList(), + linearString: String = "", + linearNumber: Long = 0L, + linearBoolean: Boolean = false, + linearTimestamp: Instant = now()): Vault { val myKey: PublicKey = services.myInfo.chooseIdentity().owningKey val me = AnonymousParty(myKey) val issuerKey = defaultNotary.keyPair