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:
Patrick Kuo
2017-08-04 11:26:31 +01:00
committed by GitHub
parent 014387162d
commit 56fda1e5b5
35 changed files with 581 additions and 424 deletions

View File

@ -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``

View File

@ -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

View File

@ -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``

View File

@ -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))
}
}