diff --git a/.ci/api-current.txt b/.ci/api-current.txt index bf13f356f7..154bd993fe 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -1132,10 +1132,6 @@ public static final class net.corda.core.contracts.TransactionVerificationExcept public (net.corda.core.crypto.SecureHash, net.corda.core.contracts.TransactionState) ## @CordaSerializable -public static final class net.corda.core.contracts.TransactionVerificationException$TransactionVerificationVersionException extends net.corda.core.contracts.TransactionVerificationException - public (net.corda.core.crypto.SecureHash, String, String, String) -## -@CordaSerializable public abstract class net.corda.core.contracts.TypeOnlyCommandData extends java.lang.Object implements net.corda.core.contracts.CommandData public () public boolean equals(Object) 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 1ce29105a8..58c306ab40 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt @@ -275,15 +275,6 @@ abstract class TransactionVerificationException(val txId: SecureHash, message: S @KeepForDJVM class OverlappingAttachmentsException(txId: SecureHash, path: String) : TransactionVerificationException(txId, "Multiple attachments define a file at $path.", null) - /** - * Thrown when a transaction appears to be trying to downgrade a state to an earlier version of the app that defines it. - * This could be an attempt to exploit a bug in the app, so we prevent it. - */ - @KeepForDJVM - class TransactionVerificationVersionException(txId: SecureHash, contractClassName: ContractClassName, inputVersion: String, outputVersion: String) - : TransactionVerificationException(txId, "No-Downgrade Rule has been breached for contract class $contractClassName. " + - "The output state contract version '$outputVersion' is lower than the version of the input state '$inputVersion'.", null) - /** * Thrown to indicate that a contract attachment is not signed by the network-wide package owner. Please note that * the [txId] will always be [SecureHash.zeroHash] because package ownership is an error with a particular attachment, diff --git a/core/src/test/kotlin/net/corda/core/contracts/ConstraintsPropagationTests.kt b/core/src/test/kotlin/net/corda/core/contracts/ConstraintsPropagationTests.kt index 73e9231217..3496e66fe2 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/ConstraintsPropagationTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/ConstraintsPropagationTests.kt @@ -375,6 +375,24 @@ class ConstraintsPropagationTests { SignableData(wireTransaction.id, SignatureMetadata(4, Crypto.findSignatureScheme(nodeKey).schemeNumberID)), nodeKey)) recordTransactions(SignedTransaction(wireTransaction, sigs)) } + @Test + fun `Input state contract version may be incompatible with lower version`() { + ledgerServices.ledger(DUMMY_NOTARY) { + ledgerServices.recordTransaction(transaction { + attachment(Cash.PROGRAM_ID, SecureHash.allOnesHash, listOf(hashToSignatureConstraintsKey), mapOf(Attributes.Name.IMPLEMENTATION_VERSION.toString() to "2")) + output(Cash.PROGRAM_ID, "c1", DUMMY_NOTARY, null, SignatureAttachmentConstraint(hashToSignatureConstraintsKey), 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, listOf(hashToSignatureConstraintsKey), mapOf(Attributes.Name.IMPLEMENTATION_VERSION.toString() to "1")) + input("c1") + output(Cash.PROGRAM_ID, "c2", DUMMY_NOTARY, null, SignatureAttachmentConstraint(hashToSignatureConstraintsKey), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY)) + command(ALICE_PUBKEY, Cash.Commands.Move()) + verifies() + } + } + } @Test fun `Input state contract version is compatible with the same version`() { @@ -414,6 +432,50 @@ class ConstraintsPropagationTests { } } + @Test + fun `Input states contract version may be lower that current contract version`() { + ledgerServices.ledger(DUMMY_NOTARY) { + ledgerServices.recordTransaction(transaction { + attachment(Cash.PROGRAM_ID, SecureHash.allOnesHash, listOf(hashToSignatureConstraintsKey), mapOf(Attributes.Name.IMPLEMENTATION_VERSION.toString() to "1")) + output(Cash.PROGRAM_ID, "c1", DUMMY_NOTARY, null, SignatureAttachmentConstraint(hashToSignatureConstraintsKey), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), ALICE_PARTY)) + command(ALICE_PUBKEY, Cash.Commands.Issue()) + verifies() + }) + ledgerServices.recordTransaction(transaction { + attachment(Cash.PROGRAM_ID, SecureHash.zeroHash, listOf(hashToSignatureConstraintsKey), mapOf(Attributes.Name.IMPLEMENTATION_VERSION.toString() to "2")) + output(Cash.PROGRAM_ID, "c2", DUMMY_NOTARY, null, SignatureAttachmentConstraint(hashToSignatureConstraintsKey), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), ALICE_PARTY)) + command(ALICE_PUBKEY, Cash.Commands.Issue()) + verifies() + }) + transaction { + input("c1") + input("c2") + output(Cash.PROGRAM_ID, "c3", DUMMY_NOTARY, null, SignatureAttachmentConstraint(hashToSignatureConstraintsKey), Cash.State(2000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY)) + command(ALICE_PUBKEY, Cash.Commands.Move()) + verifies() + } + } + } + + @Test + fun `Input state with contract version can be downgraded to no version`() { + ledgerServices.ledger(DUMMY_NOTARY) { + ledgerServices.recordTransaction(transaction { + attachment(Cash.PROGRAM_ID, SecureHash.allOnesHash, listOf(hashToSignatureConstraintsKey), mapOf(Attributes.Name.IMPLEMENTATION_VERSION.toString() to "2")) + output(Cash.PROGRAM_ID, "c1", DUMMY_NOTARY, null, SignatureAttachmentConstraint(hashToSignatureConstraintsKey), 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, listOf(hashToSignatureConstraintsKey), emptyMap()) + input("c1") + output(Cash.PROGRAM_ID, "c2", DUMMY_NOTARY, null, SignatureAttachmentConstraint(hashToSignatureConstraintsKey), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY)) + command(ALICE_PUBKEY, Cash.Commands.Move()) + verifies() + } + } + } + @Test fun `Input state without contract version is compatible with any version`() { ledgerServices.ledger(DUMMY_NOTARY) { diff --git a/docs/source/api-contract-constraints.rst b/docs/source/api-contract-constraints.rst index 46f2f28a69..dabfb84324 100644 --- a/docs/source/api-contract-constraints.rst +++ b/docs/source/api-contract-constraints.rst @@ -116,7 +116,7 @@ CorDapp is 4 or greater, then transaction verification will fail with a ``Transa the owning ``Contract`` *can* be identified, but the ``ContractState`` has been bundled with a different contract, then transaction verification will fail with a ``TransactionContractConflictException``. -.. _contract_non-downgrade_rule_ref: +.. _contract_downgrade_rule_ref: App versioning with signature constraints ----------------------------------------- diff --git a/docs/source/app-upgrade-notes.rst b/docs/source/app-upgrade-notes.rst index 49cd23af83..73555ba3a7 100644 --- a/docs/source/app-upgrade-notes.rst +++ b/docs/source/app-upgrade-notes.rst @@ -100,10 +100,7 @@ properly for future releases. future not hold true. You should know the platform version of the node releases you want to target. The new ``versionId`` number is a version code for **your** app, and is unrelated to Corda's own versions. -It is used to block state downgrades: when a state constraint can be satisfied -by multiple attachments, the version is tracked in the ledger and cannot decrement. This ensures security -fixes in CorDapps stick and can't be reversed by downgrading states to an earlier version. See -":ref:`contract_non-downgrade_rule_ref`" for more information. +It is used to informative purposes only. See ":ref:`contract_downgrade_rule_ref`" for more information. **Split your app into contract and workflow JARs.** The duplication between ``contract`` and ``workflow`` blocks exists because you should split your app into two separate JARs/modules, one that contains on-ledger validation code like states and contracts, and one diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 7b984cf429..86e5a33aed 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -50,10 +50,6 @@ Version 4.0 * ``JacksonSupport.createInMemoryMapper`` was incorrectly marked as deprecated and is no longer so. -* Transaction building and verification enforces new contract attachment version non-downgrade rule. - For a given contract class, the contract attachment of the output states must be of the same or newer version than the contract attachment of the input states. - See :ref:`Contract attachment non-downgrade rule ` for further information. - * Standardised CorDapp version identifiers in jar manifests (aligned with associated cordapp Gradle plugin changes). Updated all samples to reflect new conventions. diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 6ccdcc7ef6..c4c515a12f 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -122,9 +122,6 @@ allows tool developers to assume that if a class name appears to be owned by an semantics of that class actually *were* defined by that organisation, thus eliminating edge cases that might otherwise cause confusion. -**No downgrades.** Transaction building and verification enforces new contract attachment version non-downgrade rule. -For a given contract class, the contract attachment of the output states must be of the same or newer version than -the contract attachment of the input states. See :ref:`Contract attachment non-downgrade rule ` for further information. Network parameters in transactions ++++++++++++++++++++++++++++++++++ diff --git a/docs/source/tutorial-cordapp.rst b/docs/source/tutorial-cordapp.rst index aa8961e7ce..7982264dca 100644 --- a/docs/source/tutorial-cordapp.rst +++ b/docs/source/tutorial-cordapp.rst @@ -59,32 +59,72 @@ The example CorDapp has the following structure: .. sourcecode:: none . + ├── LICENCE + ├── README.md + ├── TRADEMARK + ├── build.gradle ├── clients + │   ├── build.gradle │   └── src │   └── main - │   └── kotlin - │   └── com.example.server - │   ├── MainController.kt - │   ├── NodeRPCConnection.kt - │   └── Server.kt - │   ├── resources - │   ├── public - │   ├── js + │   ├── kotlin + │   │   └── com + │   │   └── example + │   │   └── server + │   │   ├── MainController.kt + │   │   ├── NodeRPCConnection.kt + │   │   └── Server.kt + │   └── resources + │   ├── application.properties + │   └── public + │   ├── index.html + │   └── js │   └── angular-module.js - │   └── index.html - │   └── application.properties - │ └── build.gradle - │   ├── config │   ├── dev │   │   └── log4j2.xml │   └── test │   └── log4j2.xml + ├── contracts-java + │   ├── build.gradle + │   └── src + │   └── main + │   └── java + │   └── com + │   └── example + │   ├── contract + │   │   └── IOUContract.java + │   ├── schema + │   │   ├── IOUSchema.java + │   │   └── IOUSchemaV1.java + │   └── state + │   └── IOUState.java + ├── contracts-kotlin + │   ├── build.gradle + │   └── src + │   └── main + │   └── kotlin + │   └── com + │   └── example + │   ├── contract + │   │   └── IOUContract.kt + │   ├── schema + │   │   └── IOUSchema.kt + │   └── state + │   └── IOUState.kt + ├── cordapp-example.iml ├── gradle │   └── wrapper │   ├── gradle-wrapper.jar │   └── gradle-wrapper.properties - ├── java-source + ├── gradle.properties + ├── gradlew + ├── gradlew.bat + ├── lib + │   ├── README.txt + │   └── quasar.jar + ├── settings.gradle + ├── workflows-java │   ├── build.gradle │   └── src │   ├── integrationTest @@ -93,26 +133,11 @@ The example CorDapp has the following structure: │   │   └── example │   │   └── DriverBasedTests.java │   ├── main - │   │   ── java - │   │      └── com - │   │      └── example - │   │      ├── api - │   │      │   └── ExampleApi.java - │   │      ├── client - │   │      │   └── ExampleClientRPC.java - │   │      ├── contract - │   │      │   └── IOUContract.java - │   │      ├── flow - │   │      │   └── ExampleFlow.java - │   │      ├── plugin - │   │      │   └── ExamplePlugin.java - │   │      ├── schema - │   │      │   ├── IOUSchema.java - │   │      │   └── IOUSchemaV1.java - │   │      └── state - │   │      └── IOUState.java - │   │  - │   │   + │   │   └── java + │   │   └── com + │   │   └── example + │   │   └── flow + │   │   └── ExampleFlow.java │   └── test │   └── java │   └── com @@ -122,20 +147,29 @@ The example CorDapp has the following structure: │   │   └── IOUContractTests.java │   └── flow │   └── IOUFlowTests.java - ├── kotlin-source - │   ├── ... - ├── lib - │   ├── README.txt - │   └── quasar.jar - ├── .gitignore - ├── LICENCE - ├── README.md - ├── TRADEMARK - ├── build.gradle - ├── gradle.properties - ├── gradlew - ├── gradlew.bat - └── settings.gradle + └── workflows-kotlin + ├── build.gradle + └── src + ├── integrationTest + │   └── kotlin + │   └── com + │   └── example + │   └── DriverBasedTests.kt + ├── main + │   └── kotlin + │   └── com + │   └── example + │   └── flow + │   └── ExampleFlow.kt + └── test + └── kotlin + └── com + └── example + ├── NodeDriver.kt + ├── contract + │   └── IOUContractTests.kt + └── flow + └── IOUFlowTests.kt The key files and directories are as follows: @@ -145,13 +179,8 @@ The key files and directories are as follows: about which version is required * **lib** contains the Quasar jar which rewrites our CorDapp's flows to be checkpointable * **clients** contains the source code for spring boot integration -* **java-source** contains the source code for the example CorDapp written in Java - - * **java-source/src/main/java** contains the source code for the example CorDapp - * **java-source/src/test/java** contains unit tests for the contracts and flows, and the driver to run the nodes - via IntelliJ - -* **kotlin-source** contains the same source code, but written in Kotlin. CorDapps can be developed in either Java and Kotlin +* **contracts-java** and **workflows-java** contain the source code for the example CorDapp written in Java +* **contracts-kotlin** and **workflows-kotlin** contain the same source code, but written in Kotlin. CorDapps can be developed in either Java and Kotlin Running the example CorDapp --------------------------- @@ -177,10 +206,10 @@ Building the example CorDapp * Windows: ``gradlew.bat deployNodes`` .. note:: CorDapps can be written in any language targeting the JVM. In our case, we've provided the example source in - both Kotlin (``/kotlin-source/src``) and Java (``/java-source/src``). Since both sets of source files are - functionally identical, we will refer to the Kotlin version throughout the documentation. + both Kotlin and Java. Since both sets of source files are functionally identical, we will refer to the Kotlin version + throughout the documentation. -* After the build finishes, you will see the following output in the ``kotlin-source/build/nodes`` folder: +* After the build finishes, you will see the following output in the ``workflows-kotlin/build/nodes`` folder: * A folder for each generated node * A ``runnodes`` shell script for running all the nodes simultaneously on osX @@ -211,12 +240,13 @@ Building the example CorDapp Running the example CorDapp ^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Start the nodes and Spring Boot servers by running the following command from the root of the ``cordapp-example`` folder: +Start the nodes by running the following command from the root of the ``cordapp-example`` folder: -* Unix/Mac OSX: ``kotlin-source/build/nodes/runnodes`` -* Windows: ``call kotlin-source\build\nodes\runnodes.bat`` +* Unix/Mac OSX: ``workflows-kotlin/build/nodes/runnodes`` +* Windows: ``call workflows-kotlin\build\nodes\runnodes.bat`` + +Each Spring Boot server needs to be started in it's own terminal, replace X with A, B and C: -Each Spring Boot server needs to be started in it's own terminal, replace X with A, B and C * Unix/Mac OSX: ``./gradlew runPartyXServer`` * Windows: ``gradlew.bat runPartyXServer`` @@ -236,7 +266,7 @@ For each node, the ``runnodes`` script creates a node tab/window: --- Corda Open Source corda-3.0 (4157c25) ----------------------------------------------- - Logs can be found in : /Users/joeldudley/Desktop/cordapp-example/kotlin-source/build/nodes/PartyA/logs + Logs can be found in : /Users/joeldudley/Desktop/cordapp-example/workflows-kotlin/build/nodes/PartyA/logs Database connection url is : jdbc:h2:tcp://localhost:59472/node Incoming connection address : localhost:10005 Listening on port : 10005 @@ -432,13 +462,13 @@ The nodes can be configured to communicate as a network even when distributed ac * Unix/Mac OSX: ``./gradlew deployNodes`` * Windows: ``gradlew.bat deployNodes`` -* Navigate to the build folder (``kotlin-source/build/nodes``) +* Navigate to the build folder (``workflows-kotlin/build/nodes``) * For each node, open its ``node.conf`` file and change ``localhost`` in its ``p2pAddress`` to the IP address of the machine where the node will be run (e.g. ``p2pAddress="10.18.0.166:10007"``) * These changes require new node-info files to be distributed amongst the nodes. Use the network bootstrapper tool (see :doc:`network-bootstrapper`) to update the files and have them distributed locally: - ``java -jar network-bootstrapper.jar kotlin-source/build/nodes`` + ``java -jar network-bootstrapper.jar workflows-kotlin/build/nodes`` * Move the node folders to their individual machines (e.g. using a USB key). It is important that none of the nodes - including the notary - end up on more than one machine. Each computer should also have a copy of ``runnodes`` diff --git a/docs/source/versioning.rst b/docs/source/versioning.rst index 18fb05a851..a9254369cf 100644 --- a/docs/source/versioning.rst +++ b/docs/source/versioning.rst @@ -96,9 +96,7 @@ It's entirely expected and reasonable to have an open source contracts module an sophisticated or proprietary business logic, machine learning models, even user interface code. There's nothing that restricts it to just being Corda flows or services. -.. important:: The ``versionId`` specified for the JAR manifest is checked by the platform. Downgrades are not allowed: you cannot take a state - that was created with version 5 of your app, and then create a state with version 4. This is to prevent attacks in which bugs - are fixed, but an adversary uses an old version of the app to continue exploiting them. Version tracking in states is handled for you - automatically as long as the information is provided in your Gradle file. See ":ref:`contract_non-downgrade_rule_ref`" for more information. +.. important:: The ``versionId`` specified for the JAR manifest is checked by the platform and is used for informative purposes only. + See ":ref:`contract_downgrade_rule_ref`" for more information. .. note:: You can read the original design doc here: :doc:`design/targetversion/design`. \ No newline at end of file