mirror of
https://github.com/corda/corda.git
synced 2024-12-29 09:18:58 +00:00
Simple trade flow for commercial paper
This commit is contained in:
parent
1cb4f56609
commit
e0b684b3ea
@ -0,0 +1,242 @@
|
|||||||
|
package net.corda.ptflows.flows
|
||||||
|
|
||||||
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import net.corda.confidential.IdentitySyncFlow
|
||||||
|
import net.corda.core.contracts.*
|
||||||
|
import net.corda.core.flows.*
|
||||||
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.identity.PartyAndCertificate
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
import net.corda.core.transactions.SignedTransaction
|
||||||
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
|
import net.corda.core.utilities.ProgressTracker
|
||||||
|
import net.corda.core.utilities.seconds
|
||||||
|
import net.corda.core.utilities.unwrap
|
||||||
|
import net.corda.ptflows.contracts.asset.PtCash
|
||||||
|
import net.corda.ptflows.utils.sumCashBy
|
||||||
|
import java.security.PublicKey
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This asset trading flow implements a "delivery vs payment" type swap. It has two parties (B and S for buyer
|
||||||
|
* and seller) and the following steps:
|
||||||
|
*
|
||||||
|
* 1. S sends the [StateAndRef] pointing to what they want to sell to B, along with info about the price they require
|
||||||
|
* B to pay. For example this has probably been agreed on an exchange.
|
||||||
|
* 2. B sends to S a [SignedTransaction] that includes the state as input, B's cash as input, the state with the new
|
||||||
|
* owner key as output, and any change cash as output. It contains a single signature from B but isn't valid because
|
||||||
|
* it lacks a signature from S authorising movement of the asset.
|
||||||
|
* 3. S signs it and commits it to the ledger, notarising it and distributing the final signed transaction back
|
||||||
|
* to B.
|
||||||
|
*
|
||||||
|
* Assuming no malicious termination, they both end the flow being in possession of a valid, signed transaction
|
||||||
|
* that represents an atomic asset swap.
|
||||||
|
*
|
||||||
|
* Note that it's the *seller* who initiates contact with the buyer, not vice-versa as you might imagine.
|
||||||
|
*/
|
||||||
|
object TwoPartyTradeFlow {
|
||||||
|
// TODO: Common elements in multi-party transaction consensus and signing should be refactored into a superclass of this
|
||||||
|
// and [AbstractStateReplacementFlow].
|
||||||
|
|
||||||
|
class UnacceptablePriceException(givenPrice: Amount<Currency>) : FlowException("Unacceptable price: $givenPrice")
|
||||||
|
|
||||||
|
class AssetMismatchException(val expectedTypeName: String, val typeName: String) : FlowException() {
|
||||||
|
override fun toString() = "The submitted asset didn't match the expected type: $expectedTypeName vs $typeName"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This object is serialised to the network and is the first flow message the seller sends to the buyer.
|
||||||
|
*
|
||||||
|
* @param payToIdentity anonymous identity of the seller, for payment to be sent to.
|
||||||
|
*/
|
||||||
|
@CordaSerializable
|
||||||
|
data class SellerTradeInfo(
|
||||||
|
val price: Amount<Currency>,
|
||||||
|
val payToIdentity: PartyAndCertificate
|
||||||
|
)
|
||||||
|
|
||||||
|
open class Seller(private val otherSideSession: FlowSession,
|
||||||
|
private val assetToSell: StateAndRef<OwnableState>,
|
||||||
|
private val price: Amount<Currency>,
|
||||||
|
private val myParty: PartyAndCertificate, // TODO Left because in tests it's used to pass anonymous party.
|
||||||
|
override val progressTracker: ProgressTracker = Seller.tracker()) : FlowLogic<SignedTransaction>() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
object AWAITING_PROPOSAL : ProgressTracker.Step("Awaiting transaction proposal")
|
||||||
|
// DOCSTART 3
|
||||||
|
object VERIFYING_AND_SIGNING : ProgressTracker.Step("Verifying and signing transaction proposal") {
|
||||||
|
override fun childProgressTracker() = SignTransactionFlow.tracker()
|
||||||
|
}
|
||||||
|
// DOCEND 3
|
||||||
|
|
||||||
|
fun tracker() = ProgressTracker(AWAITING_PROPOSAL, VERIFYING_AND_SIGNING)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DOCSTART 4
|
||||||
|
@Suspendable
|
||||||
|
override fun call(): SignedTransaction {
|
||||||
|
progressTracker.currentStep = AWAITING_PROPOSAL
|
||||||
|
// Make the first message we'll send to kick off the flow.
|
||||||
|
val hello = SellerTradeInfo(price, myParty)
|
||||||
|
// What we get back from the other side is a transaction that *might* be valid and acceptable to us,
|
||||||
|
// but we must check it out thoroughly before we sign!
|
||||||
|
// SendTransactionFlow allows seller to access our data to resolve the transaction.
|
||||||
|
subFlow(SendStateAndRefFlow(otherSideSession, listOf(assetToSell)))
|
||||||
|
otherSideSession.send(hello)
|
||||||
|
|
||||||
|
// Verify and sign the transaction.
|
||||||
|
progressTracker.currentStep = VERIFYING_AND_SIGNING
|
||||||
|
|
||||||
|
// Sync identities to ensure we know all of the identities involved in the transaction we're about to
|
||||||
|
// be asked to sign
|
||||||
|
subFlow(IdentitySyncFlow.Receive(otherSideSession))
|
||||||
|
|
||||||
|
// DOCSTART 5
|
||||||
|
val signTransactionFlow = object : SignTransactionFlow(otherSideSession, VERIFYING_AND_SIGNING.childProgressTracker()) {
|
||||||
|
override fun checkTransaction(stx: SignedTransaction) {
|
||||||
|
// Verify that we know who all the participants in the transaction are
|
||||||
|
val states: Iterable<ContractState> = stx.tx.inputs.map { serviceHub.loadState(it).data } + stx.tx.outputs.map { it.data }
|
||||||
|
states.forEach { state ->
|
||||||
|
state.participants.forEach { anon ->
|
||||||
|
require(serviceHub.identityService.wellKnownPartyFromAnonymous(anon) != null) {
|
||||||
|
"Transaction state $state involves unknown participant $anon"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stx.tx.outputStates.sumCashBy(myParty.party).withoutIssuer() != price)
|
||||||
|
throw FlowException("Transaction is not sending us the right amount of cash")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val txId = subFlow(signTransactionFlow).id
|
||||||
|
// DOCEND 5
|
||||||
|
|
||||||
|
return waitForLedgerCommit(txId)
|
||||||
|
}
|
||||||
|
// DOCEND 4
|
||||||
|
|
||||||
|
// Following comment moved here so that it doesn't appear in the docsite:
|
||||||
|
// There are all sorts of funny games a malicious secondary might play with it sends maybeSTX,
|
||||||
|
// we should fix them:
|
||||||
|
//
|
||||||
|
// - This tx may attempt to send some assets we aren't intending to sell to the secondary, if
|
||||||
|
// we're reusing keys! So don't reuse keys!
|
||||||
|
// - This tx may include output states that impose odd conditions on the movement of the cash,
|
||||||
|
// once we implement state pairing.
|
||||||
|
//
|
||||||
|
// but the goal of this code is not to be fully secure (yet), but rather, just to find good ways to
|
||||||
|
// express flow state machines on top of the messaging layer.
|
||||||
|
}
|
||||||
|
|
||||||
|
open class Buyer(private val sellerSession: FlowSession,
|
||||||
|
private val notary: Party,
|
||||||
|
private val acceptablePrice: Amount<Currency>,
|
||||||
|
private val typeToBuy: Class<out OwnableState>,
|
||||||
|
private val anonymous: Boolean) : FlowLogic<SignedTransaction>() {
|
||||||
|
constructor(otherSideSession: FlowSession, notary: Party, acceptablePrice: Amount<Currency>, typeToBuy: Class<out OwnableState>) :
|
||||||
|
this(otherSideSession, notary, acceptablePrice, typeToBuy, true)
|
||||||
|
// DOCSTART 2
|
||||||
|
object RECEIVING : ProgressTracker.Step("Waiting for seller trading info")
|
||||||
|
|
||||||
|
object VERIFYING : ProgressTracker.Step("Verifying seller assets")
|
||||||
|
object SIGNING : ProgressTracker.Step("Generating and signing transaction proposal")
|
||||||
|
object COLLECTING_SIGNATURES : ProgressTracker.Step("Collecting signatures from other parties") {
|
||||||
|
override fun childProgressTracker() = CollectSignaturesFlow.tracker()
|
||||||
|
}
|
||||||
|
|
||||||
|
object RECORDING : ProgressTracker.Step("Recording completed transaction") {
|
||||||
|
// TODO: Currently triggers a race condition on Team City. See https://github.com/corda/corda/issues/733.
|
||||||
|
// override fun childProgressTracker() = FinalityFlow.tracker()
|
||||||
|
}
|
||||||
|
|
||||||
|
override val progressTracker = ProgressTracker(RECEIVING, VERIFYING, SIGNING, COLLECTING_SIGNATURES, RECORDING)
|
||||||
|
// DOCEND 2
|
||||||
|
|
||||||
|
// DOCSTART 1
|
||||||
|
@Suspendable
|
||||||
|
override fun call(): SignedTransaction {
|
||||||
|
// Wait for a trade request to come in from the other party.
|
||||||
|
progressTracker.currentStep = RECEIVING
|
||||||
|
val (assetForSale, tradeRequest) = receiveAndValidateTradeRequest()
|
||||||
|
|
||||||
|
// Create the identity we'll be paying to, and send the counterparty proof we own the identity
|
||||||
|
val buyerAnonymousIdentity = if (anonymous)
|
||||||
|
serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, false)
|
||||||
|
else
|
||||||
|
ourIdentityAndCert
|
||||||
|
// Put together a proposed transaction that performs the trade, and sign it.
|
||||||
|
progressTracker.currentStep = SIGNING
|
||||||
|
val (ptx, cashSigningPubKeys) = assembleSharedTX(assetForSale, tradeRequest, buyerAnonymousIdentity)
|
||||||
|
|
||||||
|
// Now sign the transaction with whatever keys we need to move the cash.
|
||||||
|
val partSignedTx = serviceHub.signInitialTransaction(ptx, cashSigningPubKeys)
|
||||||
|
|
||||||
|
// Sync up confidential identities in the transaction with our counterparty
|
||||||
|
subFlow(IdentitySyncFlow.Send(sellerSession, ptx.toWireTransaction(serviceHub)))
|
||||||
|
|
||||||
|
// Send the signed transaction to the seller, who must then sign it themselves and commit
|
||||||
|
// it to the ledger by sending it to the notary.
|
||||||
|
progressTracker.currentStep = COLLECTING_SIGNATURES
|
||||||
|
val sellerSignature = subFlow(CollectSignatureFlow(partSignedTx, sellerSession, sellerSession.counterparty.owningKey))
|
||||||
|
val twiceSignedTx = partSignedTx + sellerSignature
|
||||||
|
|
||||||
|
// Notarise and record the transaction.
|
||||||
|
progressTracker.currentStep = RECORDING
|
||||||
|
return subFlow(FinalityFlow(twiceSignedTx))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
private fun receiveAndValidateTradeRequest(): Pair<StateAndRef<OwnableState>, SellerTradeInfo> {
|
||||||
|
val assetForSale = subFlow(ReceiveStateAndRefFlow<OwnableState>(sellerSession)).single()
|
||||||
|
return assetForSale to sellerSession.receive<SellerTradeInfo>().unwrap {
|
||||||
|
progressTracker.currentStep = VERIFYING
|
||||||
|
// What is the seller trying to sell us?
|
||||||
|
val asset = assetForSale.state.data
|
||||||
|
val assetTypeName = asset.javaClass.name
|
||||||
|
|
||||||
|
// The asset must either be owned by the well known identity of the counterparty, or we must be able to
|
||||||
|
// prove the owner is a confidential identity of the counterparty.
|
||||||
|
val assetForSaleIdentity = serviceHub.identityService.wellKnownPartyFromAnonymous(asset.owner)
|
||||||
|
require(assetForSaleIdentity == sellerSession.counterparty)
|
||||||
|
|
||||||
|
// Register the identity we're about to send payment to. This shouldn't be the same as the asset owner
|
||||||
|
// identity, so that anonymity is enforced.
|
||||||
|
val wellKnownPayToIdentity = serviceHub.identityService.verifyAndRegisterIdentity(it.payToIdentity)
|
||||||
|
require(wellKnownPayToIdentity?.party == sellerSession.counterparty) { "Well known identity to pay to must match counterparty identity" }
|
||||||
|
|
||||||
|
if (it.price > acceptablePrice)
|
||||||
|
throw UnacceptablePriceException(it.price)
|
||||||
|
if (!typeToBuy.isInstance(asset))
|
||||||
|
throw AssetMismatchException(typeToBuy.name, assetTypeName)
|
||||||
|
|
||||||
|
it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
private fun assembleSharedTX(assetForSale: StateAndRef<OwnableState>, tradeRequest: SellerTradeInfo, buyerAnonymousIdentity: PartyAndCertificate): SharedTx {
|
||||||
|
val ptx = TransactionBuilder(notary)
|
||||||
|
|
||||||
|
// Add input and output states for the movement of cash, by using the Cash contract to generate the states
|
||||||
|
val (tx, cashSigningPubKeys) = PtCash.generateSpend(serviceHub, ptx, tradeRequest.price, ourIdentityAndCert, tradeRequest.payToIdentity.party)
|
||||||
|
|
||||||
|
// Add inputs/outputs/a command for the movement of the asset.
|
||||||
|
tx.addInputState(assetForSale)
|
||||||
|
|
||||||
|
val (command, state) = assetForSale.state.data.withNewOwner(buyerAnonymousIdentity.party)
|
||||||
|
tx.addOutputState(state, assetForSale.state.contract, assetForSale.state.notary)
|
||||||
|
tx.addCommand(command, assetForSale.state.data.owner.owningKey)
|
||||||
|
|
||||||
|
// We set the transaction's time-window: it may be that none of the contracts need this!
|
||||||
|
// But it can't hurt to have one.
|
||||||
|
val currentTime = serviceHub.clock.instant()
|
||||||
|
tx.setTimeWindow(currentTime, 30.seconds)
|
||||||
|
|
||||||
|
return SharedTx(tx, cashSigningPubKeys)
|
||||||
|
}
|
||||||
|
// DOCEND 1
|
||||||
|
|
||||||
|
data class SharedTx(val tx: TransactionBuilder, val cashSigningPubKeys: List<PublicKey>)
|
||||||
|
}
|
||||||
|
}
|
@ -9,8 +9,8 @@ import net.corda.core.transactions.SignedTransaction
|
|||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.core.utilities.days
|
import net.corda.core.utilities.days
|
||||||
import net.corda.core.utilities.seconds
|
import net.corda.core.utilities.seconds
|
||||||
import net.corda.finance.DOLLARS
|
import net.corda.ptflows.DOLLARS
|
||||||
import net.corda.finance.`issued by`
|
import net.corda.ptflows.`issued by`
|
||||||
import net.corda.ptflows.contracts.asset.*
|
import net.corda.ptflows.contracts.asset.*
|
||||||
import net.corda.testing.*
|
import net.corda.testing.*
|
||||||
import net.corda.ptflows.contracts.asset.fillWithSomeTestCash
|
import net.corda.ptflows.contracts.asset.fillWithSomeTestCash
|
||||||
|
@ -0,0 +1,779 @@
|
|||||||
|
package net.corda.ptflows.contracts.flows
|
||||||
|
|
||||||
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import net.corda.core.concurrent.CordaFuture
|
||||||
|
import net.corda.core.contracts.*
|
||||||
|
import net.corda.core.crypto.*
|
||||||
|
import net.corda.core.flows.*
|
||||||
|
import net.corda.core.identity.AbstractParty
|
||||||
|
import net.corda.core.identity.AnonymousParty
|
||||||
|
import net.corda.core.identity.CordaX500Name
|
||||||
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.internal.FlowStateMachine
|
||||||
|
import net.corda.core.internal.concurrent.map
|
||||||
|
import net.corda.core.internal.rootCause
|
||||||
|
import net.corda.core.messaging.DataFeed
|
||||||
|
import net.corda.core.messaging.SingleMessageRecipient
|
||||||
|
import net.corda.core.messaging.StateMachineTransactionMapping
|
||||||
|
import net.corda.core.node.services.Vault
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||||
|
import net.corda.core.toFuture
|
||||||
|
import net.corda.core.transactions.SignedTransaction
|
||||||
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
|
import net.corda.core.transactions.WireTransaction
|
||||||
|
import net.corda.core.utilities.days
|
||||||
|
import net.corda.core.utilities.getOrThrow
|
||||||
|
import net.corda.core.utilities.toNonEmptySet
|
||||||
|
import net.corda.core.utilities.unwrap
|
||||||
|
import net.corda.ptflows.DOLLARS
|
||||||
|
import net.corda.ptflows.`issued by`
|
||||||
|
import net.corda.ptflows.contracts.PtCommercialPaper
|
||||||
|
import net.corda.ptflows.contracts.asset.CASH
|
||||||
|
import net.corda.ptflows.contracts.asset.PtCash
|
||||||
|
import net.corda.ptflows.contracts.asset.`issued by`
|
||||||
|
import net.corda.ptflows.contracts.asset.`owned by`
|
||||||
|
import net.corda.ptflows.flows.TwoPartyTradeFlow.Buyer
|
||||||
|
import net.corda.ptflows.flows.TwoPartyTradeFlow.Seller
|
||||||
|
import net.corda.node.internal.StartedNode
|
||||||
|
import net.corda.node.services.api.WritableTransactionStorage
|
||||||
|
import net.corda.node.services.config.NodeConfiguration
|
||||||
|
import net.corda.node.services.persistence.DBTransactionStorage
|
||||||
|
import net.corda.node.utilities.CordaPersistence
|
||||||
|
import net.corda.nodeapi.internal.ServiceInfo
|
||||||
|
import net.corda.testing.*
|
||||||
|
import net.corda.ptflows.contracts.asset.fillWithSomeTestCash
|
||||||
|
import net.corda.testing.node.InMemoryMessagingNetwork
|
||||||
|
import net.corda.testing.node.MockNetwork
|
||||||
|
import net.corda.testing.node.pumpReceive
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import rx.Observable
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.math.BigInteger
|
||||||
|
import java.security.KeyPair
|
||||||
|
import java.util.*
|
||||||
|
import java.util.jar.JarOutputStream
|
||||||
|
import java.util.zip.ZipEntry
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In this example, Alice wishes to sell her commercial paper to Bob in return for $1,000,000 and they wish to do
|
||||||
|
* it on the ledger atomically. Therefore they must work together to build a transaction.
|
||||||
|
*
|
||||||
|
* We assume that Alice and Bob already found each other via some market, and have agreed the details already.
|
||||||
|
*/
|
||||||
|
class TwoPartyTradeFlowTests {
|
||||||
|
private lateinit var mockNet: MockNetwork
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun before() {
|
||||||
|
setCordappPackages("net.corda.ptflows.contracts")
|
||||||
|
LogHelper.setLevel("platform.trade", "core.contract.TransactionGroup", "recordingmap")
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun after() {
|
||||||
|
mockNet.stopNodes()
|
||||||
|
LogHelper.reset("platform.trade", "core.contract.TransactionGroup", "recordingmap")
|
||||||
|
unsetCordappPackages()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `trade cash for commercial paper`() {
|
||||||
|
// We run this in parallel threads to help catch any race conditions that may exist. The other tests
|
||||||
|
// we run in the unit test thread exclusively to speed things up, ensure deterministic results and
|
||||||
|
// allow interruption half way through.
|
||||||
|
mockNet = MockNetwork(false, true)
|
||||||
|
|
||||||
|
ledger(initialiseSerialization = false) {
|
||||||
|
val basketOfNodes = mockNet.createSomeNodes(3)
|
||||||
|
val notaryNode = basketOfNodes.notaryNode
|
||||||
|
val aliceNode = basketOfNodes.partyNodes[0]
|
||||||
|
val bobNode = basketOfNodes.partyNodes[1]
|
||||||
|
val bankNode = basketOfNodes.partyNodes[2]
|
||||||
|
val cashIssuer = bankNode.info.chooseIdentity().ref(1)
|
||||||
|
val cpIssuer = bankNode.info.chooseIdentity().ref(1, 2, 3)
|
||||||
|
val notary = aliceNode.services.getDefaultNotary()
|
||||||
|
|
||||||
|
|
||||||
|
aliceNode.internals.disableDBCloseOnStop()
|
||||||
|
bobNode.internals.disableDBCloseOnStop()
|
||||||
|
|
||||||
|
bobNode.database.transaction {
|
||||||
|
bobNode.services.fillWithSomeTestCash(2000.DOLLARS, bankNode.services, outputNotary = notary,
|
||||||
|
issuedBy = cashIssuer)
|
||||||
|
}
|
||||||
|
|
||||||
|
val alicesFakePaper = aliceNode.database.transaction {
|
||||||
|
fillUpForSeller(false, cpIssuer, aliceNode.info.chooseIdentity(),
|
||||||
|
1200.DOLLARS `issued by` bankNode.info.chooseIdentity().ref(0), null, notary).second
|
||||||
|
}
|
||||||
|
|
||||||
|
insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode)
|
||||||
|
|
||||||
|
val (bobStateMachine, aliceResult) = runBuyerAndSeller(notary, aliceNode, bobNode,
|
||||||
|
"alice's paper".outputStateAndRef())
|
||||||
|
|
||||||
|
// TODO: Verify that the result was inserted into the transaction database.
|
||||||
|
// assertEquals(bobResult.get(), aliceNode.storage.validatedTransactions[aliceResult.get().id])
|
||||||
|
assertEquals(aliceResult.getOrThrow(), bobStateMachine.getOrThrow().resultFuture.getOrThrow())
|
||||||
|
|
||||||
|
aliceNode.dispose()
|
||||||
|
bobNode.dispose()
|
||||||
|
|
||||||
|
// aliceNode.database.transaction {
|
||||||
|
// assertThat(aliceNode.checkpointStorage.checkpoints()).isEmpty()
|
||||||
|
// }
|
||||||
|
aliceNode.internals.manuallyCloseDB()
|
||||||
|
// bobNode.database.transaction {
|
||||||
|
// assertThat(bobNode.checkpointStorage.checkpoints()).isEmpty()
|
||||||
|
// }
|
||||||
|
bobNode.internals.manuallyCloseDB()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = InsufficientBalanceException::class)
|
||||||
|
fun `trade cash for commercial paper fails using soft locking`() {
|
||||||
|
mockNet = MockNetwork(false, true)
|
||||||
|
|
||||||
|
ledger(initialiseSerialization = false) {
|
||||||
|
val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name)
|
||||||
|
val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name)
|
||||||
|
val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name)
|
||||||
|
val bankNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name)
|
||||||
|
val issuer = bankNode.info.chooseIdentity().ref(1)
|
||||||
|
val notary = aliceNode.services.getDefaultNotary()
|
||||||
|
|
||||||
|
aliceNode.internals.disableDBCloseOnStop()
|
||||||
|
bobNode.internals.disableDBCloseOnStop()
|
||||||
|
|
||||||
|
val cashStates = bobNode.database.transaction {
|
||||||
|
bobNode.services.fillWithSomeTestCash(2000.DOLLARS, bankNode.services, notary, 3, 3,
|
||||||
|
issuedBy = issuer)
|
||||||
|
}
|
||||||
|
|
||||||
|
val alicesFakePaper = aliceNode.database.transaction {
|
||||||
|
fillUpForSeller(false, issuer, aliceNode.info.chooseIdentity(),
|
||||||
|
1200.DOLLARS `issued by` bankNode.info.chooseIdentity().ref(0), null, notary).second
|
||||||
|
}
|
||||||
|
|
||||||
|
insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode)
|
||||||
|
|
||||||
|
val cashLockId = UUID.randomUUID()
|
||||||
|
bobNode.database.transaction {
|
||||||
|
// lock the cash states with an arbitrary lockId (to prevent the Buyer flow from claiming the states)
|
||||||
|
val refs = cashStates.states.map { it.ref }
|
||||||
|
if (refs.isNotEmpty()) {
|
||||||
|
bobNode.services.vaultService.softLockReserve(cashLockId, refs.toNonEmptySet())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val (bobStateMachine, aliceResult) = runBuyerAndSeller(notary, aliceNode, bobNode,
|
||||||
|
"alice's paper".outputStateAndRef())
|
||||||
|
|
||||||
|
assertEquals(aliceResult.getOrThrow(), bobStateMachine.getOrThrow().resultFuture.getOrThrow())
|
||||||
|
|
||||||
|
aliceNode.dispose()
|
||||||
|
bobNode.dispose()
|
||||||
|
|
||||||
|
// aliceNode.database.transaction {
|
||||||
|
// assertThat(aliceNode.checkpointStorage.checkpoints()).isEmpty()
|
||||||
|
// }
|
||||||
|
aliceNode.internals.manuallyCloseDB()
|
||||||
|
// bobNode.database.transaction {
|
||||||
|
// assertThat(bobNode.checkpointStorage.checkpoints()).isEmpty()
|
||||||
|
// }
|
||||||
|
bobNode.internals.manuallyCloseDB()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `shutdown and restore`() {
|
||||||
|
mockNet = MockNetwork(false)
|
||||||
|
ledger(initialiseSerialization = false) {
|
||||||
|
val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name)
|
||||||
|
val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name)
|
||||||
|
var bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name)
|
||||||
|
val bankNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name)
|
||||||
|
val issuer = bankNode.info.chooseIdentity().ref(1, 2, 3)
|
||||||
|
|
||||||
|
// Let the nodes know about each other - normally the network map would handle this
|
||||||
|
mockNet.registerIdentities()
|
||||||
|
|
||||||
|
aliceNode.database.transaction {
|
||||||
|
aliceNode.services.identityService.verifyAndRegisterIdentity(bobNode.info.chooseIdentityAndCert())
|
||||||
|
}
|
||||||
|
bobNode.database.transaction {
|
||||||
|
bobNode.services.identityService.verifyAndRegisterIdentity(aliceNode.info.chooseIdentityAndCert())
|
||||||
|
}
|
||||||
|
aliceNode.internals.disableDBCloseOnStop()
|
||||||
|
bobNode.internals.disableDBCloseOnStop()
|
||||||
|
|
||||||
|
val bobAddr = bobNode.network.myAddress as InMemoryMessagingNetwork.PeerHandle
|
||||||
|
val networkMapAddress = notaryNode.network.myAddress
|
||||||
|
|
||||||
|
mockNet.runNetwork() // Clear network map registration messages
|
||||||
|
val notary = aliceNode.services.getDefaultNotary()
|
||||||
|
|
||||||
|
bobNode.database.transaction {
|
||||||
|
bobNode.services.fillWithSomeTestCash(2000.DOLLARS, bankNode.services, outputNotary = notary,
|
||||||
|
issuedBy = issuer)
|
||||||
|
}
|
||||||
|
val alicesFakePaper = aliceNode.database.transaction {
|
||||||
|
fillUpForSeller(false, issuer, aliceNode.info.chooseIdentity(),
|
||||||
|
1200.DOLLARS `issued by` bankNode.info.chooseIdentity().ref(0), null, notary).second
|
||||||
|
}
|
||||||
|
insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode)
|
||||||
|
val aliceFuture = runBuyerAndSeller(notary, aliceNode, bobNode, "alice's paper".outputStateAndRef()).sellerResult
|
||||||
|
|
||||||
|
// Everything is on this thread so we can now step through the flow one step at a time.
|
||||||
|
// Seller Alice already sent a message to Buyer Bob. Pump once:
|
||||||
|
bobNode.pumpReceive()
|
||||||
|
|
||||||
|
// Bob sends a couple of queries for the dependencies back to Alice. Alice reponds.
|
||||||
|
aliceNode.pumpReceive()
|
||||||
|
bobNode.pumpReceive()
|
||||||
|
aliceNode.pumpReceive()
|
||||||
|
bobNode.pumpReceive()
|
||||||
|
aliceNode.pumpReceive()
|
||||||
|
bobNode.pumpReceive()
|
||||||
|
|
||||||
|
// // OK, now Bob has sent the partial transaction back to Alice and is waiting for Alice's signature.
|
||||||
|
// bobNode.database.transaction {
|
||||||
|
// assertThat(bobNode.checkpointStorage.checkpoints()).hasSize(1)
|
||||||
|
// }
|
||||||
|
|
||||||
|
val storage = bobNode.services.validatedTransactions
|
||||||
|
val bobTransactionsBeforeCrash = bobNode.database.transaction {
|
||||||
|
(storage as DBTransactionStorage).transactions
|
||||||
|
}
|
||||||
|
assertThat(bobTransactionsBeforeCrash).isNotEmpty
|
||||||
|
|
||||||
|
// .. and let's imagine that Bob's computer has a power cut. He now has nothing now beyond what was on disk.
|
||||||
|
bobNode.dispose()
|
||||||
|
|
||||||
|
// Alice doesn't know that and carries on: she wants to know about the cash transactions he's trying to use.
|
||||||
|
// She will wait around until Bob comes back.
|
||||||
|
assertThat(aliceNode.pumpReceive()).isNotNull()
|
||||||
|
|
||||||
|
// FIXME: Knowledge of confidential identities is lost on node shutdown, so Bob's node now refuses to sign the
|
||||||
|
// transaction because it has no idea who the parties are.
|
||||||
|
|
||||||
|
// ... bring the node back up ... the act of constructing the SMM will re-register the message handlers
|
||||||
|
// that Bob was waiting on before the reboot occurred.
|
||||||
|
bobNode = mockNet.createNode(networkMapAddress, bobAddr.id, object : MockNetwork.Factory<MockNetwork.MockNode> {
|
||||||
|
override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?,
|
||||||
|
advertisedServices: Set<ServiceInfo>, id: Int, overrideServices: Map<ServiceInfo, KeyPair>?,
|
||||||
|
entropyRoot: BigInteger): MockNetwork.MockNode {
|
||||||
|
return MockNetwork.MockNode(config, network, networkMapAddr, advertisedServices, bobAddr.id, overrideServices, entropyRoot)
|
||||||
|
}
|
||||||
|
}, BOB.name)
|
||||||
|
|
||||||
|
// Find the future representing the result of this state machine again.
|
||||||
|
val bobFuture = bobNode.smm.findStateMachines(BuyerAcceptor::class.java).single().second
|
||||||
|
|
||||||
|
// And off we go again.
|
||||||
|
mockNet.runNetwork()
|
||||||
|
|
||||||
|
// Bob is now finished and has the same transaction as Alice.
|
||||||
|
assertThat(bobFuture.getOrThrow()).isEqualTo(aliceFuture.getOrThrow())
|
||||||
|
|
||||||
|
assertThat(bobNode.smm.findStateMachines(Buyer::class.java)).isEmpty()
|
||||||
|
// bobNode.database.transaction {
|
||||||
|
// assertThat(bobNode.checkpointStorage.checkpoints()).isEmpty()
|
||||||
|
// }
|
||||||
|
// aliceNode.database.transaction {
|
||||||
|
// assertThat(aliceNode.checkpointStorage.checkpoints()).isEmpty()
|
||||||
|
// }
|
||||||
|
|
||||||
|
bobNode.database.transaction {
|
||||||
|
val restoredBobTransactions = bobTransactionsBeforeCrash.filter {
|
||||||
|
bobNode.services.validatedTransactions.getTransaction(it.id) != null
|
||||||
|
}
|
||||||
|
assertThat(restoredBobTransactions).containsAll(bobTransactionsBeforeCrash)
|
||||||
|
}
|
||||||
|
|
||||||
|
aliceNode.internals.manuallyCloseDB()
|
||||||
|
bobNode.internals.manuallyCloseDB()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a mock node with an overridden storage service that uses a RecordingMap, that lets us test the order
|
||||||
|
// of gets and puts.
|
||||||
|
private fun makeNodeWithTracking(
|
||||||
|
networkMapAddress: SingleMessageRecipient?,
|
||||||
|
name: CordaX500Name): StartedNode<MockNetwork.MockNode> {
|
||||||
|
// Create a node in the mock network ...
|
||||||
|
return mockNet.createNode(networkMapAddress, nodeFactory = object : MockNetwork.Factory<MockNetwork.MockNode> {
|
||||||
|
override fun create(config: NodeConfiguration,
|
||||||
|
network: MockNetwork,
|
||||||
|
networkMapAddr: SingleMessageRecipient?,
|
||||||
|
advertisedServices: Set<ServiceInfo>, id: Int,
|
||||||
|
overrideServices: Map<ServiceInfo, KeyPair>?,
|
||||||
|
entropyRoot: BigInteger): MockNetwork.MockNode {
|
||||||
|
return object : MockNetwork.MockNode(config, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) {
|
||||||
|
// That constructs a recording tx storage
|
||||||
|
override fun makeTransactionStorage(): WritableTransactionStorage {
|
||||||
|
return RecordingTransactionStorage(database, super.makeTransactionStorage())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, legalName = name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `check dependencies of sale asset are resolved`() {
|
||||||
|
mockNet = MockNetwork(false)
|
||||||
|
|
||||||
|
val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name)
|
||||||
|
val aliceNode = makeNodeWithTracking(notaryNode.network.myAddress, ALICE.name)
|
||||||
|
val bobNode = makeNodeWithTracking(notaryNode.network.myAddress, BOB.name)
|
||||||
|
val bankNode = makeNodeWithTracking(notaryNode.network.myAddress, BOC.name)
|
||||||
|
val issuer = bankNode.info.chooseIdentity().ref(1, 2, 3)
|
||||||
|
mockNet.runNetwork()
|
||||||
|
notaryNode.internals.ensureRegistered()
|
||||||
|
val notary = aliceNode.services.getDefaultNotary()
|
||||||
|
|
||||||
|
mockNet.registerIdentities()
|
||||||
|
|
||||||
|
ledger(aliceNode.services, initialiseSerialization = false) {
|
||||||
|
|
||||||
|
// Insert a prospectus type attachment into the commercial paper transaction.
|
||||||
|
val stream = ByteArrayOutputStream()
|
||||||
|
JarOutputStream(stream).use {
|
||||||
|
it.putNextEntry(ZipEntry("Prospectus.txt"))
|
||||||
|
it.write("Our commercial paper is top notch stuff".toByteArray())
|
||||||
|
it.closeEntry()
|
||||||
|
}
|
||||||
|
val attachmentID = aliceNode.database.transaction {
|
||||||
|
attachment(ByteArrayInputStream(stream.toByteArray()))
|
||||||
|
}
|
||||||
|
|
||||||
|
val bobsFakeCash = bobNode.database.transaction {
|
||||||
|
fillUpForBuyer(false, issuer, AnonymousParty(bobNode.info.chooseIdentity().owningKey), notary)
|
||||||
|
}.second
|
||||||
|
val bobsSignedTxns = insertFakeTransactions(bobsFakeCash, bobNode, notaryNode, bankNode)
|
||||||
|
val alicesFakePaper = aliceNode.database.transaction {
|
||||||
|
fillUpForSeller(false, issuer, aliceNode.info.chooseIdentity(),
|
||||||
|
1200.DOLLARS `issued by` bankNode.info.chooseIdentity().ref(0), attachmentID, notary).second
|
||||||
|
}
|
||||||
|
val alicesSignedTxns = insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode)
|
||||||
|
|
||||||
|
mockNet.runNetwork() // Clear network map registration messages
|
||||||
|
|
||||||
|
runBuyerAndSeller(notary, aliceNode, bobNode, "alice's paper".outputStateAndRef())
|
||||||
|
|
||||||
|
mockNet.runNetwork()
|
||||||
|
|
||||||
|
run {
|
||||||
|
val records = (bobNode.services.validatedTransactions as RecordingTransactionStorage).records
|
||||||
|
// Check Bobs's database accesses as Bob's cash transactions are downloaded by Alice.
|
||||||
|
records.expectEvents(isStrict = false) {
|
||||||
|
sequence(
|
||||||
|
// Buyer Bob is told about Alice's commercial paper, but doesn't know it ..
|
||||||
|
expect(TxRecord.Get(alicesFakePaper[0].id)),
|
||||||
|
// He asks and gets the tx, validates it, sees it's a self issue with no dependencies, stores.
|
||||||
|
expect(TxRecord.Add(alicesSignedTxns.values.first())),
|
||||||
|
// Alice gets Bob's proposed transaction and doesn't know his two cash states. She asks, Bob answers.
|
||||||
|
expect(TxRecord.Get(bobsFakeCash[1].id)),
|
||||||
|
expect(TxRecord.Get(bobsFakeCash[2].id)),
|
||||||
|
// Alice notices that Bob's cash txns depend on a third tx she also doesn't know. She asks, Bob answers.
|
||||||
|
expect(TxRecord.Get(bobsFakeCash[0].id))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bob has downloaded the attachment.
|
||||||
|
bobNode.database.transaction {
|
||||||
|
bobNode.services.attachments.openAttachment(attachmentID)!!.openAsJAR().use {
|
||||||
|
it.nextJarEntry
|
||||||
|
val contents = it.reader().readText()
|
||||||
|
assertTrue(contents.contains("Our commercial paper is top notch stuff"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// And from Alice's perspective ...
|
||||||
|
run {
|
||||||
|
val records = (aliceNode.services.validatedTransactions as RecordingTransactionStorage).records
|
||||||
|
records.expectEvents(isStrict = false) {
|
||||||
|
sequence(
|
||||||
|
// Seller Alice sends her seller info to Bob, who wants to check the asset for sale.
|
||||||
|
// He requests, Alice looks up in her DB to send the tx to Bob
|
||||||
|
expect(TxRecord.Get(alicesFakePaper[0].id)),
|
||||||
|
// Seller Alice gets a proposed tx which depends on Bob's two cash txns and her own tx.
|
||||||
|
expect(TxRecord.Get(bobsFakeCash[1].id)),
|
||||||
|
expect(TxRecord.Get(bobsFakeCash[2].id)),
|
||||||
|
expect(TxRecord.Get(alicesFakePaper[0].id)),
|
||||||
|
// Alice notices that Bob's cash txns depend on a third tx she also doesn't know.
|
||||||
|
expect(TxRecord.Get(bobsFakeCash[0].id)),
|
||||||
|
// Bob answers with the transactions that are now all verifiable, as Alice bottomed out.
|
||||||
|
// Bob's transactions are valid, so she commits to the database
|
||||||
|
expect(TxRecord.Add(bobsSignedTxns[bobsFakeCash[0].id]!!)),
|
||||||
|
expect(TxRecord.Get(bobsFakeCash[0].id)), // Verify
|
||||||
|
expect(TxRecord.Add(bobsSignedTxns[bobsFakeCash[2].id]!!)),
|
||||||
|
expect(TxRecord.Get(bobsFakeCash[0].id)), // Verify
|
||||||
|
expect(TxRecord.Add(bobsSignedTxns[bobsFakeCash[1].id]!!)),
|
||||||
|
// Now she verifies the transaction is contract-valid (not signature valid) which means
|
||||||
|
// looking up the states again.
|
||||||
|
expect(TxRecord.Get(bobsFakeCash[1].id)),
|
||||||
|
expect(TxRecord.Get(bobsFakeCash[2].id)),
|
||||||
|
expect(TxRecord.Get(alicesFakePaper[0].id)),
|
||||||
|
// Alice needs to look up the input states to find out which Notary they point to
|
||||||
|
expect(TxRecord.Get(bobsFakeCash[1].id)),
|
||||||
|
expect(TxRecord.Get(bobsFakeCash[2].id)),
|
||||||
|
expect(TxRecord.Get(alicesFakePaper[0].id))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `track works`() {
|
||||||
|
mockNet = MockNetwork(false)
|
||||||
|
|
||||||
|
val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name)
|
||||||
|
val aliceNode = makeNodeWithTracking(notaryNode.network.myAddress, ALICE.name)
|
||||||
|
val bobNode = makeNodeWithTracking(notaryNode.network.myAddress, BOB.name)
|
||||||
|
val bankNode = makeNodeWithTracking(notaryNode.network.myAddress, BOC.name)
|
||||||
|
val issuer = bankNode.info.chooseIdentity().ref(1, 2, 3)
|
||||||
|
|
||||||
|
mockNet.runNetwork()
|
||||||
|
notaryNode.internals.ensureRegistered()
|
||||||
|
val notary = aliceNode.services.getDefaultNotary()
|
||||||
|
|
||||||
|
mockNet.registerIdentities()
|
||||||
|
|
||||||
|
ledger(aliceNode.services, initialiseSerialization = false) {
|
||||||
|
// Insert a prospectus type attachment into the commercial paper transaction.
|
||||||
|
val stream = ByteArrayOutputStream()
|
||||||
|
JarOutputStream(stream).use {
|
||||||
|
it.putNextEntry(ZipEntry("Prospectus.txt"))
|
||||||
|
it.write("Our commercial paper is top notch stuff".toByteArray())
|
||||||
|
it.closeEntry()
|
||||||
|
}
|
||||||
|
val attachmentID = aliceNode.database.transaction {
|
||||||
|
attachment(ByteArrayInputStream(stream.toByteArray()))
|
||||||
|
}
|
||||||
|
|
||||||
|
val bobsKey = bobNode.services.keyManagementService.keys.single()
|
||||||
|
val bobsFakeCash = bobNode.database.transaction {
|
||||||
|
fillUpForBuyer(false, issuer, AnonymousParty(bobsKey), notary)
|
||||||
|
}.second
|
||||||
|
insertFakeTransactions(bobsFakeCash, bobNode, notaryNode, bankNode)
|
||||||
|
|
||||||
|
val alicesFakePaper = aliceNode.database.transaction {
|
||||||
|
fillUpForSeller(false, issuer, aliceNode.info.chooseIdentity(),
|
||||||
|
1200.DOLLARS `issued by` bankNode.info.chooseIdentity().ref(0), attachmentID, notary).second
|
||||||
|
}
|
||||||
|
|
||||||
|
insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode)
|
||||||
|
|
||||||
|
mockNet.runNetwork() // Clear network map registration messages
|
||||||
|
|
||||||
|
val aliceTxStream = aliceNode.services.validatedTransactions.track().updates
|
||||||
|
val aliceTxMappings = with(aliceNode) {
|
||||||
|
database.transaction { services.stateMachineRecordedTransactionMapping.track().updates }
|
||||||
|
}
|
||||||
|
val aliceSmId = runBuyerAndSeller(notary, aliceNode, bobNode,
|
||||||
|
"alice's paper".outputStateAndRef()).sellerId
|
||||||
|
|
||||||
|
mockNet.runNetwork()
|
||||||
|
|
||||||
|
// We need to declare this here, if we do it inside [expectEvents] kotlin throws an internal compiler error(!).
|
||||||
|
val aliceTxExpectations = sequence(
|
||||||
|
expect { tx: SignedTransaction ->
|
||||||
|
require(tx.id == bobsFakeCash[0].id)
|
||||||
|
},
|
||||||
|
expect { tx: SignedTransaction ->
|
||||||
|
require(tx.id == bobsFakeCash[2].id)
|
||||||
|
},
|
||||||
|
expect { tx: SignedTransaction ->
|
||||||
|
require(tx.id == bobsFakeCash[1].id)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
aliceTxStream.expectEvents { aliceTxExpectations }
|
||||||
|
val aliceMappingExpectations = sequence(
|
||||||
|
expect<StateMachineTransactionMapping> { (stateMachineRunId, transactionId) ->
|
||||||
|
require(stateMachineRunId == aliceSmId)
|
||||||
|
require(transactionId == bobsFakeCash[0].id)
|
||||||
|
},
|
||||||
|
expect { (stateMachineRunId, transactionId) ->
|
||||||
|
require(stateMachineRunId == aliceSmId)
|
||||||
|
require(transactionId == bobsFakeCash[2].id)
|
||||||
|
},
|
||||||
|
expect { (stateMachineRunId, transactionId) ->
|
||||||
|
require(stateMachineRunId == aliceSmId)
|
||||||
|
require(transactionId == bobsFakeCash[1].id)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
aliceTxMappings.expectEvents { aliceMappingExpectations }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `dependency with error on buyer side`() {
|
||||||
|
mockNet = MockNetwork(false)
|
||||||
|
ledger(initialiseSerialization = false) {
|
||||||
|
runWithError(true, false, "at least one cash input")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `dependency with error on seller side`() {
|
||||||
|
mockNet = MockNetwork(false)
|
||||||
|
ledger(initialiseSerialization = false) {
|
||||||
|
runWithError(false, true, "Issuances have a time-window")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class RunResult(
|
||||||
|
// The buyer is not created immediately, only when the seller starts running
|
||||||
|
val buyer: CordaFuture<FlowStateMachine<*>>,
|
||||||
|
val sellerResult: CordaFuture<SignedTransaction>,
|
||||||
|
val sellerId: StateMachineRunId
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun runBuyerAndSeller(notary: Party,
|
||||||
|
sellerNode: StartedNode<MockNetwork.MockNode>,
|
||||||
|
buyerNode: StartedNode<MockNetwork.MockNode>,
|
||||||
|
assetToSell: StateAndRef<OwnableState>,
|
||||||
|
anonymous: Boolean = true): RunResult {
|
||||||
|
val buyerFlows: Observable<out FlowLogic<*>> = buyerNode.internals.registerInitiatedFlow(BuyerAcceptor::class.java)
|
||||||
|
val firstBuyerFiber = buyerFlows.toFuture().map { it.stateMachine }
|
||||||
|
val seller = SellerInitiator(buyerNode.info.chooseIdentity(), notary, assetToSell, 1000.DOLLARS, anonymous)
|
||||||
|
val sellerResult = sellerNode.services.startFlow(seller).resultFuture
|
||||||
|
return RunResult(firstBuyerFiber, sellerResult, seller.stateMachine.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@InitiatingFlow
|
||||||
|
class SellerInitiator(private val buyer: Party,
|
||||||
|
private val notary: Party,
|
||||||
|
private val assetToSell: StateAndRef<OwnableState>,
|
||||||
|
private val price: Amount<Currency>,
|
||||||
|
private val anonymous: Boolean) : FlowLogic<SignedTransaction>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call(): SignedTransaction {
|
||||||
|
val myPartyAndCert = if (anonymous) {
|
||||||
|
serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, false)
|
||||||
|
} else {
|
||||||
|
ourIdentityAndCert
|
||||||
|
}
|
||||||
|
val buyerSession = initiateFlow(buyer)
|
||||||
|
buyerSession.send(TestTx(notary, price, anonymous))
|
||||||
|
return subFlow(Seller(
|
||||||
|
buyerSession,
|
||||||
|
assetToSell,
|
||||||
|
price,
|
||||||
|
myPartyAndCert))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@InitiatedBy(SellerInitiator::class)
|
||||||
|
class BuyerAcceptor(private val sellerSession: FlowSession) : FlowLogic<SignedTransaction>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call(): SignedTransaction {
|
||||||
|
val (notary, price, anonymous) = sellerSession.receive<TestTx>().unwrap {
|
||||||
|
require(serviceHub.networkMapCache.isNotary(it.notaryIdentity)) { "${it.notaryIdentity} is not a notary" }
|
||||||
|
it
|
||||||
|
}
|
||||||
|
return subFlow(Buyer(sellerSession, notary, price, PtCommercialPaper.State::class.java, anonymous))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@CordaSerializable
|
||||||
|
data class TestTx(val notaryIdentity: Party, val price: Amount<Currency>, val anonymous: Boolean)
|
||||||
|
|
||||||
|
private fun LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.runWithError(
|
||||||
|
bobError: Boolean,
|
||||||
|
aliceError: Boolean,
|
||||||
|
expectedMessageSubstring: String
|
||||||
|
) {
|
||||||
|
val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name)
|
||||||
|
val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name)
|
||||||
|
val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name)
|
||||||
|
val bankNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name)
|
||||||
|
val issuer = bankNode.info.chooseIdentity().ref(1, 2, 3)
|
||||||
|
|
||||||
|
mockNet.runNetwork()
|
||||||
|
notaryNode.internals.ensureRegistered()
|
||||||
|
val notary = aliceNode.services.getDefaultNotary()
|
||||||
|
|
||||||
|
// Let the nodes know about each other - normally the network map would handle this
|
||||||
|
mockNet.registerIdentities()
|
||||||
|
|
||||||
|
val bobsBadCash = bobNode.database.transaction {
|
||||||
|
fillUpForBuyer(bobError, issuer, bobNode.info.chooseIdentity(),
|
||||||
|
notary).second
|
||||||
|
}
|
||||||
|
val alicesFakePaper = aliceNode.database.transaction {
|
||||||
|
fillUpForSeller(aliceError, issuer, aliceNode.info.chooseIdentity(),
|
||||||
|
1200.DOLLARS `issued by` issuer, null, notary).second
|
||||||
|
}
|
||||||
|
|
||||||
|
insertFakeTransactions(bobsBadCash, bobNode, notaryNode, bankNode)
|
||||||
|
insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode)
|
||||||
|
|
||||||
|
mockNet.runNetwork() // Clear network map registration messages
|
||||||
|
|
||||||
|
val (bobStateMachine, aliceResult) = runBuyerAndSeller(notary, aliceNode, bobNode, "alice's paper".outputStateAndRef())
|
||||||
|
|
||||||
|
mockNet.runNetwork()
|
||||||
|
|
||||||
|
val e = assertFailsWith<TransactionVerificationException> {
|
||||||
|
if (bobError)
|
||||||
|
aliceResult.getOrThrow()
|
||||||
|
else
|
||||||
|
bobStateMachine.getOrThrow().resultFuture.getOrThrow()
|
||||||
|
}
|
||||||
|
val underlyingMessage = e.rootCause.message!!
|
||||||
|
if (expectedMessageSubstring !in underlyingMessage) {
|
||||||
|
assertEquals(expectedMessageSubstring, underlyingMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun insertFakeTransactions(
|
||||||
|
wtxToSign: List<WireTransaction>,
|
||||||
|
node: StartedNode<*>,
|
||||||
|
notaryNode: StartedNode<*>,
|
||||||
|
vararg extraSigningNodes: StartedNode<*>): Map<SecureHash, SignedTransaction> {
|
||||||
|
|
||||||
|
val signed = wtxToSign.map {
|
||||||
|
val id = it.id
|
||||||
|
val sigs = mutableListOf<TransactionSignature>()
|
||||||
|
val nodeKey = node.info.chooseIdentity().owningKey
|
||||||
|
sigs.add(node.services.keyManagementService.sign(SignableData(id, SignatureMetadata(1, Crypto.findSignatureScheme(nodeKey).schemeNumberID)), nodeKey))
|
||||||
|
sigs.add(notaryNode.services.keyManagementService.sign(SignableData(id, SignatureMetadata(1,
|
||||||
|
Crypto.findSignatureScheme(notaryNode.info.legalIdentities[1].owningKey).schemeNumberID)), notaryNode.info.legalIdentities[1].owningKey))
|
||||||
|
extraSigningNodes.forEach { currentNode ->
|
||||||
|
sigs.add(currentNode.services.keyManagementService.sign(
|
||||||
|
SignableData(id, SignatureMetadata(1, Crypto.findSignatureScheme(currentNode.info.chooseIdentity().owningKey).schemeNumberID)),
|
||||||
|
currentNode.info.chooseIdentity().owningKey)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SignedTransaction(it, sigs)
|
||||||
|
}
|
||||||
|
return node.database.transaction {
|
||||||
|
node.services.recordTransactions(signed)
|
||||||
|
val validatedTransactions = node.services.validatedTransactions
|
||||||
|
if (validatedTransactions is RecordingTransactionStorage) {
|
||||||
|
validatedTransactions.records.clear()
|
||||||
|
}
|
||||||
|
signed.associateBy { it.id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.fillUpForBuyer(
|
||||||
|
withError: Boolean,
|
||||||
|
issuer: PartyAndReference,
|
||||||
|
owner: AbstractParty,
|
||||||
|
notary: Party): Pair<Vault<ContractState>, List<WireTransaction>> {
|
||||||
|
val interimOwner = issuer.party
|
||||||
|
// Bob (Buyer) has some cash he got from the Bank of Elbonia, Alice (Seller) has some commercial paper she
|
||||||
|
// wants to sell to Bob.
|
||||||
|
val eb1 = transaction(transactionBuilder = TransactionBuilder(notary = notary)) {
|
||||||
|
// Issued money to itself.
|
||||||
|
output(PtCash.PROGRAM_ID, "elbonian money 1", notary = notary) { 800.DOLLARS.CASH `issued by` issuer `owned by` interimOwner }
|
||||||
|
output(PtCash.PROGRAM_ID, "elbonian money 2", notary = notary) { 1000.DOLLARS.CASH `issued by` issuer `owned by` interimOwner }
|
||||||
|
if (!withError) {
|
||||||
|
command(issuer.party.owningKey) { PtCash.Commands.Issue() }
|
||||||
|
} else {
|
||||||
|
// Put a broken command on so at least a signature is created
|
||||||
|
command(issuer.party.owningKey) { PtCash.Commands.Move() }
|
||||||
|
}
|
||||||
|
timeWindow(TEST_TX_TIME)
|
||||||
|
if (withError) {
|
||||||
|
this.fails()
|
||||||
|
} else {
|
||||||
|
this.verifies()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bob gets some cash onto the ledger from BoE
|
||||||
|
val bc1 = transaction(transactionBuilder = TransactionBuilder(notary = notary)) {
|
||||||
|
input("elbonian money 1")
|
||||||
|
output(PtCash.PROGRAM_ID, "bob cash 1", notary = notary) { 800.DOLLARS.CASH `issued by` issuer `owned by` owner }
|
||||||
|
command(interimOwner.owningKey) { PtCash.Commands.Move() }
|
||||||
|
this.verifies()
|
||||||
|
}
|
||||||
|
|
||||||
|
val bc2 = transaction(transactionBuilder = TransactionBuilder(notary = notary)) {
|
||||||
|
input("elbonian money 2")
|
||||||
|
output(PtCash.PROGRAM_ID, "bob cash 2", notary = notary) { 300.DOLLARS.CASH `issued by` issuer `owned by` owner }
|
||||||
|
output(PtCash.PROGRAM_ID, notary = notary) { 700.DOLLARS.CASH `issued by` issuer `owned by` interimOwner } // Change output.
|
||||||
|
command(interimOwner.owningKey) { PtCash.Commands.Move() }
|
||||||
|
this.verifies()
|
||||||
|
}
|
||||||
|
|
||||||
|
val vault = Vault<ContractState>(listOf("bob cash 1".outputStateAndRef(), "bob cash 2".outputStateAndRef()))
|
||||||
|
return Pair(vault, listOf(eb1, bc1, bc2))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.fillUpForSeller(
|
||||||
|
withError: Boolean,
|
||||||
|
issuer: PartyAndReference,
|
||||||
|
owner: AbstractParty,
|
||||||
|
amount: Amount<Issued<Currency>>,
|
||||||
|
attachmentID: SecureHash?,
|
||||||
|
notary: Party): Pair<Vault<ContractState>, List<WireTransaction>> {
|
||||||
|
val ap = transaction(transactionBuilder = TransactionBuilder(notary = notary)) {
|
||||||
|
output(PtCommercialPaper.CP_PROGRAM_ID, "alice's paper", notary = notary) {
|
||||||
|
PtCommercialPaper.State(issuer, owner, amount, TEST_TX_TIME + 7.days)
|
||||||
|
}
|
||||||
|
command(issuer.party.owningKey) { PtCommercialPaper.Commands.Issue() }
|
||||||
|
if (!withError)
|
||||||
|
timeWindow(time = TEST_TX_TIME)
|
||||||
|
if (attachmentID != null)
|
||||||
|
attachment(attachmentID)
|
||||||
|
if (withError) {
|
||||||
|
this.fails()
|
||||||
|
} else {
|
||||||
|
this.verifies()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val vault = Vault<ContractState>(listOf("alice's paper".outputStateAndRef()))
|
||||||
|
return Pair(vault, listOf(ap))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RecordingTransactionStorage(val database: CordaPersistence, val delegate: WritableTransactionStorage) : WritableTransactionStorage, SingletonSerializeAsToken() {
|
||||||
|
override fun track(): DataFeed<List<SignedTransaction>, SignedTransaction> {
|
||||||
|
return database.transaction {
|
||||||
|
delegate.track()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val records: MutableList<TxRecord> = Collections.synchronizedList(ArrayList<TxRecord>())
|
||||||
|
override val updates: Observable<SignedTransaction>
|
||||||
|
get() = delegate.updates
|
||||||
|
|
||||||
|
override fun addTransaction(transaction: SignedTransaction): Boolean {
|
||||||
|
database.transaction {
|
||||||
|
records.add(TxRecord.Add(transaction))
|
||||||
|
delegate.addTransaction(transaction)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTransaction(id: SecureHash): SignedTransaction? {
|
||||||
|
return database.transaction {
|
||||||
|
records.add(TxRecord.Get(id))
|
||||||
|
delegate.getTransaction(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TxRecord {
|
||||||
|
data class Add(val transaction: SignedTransaction) : TxRecord
|
||||||
|
data class Get(val id: SecureHash) : TxRecord
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user