From 34300df3ff6a01d30cd393727d6c235f599738be Mon Sep 17 00:00:00 2001 From: Filipe Oliveira Date: Tue, 31 Mar 2026 11:07:14 +0100 Subject: [PATCH] ENT-14906 - Sample for Enabling Proof-Free Transactions --- .../contracts/ProposalAndTradeContract.java | 10 +++-- .../negotiation-cordapp/repositories.gradle | 24 +++++++++++ .../negotiation/flows/ModificationFlow.java | 41 +++++++++++++++---- .../negotiation/flows/ProposalFlow.java | 2 +- 4 files changed, 63 insertions(+), 14 deletions(-) diff --git a/Advanced/negotiation-cordapp/contracts/src/main/java/net/corda/samples/negotiation/contracts/ProposalAndTradeContract.java b/Advanced/negotiation-cordapp/contracts/src/main/java/net/corda/samples/negotiation/contracts/ProposalAndTradeContract.java index d72648d4..59502f4b 100644 --- a/Advanced/negotiation-cordapp/contracts/src/main/java/net/corda/samples/negotiation/contracts/ProposalAndTradeContract.java +++ b/Advanced/negotiation-cordapp/contracts/src/main/java/net/corda/samples/negotiation/contracts/ProposalAndTradeContract.java @@ -1,6 +1,7 @@ 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; @@ -61,13 +62,14 @@ public void verify(LedgerTransaction tx) throws IllegalArgumentException { ProposalState input = tx.inputsOfType(ProposalState.class).get(0); ProposalState output = tx.outputsOfType(ProposalState.class).get(0); + PartyIdentityResolver resolver = new PartyIdentityResolver(command.getKeyRotationProofChainMap()); require.using("The amount is unmodified in the output", output.getAmount() != input.getAmount()); - require.using("The buyer is unmodified in the output", input.getBuyer().equals(output.getBuyer())); - require.using("The seller is unmodified in the output", input.getSeller().equals(output.getSeller())); + require.using("The buyer is unmodified in the output", resolver.isSameParty(input.getBuyer(), output.getBuyer())); + require.using("The seller is unmodified in the ogutput", resolver.isSameParty(input.getSeller(), output.getSeller())); - require.using("The proposer is a required signer", command.getSigners().contains(input.getProposer().getOwningKey())); - require.using("The proposee is a required signer", command.getSigners().contains(input.getProposee().getOwningKey())); + 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; }); diff --git a/Advanced/negotiation-cordapp/repositories.gradle b/Advanced/negotiation-cordapp/repositories.gradle index 9797c0ea..57d5c424 100644 --- a/Advanced/negotiation-cordapp/repositories.gradle +++ b/Advanced/negotiation-cordapp/repositories.gradle @@ -4,4 +4,28 @@ repositories { maven { url 'https://jitpack.io' } maven { url 'https://download.corda.net/maven/corda-dependencies' } maven { url 'https://repo.gradle.org/gradle/libs-releases' } + + // Repository where the user-reported artifact resides + maven { + url "https://software.r3.com/artifactory/r3-corda-releases" + credentials { + username = findProperty('cordaArtifactoryUsername') ?: System.getenv('CORDA_ARTIFACTORY_USERNAME') + password = findProperty('cordaArtifactoryPassword') ?: System.getenv('CORDA_ARTIFACTORY_PASSWORD') + } + content { + includeGroupByRegex 'com\\.r3(\\..*)?' + } + } + + // Repository for corda-dev artifacts (contains net.corda SNAPSHOTs like corda-shell 4.14-SNAPSHOT) + maven { + url "https://software.r3.com/artifactory/corda-dev" + credentials { + username = findProperty('cordaArtifactoryUsername') ?: System.getenv('CORDA_ARTIFACTORY_USERNAME') + password = findProperty('cordaArtifactoryPassword') ?: System.getenv('CORDA_ARTIFACTORY_PASSWORD') + } + content { + includeGroupByRegex 'net\\.corda(\\..*)?' + } + } } diff --git a/Advanced/negotiation-cordapp/workflows/src/main/java/net/corda/samples/negotiation/flows/ModificationFlow.java b/Advanced/negotiation-cordapp/workflows/src/main/java/net/corda/samples/negotiation/flows/ModificationFlow.java index 2d6e8d39..d9f3d1b4 100644 --- a/Advanced/negotiation-cordapp/workflows/src/main/java/net/corda/samples/negotiation/flows/ModificationFlow.java +++ b/Advanced/negotiation-cordapp/workflows/src/main/java/net/corda/samples/negotiation/flows/ModificationFlow.java @@ -2,6 +2,9 @@ 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; @@ -21,6 +24,9 @@ import java.security.PublicKey; import java.security.SignatureException; import java.util.List; +import java.util.Map; + +import static net.corda.core.internal.verification.AbstractVerifier.logger; public class ModificationFlow { @@ -43,13 +49,28 @@ public SignedTransaction call() throws FlowException { StateAndRef inputStateAndRef = getServiceHub().getVaultService().queryBy(ProposalState.class, inputCriteria).getStates().get(0); ProposalState input = (ProposalState) inputStateAndRef.getState().getData(); - //Creating the output - Party counterparty = (getOurIdentity().equals(input.getProposer()))? input.getProposee() : input.getProposer(); - ProposalState output = new ProposalState(newAmount, input.getBuyer(),input.getSeller(), getOurIdentity(), counterparty, input.getLinearId()); - - //Creating the command - List requiredSigners = ImmutableList.of(input.getProposee().getOwningKey(), input.getProposer().getOwningKey()); - Command command = new Command(new ProposalAndTradeContract.Commands.Modify(), requiredSigners); + // Check if any of the parties have rotated their keys + PartyIdentityResolver resolver = new PartyIdentityResolver(getServiceHub().getIdentityService()); // The identity service must always have the proofs of the own node. d + PartyIdentityResolved buyerKeyResolution = resolver.resolve(input.getBuyer()); + PartyIdentityResolved sellerKeyResolution = resolver.resolve(input.getSeller()); + PartyIdentityResolved proposerKeyResolution = resolver.resolve(input.getProposer()); + PartyIdentityResolved proposeeKeyResolution = resolver.resolve(input.getProposee()); + Map proofMap = PartyIdentityResolver.Companion.generateProofChainMap(buyerKeyResolution, sellerKeyResolution); + if(proofMap.isEmpty()){ + 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. Remove all the old keys from the output state if possible. Otherwise, keep using the old keys. Do not mix old and new keys in the output state, as that would cause the transaction to fail. To swap keys we must ensure the transaction contains a key rotation proof + Party ourIdentityFromInput = (getOurIdentity().equals(proposerKeyResolution.getOriginalOrCurrentParty()))? proposerKeyResolution.getOriginalOrCurrentParty() : proposeeKeyResolution.getOriginalOrCurrentParty(); + Party counterpartyFromInput = (getOurIdentity().equals(proposerKeyResolution.getOriginalOrCurrentParty()))? proposeeKeyResolution.getOriginalOrCurrentParty() : proposerKeyResolution.getOriginalOrCurrentParty(); + + ProposalState output = new ProposalState(newAmount, buyerKeyResolution.getOriginalOrCurrentParty(), sellerKeyResolution.getOriginalOrCurrentParty(), ourIdentityFromInput, counterpartyFromInput, input.getLinearId()); + + //Creating the command. Old keys should not be used as signers, only the new keys + 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(); @@ -62,7 +83,8 @@ public SignedTransaction call() throws FlowException { SignedTransaction partStx = getServiceHub().signInitialTransaction(txBuilder); //Gathering the counterparty's signatures - FlowSession counterpartySession = initiateFlow(counterparty); + Party counterParty = PartyIdentityResolver.Companion.resolveToCurrentParty(counterpartyFromInput, getServiceHub().getIdentityService()); + FlowSession counterpartySession = initiateFlow(counterParty); SignedTransaction fullyStx = subFlow(new CollectSignaturesFlow(partStx, ImmutableList.of(counterpartySession))); //Finalising the transaction @@ -88,7 +110,8 @@ public SignedTransaction call() throws FlowException { protected void checkTransaction(@NotNull SignedTransaction stx) throws FlowException { try { LedgerTransaction ledgerTx = stx.toLedgerTransaction(getServiceHub(), false); - Party proposee = ledgerTx.inputsOfType(ProposalState.class).get(0).getProposee(); + ProposalState input = ledgerTx.inputsOfType(ProposalState.class).get(0); + Party proposee = PartyIdentityResolver.Companion.resolveToCurrentParty(input.getProposee(), getServiceHub().getIdentityService()); if(!proposee.equals(counterpartySession.getCounterparty())){ throw new FlowException("Only the proposee can modify a proposal."); } diff --git a/Advanced/negotiation-cordapp/workflows/src/main/java/net/corda/samples/negotiation/flows/ProposalFlow.java b/Advanced/negotiation-cordapp/workflows/src/main/java/net/corda/samples/negotiation/flows/ProposalFlow.java index 45def7ee..9d1f5b93 100644 --- a/Advanced/negotiation-cordapp/workflows/src/main/java/net/corda/samples/negotiation/flows/ProposalFlow.java +++ b/Advanced/negotiation-cordapp/workflows/src/main/java/net/corda/samples/negotiation/flows/ProposalFlow.java @@ -53,7 +53,7 @@ public UniqueIdentifier call() throws FlowException { // 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")); + final Party notary = getServiceHub().getNetworkMapCache().getNotary(CordaX500Name.parse("O=TestNotaryService, L=London, C=GB")); TransactionBuilder txBuilder = new TransactionBuilder(notary) .addOutputState(output, ProposalAndTradeContract.ID)