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..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 @@ -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; @@ -18,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); @@ -42,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) { @@ -62,12 +81,28 @@ 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()); - 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; }); 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/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 2d6e8d39..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 @@ -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,29 +49,96 @@ 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); - - //Building the transaction + // 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."); + } 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 + // Signing the transaction ourselves SignedTransaction partStx = getServiceHub().signInitialTransaction(txBuilder); - //Gathering the counterparty's signatures - FlowSession counterpartySession = initiateFlow(counterparty); + // 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; } @@ -88,7 +161,15 @@ 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 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..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 @@ -53,7 +55,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)