diff --git a/Advanced/README.md b/Advanced/README.md index f5e9271c..2a63d947 100644 --- a/Advanced/README.md +++ b/Advanced/README.md @@ -16,6 +16,12 @@ An capital market themed app that depicts an ideal shareable due diligence proce An application that depicts the businsess negotiation and communication process over a distributed ledger system. It consists of the proposing, negotiating, and settling a corda transaction. +### [Negotiation Cordapp (Key Rotation Proof-Free)](./negotiation-cordapp-key-rotation-proof-free): +An application that depicts the business negotiation and communication process over a distributed ledger system. +It consists of the proposing, negotiating, and settling a corda transaction. +This is a modified version of the Negotiation CorDapp that demonstrates how to update a CorDapp to support proof-free +key rotation. + ### [Obligation Cordapp](./obligation-cordapp): A simple i-owe-you application illustrates all of the steps of creating an obligation for a resource to change owners. diff --git a/Advanced/constants.properties b/Advanced/constants.properties index 662a5a6e..e4ad4c9f 100644 --- a/Advanced/constants.properties +++ b/Advanced/constants.properties @@ -1,5 +1,6 @@ cordaReleaseGroup=net.corda cordaCoreReleaseGroup=net.corda +cordaEnterpriseReleaseGroup=com.r3.corda cordaVersion=4.15-SNAPSHOT cordaCoreVersion=4.15-SNAPSHOT cordaShellVersion=4.15-SNAPSHOT diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/.settings/org.eclipse.jdt.core.prefs b/Advanced/negotiation-cordapp-key-rotation-proof-free/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..ac86f90c --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1 @@ +org.eclipse.jdt.core.compiler.codegen.methodParameters=generate \ No newline at end of file diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/LICENCE b/Advanced/negotiation-cordapp-key-rotation-proof-free/LICENCE new file mode 100644 index 00000000..3ff572d1 --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/LICENCE @@ -0,0 +1,13 @@ + Copyright 2016, R3 Limited. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/README.md b/Advanced/negotiation-cordapp-key-rotation-proof-free/README.md new file mode 100644 index 00000000..9b68270e --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/README.md @@ -0,0 +1,69 @@ +# Negotiation Cordapp Proof-Free Transactions + +This CorDapp shows how multi-party negotiation is handled on the Corda ledger, in the absence of an API for user +interaction. This version of the CorDapp demonstrates how to implement a negotiation flow with key rotation support +that only requires the key rotation proof to be provided when consuming a state signed by the old key. + +## Concepts + +A flow is provided that allows a node to propose a trade to a counterparty. The counterparty has two options: + +* Accepting the proposal, converting the `ProposalState` into a `TradeState` with identical attributes +* Modifying the proposal, consuming the existing `ProposalState` and replacing it with a new `ProposalState` for a new + amount + +Only the recipient of the proposal has the ability to accept it or modify it. If the sender of the proposal tries to +accept or modify the proposal, this attempt will be rejected automatically at the flow level. + +### Flows + +We start with the proposal flow implemented in `ProposalFlow.java`. + + +The modification of the proposal is implemented in `ModificationFlow.java`. + + +In the `AcceptanceFlow.java`, we receive the modified ProposalState and it's converted into a TradeState. + + + +## Usage + +### Pre-Requisites +For development environment setup, please refer to: [Setup Guide](https://docs.corda.net/getting-set-up.html). + + + +### Running the nodes: + +Open a terminal and go to the project root directory and type: (to deploy the nodes using bootstrapper) +``` +./gradlew clean deployNodes +``` +Then type: (to run the nodes) +``` +./build/nodes/runnodes +``` + +We will interact with this CorDapp via the nodes' [CRaSH](https://docs.corda.net/docs/corda-os/shell.html) shells. + +First, go the the shell of PartyA, and propose a deal with yourself as buyer and a value of 10 to PartyB: + + flow start ProposalFlow$Initiator isBuyer: true, amount: 10, counterparty: PartyB + +We can now look at the proposals in the PartyA's vault: + + run vaultQuery contractStateType: ProposalState + +If we note down the state's `linearId.id`, we can now modify the proposal from the shell of PartyB by running: + + flow start ModificationFlow$Initiator proposalId: , newAmount: 8 + +Finally, let's have PartyA accept the proposal: + + flow start AcceptanceFlow$Initiator proposalId: + +We can now see the accepted trade in our vault with the new value by running the command (note we are now querying for +`TradeState`s, not `ProposalState`s): + + run vaultQuery contractStateType: TradeState diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/TRADEMARK b/Advanced/negotiation-cordapp-key-rotation-proof-free/TRADEMARK new file mode 100644 index 00000000..aa7e20f6 --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/TRADEMARK @@ -0,0 +1,4 @@ +Corda and the Corda logo are trademarks of R3CEV LLC and its affiliates. All rights reserved. + +For R3CEV LLC's trademark and logo usage information, please consult our Trademark Usage Policy at +https://www.r3.com/trademark-policy/. diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/build.gradle b/Advanced/negotiation-cordapp-key-rotation-proof-free/build.gradle new file mode 100644 index 00000000..806dfaa8 --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/build.gradle @@ -0,0 +1,192 @@ +import static org.gradle.api.JavaVersion.VERSION_17 + +buildscript { + Properties constants = new Properties() + file("$projectDir/../constants.properties").withInputStream { constants.load(it) } + + ext { + corda_release_group = constants.getProperty("cordaReleaseGroup") + corda_core_release_group = constants.getProperty("cordaCoreReleaseGroup") + // This sample runs on Corda ENTERPRISE: its node, driver, shell and test artifacts come from cordaEnterpriseReleaseGroup + // (com.r3.corda). Enterprise builds on Corda OS's corda-core, which stays under cordaCoreReleaseGroup. + corda_enterprise_release_group = constants.getProperty("cordaEnterpriseReleaseGroup") + corda_release_version = constants.getProperty("cordaVersion") + corda_core_release_version = constants.getProperty("cordaCoreVersion") + corda_gradle_plugins_version = constants.getProperty("gradlePluginsVersion") + kotlin_version = constants.getProperty("kotlinVersion") + junit_version = constants.getProperty("junitVersion") + quasar_version = constants.getProperty("quasarVersion") + quasar_exclusions = [ + 'co.paralleluniverse**', + 'groovy**', + 'com.esotericsoftware.**', + 'jdk**', + 'junit**', + 'kotlin**', + 'net.rubygrapefruit.**', + 'org.gradle.**', + 'org.apache.**', + 'org.jacoco.**', + 'org.junit**', + 'org.slf4j**', + 'worker.org.gradle.**', + 'org.mockito.kotlin**', + 'org.assertj**', + 'org.hamcrest**', + 'org.mockito**', + 'org.opentest4j**', + 'org.springframework**' + ] + log4j_version = constants.getProperty("log4jVersion") + slf4j_version = constants.getProperty("slf4jVersion") + corda_platform_version = constants.getProperty("platformVersion").toInteger() + //springboot + spring_boot_version = '3.2.5' + spring_boot_gradle_plugin_version = '3.2.5' + + testJvmArgs = ['--add-opens', 'java.base/java.time=ALL-UNNAMED', '--add-opens', 'java.base/java.io=ALL-UNNAMED', + '--add-opens', 'java.base/java.util=ALL-UNNAMED', '--add-opens', 'java.base/java.net=ALL-UNNAMED', + '--add-opens', 'java.base/java.nio=ALL-UNNAMED', '--add-opens', 'java.base/java.lang.invoke=ALL-UNNAMED', + '--add-opens', 'java.base/java.security.cert=ALL-UNNAMED', '--add-opens', 'java.base/java.security=ALL-UNNAMED', + '--add-opens', 'java.base/javax.net.ssl=ALL-UNNAMED', '--add-opens', 'java.base/java.lang=ALL-UNNAMED', + '--add-opens', 'java.base/java.util.concurrent=ALL-UNNAMED', '--add-opens', 'java.sql/java.sql=ALL-UNNAMED',] + } + + repositories { + mavenLocal() + mavenCentral() + + maven { url 'https://download.corda.net/maven/corda-dependencies' } + maven { url 'https://download.corda.net/maven/corda-releases' } + } + + dependencies { + classpath "net.corda.plugins:cordapp:$corda_gradle_plugins_version" + classpath "net.corda.plugins:cordformation:$corda_gradle_plugins_version" + classpath "net.corda.plugins:quasar-utils:$corda_gradle_plugins_version" + classpath "org.springframework.boot:spring-boot-gradle-plugin:$spring_boot_gradle_plugin_version" + } +} + +allprojects { + apply from: "${rootProject.projectDir}/repositories.gradle" + apply plugin: 'java' + + tasks.withType(Test) { + jvmArgs = rootProject.ext.testJvmArgs + } + + repositories { + mavenLocal() + + mavenCentral() + maven { url 'https://download.corda.net/maven/corda-dependencies' } + // Can be removed post-release - used to get nightly snapshot build. + maven { url 'https://download.corda.net/maven/corda-releases' } + maven { url 'https://download.corda.net/maven/corda-lib' } + maven { url 'https://download.corda.net/maven/corda-lib-dev' } + maven { url 'https://jitpack.io' } + maven { url "https://repo.gradle.org/gradle/libs-releases-local" } + } + + java { + sourceCompatibility = VERSION_17 + targetCompatibility = VERSION_17 + } + + compileJava { + options.compilerArgs << '-parameters' + } + + jar { + // This makes the JAR's SHA-256 hash repeatable. + preserveFileTimestamps = false + reproducibleFileOrder = true + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + } +} + +apply plugin: 'net.corda.plugins.cordapp' +apply plugin: 'net.corda.plugins.cordformation' +apply plugin: 'net.corda.plugins.quasar-utils' + +sourceSets { + main { + resources { + srcDir rootProject.file("config/dev") + } + } +} + +dependencies { + // Corda dependencies. + cordaProvided "$corda_core_release_group:corda-core:$corda_core_release_version" + cordaBootstrapper ("$corda_enterprise_release_group:corda-node-api:$corda_release_version") { + exclude group: "ch.qos.logback", module: "logback-classic" + } + corda "$corda_enterprise_release_group:corda:$corda_release_version" + + // CorDapp dependencies. + cordapp project(":workflows") + cordapp project(":contracts") + + cordaProvided "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}" + cordaProvided "org.apache.logging.log4j:log4j-web:${log4j_version}" + cordaProvided "org.slf4j:jul-to-slf4j:$slf4j_version" + cordaDriver "$corda_enterprise_release_group:corda-shell:$corda_release_version" +} + +cordapp { + targetPlatformVersion corda_platform_version.toInteger() + minimumPlatformVersion corda_platform_version.toInteger() + workflow { + name "Negotiation CorDapp" + vendor "Corda Open Source" + licence "Apache License, Version 2.0" + versionId 1 + } +} + +task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { + + nodeDefaults { + cordapp project(':contracts') + cordapp project(':workflows') + runSchemaMigration = true + } + + node { + name "O=Notary,L=London,C=GB" + notary = [validating: false] + p2pPort 10002 + rpcSettings { + address("localhost:10003") + adminAddress("localhost:10043") + } + } + node { + name "O=PartyA,L=London,C=GB" + p2pPort 10005 + rpcSettings { + address("localhost:10006") + adminAddress("localhost:10046") + } + rpcUsers = [[user: "user1", "password": "test", "permissions": ["ALL"]]] + } + node { + name "O=PartyB,L=New York,C=US" + p2pPort 10008 + rpcSettings { + address("localhost:10009") + adminAddress("localhost:10049") + } + rpcUsers = [[user: "user1", "password": "test", "permissions": ["ALL"]]] + } +} + +task installQuasar(type: Copy) { + destinationDir rootProject.file("lib") + from(configurations.quasar) { + rename 'quasar-core(.*).jar', 'quasar.jar' + } +} diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/config/dev/log4j2.xml b/Advanced/negotiation-cordapp-key-rotation-proof-free/config/dev/log4j2.xml new file mode 100644 index 00000000..34ba4d45 --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/config/dev/log4j2.xml @@ -0,0 +1,59 @@ + + + + + logs + node-${hostName} + ${log-path}/archive + + + + + + + + + %highlight{%level{length=1} %d{HH:mm:ss} %T %c{1}.%M - %msg%n}{INFO=white,WARN=red,FATAL=bright red blink} + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/config/test/log4j2-test.xml b/Advanced/negotiation-cordapp-key-rotation-proof-free/config/test/log4j2-test.xml new file mode 100644 index 00000000..a7697a38 --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/config/test/log4j2-test.xml @@ -0,0 +1,31 @@ + + + + build/logs + node-${hostName} + + + + + + + [%-5level] %d{HH:mm:ss.SSS} [%t] %c{1}.%M - %msg%n + > + + + + + + + + + + + + + + + + + diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/contracts/build.gradle b/Advanced/negotiation-cordapp-key-rotation-proof-free/contracts/build.gradle new file mode 100644 index 00000000..49837c7a --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/contracts/build.gradle @@ -0,0 +1,31 @@ +apply plugin: 'java' +apply plugin: 'net.corda.plugins.cordapp' + +cordapp { + targetPlatformVersion corda_platform_version.toInteger() + minimumPlatformVersion corda_platform_version.toInteger() + contract { + name "Negotiation Contract" + vendor "Corda Open Source" + licence "Apache License, Version 2.0" + versionId 1 + } +} + +dependencies { + testImplementation "junit:junit:$junit_version" + cordaProvided group: 'com.google.guava', name: 'guava', version: '23.5-jre' + + // Corda dependencies. + cordaProvided "$corda_enterprise_release_group:corda-node-driver:$corda_release_version" + cordaProvided "$corda_core_release_group:corda-core:$corda_core_release_version" + cordaProvided "$corda_enterprise_release_group:corda-jackson:$corda_release_version" + cordaProvided "$corda_enterprise_release_group:corda-rpc:$corda_release_version" + cordaProvided "$corda_enterprise_release_group:corda:$corda_release_version" + testImplementation "$corda_enterprise_release_group:corda-test-utils:$corda_release_version" + testImplementation "$corda_enterprise_release_group:corda-core-test-utils:$corda_release_version" +} + +test { + jvmArgs = rootProject.ext.testJvmArgs +} diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/contracts/src/main/java/net/corda/samples/negotiation/contracts/ProposalAndTradeContract.java b/Advanced/negotiation-cordapp-key-rotation-proof-free/contracts/src/main/java/net/corda/samples/negotiation/contracts/ProposalAndTradeContract.java new file mode 100644 index 00000000..15499149 --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/contracts/src/main/java/net/corda/samples/negotiation/contracts/ProposalAndTradeContract.java @@ -0,0 +1,132 @@ +package net.corda.samples.negotiation.contracts; + +import com.google.common.collect.ImmutableSet; +import net.corda.core.crypto.keyrotation.crossprovider.PartyIdentityResolver; +import net.corda.samples.negotiation.states.ProposalState; +import net.corda.samples.negotiation.states.TradeState; +import net.corda.core.contracts.CommandData; +import net.corda.core.contracts.CommandWithParties; +import net.corda.core.contracts.Contract; +import net.corda.core.transactions.LedgerTransaction; + +import static net.corda.core.contracts.ContractsDSL.requireThat; + +public class ProposalAndTradeContract implements Contract { + public static String ID = "net.corda.samples.negotiation.contracts.ProposalAndTradeContract"; + + @Override + public void verify(LedgerTransaction tx) throws IllegalArgumentException { + final CommandWithParties command = tx.getCommands().get(0); + + if (command.getValue() instanceof Commands.Propose) { + // No change is required since the flow creating the transaction will always use the most up-to-date identities + // when building the transaction. + requireThat(require -> { + require.using("There are no inputs", tx.getInputs().isEmpty()); + require.using("Only one output state should be created.", tx.getOutputs().size() == 1); + require.using("The single output is of type ProposalState", tx.outputsOfType(ProposalState.class).size() == 1); + require.using("There is exactly one command", tx.getCommands().size() == 1); + require.using("There is no timestamp", tx.getTimeWindow() == null); + ProposalState output = tx.outputsOfType(ProposalState.class).get(0); + require.using("The buyer and seller are the proposer and the proposee", ImmutableSet.of(output.getBuyer(), output.getSeller()).equals(ImmutableSet.of(output.getProposee(), output.getProposer()))); + require.using("The proposer is a required signer", command.getSigners().contains(output.getProposer().getOwningKey())); + require.using("The proposee is a required signer", command.getSigners().contains(output.getProposee().getOwningKey())); + return null; + }); + } else if (command.getValue() instanceof Commands.Accept) { + requireThat(require -> { + require.using("There is exactly one input", tx.getInputStates().size() == 1); + require.using("The single input is of type ProposalState", tx.inputsOfType(ProposalState.class).size() == 1); + require.using("There is exactly one output", tx.getOutputs().size() == 1); + require.using("The single output is of type TradeState", tx.outputsOfType(TradeState.class).size() == 1); + require.using("There is exactly one command", tx.getCommands().size() == 1); + require.using("There is no timestamp", tx.getTimeWindow() == null); + + ProposalState input = tx.inputsOfType(ProposalState.class).get(0); + TradeState output = tx.outputsOfType(TradeState.class).get(0); + + // Create a resolver using the proof chain map from the command. + // + // This allows the resolver to resolve parties across key rotations, + // ensuring that the same logical parties are identified in the transaction + // even when their public keys have changed. + PartyIdentityResolver resolver = new PartyIdentityResolver(command.getKeyRotationProofChainMap()); + + require.using("The amount is unmodified in the output", output.getAmount() == input.getAmount()); + + // After a key rotation, parties in the input and output states should be compared using the resolver, + // rather than relying on `equals`, which may fail if a party’s public key has changed. + // + // This is only strictly necessary when the flow has been updated to replace the old party with the new one in the output state. + // The proof chain map will be used by the resolver to determine that the old and new parties are in fact the same, allowing the contract to verify successfully. + require.using("The buyer is unmodified in the output", resolver.isSameParty(input.getBuyer(), output.getBuyer())); + require.using("The seller is unmodified in the output", resolver.isSameParty(input.getSeller(), output.getSeller())); + + // Similarly, the required signers should be checked using the resolver to account for any key rotations. + // The proof chain map will be used by the resolver to determine that the old and new parties are in fact the same, allowing the contract to verify successfully. + require.using("The proposer is a required signer", resolver.isRequiredSigner(command.getSigners(), input.getProposer())); + require.using("The proposee is a required signer", resolver.isRequiredSigner(command.getSigners(), input.getProposee())); + + return null; + }); + } else if (command.getValue() instanceof Commands.Modify) { + requireThat(require -> { + require.using("There is exactly one input", tx.getInputStates().size() == 1); + require.using("The single input is of type ProposalState", tx.inputsOfType(ProposalState.class).size() == 1); + require.using("There is exactly one output", tx.getOutputs().size() == 1); + require.using("The single output is of type ProposalState", tx.outputsOfType(ProposalState.class).size() == 1); + require.using("There is exactly one command", tx.getCommands().size() == 1); + require.using("There is no timestamp", tx.getTimeWindow() == null); + + ProposalState input = tx.inputsOfType(ProposalState.class).get(0); + ProposalState output = tx.outputsOfType(ProposalState.class).get(0); + + // Create a resolver using the proof chain map from the command. + // + // This allows the resolver to resolve parties across key rotations, + // ensuring that the same logical parties are identified in the transaction + // even when their public keys have changed. + PartyIdentityResolver resolver = new PartyIdentityResolver(command.getKeyRotationProofChainMap()); + + require.using("The amount is unmodified in the output", output.getAmount() != input.getAmount()); + + // After a key rotation, parties in the input and output states should be compared using the resolver, + // rather than relying on `equals`, which may fail if a party’s public key has changed. + // + // This is only strictly necessary when the flow has been updated to replace the old party with the new one in the output state. + // The proof chain map will be used by the resolver to determine that the old and new parties are in fact the same, allowing the contract to verify successfully. + require.using("The buyer is unmodified in the output", resolver.isSameParty(input.getBuyer(), output.getBuyer())); + require.using("The seller is unmodified in the output", resolver.isSameParty(input.getSeller(), output.getSeller())); + + // Similarly, the required signers should be checked using the resolver to account for any key rotations. + // The proof chain map will be used by the resolver to determine that the old and new parties are in fact the same, allowing the contract to verify successfully. + require.using("The proposer is a required signer", resolver.isRequiredSigner(command.getSigners(), input.getProposer())); + require.using("The proposee is a required signer", resolver.isRequiredSigner(command.getSigners(), input.getProposee())); + + return null; + + }); + } else { + throw new IllegalArgumentException("Command of incorrect type"); + } + + } + + + public interface Commands extends CommandData { + class Propose implements Commands { + } + + ; + + class Accept implements Commands { + } + + ; + + class Modify implements Commands { + } + + ; + } +} diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/contracts/src/main/java/net/corda/samples/negotiation/states/ProposalState.java b/Advanced/negotiation-cordapp-key-rotation-proof-free/contracts/src/main/java/net/corda/samples/negotiation/states/ProposalState.java new file mode 100644 index 00000000..d3c2ed05 --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/contracts/src/main/java/net/corda/samples/negotiation/states/ProposalState.java @@ -0,0 +1,73 @@ +package net.corda.samples.negotiation.states; + +import com.google.common.collect.ImmutableList; + +import net.corda.core.contracts.BelongsToContract; +import net.corda.core.contracts.LinearState; +import net.corda.core.contracts.UniqueIdentifier; +import net.corda.core.identity.AbstractParty; +import net.corda.core.identity.Party; +import net.corda.core.serialization.ConstructorForDeserialization; +import net.corda.samples.negotiation.contracts.ProposalAndTradeContract; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +@BelongsToContract(ProposalAndTradeContract.class) +public class ProposalState implements LinearState { + private int amount; + private Party buyer; + private Party seller; + private Party proposer; + private Party proposee; + private UniqueIdentifier linearId; + + @ConstructorForDeserialization + public ProposalState(int amount, Party buyer, Party seller, Party proposer, Party proposee, UniqueIdentifier linearId) { + this.amount = amount; + this.buyer = buyer; + this.seller = seller; + this.proposer = proposer; + this.proposee = proposee; + this.linearId = linearId; + } + + public ProposalState(int amount, Party buyer, Party seller, Party proposer, Party proposee) { + this.amount = amount; + this.buyer = buyer; + this.seller = seller; + this.proposer = proposer; + this.proposee = proposee; + this.linearId = new UniqueIdentifier(); + } + + public int getAmount() { + return amount; + } + + public Party getBuyer() { + return buyer; + } + + public Party getSeller() { + return seller; + } + + public Party getProposer() { + return proposer; + } + + public Party getProposee() { + return proposee; + } + + public UniqueIdentifier getLinearId() { + return linearId; + } + + @NotNull + @Override + public List getParticipants() { + return ImmutableList.of(proposer, proposee); + } +} diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/contracts/src/main/java/net/corda/samples/negotiation/states/TradeState.java b/Advanced/negotiation-cordapp-key-rotation-proof-free/contracts/src/main/java/net/corda/samples/negotiation/states/TradeState.java new file mode 100644 index 00000000..597bf922 --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/contracts/src/main/java/net/corda/samples/negotiation/states/TradeState.java @@ -0,0 +1,56 @@ +package net.corda.samples.negotiation.states; + +import com.google.common.collect.ImmutableList; +import net.corda.samples.negotiation.contracts.ProposalAndTradeContract; +import net.corda.core.contracts.BelongsToContract; +import net.corda.core.contracts.LinearState; +import net.corda.core.contracts.UniqueIdentifier; +import net.corda.core.identity.AbstractParty; +import net.corda.core.identity.Party; +import net.corda.core.serialization.ConstructorForDeserialization; + +import java.util.List; + +@BelongsToContract(ProposalAndTradeContract.class) +public class TradeState implements LinearState { + private int amount; + private Party buyer; + private Party seller; + private UniqueIdentifier linearId; + + @ConstructorForDeserialization + public TradeState(int amount, Party buyer, Party seller, UniqueIdentifier linearId) { + this.amount = amount; + this.buyer = buyer; + this.seller = seller; + this.linearId = linearId; + } + + public TradeState(int amount, Party buyer, Party seller) { + this.amount = amount; + this.buyer = buyer; + this.seller = seller; + this.linearId = new UniqueIdentifier(); + } + + public int getAmount() { + return amount; + } + + public Party getBuyer() { + return buyer; + } + + public Party getSeller() { + return seller; + } + + public UniqueIdentifier getLinearId() { + return linearId; + } + + @Override + public List getParticipants() { + return ImmutableList.of(buyer, seller); + } +} diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/contracts/src/test/java/net/corda/samples/negotiation/contracts/AcceptanceContractTests.java b/Advanced/negotiation-cordapp-key-rotation-proof-free/contracts/src/test/java/net/corda/samples/negotiation/contracts/AcceptanceContractTests.java new file mode 100644 index 00000000..9897dfc3 --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/contracts/src/test/java/net/corda/samples/negotiation/contracts/AcceptanceContractTests.java @@ -0,0 +1,184 @@ +package net.corda.samples.negotiation.contracts; + +import com.google.common.collect.ImmutableList; +import net.corda.samples.negotiation.states.ProposalState; +import net.corda.samples.negotiation.states.TradeState; +import net.corda.testing.core.DummyCommandData; +import net.corda.testing.core.TestIdentity; +import net.corda.testing.node.MockServices; +import org.junit.Test; + +import java.time.Instant; + +import static net.corda.testing.node.NodeTestUtils.ledger; + +public class AcceptanceContractTests { + + private MockServices ledgerServices = new MockServices(ImmutableList.of("net.corda.samples.negotiation.contracts")); + private TestIdentity alice = new TestIdentity(new net.corda.core.identity.CordaX500Name("alice","New York", "US")); + private TestIdentity bob = new TestIdentity(new net.corda.core.identity.CordaX500Name("bob","Tokyo", "JP")); + private TestIdentity charlie = new TestIdentity(new net.corda.core.identity.CordaX500Name("charlie","London", "GB")); + + @Test + public void proposalAcceptanceTransactionsHaveOnlyOneInputAndOneOutputState(){ + ledger(ledgerServices, ledger ->{ + ledger.transaction(tx ->{ + tx.command(ImmutableList.of(alice.getPublicKey(), bob.getPublicKey()), new ProposalAndTradeContract.Commands.Accept()); + tx.tweak(tdsl->{ + tdsl.input(ProposalAndTradeContract.ID, new ProposalState(1,alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tdsl.input(ProposalAndTradeContract.ID, new ProposalState(1,alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tdsl.fails(); + return null; + }); + tx.tweak(tdsl -> { + tdsl.output(ProposalAndTradeContract.ID, new TradeState(1, alice.getParty(), bob.getParty())); + tdsl.output(ProposalAndTradeContract.ID, new TradeState(1, alice.getParty(), bob.getParty())); + tdsl.fails(); + return null; + }); + tx.input(ProposalAndTradeContract.ID,new ProposalState(1, alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tx.output(ProposalAndTradeContract.ID, new TradeState(1,alice.getParty(), bob.getParty())); + tx.verifies(); + return null; + }); + return null; + }); + } + + @Test + public void proposalAcceptanceTransactionsHaveInputOfTypeProposalStateAndOutputOfTypeTradeState(){ + ledger(ledgerServices, (ledger -> { + ledger.transaction(tx -> { + tx.command(ImmutableList.of(alice.getPublicKey(), bob.getPublicKey()), new ProposalAndTradeContract.Commands.Accept()); + tx.tweak(tdsl ->{ + tdsl.input(ProposalAndTradeContract.ID,new TradeState(1,alice.getParty(), bob.getParty())); + tdsl.fails(); + return null; + }); + tx.tweak(tdsl ->{ + tdsl.output(ProposalAndTradeContract.ID, new ProposalState(1, alice.getParty(),bob.getParty(), alice.getParty(), bob.getParty())); + tdsl.fails(); + return null; + }); + + tx.input(ProposalAndTradeContract.ID, new ProposalState(1, alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tx.output(ProposalAndTradeContract.ID, new TradeState(1, alice.getParty(), bob.getParty())); + tx.verifies(); + return null; + }); + return null; + })); + } + + @Test + public void proposalAcceptanceTransactionsHaveExactlyOneCommandOfTypeAccept(){ + ledger(ledgerServices, ledger ->{ + ledger.transaction(tx ->{ + tx.input(ProposalAndTradeContract.ID, new ProposalState(1, alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tx.output(ProposalAndTradeContract.ID, new TradeState(1, alice.getParty(), bob.getParty())); + tx.tweak(tdsl -> { + tdsl.command(ImmutableList.of(alice.getPublicKey(), bob.getPublicKey()), DummyCommandData.INSTANCE); + tdsl.fails(); + return null; + }); + tx.command(ImmutableList.of(alice.getPublicKey(), bob.getPublicKey()), new ProposalAndTradeContract.Commands.Accept()); + tx.verifies(); + return null; + }); + return null; + }); + } + + @Test + public void inputProposalStateAndOutputTradeStateShouldHaveExactlyTheSameAmount(){ + ledger(ledgerServices, ledger ->{ + ledger.transaction(tx ->{ + tx.command(ImmutableList.of(alice.getPublicKey(), bob.getPublicKey()), new ProposalAndTradeContract.Commands.Accept()); + tx.tweak(tdsl ->{ + tdsl.input(ProposalAndTradeContract.ID,new ProposalState(1, alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tdsl.output(ProposalAndTradeContract.ID, new TradeState(2, alice.getParty(), bob.getParty())); + tdsl.fails(); + return null; + }); + tx.tweak(tdsl ->{ + tdsl.input(ProposalAndTradeContract.ID, new ProposalState(2, alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tdsl.output(ProposalAndTradeContract.ID, new TradeState(1, alice.getParty(), bob.getParty())); + tdsl.fails(); + return null; + }); + tx.input(ProposalAndTradeContract.ID,new ProposalState(1, alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tx.output(ProposalAndTradeContract.ID, new TradeState(1, alice.getParty(), bob.getParty())); + tx.verifies(); + return null; + }); + return null; + }); + } + + + @Test + public void buyerAndSellerAreUnmodifiedInTheOutput(){ + ledger(ledgerServices, ledger ->{ + ledger.transaction(tx ->{ + tx.input(ProposalAndTradeContract.ID, new ProposalState(1, alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tx.output(ProposalAndTradeContract.ID, new TradeState(1, alice.getParty(), bob.getParty())); + tx.command(ImmutableList.of(alice.getPublicKey(), bob.getPublicKey()), new ProposalAndTradeContract.Commands.Accept()); + tx.tweak(tdsl ->{ + tdsl.output(ProposalAndTradeContract.ID, new TradeState(1, alice.getParty(), charlie.getParty())); + tdsl.fails(); + return null; + }); + tx.tweak(tdsl ->{ + tdsl.output(ProposalAndTradeContract.ID, new TradeState(1, alice.getParty(), charlie.getParty())); + tdsl.fails(); + return null; + }); + tx.verifies(); + return null; + }); + return null; + }); + } + + @Test + public void proposalAcceptanceTransactionsHaveTwoRequiredSignersTheProposerAndProposee(){ + ledger(ledgerServices, ledger ->{ + ledger.transaction(tx ->{ + tx.input(ProposalAndTradeContract.ID, new ProposalState(1, alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tx.output(ProposalAndTradeContract.ID, new TradeState(1, alice.getParty(), bob.getParty())); + tx.tweak(tdsl ->{ + tdsl.command(ImmutableList.of(alice.getPublicKey(), charlie.getPublicKey()), new ProposalAndTradeContract.Commands.Accept()); + tdsl.fails(); + return null; + }); + tx.tweak(tdsl -> { + tdsl.command(ImmutableList.of(charlie.getPublicKey(), bob.getPublicKey()), new ProposalAndTradeContract.Commands.Accept()); + tdsl.fails(); + return null; + }); + tx.command(ImmutableList.of(alice.getPublicKey(), bob.getPublicKey()), new ProposalAndTradeContract.Commands.Accept()); + tx.verifies(); + return null; + }); + return null; + }); + } + + @Test + public void proposalAcceptanceTransactionsHaveNoTimeStamp(){ + ledger(ledgerServices, ledger ->{ + ledger.transaction(tx ->{ + tx.input(ProposalAndTradeContract.ID, new ProposalState(1, alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tx.output(ProposalAndTradeContract.ID, new TradeState(1, alice.getParty(), bob.getParty())); + tx.command(ImmutableList.of(alice.getPublicKey(), bob.getPublicKey()), new ProposalAndTradeContract.Commands.Accept()); + tx.tweak(tdsl ->{ + tdsl.timeWindow(Instant.now()); + tdsl.fails(); + return null; + }); + return null; + }); + return null; + }); + } +} diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/contracts/src/test/java/net/corda/samples/negotiation/contracts/ModificationContractTests.java b/Advanced/negotiation-cordapp-key-rotation-proof-free/contracts/src/test/java/net/corda/samples/negotiation/contracts/ModificationContractTests.java new file mode 100644 index 00000000..089b77e9 --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/contracts/src/test/java/net/corda/samples/negotiation/contracts/ModificationContractTests.java @@ -0,0 +1,213 @@ +package net.corda.samples.negotiation.contracts; + +import com.google.common.collect.ImmutableList; +import net.corda.samples.negotiation.states.ProposalState; +import net.corda.samples.negotiation.states.TradeState; +import net.corda.testing.core.DummyCommandData; +import net.corda.testing.core.TestIdentity; +import net.corda.testing.node.MockServices; +import org.junit.Test; + +import java.time.Instant; + +import static net.corda.testing.node.NodeTestUtils.ledger; + +public class ModificationContractTests { + + private MockServices ledgerServices = new MockServices(ImmutableList.of("net.corda.samples.negotiation.contracts")); + private TestIdentity alice = new TestIdentity(new net.corda.core.identity.CordaX500Name("alice","New York", "US")); + private TestIdentity bob = new TestIdentity(new net.corda.core.identity.CordaX500Name("bob","Tokyo", "JP")); + private TestIdentity charlie = new TestIdentity(new net.corda.core.identity.CordaX500Name("charlie","London", "GB")); + + @Test + public void proposalModificationTransactionsHaveOnlyOneInputAndOneOutputState(){ + ledger(ledgerServices, ledger ->{ + ledger.transaction(tx ->{ + tx.command(ImmutableList.of(alice.getPublicKey(), bob.getPublicKey()), new ProposalAndTradeContract.Commands.Modify()); + tx.tweak(tdsl->{ + tdsl.input(ProposalAndTradeContract.ID, new ProposalState(1,alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tdsl.input(ProposalAndTradeContract.ID, new ProposalState(1,alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tdsl.fails(); + return null; + }); + tx.tweak(tdsl -> { + tdsl.output(ProposalAndTradeContract.ID, new ProposalState(2,alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tdsl.output(ProposalAndTradeContract.ID, new ProposalState(2,alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tdsl.fails(); + return null; + }); + tx.input(ProposalAndTradeContract.ID,new ProposalState(1, alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tx.output(ProposalAndTradeContract.ID, new ProposalState(2,alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tx.verifies(); + return null; + }); + return null; + }); + } + + @Test + public void proposalModificationTransactionsHaveInputOfTypeProposalStateAndOutputOfTypeProposalState(){ + ledger(ledgerServices, (ledger -> { + ledger.transaction(tx -> { + tx.command(ImmutableList.of(alice.getPublicKey(), bob.getPublicKey()), new ProposalAndTradeContract.Commands.Modify()); + tx.tweak(tdsl ->{ + tdsl.input(ProposalAndTradeContract.ID,new TradeState(1,alice.getParty(), bob.getParty())); + tdsl.fails(); + return null; + }); + tx.tweak(tdsl ->{ + tdsl.output(ProposalAndTradeContract.ID,new TradeState(1,alice.getParty(), bob.getParty())); + tdsl.fails(); + return null; + }); + + tx.input(ProposalAndTradeContract.ID, new ProposalState(1, alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tx.output(ProposalAndTradeContract.ID, new ProposalState(2,alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tx.verifies(); + return null; + }); + return null; + })); + } + + @Test + public void proposalAcceptanceTransactionsHaveExactlyOneCommandOfTypeAccept(){ + ledger(ledgerServices, ledger ->{ + ledger.transaction(tx ->{ + tx.input(ProposalAndTradeContract.ID, new ProposalState(1, alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tx.output(ProposalAndTradeContract.ID, new ProposalState(2, alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tx.tweak(tdsl -> { + tdsl.command(ImmutableList.of(alice.getPublicKey(), bob.getPublicKey()), DummyCommandData.INSTANCE); + tdsl.fails(); + return null; + }); + tx.command(ImmutableList.of(alice.getPublicKey(), bob.getPublicKey()), new ProposalAndTradeContract.Commands.Modify()); + tx.verifies(); + return null; + }); + return null; + }); + } + + @Test + public void inputProposalStateAndOutputProposalStateShouldHaveDifferentAmounts(){ + ledger(ledgerServices, ledger ->{ + ledger.transaction(tx ->{ + tx.command(ImmutableList.of(alice.getPublicKey(), bob.getPublicKey()), new ProposalAndTradeContract.Commands.Modify()); + tx.tweak(tdsl ->{ + tdsl.input(ProposalAndTradeContract.ID,new ProposalState(1, alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tdsl.output(ProposalAndTradeContract.ID,new ProposalState(1, alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tdsl.fails(); + return null; + }); + tx.input(ProposalAndTradeContract.ID,new ProposalState(1, alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tx.output(ProposalAndTradeContract.ID, new ProposalState(2, alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tx.verifies(); + return null; + }); + return null; + }); + } + + + @Test + public void proposalAcceptanceTransactionsHaveTwoRequiredSignersTheProposerAndProposee(){ + ledger(ledgerServices, ledger ->{ + ledger.transaction(tx ->{ + tx.input(ProposalAndTradeContract.ID, new ProposalState(1, alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tx.output(ProposalAndTradeContract.ID, new ProposalState(2, alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tx.tweak(tdsl ->{ + tdsl.command(ImmutableList.of(alice.getPublicKey(), charlie.getPublicKey()), new ProposalAndTradeContract.Commands.Modify()); + tdsl.fails(); + return null; + }); + tx.tweak(tdsl -> { + tdsl.command(ImmutableList.of(charlie.getPublicKey(), bob.getPublicKey()), new ProposalAndTradeContract.Commands.Modify()); + tdsl.fails(); + return null; + }); + tx.command(ImmutableList.of(alice.getPublicKey(), bob.getPublicKey()), new ProposalAndTradeContract.Commands.Modify()); + tx.verifies(); + return null; + }); + return null; + }); + } + + + @Test + public void buyerAndSellerAreUnmodifiedInTheOutput(){ + ledger(ledgerServices, ledger ->{ + ledger.transaction(tx ->{ + tx.input(ProposalAndTradeContract.ID, new ProposalState(1, alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tx.output(ProposalAndTradeContract.ID, new ProposalState(2, alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tx.command(ImmutableList.of(alice.getPublicKey(), bob.getPublicKey()), new ProposalAndTradeContract.Commands.Modify()); + tx.tweak(tdsl ->{ + tdsl.output(ProposalAndTradeContract.ID, new ProposalState(1, alice.getParty(), charlie.getParty(), alice.getParty(), bob.getParty())); + tdsl.fails(); + return null; + }); + tx.tweak(tdsl ->{ + tdsl.output(ProposalAndTradeContract.ID, new ProposalState(2, charlie.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tdsl.fails(); + return null; + }); + tx.tweak(tdsl ->{ + tdsl.output(ProposalAndTradeContract.ID, new ProposalState(2, bob.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tdsl.fails(); + return null; + }); + tx.verifies(); + return null; + }); + return null; + }); + } + + + @Test + public void proposalModificationTransactionsHaveTwoRequiredSignersTheProposerAndProposee(){ + ledger(ledgerServices, ledger ->{ + ledger.transaction(tx ->{ + tx.input(ProposalAndTradeContract.ID, new ProposalState(1, alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tx.output(ProposalAndTradeContract.ID, new ProposalState(2, alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tx.tweak(tdsl ->{ + tdsl.command(ImmutableList.of(alice.getPublicKey(), charlie.getPublicKey()), new ProposalAndTradeContract.Commands.Modify()); + tdsl.fails(); + return null; + }); + tx.tweak(tdsl -> { + tdsl.command(ImmutableList.of(charlie.getPublicKey(), bob.getPublicKey()), new ProposalAndTradeContract.Commands.Modify()); + tdsl.fails(); + return null; + }); + tx.command(ImmutableList.of(alice.getPublicKey(), bob.getPublicKey()), new ProposalAndTradeContract.Commands.Modify()); + tx.verifies(); + return null; + }); + return null; + }); + } + + + @Test + public void proposalAcceptanceTransactionsHaveNoTimeStamp(){ + ledger(ledgerServices, ledger ->{ + ledger.transaction(tx ->{ + tx.input(ProposalAndTradeContract.ID, new ProposalState(1, alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tx.output(ProposalAndTradeContract.ID, new ProposalState(2, alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tx.command(ImmutableList.of(alice.getPublicKey(), bob.getPublicKey()), new ProposalAndTradeContract.Commands.Modify()); + tx.tweak(tdsl ->{ + tdsl.timeWindow(Instant.now()); + tdsl.fails(); + return null; + }); + return null; + }); + return null; + }); + } + + + +} diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/contracts/src/test/java/net/corda/samples/negotiation/contracts/ProposalContractTests.java b/Advanced/negotiation-cordapp-key-rotation-proof-free/contracts/src/test/java/net/corda/samples/negotiation/contracts/ProposalContractTests.java new file mode 100644 index 00000000..74df7938 --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/contracts/src/test/java/net/corda/samples/negotiation/contracts/ProposalContractTests.java @@ -0,0 +1,143 @@ +package net.corda.samples.negotiation.contracts; + +import com.google.common.collect.ImmutableList; +import net.corda.samples.negotiation.states.ProposalState; +import net.corda.testing.contracts.DummyState; +import net.corda.testing.core.DummyCommandData; +import net.corda.testing.core.TestIdentity; +import net.corda.testing.node.MockServices; +import org.junit.Test; + + +import java.time.Instant; + +import static net.corda.testing.node.NodeTestUtils.ledger; + +public class ProposalContractTests { + + private MockServices ledgerServices = new MockServices(ImmutableList.of("net.corda.samples.negotiation.contracts")); + private TestIdentity alice = new TestIdentity(new net.corda.core.identity.CordaX500Name("alice","New York", "US")); + private TestIdentity bob = new TestIdentity(new net.corda.core.identity.CordaX500Name("bob","Tokyo", "JP")); + private TestIdentity charlie = new TestIdentity(new net.corda.core.identity.CordaX500Name("charlie","London", "GB")); + + @Test + public void proposalTransactionsHaveOnlyOneOutputOfTypeProposalState(){ + ledger(ledgerServices, ledger ->{ + ledger.transaction(tx ->{ + tx.command(ImmutableList.of(alice.getPublicKey(), bob.getPublicKey()), new ProposalAndTradeContract.Commands.Propose()); + tx.tweak(tdsl->{ + tdsl.output(ProposalAndTradeContract.ID,new DummyState()); + tdsl.fails(); + return null; + }); + tx.tweak(tdsl -> { + tdsl.output(ProposalAndTradeContract.ID, new ProposalState(2,alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tdsl.output(ProposalAndTradeContract.ID, new ProposalState(2,alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tdsl.fails(); + return null; + }); + tx.output(ProposalAndTradeContract.ID, new ProposalState(2,alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tx.verifies(); + return null; + }); + return null; + }); + } + + @Test + public void proposalTransactionsHaveExactlyOneCommandOfTypePropose(){ + ledger(ledgerServices, ledger ->{ + ledger.transaction(tx ->{ + tx.output(ProposalAndTradeContract.ID, new ProposalState(2, alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tx.tweak(tdsl -> { + tdsl.command(ImmutableList.of(alice.getPublicKey(), bob.getPublicKey()), DummyCommandData.INSTANCE); + tdsl.fails(); + return null; + }); + tx.command(ImmutableList.of(alice.getPublicKey(), bob.getPublicKey()), new ProposalAndTradeContract.Commands.Propose()); + tx.verifies(); + return null; + }); + return null; + }); + } + + + + @Test + public void proposalTransactionsHaveTwoRequiredSignersTheProposerAndProposee(){ + ledger(ledgerServices, ledger ->{ + ledger.transaction(tx ->{ + tx.output(ProposalAndTradeContract.ID, new ProposalState(2, alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tx.tweak(tdsl ->{ + tdsl.command(ImmutableList.of(alice.getPublicKey(), charlie.getPublicKey()), new ProposalAndTradeContract.Commands.Propose()); + tdsl.fails(); + return null; + }); + tx.tweak(tdsl -> { + tdsl.command(ImmutableList.of(charlie.getPublicKey(), bob.getPublicKey()), new ProposalAndTradeContract.Commands.Propose()); + tdsl.fails(); + return null; + }); + tx.command(ImmutableList.of(alice.getPublicKey(), bob.getPublicKey()), new ProposalAndTradeContract.Commands.Propose()); + tx.verifies(); + return null; + }); + return null; + }); + } + + @Test + public void inProposalTransactionsProposerAndProposeeAreBuyerAndSeller(){ + ledger(ledgerServices, ledger ->{ + ledger.transaction(tx ->{ + tx.command(ImmutableList.of(alice.getPublicKey(), bob.getPublicKey()), new ProposalAndTradeContract.Commands.Propose()); + tx.tweak(tdsl ->{ + //order reversed - buyer = proposee, seller = proposer + tdsl.output(ProposalAndTradeContract.ID, new ProposalState(2, alice.getParty(), bob.getParty(), bob.getParty(), alice.getParty())); + tdsl.verifies(); + return null; + }); + tx.tweak(tdsl -> { + tdsl.output(ProposalAndTradeContract.ID, new ProposalState(2, charlie.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tdsl.fails(); + return null; + }); + tx.tweak(tdsl -> { + tdsl.output(ProposalAndTradeContract.ID, new ProposalState(2, alice.getParty(), charlie.getParty(), alice.getParty(), bob.getParty())); + tdsl.fails(); + return null; + }); + tx.output(ProposalAndTradeContract.ID, new ProposalState(2, alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tx.verifies(); + return null; + }); + return null; + }); + } + + + + @Test + public void proposalAcceptanceTransactionsHaveNoInputsAndNoTimeStamp(){ + ledger(ledgerServices, ledger ->{ + ledger.transaction(tx ->{ + tx.output(ProposalAndTradeContract.ID, new ProposalState(2, alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tx.command(ImmutableList.of(alice.getPublicKey(), bob.getPublicKey()), new ProposalAndTradeContract.Commands.Propose()); + tx.tweak(tdsl ->{ + tdsl.input(ProposalAndTradeContract.ID, new ProposalState(1, alice.getParty(), bob.getParty(), alice.getParty(), bob.getParty())); + tdsl.fails(); + return null; + }); + tx.tweak(tdsl ->{ + tdsl.timeWindow(Instant.now()); + tdsl.fails(); + return null; + }); + return null; + }); + return null; + }); + } + +} diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/contracts/src/test/java/net/corda/samples/negotiation/states/ProposalStateTests.java b/Advanced/negotiation-cordapp-key-rotation-proof-free/contracts/src/test/java/net/corda/samples/negotiation/states/ProposalStateTests.java new file mode 100644 index 00000000..c4550d67 --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/contracts/src/test/java/net/corda/samples/negotiation/states/ProposalStateTests.java @@ -0,0 +1,57 @@ +package net.corda.samples.negotiation.states; + +import net.corda.core.contracts.UniqueIdentifier; +import net.corda.core.identity.CordaX500Name; +import net.corda.core.identity.Party; +import net.corda.testing.core.TestIdentity; +import org.junit.Test; + +import static junit.framework.TestCase.assertEquals; +import static org.junit.Assert.*; + + +public class ProposalStateTests { + + private int amount = 10; + private final Party proposer = new TestIdentity(new CordaX500Name("Alice", "", "GB")).getParty(); + private final Party buyer = new TestIdentity(new CordaX500Name("Bob", "", "GB")).getParty(); + private final Party seller = new TestIdentity(new CordaX500Name("Charlie", "", "GB")).getParty(); + private final Party proposee = new TestIdentity(new CordaX500Name("Dan", "", "GB")).getParty(); + + @Test + public void constructorTest() { + + UniqueIdentifier tempId = new UniqueIdentifier(); + ProposalState ps = new ProposalState(amount, buyer, seller, proposer, proposee, tempId); + + assertEquals(ps.getAmount(), amount); + assertEquals(ps.getBuyer(), buyer); + assertEquals(ps.getSeller(), seller); + assertEquals(ps.getProposer(), proposer); + assertEquals(ps.getProposee(), proposee); + assertEquals(ps.getLinearId(), tempId); + + assertNotEquals(ps.getAmount(), 0); + assertNotEquals(ps.getAmount(), -5); + assertNotEquals(ps.getAmount(), null); + } + + @Test + public void linearIdTest() { + ProposalState ps = new ProposalState(amount, buyer, seller, proposer, proposee); + + // ensure ID is generated with shorter constructor stub + assertTrue(ps.getLinearId() instanceof UniqueIdentifier); + } + + @Test + public void participantTest() { + ProposalState ps = new ProposalState(amount, buyer, seller, proposer, proposee); + + // ensure participants are generated correctly + assertTrue(ps.getParticipants().contains(proposer)); + assertTrue(ps.getParticipants().contains(proposee)); + assertFalse(ps.getParticipants().contains(buyer)); + assertFalse(ps.getParticipants().contains(seller)); + } +} diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/contracts/src/test/java/net/corda/samples/negotiation/states/TradeStateTests.java b/Advanced/negotiation-cordapp-key-rotation-proof-free/contracts/src/test/java/net/corda/samples/negotiation/states/TradeStateTests.java new file mode 100644 index 00000000..3828ef69 --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/contracts/src/test/java/net/corda/samples/negotiation/states/TradeStateTests.java @@ -0,0 +1,52 @@ +package net.corda.samples.negotiation.states; + +import net.corda.core.contracts.UniqueIdentifier; +import net.corda.core.identity.CordaX500Name; +import net.corda.core.identity.Party; +import net.corda.testing.core.TestIdentity; +import org.junit.Test; + +import static junit.framework.TestCase.assertEquals; +import static org.junit.Assert.*; + + +public class TradeStateTests { + + private int amount = 10; + private final Party buyer = new TestIdentity(new CordaX500Name("Bob", "", "GB")).getParty(); + private final Party seller = new TestIdentity(new CordaX500Name("Charlie", "", "GB")).getParty(); + + @Test + public void constructorTest() { + + UniqueIdentifier tempId = new UniqueIdentifier(); + TradeState ts = new TradeState(amount, buyer, seller, tempId); + + assertEquals(ts.getAmount(), amount); + assertEquals(ts.getBuyer(), buyer); + assertEquals(ts.getSeller(), seller); + assertEquals(ts.getLinearId(), tempId); + + assertNotEquals(ts.getAmount(), 0); + assertNotEquals(ts.getAmount(), -5); + assertNotEquals(ts.getAmount(), null); + } + + @Test + public void linearIdTest() { + TradeState ts = new TradeState(amount, buyer, seller); + + // ensure ID is generated with shorter constructor stub + assertTrue(ts.getLinearId() instanceof UniqueIdentifier); + } + + @Test + public void participantTest() { + TradeState ts = new TradeState(amount, buyer, seller); + + // ensure participants are generated correctly + assertTrue(ts.getParticipants().contains(buyer)); + assertTrue(ts.getParticipants().contains(seller)); + } + +} diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/gradle.properties b/Advanced/negotiation-cordapp-key-rotation-proof-free/gradle.properties new file mode 100644 index 00000000..03a6057a --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/gradle.properties @@ -0,0 +1,3 @@ +name=Negotiation Cordapp +group=net.corda.samples.negotiation +version=0.2 diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/gradle/wrapper/gradle-wrapper.jar b/Advanced/negotiation-cordapp-key-rotation-proof-free/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..033e24c4 Binary files /dev/null and b/Advanced/negotiation-cordapp-key-rotation-proof-free/gradle/wrapper/gradle-wrapper.jar differ diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/gradle/wrapper/gradle-wrapper.properties b/Advanced/negotiation-cordapp-key-rotation-proof-free/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..164080a8 --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/gradlew b/Advanced/negotiation-cordapp-key-rotation-proof-free/gradlew new file mode 100755 index 00000000..fcb6fca1 --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/gradlew.bat b/Advanced/negotiation-cordapp-key-rotation-proof-free/gradlew.bat new file mode 100644 index 00000000..93e3f59f --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/lib/README.txt b/Advanced/negotiation-cordapp-key-rotation-proof-free/lib/README.txt new file mode 100644 index 00000000..b0592c95 --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/lib/README.txt @@ -0,0 +1,8 @@ +The Quasar.jar in this directory is for runtime instrumentation of classes by Quasar. + +When running corda outside of the given gradle building you must add the following flag with the +correct path to your call to Java: + + java -javaagent:path-to-quasar-jar.jar ... + +See the Quasar docs for more information: http://docs.paralleluniverse.co/quasar/ \ No newline at end of file diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/lib/quasar.jar b/Advanced/negotiation-cordapp-key-rotation-proof-free/lib/quasar.jar new file mode 100644 index 00000000..7282d8c8 Binary files /dev/null and b/Advanced/negotiation-cordapp-key-rotation-proof-free/lib/quasar.jar differ diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/repositories.gradle b/Advanced/negotiation-cordapp-key-rotation-proof-free/repositories.gradle new file mode 100644 index 00000000..0fb456dd --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/repositories.gradle @@ -0,0 +1,30 @@ +repositories { + mavenLocal() + mavenCentral() + maven { url 'https://jitpack.io' } + maven { url 'https://download.corda.net/maven/corda-dependencies' } + maven { url 'https://repo.gradle.org/gradle/libs-releases' } + + // Corda Enterprise artifacts (com.r3.*) are only available to Corda Enterprise customers, so no repository URL is + // hard-coded here. Point the build at the repository your organisation downloads Corda Enterprise from by setting, in + // ~/.gradle/gradle.properties: + // + // cordaEnterpriseRepositoryUrl= + // cordaEnterpriseRepositoryUsername= + // cordaEnterpriseRepositoryPassword= + // + // or the environment variables CORDA_ENTERPRISE_REPOSITORY_URL, CORDA_ENTERPRISE_REPOSITORY_USERNAME and + // CORDA_ENTERPRISE_REPOSITORY_PASSWORD. Without a URL the entry is skipped and the com.r3.* artifacts must already be in + // your local Maven repository (mavenLocal above). + def cordaEnterpriseRepositoryUrl = findProperty('cordaEnterpriseRepositoryUrl') ?: System.getenv('CORDA_ENTERPRISE_REPOSITORY_URL') + if (cordaEnterpriseRepositoryUrl) { + maven { + url cordaEnterpriseRepositoryUrl + credentials { + username = findProperty('cordaEnterpriseRepositoryUsername') ?: System.getenv('CORDA_ENTERPRISE_REPOSITORY_USERNAME') + password = findProperty('cordaEnterpriseRepositoryPassword') ?: System.getenv('CORDA_ENTERPRISE_REPOSITORY_PASSWORD') + } + content { includeGroupByRegex 'com\\.r3(\\..*)?' } + } + } +} diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/settings.gradle b/Advanced/negotiation-cordapp-key-rotation-proof-free/settings.gradle new file mode 100644 index 00000000..0473ad43 --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/settings.gradle @@ -0,0 +1,2 @@ +include 'workflows' +include 'contracts' diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/src/main/resources/simplelogger.properties b/Advanced/negotiation-cordapp-key-rotation-proof-free/src/main/resources/simplelogger.properties new file mode 100644 index 00000000..8ce4c882 --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/src/main/resources/simplelogger.properties @@ -0,0 +1,3 @@ +org.slf4j.simpleLogger.defaultLogLevel=info +org.slf4j.simpleLogger.showDateTime=true +org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS Z diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/build.gradle b/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/build.gradle new file mode 100644 index 00000000..47e487a9 --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/build.gradle @@ -0,0 +1,76 @@ + +apply plugin: 'java' +apply plugin: 'net.corda.plugins.cordapp' +apply plugin: 'net.corda.plugins.quasar-utils' +apply plugin: 'net.corda.plugins.cordformation' + + +cordapp { + targetPlatformVersion corda_platform_version.toInteger() + minimumPlatformVersion corda_platform_version.toInteger() + workflow { + name "Negotiation flow" + vendor "Corda Open Source" + licence "Apache License, Version 2.0" + versionId 1 + } +} +sourceSets { + main { + java { + srcDir 'src/main/java' + java.outputDir = file('bin/main') + } + resources { + srcDir rootProject.file("config/dev") + } + } + test { + java { + srcDir 'src/test/java' + java.outputDir = file('bin/main') + } + resources { + srcDir rootProject.file("config/test") + } + } + integrationTest { + java { + compileClasspath += main.output + test.output + runtimeClasspath += main.output + test.output + srcDir file('src/integrationTest/java') + } + } +} + +configurations { + integrationTestImplementation.extendsFrom testImplementation + integrationTestRuntimeOnly.extendsFrom testRuntimeOnly +} + +dependencies { + testImplementation "junit:junit:$junit_version" + cordaProvided group: 'com.google.guava', name: 'guava', version: '23.5-jre' + + // Corda dependencies. + cordaProvided "$corda_core_release_group:corda-core:$corda_core_release_version" + cordaProvided "$corda_enterprise_release_group:corda:$corda_release_version" + testImplementation "$corda_enterprise_release_group:corda-node-driver:$corda_release_version" + testImplementation "$corda_enterprise_release_group:corda-core-test-utils:$corda_release_version" + + testCompileOnly "$corda_enterprise_release_group:corda-node:$corda_release_version" + testCompileOnly "$corda_enterprise_release_group:corda-node-api:$corda_release_version" + testCompileOnly "$corda_enterprise_release_group:corda-extensions-api:$corda_release_version" + + // CorDapp dependencies. + cordapp project(":contracts") +} + +task integrationTest(type: Test, dependsOn: []) { + testClassesDirs = sourceSets.integrationTest.output.classesDirs + classpath = sourceSets.integrationTest.runtimeClasspath +} + +test { + jvmArgs = rootProject.ext.testJvmArgs +} \ No newline at end of file diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/src/integrationTest/java/net/corda/samples/negotiation/flows/DriverBasedTest.java b/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/src/integrationTest/java/net/corda/samples/negotiation/flows/DriverBasedTest.java new file mode 100644 index 00000000..a68ffa86 --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/src/integrationTest/java/net/corda/samples/negotiation/flows/DriverBasedTest.java @@ -0,0 +1,48 @@ +package net.corda.samples.negotiation.flows; + +import com.google.common.collect.ImmutableList; +import net.corda.core.concurrent.CordaFuture; +import net.corda.core.identity.CordaX500Name; +import net.corda.testing.core.TestIdentity; +import net.corda.testing.driver.DriverParameters; +import net.corda.testing.driver.NodeHandle; +import net.corda.testing.driver.NodeParameters; +import org.junit.Test; + +import java.util.List; + +import static net.corda.testing.driver.Driver.driver; +import static org.junit.Assert.assertEquals; + +public class DriverBasedTest { + private final TestIdentity bankA = new TestIdentity(new CordaX500Name("BankA", "", "GB")); + private final TestIdentity bankB = new TestIdentity(new CordaX500Name("BankB", "", "US")); + + @Test + public void nodeTest() { + driver(new DriverParameters().withIsDebug(true).withStartNodesInProcess(true), dsl -> { + // Start a pair of nodes and wait for them both to be ready. + List> handleFutures = ImmutableList.of( + dsl.startNode(new NodeParameters().withProvidedName(bankA.getName())), + dsl.startNode(new NodeParameters().withProvidedName(bankB.getName())) + ); + + try { + NodeHandle partyAHandle = handleFutures.get(0).get(); + NodeHandle partyBHandle = handleFutures.get(1).get(); + + // From each node, make an RPC call to retrieve another node's name from the network map, to verify that the + // nodes have started and can communicate. + + // This is a very basic test: in practice tests would be starting flows, and verifying the states in the vault + // and other important metrics to ensure that your CorDapp is working as intended. + assertEquals(partyAHandle.getRpc().wellKnownPartyFromX500Name(bankB.getName()).getName(), bankB.getName()); + assertEquals(partyBHandle.getRpc().wellKnownPartyFromX500Name(bankA.getName()).getName(), bankA.getName()); + } catch (Exception e) { + throw new RuntimeException("Caught exception during test: ", e); + } + + return null; + }); + } +} diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/src/main/java/net/corda/samples/negotiation/flows/AcceptanceFlow.java b/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/src/main/java/net/corda/samples/negotiation/flows/AcceptanceFlow.java new file mode 100644 index 00000000..1f600444 --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/src/main/java/net/corda/samples/negotiation/flows/AcceptanceFlow.java @@ -0,0 +1,150 @@ +package net.corda.samples.negotiation.flows; + +import co.paralleluniverse.fibers.Suspendable; +import com.google.common.collect.ImmutableList; + +import net.corda.core.crypto.keyrotation.crossprovider.KeyRotationProofChain; +import net.corda.core.crypto.keyrotation.crossprovider.PartyIdentityResolved; +import net.corda.core.crypto.keyrotation.crossprovider.PartyIdentityResolver; +import net.corda.samples.negotiation.contracts.ProposalAndTradeContract; +import net.corda.samples.negotiation.states.ProposalState; +import net.corda.samples.negotiation.states.TradeState; +import net.corda.core.contracts.Command; +import net.corda.core.contracts.StateAndRef; +import net.corda.core.contracts.UniqueIdentifier; +import net.corda.core.crypto.SecureHash; +import net.corda.core.flows.*; +import net.corda.core.identity.Party; +import net.corda.core.node.services.Vault; +import net.corda.core.node.services.vault.QueryCriteria; +import net.corda.core.transactions.LedgerTransaction; +import net.corda.core.transactions.SignedTransaction; +import net.corda.core.transactions.TransactionBuilder; +import net.corda.core.utilities.ProgressTracker; +import org.jetbrains.annotations.NotNull; + +import java.security.PublicKey; +import java.security.SignatureException; +import java.util.List; +import java.util.SortedMap; + +import static net.corda.core.internal.verification.AbstractVerifier.logger; + +public class AcceptanceFlow { + + @InitiatingFlow + @StartableByRPC + public static class Initiator extends FlowLogic { + + private UniqueIdentifier proposalId; + private ProgressTracker progressTracker = new ProgressTracker(); + + public Initiator(UniqueIdentifier proposalId) { + this.proposalId = proposalId; + } + + @Suspendable + @Override + public SignedTransaction call() throws FlowException { + + QueryCriteria.LinearStateQueryCriteria inputCriteria = new QueryCriteria.LinearStateQueryCriteria(null, ImmutableList.of(proposalId), Vault.StateStatus.UNCONSUMED,null); + + StateAndRef inputStateAndRef = getServiceHub().getVaultService().queryBy(ProposalState.class, inputCriteria).getStates().get(0); + + ProposalState input = (ProposalState) inputStateAndRef.getState().getData(); + + // The parties are being resolved so that we can move way from using possible outdated keys from the input state + // and instead use the most up-to-date keys when building the transaction. + PartyIdentityResolver resolver = new PartyIdentityResolver(getServiceHub().getIdentityService()); + PartyIdentityResolved buyerKeyResolution = resolver.resolve(input.getBuyer()); + PartyIdentityResolved sellerKeyResolution = resolver.resolve(input.getSeller()); + PartyIdentityResolved proposerKeyResolution = resolver.resolve(input.getProposer()); + PartyIdentityResolved proposeeKeyResolution = resolver.resolve(input.getProposee()); + + SortedMap proofMap = PartyIdentityResolver.Companion.generateProofChainMap(buyerKeyResolution, sellerKeyResolution); + if(proofMap == null){ + logger.info("No proof."); + } else { + logger.info("One or more parties have rotated their keys, including the proof map in the transaction."); + } + + // Creating the output + TradeState output = new TradeState(input.getAmount(), buyerKeyResolution.getOriginalOrCurrentParty(), sellerKeyResolution.getOriginalOrCurrentParty(), input.getLinearId()); + + // Creating the command + List requiredSigners = ImmutableList.of(proposeeKeyResolution.getOwningKey(), proposerKeyResolution.getOwningKey()); + Command command = new Command(new ProposalAndTradeContract.Commands.Accept(), requiredSigners, proofMap); + + // Building the transaction + Party notary = inputStateAndRef.getState().getNotary(); + TransactionBuilder txBuilder = new TransactionBuilder(notary) + .addInputState(inputStateAndRef) + .addOutputState(output, ProposalAndTradeContract.ID) + .addCommand(command); + + // Signing the transaction ourselves + SignedTransaction partStx = getServiceHub().signInitialTransaction(txBuilder); + + // Gathering the counterparty's signature. + // + // The identity returned by `getOurIdentity` cannot be compared directly with the proposer from the input state, + // as the node may have rotated its keys since the proposal was created. The resolved party must be used instead. + // The resolver will always be able to resolve the node's own identity because the proof will always be available for the node's own key. + Party counterparty = (getOurIdentity().equals(proposerKeyResolution.getOriginalOrCurrentParty())) ? input.getProposee() : input.getProposer(); + + // The counterparty might be an old key, but the session will be initiated with the most up-to-date identity. + // No need to use the resolved party in this case. + FlowSession counterpartySession = initiateFlow(counterparty); + SignedTransaction fullyStx = subFlow(new CollectSignaturesFlow(partStx, ImmutableList.of(counterpartySession))); + + // Finalising the transaction + SignedTransaction finalisedTx = subFlow(new FinalityFlow(fullyStx, ImmutableList.of(counterpartySession))); + return finalisedTx; + } + } + + @InitiatedBy(Initiator.class) + public static class Responder extends FlowLogic{ + private FlowSession counterpartySession; + + public Responder(FlowSession counterpartySession) { + this.counterpartySession = counterpartySession; + } + + @Suspendable + @Override + public SignedTransaction call() throws FlowException { + SignTransactionFlow signTransactionFlow = new SignTransactionFlow(counterpartySession){ + + @Override + protected void checkTransaction(@NotNull SignedTransaction stx) throws FlowException { + try { + LedgerTransaction ledgerTx = stx.toLedgerTransaction(getServiceHub(), false); + + ProposalState input = ledgerTx.inputsOfType(ProposalState.class).get(0); + + // The counterparty session always provides the most up-to-date identity for the counterparty. + // + // Therefore, any party retrieved from a state must be resolved using `resolveToCurrentParty`. + // While `resolveToCurrentParty` does not rely on a proof, it resolves the party to its latest valid identity. + // + // This ensures that equality checks behave as expected after key rotation. + Party proposee = PartyIdentityResolver.Companion.resolveToCurrentParty(input.getProposee(), getServiceHub().getIdentityService()); + if(!proposee.equals(counterpartySession.getCounterparty())){ + throw new FlowException("Only the proposee can accept a proposal."); + } + } catch (SignatureException e) { + throw new FlowException("Check transaction failed"); + } + + + } + }; + SecureHash txId = subFlow(signTransactionFlow).getId(); + SignedTransaction finalisedTx = subFlow(new ReceiveFinalityFlow(counterpartySession, txId)); + return finalisedTx; + } + } +} + + diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/src/main/java/net/corda/samples/negotiation/flows/ModificationFlow.java b/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/src/main/java/net/corda/samples/negotiation/flows/ModificationFlow.java new file mode 100644 index 00000000..575a75e3 --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/src/main/java/net/corda/samples/negotiation/flows/ModificationFlow.java @@ -0,0 +1,187 @@ +package net.corda.samples.negotiation.flows; + +import co.paralleluniverse.fibers.Suspendable; +import com.google.common.collect.ImmutableList; +import net.corda.core.crypto.keyrotation.crossprovider.KeyRotationProofChain; +import net.corda.core.crypto.keyrotation.crossprovider.PartyIdentityResolved; +import net.corda.core.crypto.keyrotation.crossprovider.PartyIdentityResolver; +import net.corda.samples.negotiation.contracts.ProposalAndTradeContract; +import net.corda.samples.negotiation.states.ProposalState; +import net.corda.core.contracts.Command; +import net.corda.core.contracts.StateAndRef; +import net.corda.core.contracts.UniqueIdentifier; +import net.corda.core.crypto.SecureHash; +import net.corda.core.flows.*; +import net.corda.core.identity.Party; +import net.corda.core.node.services.Vault; +import net.corda.core.node.services.vault.QueryCriteria; +import net.corda.core.transactions.LedgerTransaction; +import net.corda.core.transactions.SignedTransaction; +import net.corda.core.transactions.TransactionBuilder; +import net.corda.core.utilities.ProgressTracker; +import org.jetbrains.annotations.NotNull; + +import java.security.PublicKey; +import java.security.SignatureException; +import java.util.List; +import java.util.SortedMap; + +import static net.corda.core.internal.verification.AbstractVerifier.logger; + +public class ModificationFlow { + + @InitiatingFlow + @StartableByRPC + public static class Initiator extends FlowLogic{ + private UniqueIdentifier proposalId; + private int newAmount; + private ProgressTracker progressTracker = new ProgressTracker(); + + public Initiator(UniqueIdentifier proposalId, int newAmount) { + this.proposalId = proposalId; + this.newAmount = newAmount; + } + + @Suspendable + @Override + public SignedTransaction call() throws FlowException { + QueryCriteria.LinearStateQueryCriteria inputCriteria = new QueryCriteria.LinearStateQueryCriteria(null, ImmutableList.of(proposalId), Vault.StateStatus.UNCONSUMED, null); + StateAndRef inputStateAndRef = getServiceHub().getVaultService().queryBy(ProposalState.class, inputCriteria).getStates().get(0); + ProposalState input = (ProposalState) inputStateAndRef.getState().getData(); + + // N.B.: Any party data retrieved from a state must be resolved using an instance of PartyIdentityResolver. + // + // `PartyIdentityResolver` checks whether the node has an associated proof for the given party. + // If a proof is available, it resolves the party to the most recent valid identity permitted by that proof. + // + // Note that a key rotation proof is not immediately available after rotation. It typically becomes available + // only after at least one transaction has been processed and the proof has been propagated to the Identity Service + // of the node that performed the rotation. + // + // Therefore, immediately after a key rotation, the resolver may still return a valid but non-updated identity. + // Once the proof becomes available, subsequent resolutions will return the updated (new-key) identity. + // + // The Identity Service will always contain the key rotation proofs for its own node. + PartyIdentityResolver resolver = new PartyIdentityResolver(getServiceHub().getIdentityService()); + PartyIdentityResolved buyerKeyResolution = resolver.resolve(input.getBuyer()); + PartyIdentityResolved sellerKeyResolution = resolver.resolve(input.getSeller()); + PartyIdentityResolved proposerKeyResolution = resolver.resolve(input.getProposer()); + PartyIdentityResolved proposeeKeyResolution = resolver.resolve(input.getProposee()); + + + // A proof map must be included in the transaction command if any of the parties have rotated their keys, + // and the input states contain the old identity while the output states contain the new identity. + // + // This is required because the contract must be able to verify the proof chains for all parties that have + // undergone key rotation, and therefore must have access to the full proof history. + // + // All resolved parties are passed to the `generateProofChainMap` method, which determines whether there are + // differences between the original and current identities. If differences are detected, it generates the + // required proof chains and returns a map of new keys to their corresponding proof chains. If no differences + // are found, an empty map is returned. + // + // In this example, only the buyer and seller are passed to `generateProofChainMap`, since the proposer and + // proposee are always either the buyer or the seller. + SortedMap proofMap = PartyIdentityResolver.Companion.generateProofChainMap(buyerKeyResolution, sellerKeyResolution); + if(proofMap == null){ + logger.info("No proof."); + } else { + logger.info("One or more parties have rotated their keys, including the proof map in the transaction."); + } + + // The `getOurIdentity` method always returns the most up-to-date identity for the node. + // + // In this context, it is safe to use the resolved party because if the node has performed a key rotation, + // the resolver will return the latest valid identity. Therefore, it is safe to compare the identity returned + // by `getOurIdentity` with the resolved identity. + // + // Never compare the identity returned by `getOurIdentity` with the original party stored in the state, + // as that party may be outdated due to key rotation, and the resolver may return a different current identity. + Party ourIdentityFromInput = (getOurIdentity().equals(proposerKeyResolution.getOriginalOrCurrentParty())) ? proposerKeyResolution.getOriginalOrCurrentParty() : proposeeKeyResolution.getOriginalOrCurrentParty(); + Party counterpartyFromInput = (getOurIdentity().equals(proposerKeyResolution.getOriginalOrCurrentParty())) ? proposeeKeyResolution.getOriginalOrCurrentParty() : proposerKeyResolution.getOriginalOrCurrentParty(); + + // Creating the output using the newest identity provided by the resolver. + // + // This ensures that any outdated keys are replaced in the output state where possible. + // If no updated identity is available, the existing (old) key is retained. + // + // Always use resolved parties when constructing the output state, as a key rotation proof + // must be included in the transaction if any party identity has been updated. + // + // This guarantees that the transaction remains verifiable after key rotation. + ProposalState output = new ProposalState(newAmount, buyerKeyResolution.getOriginalOrCurrentParty(), sellerKeyResolution.getOriginalOrCurrentParty(), ourIdentityFromInput, counterpartyFromInput, input.getLinearId()); + + // Creating a command that includes the required signers and the proof map as part of the command data. + // + // Use resolved parties to determine the required signers, as the contract verifies signatures against resolved identities. + // Using original state parties may lead to signature verification failures if those parties have undergone key rotation. + // Consistency is required: the same resolved identities must be used both for determining required signers and for + // constructing the output state. Old and new keys for a given node must not be mixed. + // + // The included proofs will be accessible to the contract during transaction verification via the command. + List requiredSigners = ImmutableList.of(proposeeKeyResolution.getOwningKey(), proposerKeyResolution.getOwningKey()); + Command command = new Command(new ProposalAndTradeContract.Commands.Modify(), requiredSigners, proofMap); + + // Building the transaction + Party notary = inputStateAndRef.getState().getNotary(); + TransactionBuilder txBuilder = new TransactionBuilder(notary) + .addInputState(inputStateAndRef) + .addOutputState(output, ProposalAndTradeContract.ID) + .addCommand(command); + + // Signing the transaction ourselves + SignedTransaction partStx = getServiceHub().signInitialTransaction(txBuilder); + + // Gathering the counterparty's signatures + // Note that the counterparty may have also rotated their keys. The initiateFlow will automatically resolve the counterparty's + // identity and select the correct key to use for the session, so we can simply pass in the counterparty as retrieved from the resolver. + FlowSession counterpartySession = initiateFlow(counterpartyFromInput); + SignedTransaction fullyStx = subFlow(new CollectSignaturesFlow(partStx, ImmutableList.of(counterpartySession))); + + // Finalising the transaction + SignedTransaction finalTx = subFlow(new FinalityFlow(fullyStx,ImmutableList.of(counterpartySession))); + return finalTx; + } + } + + @InitiatedBy(Initiator.class) + public static class Responder extends FlowLogic { + private FlowSession counterpartySession; + + public Responder(FlowSession counterpartySession) { + this.counterpartySession = counterpartySession; + } + + @Suspendable + @Override + public SignedTransaction call() throws FlowException { + SignTransactionFlow signTransactionFlow = new SignTransactionFlow(counterpartySession){ + + @Override + protected void checkTransaction(@NotNull SignedTransaction stx) throws FlowException { + try { + LedgerTransaction ledgerTx = stx.toLedgerTransaction(getServiceHub(), false); + ProposalState input = ledgerTx.inputsOfType(ProposalState.class).get(0); + + // The counterparty session always provides the most up-to-date identity for the counterparty. + // + // Therefore, any party retrieved from a state must be resolved using `resolveToCurrentParty`. + // While `resolveToCurrentParty` does not rely on a proof, it resolves the party to its latest valid identity. + // + // This ensures that equality checks behave as expected after key rotation. + Party proposee = PartyIdentityResolver.Companion.resolveToCurrentParty(input.getProposee(), getServiceHub().getIdentityService()); + if(!proposee.equals(counterpartySession.getCounterparty())){ + throw new FlowException("Only the proposee can modify a proposal."); + } + } catch (SignatureException e) { + throw new FlowException(); + } + } + }; + SecureHash txId = subFlow(signTransactionFlow).getId(); + + SignedTransaction finalisedTx = subFlow(new ReceiveFinalityFlow(counterpartySession, txId)); + return finalisedTx; + } + } +} diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/src/main/java/net/corda/samples/negotiation/flows/ProposalFlow.java b/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/src/main/java/net/corda/samples/negotiation/flows/ProposalFlow.java new file mode 100644 index 00000000..86ceda75 --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/src/main/java/net/corda/samples/negotiation/flows/ProposalFlow.java @@ -0,0 +1,102 @@ +package net.corda.samples.negotiation.flows; + +import co.paralleluniverse.fibers.Suspendable; +import com.google.common.collect.ImmutableList; +import net.corda.samples.negotiation.contracts.ProposalAndTradeContract; +import net.corda.samples.negotiation.states.ProposalState; +import net.corda.core.contracts.Command; +import net.corda.core.contracts.UniqueIdentifier; +import net.corda.core.crypto.SecureHash; +import net.corda.core.flows.*; +import net.corda.core.identity.Party; +import net.corda.core.transactions.SignedTransaction; +import net.corda.core.transactions.TransactionBuilder; +import org.jetbrains.annotations.NotNull; + +import java.security.PublicKey; +import java.util.List; +import net.corda.core.identity.CordaX500Name; + +// No changes are required in this flow, as the transaction contains no input states and all parties are obtained either +// from `getOurIdentity` or from the counterparty session, both of which always provide the most up-to-date identity. +public class ProposalFlow { + @InitiatingFlow + @StartableByRPC + public static class Initiator extends FlowLogic { + private Boolean isBuyer; + private int amount; + private Party counterparty; + + public Initiator(Boolean isBuyer, int amount, Party counterparty) { + this.isBuyer = isBuyer; + this.amount = amount; + this.counterparty = counterparty; + } + + @Suspendable + @Override + public UniqueIdentifier call() throws FlowException { + Party buyer, seller; + if(isBuyer){ + buyer = getOurIdentity(); + seller = counterparty; + }else{ + buyer = counterparty; + seller = getOurIdentity(); + } + ProposalState output = new ProposalState(amount, buyer, seller,getOurIdentity(), counterparty); + + //Creating the command + ProposalAndTradeContract.Commands.Propose commandType = new ProposalAndTradeContract.Commands.Propose(); + List requiredSigners = ImmutableList.of(getOurIdentity().getOwningKey(), counterparty.getOwningKey()); + Command command = new Command(commandType, requiredSigners); + + //Building the transaction + + // Obtain a reference to a notary we wish to use. + /** Explicit selection of notary by CordaX500Name - argument can by coded in flows or parsed from config (Preferred)*/ + final Party notary = getServiceHub().getNetworkMapCache().getNotary(CordaX500Name.parse("O=Notary, L=London, C=GB")); + + TransactionBuilder txBuilder = new TransactionBuilder(notary) + .addOutputState(output, ProposalAndTradeContract.ID) + .addCommand(command); + + //Signing the transaction ourselves + SignedTransaction partStx = getServiceHub().signInitialTransaction(txBuilder); + + //Gather counterparty sigs + FlowSession counterpartySession = initiateFlow(counterparty); + SignedTransaction fullyStx = subFlow(new CollectSignaturesFlow(partStx, ImmutableList.of(counterpartySession))); + + //Finalise the transaction + SignedTransaction finalisedTx = subFlow(new FinalityFlow(fullyStx, ImmutableList.of(counterpartySession))); + return finalisedTx.getTx().outputsOfType(ProposalState.class).get(0).getLinearId(); + } + } + + @InitiatedBy(Initiator.class) + public static class Responder extends FlowLogic{ + private FlowSession counterpartySession; + + public Responder(FlowSession counterpartySession) { + this.counterpartySession = counterpartySession; + } + + @Suspendable + @Override + public SignedTransaction call() throws FlowException { + + SignTransactionFlow signTransactionFlow = new SignTransactionFlow(counterpartySession){ + + @Override + protected void checkTransaction(@NotNull SignedTransaction stx) throws FlowException { + + } + }; + SecureHash txId = subFlow(signTransactionFlow).getId(); + + SignedTransaction finalisedTx = subFlow(new ReceiveFinalityFlow(counterpartySession, txId)); + return finalisedTx; + } + } +} diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/src/test/java/net/corda/samples/negotiation/flows/AcceptanceFlowTests.java b/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/src/test/java/net/corda/samples/negotiation/flows/AcceptanceFlowTests.java new file mode 100644 index 00000000..6bf73176 --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/src/test/java/net/corda/samples/negotiation/flows/AcceptanceFlowTests.java @@ -0,0 +1,74 @@ +package net.corda.samples.negotiation.flows; + +import com.google.common.collect.ImmutableList; +import net.corda.samples.negotiation.states.ProposalState; +import net.corda.samples.negotiation.states.TradeState; +import net.corda.core.contracts.StateAndRef; +import net.corda.core.contracts.UniqueIdentifier; +import net.corda.core.identity.Party; +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +public class AcceptanceFlowTests extends FlowTestBase { + + @Test + public void acceptanceFlowConsumesTheProposalsInBothNodesVaultsAndReplacesWithEquivAccTradesWhenInitiatorIsBuyer() throws ExecutionException, InterruptedException { + testAcceptance(true); + + } + + @Test + public void acceptanceFlowConsumesTheProposalsInBothNodesVaultsAndReplacesWithEquivAccTradesWhenInitiatorIsSeller() throws ExecutionException, InterruptedException { + testAcceptance(false); + } + + @Test(expected = ExecutionException.class) + public void acceptanceFlowThrowsAnErrorIfTheProposerTriesToAcceptTheProposal() throws ExecutionException, InterruptedException { + int amount = 1; + Party counterparty = b.getInfo().getLegalIdentitiesAndCerts().get(0).getParty(); + UniqueIdentifier proposalID = nodeACreatesProposal(true, amount, counterparty); + + AcceptanceFlow.Initiator flow = new AcceptanceFlow.Initiator(proposalID); + Future future = a.startFlow(flow); + network.runNetwork(); + future.get(); + } + + private void testAcceptance(Boolean isBuyer) throws ExecutionException, InterruptedException { + int amount = 1; + Party counterparty = b.getInfo().getLegalIdentitiesAndCerts().get(0).getParty(); + UniqueIdentifier proposalId = nodeACreatesProposal(isBuyer, amount, counterparty); + nodeBAcceptsProposal(proposalId); + ImmutableList.of(a, b).forEach(node -> { + node.transaction(() -> { + List> proposals = node.getServices().getVaultService().queryBy(ProposalState.class).getStates(); + Assert.assertEquals(0, proposals.size()); + + List> trades = node.getServices().getVaultService().queryBy(TradeState.class).getStates(); + Assert.assertEquals(1, trades.size()); + TradeState trade = trades.get(0).getState().getData(); + + Assert.assertEquals(amount, trade.getAmount()); + Party buyer; + Party seller; + + if (isBuyer) { + buyer = a.getInfo().getLegalIdentitiesAndCerts().get(0).getParty(); + seller = b.getInfo().getLegalIdentitiesAndCerts().get(0).getParty(); + } else { + seller = a.getInfo().getLegalIdentitiesAndCerts().get(0).getParty(); + buyer = b.getInfo().getLegalIdentitiesAndCerts().get(0).getParty(); + } + + Assert.assertEquals(buyer, trade.getBuyer()); + Assert.assertEquals(seller, trade.getSeller()); + return null; + }); + return; + }); + } +} diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/src/test/java/net/corda/samples/negotiation/flows/FlowTestBase.java b/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/src/test/java/net/corda/samples/negotiation/flows/FlowTestBase.java new file mode 100644 index 00000000..d6e2fd85 --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/src/test/java/net/corda/samples/negotiation/flows/FlowTestBase.java @@ -0,0 +1,71 @@ +package net.corda.samples.negotiation.flows; + +import com.google.common.collect.ImmutableList; +import net.corda.core.contracts.UniqueIdentifier; +import net.corda.core.identity.CordaX500Name; +import net.corda.core.identity.Party; +import net.corda.testing.driver.VerifierType; +import net.corda.testing.node.*; +import org.junit.After; +import org.junit.Before; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +abstract class FlowTestBase { + protected MockNetwork network; + protected StartedMockNode a; + protected StartedMockNode b; + + @Before + public void setup() { + network = new MockNetwork(new MockNetworkParameters( + ImmutableList.of( + TestCordapp.findCordapp("net.corda.samples.negotiation.flows"), + TestCordapp.findCordapp("net.corda.samples.negotiation.contracts")) + ).withNotarySpecs(ImmutableList.of(new MockNetworkNotarySpec(CordaX500Name.parse("O=Notary,L=London,C=GB"))))); + a = network.createPartyNode(null); + b = network.createPartyNode(null); + + List responseflows = ImmutableList.of(ProposalFlow.Responder.class, AcceptanceFlow.Responder.class, ModificationFlow.Responder.class); + + ImmutableList.of(a,b).forEach(node -> { + for (Class flow:responseflows + ) { + node.registerInitiatedFlow(flow); + + } + }); + network.runNetwork(); + + } + + @After + public void tearDown(){ + network.stopNodes(); + } + + public UniqueIdentifier nodeACreatesProposal(Boolean isBuyer, int amount, Party counterparty) throws ExecutionException, InterruptedException { + ProposalFlow.Initiator flow = new ProposalFlow.Initiator(isBuyer, amount, counterparty); + Future future = a.startFlow(flow); + network.runNetwork(); + return (UniqueIdentifier) future.get(); + + } + + public void nodeBAcceptsProposal(UniqueIdentifier proposalId) throws ExecutionException, InterruptedException { + AcceptanceFlow.Initiator flow = new AcceptanceFlow.Initiator(proposalId); + Future future = b.startFlow(flow); + network.runNetwork(); + future.get(); + } + + public void nodeBModifiesProposal(UniqueIdentifier proposalId, int newAmount) throws ExecutionException, InterruptedException { + ModificationFlow.Initiator flow = new ModificationFlow.Initiator(proposalId, newAmount); + Future future = b.startFlow(flow); + network.runNetwork(); + future.get(); + } +} diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/src/test/java/net/corda/samples/negotiation/flows/KeyRotationFlowTests.java b/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/src/test/java/net/corda/samples/negotiation/flows/KeyRotationFlowTests.java new file mode 100644 index 00000000..f5454620 --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/src/test/java/net/corda/samples/negotiation/flows/KeyRotationFlowTests.java @@ -0,0 +1,145 @@ +package net.corda.samples.negotiation.flows; + +import net.corda.core.contracts.UniqueIdentifier; +import net.corda.core.crypto.TransactionSignature; +import net.corda.core.crypto.keyrotation.crossprovider.KeyRotationProofChain; +import net.corda.core.identity.Party; +import net.corda.core.transactions.SignedTransaction; +import net.corda.samples.negotiation.states.ProposalState; +import net.corda.samples.negotiation.states.TradeState; +import org.junit.Test; + +import java.security.PublicKey; +import java.util.SortedMap; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class KeyRotationFlowTests extends KeyRotationTestBase { + + @Test + public void nodeARotatesAndNodeAStartsTheTransaction() throws Exception { + Party partyAOriginal = identityOf(a); + Party partyB = identityOf(b); + + // Node B proposes to Node A. + UniqueIdentifier proposalId = propose(b, 1, partyAOriginal); + assertProposal(proposalOf(a, proposalId), partyB, partyAOriginal, partyB, partyAOriginal, 1); + assertProposal(proposalOf(b, proposalId), partyB, partyAOriginal, partyB, partyAOriginal, 1); + + a = rotate(a); + Party partyARotated = identityOf(a); + + // 1. Node A modifies. + SignedTransaction modifiedByA = modify(a, proposalId, 2); + assertProposal(outputOf(modifiedByA), partyB, partyARotated, partyARotated, partyB, 2); + assertProofInCommand(modifiedByA, partyAOriginal, partyARotated); + assertNoProofInSignatures(modifiedByA); + + // 2. Node B modifies. + assertNoProof(modify(b, proposalId, 3)); + + // 3. Node A modifies again: still no proof. + assertNoProof(modify(a, proposalId, 4)); + + // 4. Node B accepts + SignedTransaction accepted = accept(b, proposalId); + assertNoProof(accepted); + + TradeState trade = (TradeState) accepted.getCoreTransaction().getOutputStates().get(0); + assertEquals(partyARotated, trade.getSeller()); + assertEquals(partyB, trade.getBuyer()); + } + + @Test + public void nodeARotatesAndNodeBStartsTheTransaction() throws Exception { + Party partyAOriginal = identityOf(a); + Party partyB = identityOf(b); + + // Node A proposes to Node B. + UniqueIdentifier proposalId = propose(a, 1, partyB); + assertProposal(proposalOf(a, proposalId), partyAOriginal, partyB, partyAOriginal, partyB, 1); + assertProposal(proposalOf(b, proposalId), partyAOriginal, partyB, partyAOriginal, partyB, 1); + + a = rotate(a); + Party partyARotated = identityOf(a); + + // 1. Node B modifies. + SignedTransaction modifiedByB = modify(b, proposalId, 2); + assertProposal(outputOf(modifiedByB), partyAOriginal, partyB, partyB, partyAOriginal, 2); + assertNull(commandProofMap(modifiedByB)); + assertProofInSignature(modifiedByB, partyAOriginal, partyARotated); + + // 2. Node A modifies. + SignedTransaction modifiedByA = modify(a, proposalId, 3); + assertProposal(outputOf(modifiedByA), partyARotated, partyB, partyARotated, partyB, 3); + assertProofInCommand(modifiedByA, partyAOriginal, partyARotated); + assertNoProofInSignatures(modifiedByA); + + // 3. Node B modifies again: still no proof. + assertNoProof(modify(b, proposalId, 4)); + + // 4. Node A accepts + SignedTransaction accepted = accept(a, proposalId); + assertNoProof(accepted); + + TradeState trade = (TradeState) accepted.getCoreTransaction().getOutputStates().get(0); + assertEquals(partyARotated, trade.getBuyer()); + assertEquals(partyB, trade.getSeller()); + } + + // --- Assertions -------------------------------------------------------------------------------------------------------- + + private static ProposalState outputOf(SignedTransaction stx) { + return (ProposalState) stx.getCoreTransaction().getOutputStates().get(0); + } + + private static void assertProposal(ProposalState proposal, Party buyer, Party seller, Party proposer, Party proposee, int amount) { + assertEquals("buyer", buyer, proposal.getBuyer()); + assertEquals("seller", seller, proposal.getSeller()); + assertEquals("proposer", proposer, proposal.getProposer()); + assertEquals("proposee", proposee, proposal.getProposee()); + assertEquals("amount", amount, proposal.getAmount()); + } + + private static SortedMap commandProofMap(SignedTransaction stx) { + return stx.getTx().getCommands().get(0).getKeyRotationProofChainMap(); + } + + /** The command's proof map links {@code before}'s key to {@code after}'s key. */ + private static void assertProofInCommand(SignedTransaction stx, Party before, Party after) { + SortedMap proofMap = commandProofMap(stx); + assertNotNull("The command should carry a proof map", proofMap); + KeyRotationProofChain chain = proofMap.get(before.getOwningKey()); + assertNotNull("The proof map should have a chain for the old key", chain); + assertTrue("The chain should link the old key to the new key", chain.isValid(before.getOwningKey(), after.getOwningKey())); + } + + /** The signature made with {@code after}'s key carries a proof chain linking {@code before}'s key to it. */ + private static void assertProofInSignature(SignedTransaction stx, Party before, Party after) { + for (TransactionSignature signature : stx.getSigs()) { + if (signature.getBy().equals(after.getOwningKey())) { + KeyRotationProofChain chain = signature.getSignatureMetadata().getProofChain(); + assertNotNull("The signature by the new key should carry a proof chain", chain); + assertTrue("The chain should link the old key to the new key", chain.isValid(before.getOwningKey(), after.getOwningKey())); + return; + } + } + throw new AssertionError("No signature by " + after.getName() + "'s new key"); + } + + private static void assertNoProofInSignatures(SignedTransaction stx) { + for (TransactionSignature signature : stx.getSigs()) { + KeyRotationProofChain chain = signature.getSignatureMetadata().getProofChain(); + assertTrue("No signature should carry a proof chain", chain == null || chain.isEmpty()); + } + } + + /** Neither the command nor any signature carries a proof: the transaction looks exactly like one without any rotation. */ + private static void assertNoProof(SignedTransaction stx) { + assertNull("The command should not carry a proof map", commandProofMap(stx)); + assertNoProofInSignatures(stx); + } +} diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/src/test/java/net/corda/samples/negotiation/flows/KeyRotationTestBase.java b/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/src/test/java/net/corda/samples/negotiation/flows/KeyRotationTestBase.java new file mode 100644 index 00000000..61cd0adc --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/src/test/java/net/corda/samples/negotiation/flows/KeyRotationTestBase.java @@ -0,0 +1,134 @@ +package net.corda.samples.negotiation.flows; + +import com.google.common.collect.ImmutableList; +import net.corda.core.contracts.UniqueIdentifier; +import net.corda.core.flows.FlowLogic; +import net.corda.core.identity.CordaX500Name; +import net.corda.core.identity.Party; +import net.corda.core.node.NetworkParameters; +import net.corda.core.node.services.Vault; +import net.corda.core.node.services.vault.QueryCriteria; +import net.corda.core.transactions.SignedTransaction; +import net.corda.samples.negotiation.states.ProposalState; +import net.corda.testing.node.InMemoryMessagingNetwork; +import net.corda.testing.node.MockNetworkNotarySpec; +import net.corda.testing.node.MockNetworkParameters; +import net.corda.testing.node.TestCordapp; +import net.corda.testing.node.internal.DriverDSLImplKt; +import net.corda.testing.node.internal.InternalMockNetwork; +import net.corda.testing.node.internal.InternalTestUtilsKt; +import net.corda.testing.node.internal.TestCordappInternal; +import net.corda.testing.node.internal.TestStartedNode; +import org.junit.After; +import org.junit.Before; + +import java.nio.file.Paths; +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +/** + * A two-node Corda Enterprise mock network for the key rotation tests, plus one-line helpers to run the sample's flows. + * + * The nodes are created with {@code InternalMockNetwork.createKeyRotationMockNode}, which is what the Corda Enterprise + * repository's own key rotation tests use: such a node runs the Enterprise versions of {@code CollectSignaturesFlow} and + * {@code FinalityFlow}, exactly like a real Enterprise node, and those are the flows that know how to collect and verify a + * signature made with a rotated key. The public {@code MockNetwork} creates plain nodes running the Corda OS flows, which do not. + */ +abstract class KeyRotationTestBase { + + /** Corda Enterprise 4.15 is platform version 170; key rotation proofs need at least that on the network. */ + private static final int MINIMUM_PLATFORM_VERSION = 170; + + private static final List>> RESPONDER_FLOWS = ImmutableList.>>of( + ProposalFlow.Responder.class, AcceptanceFlow.Responder.class, ModificationFlow.Responder.class); + + protected InternalMockNetwork network; + protected TestStartedNode a; + protected TestStartedNode b; + + @Before + public void setUp() { + NetworkParameters networkParameters = new NetworkParameters(MINIMUM_PLATFORM_VERSION, Collections.emptyList(), + 10485760, 10485760 * 50, Instant.now(), 1, Collections.emptyMap()); + + network = new InternalMockNetwork( + Collections.emptyList(), + new MockNetworkParameters(), + false, + false, + new InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random(), + ImmutableList.of(new MockNetworkNotarySpec(CordaX500Name.parse("O=Notary,L=London,C=GB"))), + Paths.get("build", "mock-network", DriverDSLImplKt.getTimestampAsDirectoryName()), + networkParameters, + InternalMockNetwork.Companion::createKeyRotationMockNode, + ImmutableList.of( + (TestCordappInternal) TestCordapp.findCordapp("net.corda.samples.negotiation.flows"), + (TestCordappInternal) TestCordapp.findCordapp("net.corda.samples.negotiation.contracts")), + true); + + a = network.createPartyNode(new CordaX500Name("Node A", "London", "GB")); + b = network.createPartyNode(new CordaX500Name("Node B", "New York", "US")); + registerResponders(a); + registerResponders(b); + network.runNetwork(); + } + + @After + public void tearDown() { + if (network != null) { + network.stopNodes(); + } + } + + /** + * Rotates the node's legal identity key the way the Corda Enterprise cross-provider key rotation does: a new key is + * generated, the OLD key signs the NEW public key (that signature is the rotation proof) and the node restarts on the new + * key with the proof in its identity service. Use the returned node from here on; the one passed in is gone. + */ + protected TestStartedNode rotate(TestStartedNode node) { + TestStartedNode rotated = network.rotateKey(node, null); + registerResponders(rotated); + return rotated; + } + + protected static Party identityOf(TestStartedNode node) { + return node.getInfo().getLegalIdentities().get(0); + } + + // --- The sample's flows, one call each ------------------------------------------------------------------------------- + + /** {@code proposer} proposes to {@code counterparty}; the proposer is the buyer. Only the proposee can then modify or accept. */ + protected UniqueIdentifier propose(TestStartedNode proposer, int amount, Party counterparty) throws ExecutionException, InterruptedException { + return run(proposer, new ProposalFlow.Initiator(true, amount, counterparty)); + } + + protected SignedTransaction modify(TestStartedNode node, UniqueIdentifier proposalId, int newAmount) throws ExecutionException, InterruptedException { + return run(node, new ModificationFlow.Initiator(proposalId, newAmount)); + } + + protected SignedTransaction accept(TestStartedNode node, UniqueIdentifier proposalId) throws ExecutionException, InterruptedException { + return run(node, new AcceptanceFlow.Initiator(proposalId)); + } + + /** The unconsumed proposal with this id, as recorded in the node's vault. */ + protected ProposalState proposalOf(TestStartedNode node, UniqueIdentifier proposalId) { + QueryCriteria criteria = new QueryCriteria.LinearStateQueryCriteria(null, ImmutableList.of(proposalId), Vault.StateStatus.UNCONSUMED, null); + return node.getDatabase().transaction(tx -> + node.getServices().getVaultService().queryBy(ProposalState.class, criteria).getStates().get(0).getState().getData()); + } + + private T run(TestStartedNode node, FlowLogic flow) throws ExecutionException, InterruptedException { + Future result = InternalTestUtilsKt.startFlow(node.getServices(), flow).getResultFuture(); + network.runNetwork(); + return result.get(); + } + + private void registerResponders(TestStartedNode node) { + for (Class> responder : RESPONDER_FLOWS) { + node.registerInitiatedFlow(responder, false); + } + } +} diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/src/test/java/net/corda/samples/negotiation/flows/ModifyFlowTests.java b/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/src/test/java/net/corda/samples/negotiation/flows/ModifyFlowTests.java new file mode 100644 index 00000000..424b6b8c --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/src/test/java/net/corda/samples/negotiation/flows/ModifyFlowTests.java @@ -0,0 +1,84 @@ +package net.corda.samples.negotiation.flows; + +import com.google.common.collect.ImmutableList; +import net.corda.samples.negotiation.states.ProposalState; +import net.corda.core.contracts.StateAndRef; +import net.corda.core.contracts.UniqueIdentifier; +import net.corda.core.identity.Party; +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +public class ModifyFlowTests extends FlowTestBase { + + @Test + public void modificationFlowConsumesTheProposalsInBothNodesVaultsAndReplacesWithEquivWithNEwAmountsWhenInitiatorISBuyer() throws ExecutionException, InterruptedException { + testModification(true); + } + + @Test + public void modificationFlowConsumesTheProposalsInBothNodesVaultsAndReplacesWithEquivWithNEwAmountsWhenInitiatorIsNotBuyer() throws ExecutionException, InterruptedException { + testModification(false); + } + + @Test(expected = ExecutionException.class) + public void modificationFlowThrowsAnErrorIfTheProposerTriesToModifyTheProposal() throws ExecutionException, InterruptedException { + int oldAmount = 1; + int newAmount = 2; + Party counterparty = b.getInfo().getLegalIdentitiesAndCerts().get(0).getParty(); + + UniqueIdentifier proposalID = nodeACreatesProposal(true, oldAmount, counterparty); + ModificationFlow.Initiator flow = new ModificationFlow.Initiator(proposalID, newAmount ); + Future future = a.startFlow(flow); + network.runNetwork(); + future.get(); + + } + + + public void testModification(Boolean isBuyer) throws ExecutionException, InterruptedException { + int oldAmount = 1; + int newAmount = 2; + Party counterparty = b.getInfo().getLegalIdentitiesAndCerts().get(0).getParty(); + + UniqueIdentifier proposalID = nodeACreatesProposal(isBuyer, oldAmount, counterparty); + nodeBModifiesProposal(proposalID,newAmount); + + ImmutableList.of(a,b).forEach(node -> { + node.transaction(() -> { + List> proposals = node.getServices().getVaultService().queryBy(ProposalState.class).getStates(); + Assert.assertEquals(1, proposals.size()); + + ProposalState proposal = proposals.get(0).getState().getData(); + + Assert.assertEquals(newAmount, proposal.getAmount()); + Party buyer; + Party seller; + Party proposer; + Party proposee; + + if(isBuyer){ + buyer = a.getInfo().getLegalIdentitiesAndCerts().get(0).getParty(); + proposer = b.getInfo().getLegalIdentitiesAndCerts().get(0).getParty(); + seller = b.getInfo().getLegalIdentitiesAndCerts().get(0).getParty(); + proposee = a.getInfo().getLegalIdentitiesAndCerts().get(0).getParty(); + }else{ + seller = a.getInfo().getLegalIdentitiesAndCerts().get(0).getParty(); + proposer = b.getInfo().getLegalIdentitiesAndCerts().get(0).getParty(); + buyer = b.getInfo().getLegalIdentitiesAndCerts().get(0).getParty(); + proposee = a.getInfo().getLegalIdentitiesAndCerts().get(0).getParty(); + } + + Assert.assertEquals(buyer, proposal.getBuyer()); + Assert.assertEquals(seller, proposal.getSeller()); + Assert.assertEquals(proposer, proposal.getProposer()); + Assert.assertEquals(proposee, proposal.getProposee()); + return null; + }); + return; + }); + } +} diff --git a/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/src/test/java/net/corda/samples/negotiation/flows/ProposalFlowTests.java b/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/src/test/java/net/corda/samples/negotiation/flows/ProposalFlowTests.java new file mode 100644 index 00000000..04a4bdc5 --- /dev/null +++ b/Advanced/negotiation-cordapp-key-rotation-proof-free/workflows/src/test/java/net/corda/samples/negotiation/flows/ProposalFlowTests.java @@ -0,0 +1,55 @@ +package net.corda.samples.negotiation.flows; + +import com.google.common.collect.ImmutableList; +import net.corda.samples.negotiation.states.ProposalState; +import net.corda.core.contracts.StateAndRef; +import net.corda.core.identity.Party; +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; +import java.util.concurrent.ExecutionException; + +public class ProposalFlowTests extends FlowTestBase { + + @Test + public void proposalFlowCreatesTheCorrectProposalsInBothNodesVaultsWhenInitiatorIsBuyer() throws ExecutionException, InterruptedException { + testProposal(true); + } + + @Test + public void proposalFlowCreatesTheCorrectProposalsInBothNodesVaultsWhenInitiatorIsSeller() throws ExecutionException, InterruptedException { + testProposal(false); + } + + private void testProposal(Boolean isBuyer) throws ExecutionException, InterruptedException { + int amount = 1; + Party counterparty = b.getInfo().getLegalIdentitiesAndCerts().get(0).getParty(); + nodeACreatesProposal(isBuyer, amount, counterparty); + ImmutableList.of(a, b).forEach(node -> { + node.transaction(() -> { + List> proposals = node.getServices().getVaultService().queryBy(ProposalState.class).getStates(); + Assert.assertEquals(1, proposals.size()); + + ProposalState proposal = proposals.get(0).getState().getData(); + + Assert.assertEquals(amount, proposal.getAmount()); + Party buyer; + Party seller; + + if (isBuyer) { + buyer = a.getInfo().getLegalIdentitiesAndCerts().get(0).getParty(); + seller = b.getInfo().getLegalIdentitiesAndCerts().get(0).getParty(); + } else { + seller = a.getInfo().getLegalIdentitiesAndCerts().get(0).getParty(); + buyer = b.getInfo().getLegalIdentitiesAndCerts().get(0).getParty(); + } + + Assert.assertEquals(buyer, proposal.getBuyer()); + Assert.assertEquals(seller, proposal.getSeller()); + return null; + }); + return; + }); + } +}