From 34300df3ff6a01d30cd393727d6c235f599738be Mon Sep 17 00:00:00 2001 From: Filipe Oliveira Date: Tue, 31 Mar 2026 11:07:14 +0100 Subject: [PATCH 1/6] 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) From 4abdc7f3c429cbc8e27037cd1be7672a99dfdbd7 Mon Sep 17 00:00:00 2001 From: Filipe Oliveira Date: Tue, 7 Apr 2026 15:28:40 +0100 Subject: [PATCH 2/6] Fixed typo. --- .../samples/negotiation/contracts/ProposalAndTradeContract.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 59502f4b..ce4c54ae 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 @@ -66,7 +66,7 @@ public void verify(LedgerTransaction tx) throws IllegalArgumentException { require.using("The amount is unmodified in the output", output.getAmount() != input.getAmount()); 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 seller is unmodified in the output", resolver.isSameParty(input.getSeller(), output.getSeller())); 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())); From 716ce38684044ac190242761d12c92240f2f4237 Mon Sep 17 00:00:00 2001 From: Filipe Oliveira Date: Mon, 20 Apr 2026 13:14:51 +0100 Subject: [PATCH 3/6] Update. --- .../net/corda/samples/negotiation/flows/ModificationFlow.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 d9f3d1b4..e451f0c7 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 @@ -83,8 +83,7 @@ public SignedTransaction call() throws FlowException { SignedTransaction partStx = getServiceHub().signInitialTransaction(txBuilder); //Gathering the counterparty's signatures - Party counterParty = PartyIdentityResolver.Companion.resolveToCurrentParty(counterpartyFromInput, getServiceHub().getIdentityService()); - FlowSession counterpartySession = initiateFlow(counterParty); + FlowSession counterpartySession = initiateFlow(counterpartyFromInput); SignedTransaction fullyStx = subFlow(new CollectSignaturesFlow(partStx, ImmutableList.of(counterpartySession))); //Finalising the transaction From a2fd7342d9195ef5ec57c54f5886f96808f6d977 Mon Sep 17 00:00:00 2001 From: Filipe Oliveira Date: Thu, 30 Apr 2026 11:39:34 +0100 Subject: [PATCH 4/6] Added comments explaining the code changes. --- .../contracts/ProposalAndTradeContract.java | 15 ++++ .../negotiation/flows/ModificationFlow.java | 75 +++++++++++++++++-- 2 files changed, 82 insertions(+), 8 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 ce4c54ae..78fc442e 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 @@ -62,14 +62,29 @@ public void verify(LedgerTransaction tx) throws IllegalArgumentException { 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; }); 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 e451f0c7..5c6fe7e0 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 @@ -49,12 +49,39 @@ public SignedTransaction call() throws FlowException { StateAndRef inputStateAndRef = getServiceHub().getVaultService().queryBy(ProposalState.class, inputCriteria).getStates().get(0); ProposalState input = (ProposalState) inputStateAndRef.getState().getData(); - // 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 + // 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. Map proofMap = PartyIdentityResolver.Companion.generateProofChainMap(buyerKeyResolution, sellerKeyResolution); if(proofMap.isEmpty()){ logger.info("No proof."); @@ -62,31 +89,56 @@ public SignedTransaction call() throws FlowException { 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 + // 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 the command. Old keys should not be used as signers, only the new keys + // 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 + // 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 + // Signing the transaction ourselves SignedTransaction partStx = getServiceHub().signInitialTransaction(txBuilder); - //Gathering the counterparty's signatures + // 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 + // Finalising the transaction SignedTransaction finalTx = subFlow(new FinalityFlow(fullyStx,ImmutableList.of(counterpartySession))); return finalTx; } @@ -110,6 +162,13 @@ protected void checkTransaction(@NotNull SignedTransaction stx) throws FlowExcep 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."); From ec9abebf589c3d24865b8b3db167b9db337c7435 Mon Sep 17 00:00:00 2001 From: Filipe Oliveira Date: Thu, 30 Apr 2026 11:49:47 +0100 Subject: [PATCH 5/6] Added commentss about the `ProposalFlow`. --- .../samples/negotiation/contracts/ProposalAndTradeContract.java | 2 ++ .../java/net/corda/samples/negotiation/flows/ProposalFlow.java | 2 ++ 2 files changed, 4 insertions(+) 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 78fc442e..3304e25e 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 @@ -19,6 +19,8 @@ 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); 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 9d1f5b93..988fcdee 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 @@ -17,6 +17,8 @@ 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 From 4c4e25fc7f46d9e3120226e66be189979d34705f Mon Sep 17 00:00:00 2001 From: Filipe Oliveira Date: Thu, 30 Apr 2026 13:43:08 +0100 Subject: [PATCH 6/6] Updated the acceptance flow. --- .../contracts/ProposalAndTradeContract.java | 24 ++++++-- .../negotiation/flows/AcceptanceFlow.java | 61 +++++++++++++++---- .../negotiation/flows/ModificationFlow.java | 4 +- 3 files changed, 71 insertions(+), 18 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 3304e25e..15499149 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 @@ -45,12 +45,28 @@ public void verify(LedgerTransaction tx) throws IllegalArgumentException { 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()); - 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 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())); + // 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) { diff --git a/Advanced/negotiation-cordapp/workflows/src/main/java/net/corda/samples/negotiation/flows/AcceptanceFlow.java b/Advanced/negotiation-cordapp/workflows/src/main/java/net/corda/samples/negotiation/flows/AcceptanceFlow.java index 62426e2c..9c054498 100644 --- a/Advanced/negotiation-cordapp/workflows/src/main/java/net/corda/samples/negotiation/flows/AcceptanceFlow.java +++ b/Advanced/negotiation-cordapp/workflows/src/main/java/net/corda/samples/negotiation/flows/AcceptanceFlow.java @@ -3,6 +3,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.samples.negotiation.states.TradeState; @@ -23,6 +26,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 AcceptanceFlow { @@ -47,25 +53,47 @@ public SignedTransaction call() throws FlowException { ProposalState input = (ProposalState) inputStateAndRef.getState().getData(); - //Creating the output - TradeState output = new TradeState(input.getAmount(), input.getBuyer(), input.getSeller(), input.getLinearId()); - - //Creating the command - List requiredSigners = ImmutableList.of(input.getProposee().getOwningKey(), input.getProposer().getOwningKey()); - Command command = new Command(new ProposalAndTradeContract.Commands.Accept(), requiredSigners); - - //Building the transaction + // 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()); + + 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 + 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 + // Signing the transaction ourselves SignedTransaction partStx = getServiceHub().signInitialTransaction(txBuilder); - //Gathering the counterparty's signature - Party counterparty = (getOurIdentity().equals(input.getProposer()))? input.getProposee() : input.getProposer(); + // 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))); @@ -92,7 +120,16 @@ 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); + + // 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."); } 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 5c6fe7e0..7bcb2afa 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 @@ -97,8 +97,8 @@ public SignedTransaction call() throws FlowException { // // 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(); + 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. //