Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -62,12 +63,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;

});
Expand Down
24 changes: 24 additions & 0 deletions Advanced/negotiation-cordapp/repositories.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be revised once the PRs corda/corda#8046 and https://github.com/corda/enterprise/pull/5597 are merged.

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(\\..*)?'
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {

Expand All @@ -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<PublicKey> 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<PublicKey, KeyRotationProofChain> 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<PublicKey> 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;
}
Expand All @@ -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.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This must be reverted.


TransactionBuilder txBuilder = new TransactionBuilder(notary)
.addOutputState(output, ProposalAndTradeContract.ID)
Expand Down