mirror of
https://github.com/corda/corda.git
synced 2025-06-13 04:38:19 +00:00
Replace data vending service with SendTransactionFlow (#964)
* WIP - Removed data Vending services, fixed all flow test * * separated out extra data, extra data are sent after the SendTransactionFlow if required * New SendProposalFlow for sending TradeProposal, which contains StateAndRef. * WIP * * removed TradeProposal interface. * changed SendProposalFlow to SendStateAndRefFlow, same for receive side. * fixup after rebase. * * undo changes in .idea folder * * remove unintended changes * * Addressed PR issues * * doc changes * * addressed pr issues * moved ResolveTransactionsFlow to internal * changed FlowLogic<Unit> to FlowLogic<Void?> for java use case * * addressed PR issues * renamed DataVendingFlow in TestUtill to TestDataVendingFlow to avoid name confusion, and moved it to core/test * * removed reference to ResolveTransactionsFlow
This commit is contained in:
@ -9,6 +9,7 @@ import net.corda.core.crypto.DigitalSignature;
|
||||
import net.corda.core.crypto.SecureHash;
|
||||
import net.corda.core.flows.*;
|
||||
import net.corda.core.identity.Party;
|
||||
import net.corda.core.internal.FetchDataFlow;
|
||||
import net.corda.core.node.services.ServiceType;
|
||||
import net.corda.core.node.services.Vault;
|
||||
import net.corda.core.node.services.Vault.Page;
|
||||
@ -23,11 +24,13 @@ import net.corda.core.utilities.UntrustworthyData;
|
||||
import net.corda.testing.contracts.DummyContract;
|
||||
import net.corda.testing.contracts.DummyState;
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.security.PublicKey;
|
||||
import java.security.SignatureException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@ -75,13 +78,15 @@ public class FlowCookbookJava {
|
||||
private static final Step SIGS_GATHERING = new Step("Gathering a transaction's signatures.") {
|
||||
// Wiring up a child progress tracker allows us to see the
|
||||
// subflow's progress steps in our flow's progress tracker.
|
||||
@Override public ProgressTracker childProgressTracker() {
|
||||
@Override
|
||||
public ProgressTracker childProgressTracker() {
|
||||
return CollectSignaturesFlow.Companion.tracker();
|
||||
}
|
||||
};
|
||||
private static final Step VERIFYING_SIGS = new Step("Verifying a transaction's signatures.");
|
||||
private static final Step FINALISATION = new Step("Finalising a transaction.") {
|
||||
@Override public ProgressTracker childProgressTracker() {
|
||||
@Override
|
||||
public ProgressTracker childProgressTracker() {
|
||||
return FinalityFlow.Companion.tracker();
|
||||
}
|
||||
};
|
||||
@ -390,18 +395,35 @@ public class FlowCookbookJava {
|
||||
----------------------------*/
|
||||
progressTracker.setCurrentStep(TX_VERIFICATION);
|
||||
|
||||
// Verifying a transaction will also verify every transaction in
|
||||
// the transaction's dependency chain. So if this was a
|
||||
// transaction we'd received from a counterparty and it had any
|
||||
// dependencies, we'd need to download all of these dependencies
|
||||
// using``ResolveTransactionsFlow`` before verifying it.
|
||||
// Verifying a transaction will also verify every transaction in the transaction's dependency chain, which will require
|
||||
// transaction data access on counterparty's node. The ``SendTransactionFlow`` can be used to automate the sending
|
||||
// and data vending process. The ``SendTransactionFlow`` will listen for data request until the transaction
|
||||
// is resolved and verified on the other side:
|
||||
// DOCSTART 12
|
||||
subFlow(new SendTransactionFlow(counterparty, twiceSignedTx));
|
||||
|
||||
// Optional request verification to further restrict data access.
|
||||
subFlow(new SendTransactionFlow(counterparty, twiceSignedTx){
|
||||
@Override
|
||||
protected void verifyDataRequest(@NotNull FetchDataFlow.Request.Data dataRequest) {
|
||||
// Extra request verification.
|
||||
}
|
||||
});
|
||||
// DOCEND 12
|
||||
|
||||
// We can receive the transaction using ``ReceiveTransactionFlow``,
|
||||
// which will automatically download all the dependencies and verify
|
||||
// the transaction
|
||||
// DOCSTART 13
|
||||
subFlow(new ResolveTransactionsFlow(twiceSignedTx, counterparty));
|
||||
SignedTransaction verifiedTransaction = subFlow(new ReceiveTransactionFlow(counterparty));
|
||||
// DOCEND 13
|
||||
|
||||
// We can also resolve a `StateRef` dependency chain.
|
||||
// We can also send and receive a `StateAndRef` dependency chain and automatically resolve its dependencies.
|
||||
// DOCSTART 14
|
||||
subFlow(new ResolveTransactionsFlow(ImmutableSet.of(ourStateRef.getTxhash()), counterparty));
|
||||
subFlow(new SendStateAndRefFlow(counterparty, dummyStates));
|
||||
|
||||
// On the receive side ...
|
||||
List<StateAndRef<DummyState>> resolvedStateAndRef = subFlow(new ReceiveStateAndRefFlow<DummyState>(counterparty));
|
||||
// DOCEND 14
|
||||
|
||||
// A ``SignedTransaction`` is a pairing of a ``WireTransaction``
|
||||
|
@ -44,12 +44,18 @@ class MyValidatingNotaryFlow(otherSide: Party, service: MyCustomValidatingNotary
|
||||
*/
|
||||
@Suspendable
|
||||
override fun receiveAndVerifyTx(): TransactionParts {
|
||||
val stx = receive<SignedTransaction>(otherSide).unwrap { it }
|
||||
checkSignatures(stx)
|
||||
resolveTransaction(stx)
|
||||
validateTransaction(stx)
|
||||
val wtx = stx.tx
|
||||
return TransactionParts(wtx.id, wtx.inputs, wtx.timeWindow)
|
||||
try {
|
||||
val stx = subFlow(ReceiveTransactionFlow(otherSide, checkSufficientSignatures = false))
|
||||
checkSignatures(stx)
|
||||
val wtx = stx.tx
|
||||
return TransactionParts(wtx.id, wtx.inputs, wtx.timeWindow)
|
||||
} catch (e: Exception) {
|
||||
throw when (e) {
|
||||
is TransactionVerificationException,
|
||||
is SignatureException -> NotaryException(NotaryError.TransactionInvalid(e))
|
||||
else -> e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun processTransaction(stx: SignedTransaction) {
|
||||
@ -63,22 +69,5 @@ class MyValidatingNotaryFlow(otherSide: Party, service: MyCustomValidatingNotary
|
||||
throw NotaryException(NotaryError.TransactionInvalid(e))
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
fun validateTransaction(stx: SignedTransaction) {
|
||||
try {
|
||||
resolveTransaction(stx)
|
||||
stx.verify(serviceHub, false)
|
||||
} catch (e: Exception) {
|
||||
throw when (e) {
|
||||
is TransactionVerificationException,
|
||||
is SignatureException -> NotaryException(NotaryError.TransactionInvalid(e))
|
||||
else -> e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun resolveTransaction(stx: SignedTransaction) = subFlow(ResolveTransactionsFlow(stx, otherSide))
|
||||
}
|
||||
// END 2
|
||||
|
@ -9,6 +9,7 @@ import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.FetchDataFlow
|
||||
import net.corda.core.node.services.ServiceType
|
||||
import net.corda.core.node.services.Vault.Page
|
||||
import net.corda.core.node.services.queryBy
|
||||
@ -378,18 +379,34 @@ object FlowCookbook {
|
||||
---------------------------**/
|
||||
progressTracker.currentStep = TX_VERIFICATION
|
||||
|
||||
// Verifying a transaction will also verify every transaction in
|
||||
// the transaction's dependency chain. So if this was a
|
||||
// transaction we'd received from a counterparty and it had any
|
||||
// dependencies, we'd need to download all of these dependencies
|
||||
// using``ResolveTransactionsFlow`` before verifying it.
|
||||
// Verifying a transaction will also verify every transaction in the transaction's dependency chain, which will require
|
||||
// transaction data access on counterparty's node. The ``SendTransactionFlow`` can be used to automate the sending
|
||||
// and data vending process. The ``SendTransactionFlow`` will listen for data request until the transaction
|
||||
// is resolved and verified on the other side:
|
||||
// DOCSTART 12
|
||||
subFlow(SendTransactionFlow(counterparty, twiceSignedTx))
|
||||
|
||||
// Optional request verification to further restrict data access.
|
||||
subFlow(object :SendTransactionFlow(counterparty, twiceSignedTx){
|
||||
override fun verifyDataRequest(dataRequest: FetchDataFlow.Request.Data) {
|
||||
// Extra request verification.
|
||||
}
|
||||
})
|
||||
// DOCEND 12
|
||||
|
||||
// We can receive the transaction using ``ReceiveTransactionFlow``,
|
||||
// which will automatically download all the dependencies and verify
|
||||
// the transaction
|
||||
// DOCSTART 13
|
||||
subFlow(ResolveTransactionsFlow(twiceSignedTx, counterparty))
|
||||
val verifiedTransaction = subFlow(ReceiveTransactionFlow(counterparty))
|
||||
// DOCEND 13
|
||||
|
||||
// We can also resolve a `StateRef` dependency chain.
|
||||
// We can also send and receive a `StateAndRef` dependency chain and automatically resolve its dependencies.
|
||||
// DOCSTART 14
|
||||
subFlow(ResolveTransactionsFlow(setOf(ourStateRef.txhash), counterparty))
|
||||
subFlow(SendStateAndRefFlow(counterparty, dummyStates))
|
||||
|
||||
// On the receive side ...
|
||||
val resolvedStateAndRef = subFlow(ReceiveStateAndRefFlow<DummyState>(counterparty))
|
||||
// DOCEND 14
|
||||
|
||||
// A ``SignedTransaction`` is a pairing of a ``WireTransaction``
|
||||
|
@ -27,10 +27,6 @@ private data class FxRequest(val tradeId: String,
|
||||
val counterparty: Party,
|
||||
val notary: Party? = null)
|
||||
|
||||
@CordaSerializable
|
||||
private data class FxResponse(val inputs: List<StateAndRef<Cash.State>>,
|
||||
val outputs: List<Cash.State>)
|
||||
|
||||
// DOCSTART 1
|
||||
// This is equivalent to the VaultService.generateSpend
|
||||
// Which is brought here to make the filtering logic more visible in the example
|
||||
@ -69,7 +65,7 @@ private fun gatherOurInputs(serviceHub: ServiceHub,
|
||||
}
|
||||
// DOCEND 1
|
||||
|
||||
private fun prepareOurInputsAndOutputs(serviceHub: ServiceHub, request: FxRequest): FxResponse {
|
||||
private fun prepareOurInputsAndOutputs(serviceHub: ServiceHub, request: FxRequest): Pair<List<StateAndRef<Cash.State>>, List<Cash.State>> {
|
||||
// Create amount with correct issuer details
|
||||
val sellAmount = request.amount
|
||||
|
||||
@ -84,14 +80,15 @@ private fun prepareOurInputsAndOutputs(serviceHub: ServiceHub, request: FxReques
|
||||
// Build and an output state for the counterparty
|
||||
val transferedFundsOutput = Cash.State(sellAmount, request.counterparty)
|
||||
|
||||
if (residual > 0L) {
|
||||
val outputs = if (residual > 0L) {
|
||||
// Build an output state for the residual change back to us
|
||||
val residualAmount = Amount(residual, sellAmount.token)
|
||||
val residualOutput = Cash.State(residualAmount, serviceHub.myInfo.legalIdentity)
|
||||
return FxResponse(inputs, listOf(transferedFundsOutput, residualOutput))
|
||||
listOf(transferedFundsOutput, residualOutput)
|
||||
} else {
|
||||
return FxResponse(inputs, listOf(transferedFundsOutput))
|
||||
listOf(transferedFundsOutput)
|
||||
}
|
||||
return Pair(inputs, outputs)
|
||||
// DOCEND 2
|
||||
}
|
||||
|
||||
@ -119,45 +116,45 @@ class ForeignExchangeFlow(val tradeId: String,
|
||||
} else throw IllegalArgumentException("Our identity must be one of the parties in the trade.")
|
||||
|
||||
// Call the helper method to identify suitable inputs and make the outputs
|
||||
val ourStates = prepareOurInputsAndOutputs(serviceHub, localRequest)
|
||||
val (outInputStates, ourOutputStates) = prepareOurInputsAndOutputs(serviceHub, localRequest)
|
||||
|
||||
// identify the notary for our states
|
||||
val notary = ourStates.inputs.first().state.notary
|
||||
val notary = outInputStates.first().state.notary
|
||||
// ensure request to other side is for a consistent notary
|
||||
val remoteRequestWithNotary = remoteRequest.copy(notary = notary)
|
||||
|
||||
// Send the request to the counterparty to verify and call their version of prepareOurInputsAndOutputs
|
||||
// Then they can return their candidate states
|
||||
val theirStates = sendAndReceive<FxResponse>(remoteRequestWithNotary.owner, remoteRequestWithNotary).unwrap {
|
||||
require(it.inputs.all { it.state.notary == notary }) {
|
||||
send(remoteRequestWithNotary.owner, remoteRequestWithNotary)
|
||||
val theirInputStates = subFlow(ReceiveStateAndRefFlow<Cash.State>(remoteRequestWithNotary.owner))
|
||||
val theirOutputStates = receive<List<Cash.State>>(remoteRequestWithNotary.owner).unwrap {
|
||||
require(theirInputStates.all { it.state.notary == notary }) {
|
||||
"notary of remote states must be same as for our states"
|
||||
}
|
||||
require(it.inputs.all { it.state.data.amount.token == remoteRequestWithNotary.amount.token }) {
|
||||
require(theirInputStates.all { it.state.data.amount.token == remoteRequestWithNotary.amount.token }) {
|
||||
"Inputs not of the correct currency"
|
||||
}
|
||||
require(it.outputs.all { it.amount.token == remoteRequestWithNotary.amount.token }) {
|
||||
require(it.all { it.amount.token == remoteRequestWithNotary.amount.token }) {
|
||||
"Outputs not of the correct currency"
|
||||
}
|
||||
require(it.inputs.map { it.state.data.amount.quantity }.sum()
|
||||
require(theirInputStates.map { it.state.data.amount.quantity }.sum()
|
||||
>= remoteRequestWithNotary.amount.quantity) {
|
||||
"the provided inputs don't provide sufficient funds"
|
||||
}
|
||||
require(it.outputs.filter { it.owner == serviceHub.myInfo.legalIdentity }.
|
||||
require(it.filter { it.owner == serviceHub.myInfo.legalIdentity }.
|
||||
map { it.amount.quantity }.sum() == remoteRequestWithNotary.amount.quantity) {
|
||||
"the provided outputs don't provide the request quantity"
|
||||
}
|
||||
// Download their inputs chains to validate that they are OK
|
||||
val dependencyTxIDs = it.inputs.map { it.ref.txhash }.toSet()
|
||||
subFlow(ResolveTransactionsFlow(dependencyTxIDs, remoteRequestWithNotary.owner))
|
||||
|
||||
it // return validated response
|
||||
}
|
||||
|
||||
// having collated the data create the full transaction.
|
||||
val signedTransaction = buildTradeProposal(ourStates, theirStates)
|
||||
val signedTransaction = buildTradeProposal(outInputStates, ourOutputStates, theirInputStates, theirOutputStates)
|
||||
|
||||
// pass transaction details to the counterparty to revalidate and confirm with a signature
|
||||
val allPartySignedTx = sendAndReceive<DigitalSignature.WithKey>(remoteRequestWithNotary.owner, signedTransaction).unwrap {
|
||||
// Allow otherParty to access our data to resolve the transaction.
|
||||
subFlow(SendTransactionFlow(remoteRequestWithNotary.owner, signedTransaction))
|
||||
val allPartySignedTx = receive<DigitalSignature.WithKey>(remoteRequestWithNotary.owner).unwrap {
|
||||
val withNewSignature = signedTransaction + it
|
||||
// check all signatures are present except the notary
|
||||
withNewSignature.verifySignaturesExcept(withNewSignature.tx.notary!!.owningKey)
|
||||
@ -177,22 +174,25 @@ class ForeignExchangeFlow(val tradeId: String,
|
||||
}
|
||||
|
||||
// DOCSTART 3
|
||||
private fun buildTradeProposal(ourStates: FxResponse, theirStates: FxResponse): SignedTransaction {
|
||||
private fun buildTradeProposal(ourInputStates: List<StateAndRef<Cash.State>>,
|
||||
ourOutputState: List<Cash.State>,
|
||||
theirInputStates: List<StateAndRef<Cash.State>>,
|
||||
theirOutputState: List<Cash.State>): SignedTransaction {
|
||||
// This is the correct way to create a TransactionBuilder,
|
||||
// do not construct directly.
|
||||
// We also set the notary to match the input notary
|
||||
val builder = TransactionBuilder(ourStates.inputs.first().state.notary)
|
||||
val builder = TransactionBuilder(ourInputStates.first().state.notary)
|
||||
|
||||
// Add the move commands and key to indicate all the respective owners and need to sign
|
||||
val ourSigners = ourStates.inputs.map { it.state.data.owner.owningKey }.toSet()
|
||||
val theirSigners = theirStates.inputs.map { it.state.data.owner.owningKey }.toSet()
|
||||
val ourSigners = ourInputStates.map { it.state.data.owner.owningKey }.toSet()
|
||||
val theirSigners = theirInputStates.map { it.state.data.owner.owningKey }.toSet()
|
||||
builder.addCommand(Cash.Commands.Move(), (ourSigners + theirSigners).toList())
|
||||
|
||||
// Build and add the inputs and outputs
|
||||
builder.withItems(*ourStates.inputs.toTypedArray())
|
||||
builder.withItems(*theirStates.inputs.toTypedArray())
|
||||
builder.withItems(*ourStates.outputs.toTypedArray())
|
||||
builder.withItems(*theirStates.outputs.toTypedArray())
|
||||
builder.withItems(*ourInputStates.toTypedArray())
|
||||
builder.withItems(*theirInputStates.toTypedArray())
|
||||
builder.withItems(*ourOutputState.toTypedArray())
|
||||
builder.withItems(*theirOutputState.toTypedArray())
|
||||
|
||||
// We have already validated their response and trust our own data
|
||||
// so we can sign. Note the returned SignedTransaction is still not fully signed
|
||||
@ -228,23 +228,22 @@ class ForeignExchangeRemoteFlow(val source: Party) : FlowLogic<Unit>() {
|
||||
// we will use query manually in the helper function below.
|
||||
// Putting this into a non-suspendable function also prevent issues when
|
||||
// the flow is suspended.
|
||||
val ourResponse = prepareOurInputsAndOutputs(serviceHub, request)
|
||||
val (ourInputState, ourOutputState) = prepareOurInputsAndOutputs(serviceHub, request)
|
||||
|
||||
// Send back our proposed states and await the full transaction to verify
|
||||
val ourKey = serviceHub.keyManagementService.filterMyKeys(ourResponse.inputs.flatMap { it.state.data.participants }.map { it.owningKey }).single()
|
||||
val proposedTrade = sendAndReceive<SignedTransaction>(source, ourResponse).unwrap {
|
||||
val ourKey = serviceHub.keyManagementService.filterMyKeys(ourInputState.flatMap { it.state.data.participants }.map { it.owningKey }).single()
|
||||
// SendStateAndRefFlow allows otherParty to access our transaction data to resolve the transaction.
|
||||
subFlow(SendStateAndRefFlow(source, ourInputState))
|
||||
send(source, ourOutputState)
|
||||
val proposedTrade = subFlow(ReceiveTransactionFlow(source, checkSufficientSignatures = false)).let {
|
||||
val wtx = it.tx
|
||||
// check all signatures are present except our own and the notary
|
||||
it.verifySignaturesExcept(ourKey, wtx.notary!!.owningKey)
|
||||
|
||||
// We need to fetch their complete input states and dependencies so that verify can operate
|
||||
checkDependencies(it)
|
||||
|
||||
// This verifies that the transaction is contract-valid, even though it is missing signatures.
|
||||
// In a full solution there would be states tracking the trade request which
|
||||
// would be included in the transaction and enforce the amounts and tradeId
|
||||
wtx.toLedgerTransaction(serviceHub).verify()
|
||||
|
||||
it // return the SignedTransaction
|
||||
}
|
||||
|
||||
@ -256,12 +255,4 @@ class ForeignExchangeRemoteFlow(val source: Party) : FlowLogic<Unit>() {
|
||||
// N.B. The FinalityProtocol will be responsible for Notarising the SignedTransaction
|
||||
// and broadcasting the result to us.
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun checkDependencies(stx: SignedTransaction) {
|
||||
// Download and check all the transactions that this transaction depends on, but do not check this
|
||||
// transaction itself.
|
||||
val dependencyTxIDs = stx.tx.inputs.map { it.txhash }.toSet()
|
||||
subFlow(ResolveTransactionsFlow(dependencyTxIDs, source))
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user