Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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 All @@ -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);
Expand All @@ -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) {
Expand All @@ -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;

});
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 @@ -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;
Expand All @@ -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 {

Expand All @@ -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<PublicKey> 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<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.");
}

// Creating the output
TradeState output = new TradeState(input.getAmount(), buyerKeyResolution.getOriginalOrCurrentParty(), sellerKeyResolution.getOriginalOrCurrentParty(), input.getLinearId());

// Creating the command
List<PublicKey> 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)));

Expand All @@ -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.");
}
Expand Down
Loading