mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
Refactor the trader demo and add comments to make it easier to customise.
This commit is contained in:
parent
719e0ad9f2
commit
5e70646bd2
@ -19,14 +19,14 @@ Trader demo
|
||||
|
||||
Open two terminals, and in the first run:::
|
||||
|
||||
gradle installDist && ./build/install/r3prototyping/trader-demo.sh --mode=buyer
|
||||
gradle installDist && ./build/install/r3prototyping/trader-demo.sh --role=BUYER
|
||||
|
||||
It will compile things, if necessary, then create a directory named trader-demo/buyer with a bunch of files inside and
|
||||
start the node. You should see it waiting for a trade to begin.
|
||||
|
||||
In the second terminal, run::
|
||||
|
||||
./build/install/r3prototyping/trader-demo.sh --mode=seller
|
||||
./build/install/r3prototyping/trader-demo.sh --role=SELLER
|
||||
|
||||
You should see some log lines scroll past, and within a few seconds the messages "Purchase complete - we are a
|
||||
happy customer!" and "Sale completed - we have a happy customer!" should be printed.
|
||||
|
@ -30,6 +30,7 @@ import core.utilities.BriefLogFormatter
|
||||
import core.utilities.Emoji
|
||||
import core.utilities.ProgressTracker
|
||||
import joptsimple.OptionParser
|
||||
import joptsimple.OptionSet
|
||||
import protocols.NotaryProtocol
|
||||
import protocols.TwoPartyTradeProtocol
|
||||
import java.nio.file.Files
|
||||
@ -43,94 +44,163 @@ import kotlin.test.assertEquals
|
||||
// TRADING DEMO
|
||||
//
|
||||
// Please see docs/build/html/running-the-trading-demo.html
|
||||
//
|
||||
// This program is a simple driver for exercising the two party trading protocol. Until Corda has a unified node server
|
||||
// programs like this are required to wire up the pieces and run a demo scenario end to end.
|
||||
//
|
||||
// If you are creating a new scenario, you can use this program as a template for creating your own driver. Make sure to
|
||||
// copy/paste the right parts of the build.gradle file to make sure it gets a script to run it deposited in
|
||||
// build/install/r3prototyping/bin
|
||||
//
|
||||
// In this scenario, a buyer wants to purchase some commercial paper by swapping his cash for the CP. The seller learns
|
||||
// that the buyer exists, and sends them a message to kick off the trade. The seller, having obtained his CP, then quits
|
||||
// and the buyer goes back to waiting. The buyer will sell as much CP as he can!
|
||||
//
|
||||
// The different roles in the scenario this program can adopt are:
|
||||
|
||||
enum class Role {
|
||||
BUYER,
|
||||
SELLER
|
||||
}
|
||||
|
||||
// And this is the directory under the current working directory where each node will create its own server directory,
|
||||
// which holds things like checkpoints, keys, databases, message logs etc.
|
||||
val DIRNAME = "trader-demo"
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
val parser = OptionParser()
|
||||
|
||||
val modeArg = parser.accepts("mode").withRequiredArg().required()
|
||||
val roleArg = parser.accepts("role").withRequiredArg().ofType(Role::class.java).required()
|
||||
val myNetworkAddress = parser.accepts("network-address").withRequiredArg().defaultsTo("localhost")
|
||||
val theirNetworkAddress = parser.accepts("other-network-address").withRequiredArg().defaultsTo("localhost")
|
||||
|
||||
val options = try {
|
||||
parser.parse(*args)
|
||||
} catch (e: Exception) {
|
||||
println(e.message)
|
||||
printHelp()
|
||||
exitProcess(1)
|
||||
}
|
||||
val options = parseOptions(args, parser)
|
||||
val role = options.valueOf(roleArg)!!
|
||||
|
||||
val mode = options.valueOf(modeArg)
|
||||
|
||||
val DIRNAME = "trader-demo"
|
||||
val BUYER = "buyer"
|
||||
val SELLER = "seller"
|
||||
|
||||
if (mode !in setOf(BUYER, SELLER)) {
|
||||
printHelp()
|
||||
exitProcess(1)
|
||||
}
|
||||
val myNetAddr = HostAndPort.fromString(options.valueOf(myNetworkAddress)).withDefaultPort(
|
||||
when (role) {
|
||||
Role.BUYER -> 31337
|
||||
Role.SELLER -> 31340
|
||||
}
|
||||
)
|
||||
val theirNetAddr = HostAndPort.fromString(options.valueOf(theirNetworkAddress)).withDefaultPort(
|
||||
when (role) {
|
||||
Role.BUYER -> 31340
|
||||
Role.SELLER -> 31337
|
||||
}
|
||||
)
|
||||
|
||||
// Suppress the Artemis MQ noise, and activate the demo logging.
|
||||
//
|
||||
// The first two strings correspond to the first argument to StateMachineManager.add() but the way we handle logging
|
||||
// for protocols will change in future.
|
||||
BriefLogFormatter.initVerbose("+demo.buyer", "+demo.seller", "-org.apache.activemq")
|
||||
|
||||
val dir = Paths.get(DIRNAME, mode)
|
||||
Files.createDirectories(dir)
|
||||
val directory = setupDirectory(role)
|
||||
|
||||
val advertisedServices: Set<ServiceType>
|
||||
val myNetAddr = HostAndPort.fromString(options.valueOf(myNetworkAddress)).withDefaultPort(if (mode == BUYER) Node.DEFAULT_PORT else 31340)
|
||||
val theirNetAddr = HostAndPort.fromString(options.valueOf(theirNetworkAddress)).withDefaultPort(if (mode == SELLER) Node.DEFAULT_PORT else 31340)
|
||||
|
||||
val listening = mode == BUYER
|
||||
// Override the default config file (which you can find in the file "reference.conf") to give each node a name.
|
||||
val config = run {
|
||||
val override = ConfigFactory.parseString("""myLegalName = ${ if (mode == BUYER) "Bank A" else "Bank B" }""")
|
||||
val myLegalName = when (role) {
|
||||
Role.BUYER -> "Bank A"
|
||||
Role.SELLER -> "Bank B"
|
||||
}
|
||||
val override = ConfigFactory.parseString("myLegalName = $myLegalName")
|
||||
NodeConfigurationFromConfig(override.withFallback(ConfigFactory.load()))
|
||||
}
|
||||
|
||||
val networkMapId = if (mode == SELLER) {
|
||||
val path = Paths.get(DIRNAME, BUYER, "identity-public")
|
||||
// Which services will this instance of the node provide to the network?
|
||||
val advertisedServices: Set<ServiceType>
|
||||
|
||||
// One of the two servers needs to run the network map and notary services. In such a trivial two-node network
|
||||
// the map is not very helpful, but we need one anyway. So just make the buyer side run the network map as it's
|
||||
// the side that sticks around waiting for the seller.
|
||||
val networkMapId = if (role == Role.BUYER) {
|
||||
advertisedServices = setOf(NetworkMapService.Type, NotaryService.Type)
|
||||
null
|
||||
} else {
|
||||
// In a real system, the identity file of the network map would be shipped with the server software, and there'd
|
||||
// be a single shared map service (this is analagous to the DNS seeds in Bitcoin).
|
||||
//
|
||||
// TODO: AbstractNode should write out the full NodeInfo object and we should just load it here.
|
||||
val path = Paths.get(DIRNAME, Role.BUYER.name.toLowerCase(), "identity-public")
|
||||
val party = Files.readAllBytes(path).deserialize<Party>()
|
||||
advertisedServices = emptySet()
|
||||
NodeInfo(ArtemisMessagingService.makeRecipient(theirNetAddr), party, setOf(NetworkMapService.Type))
|
||||
} else {
|
||||
// We must be the network map service
|
||||
advertisedServices = setOf(NetworkMapService.Type, NotaryService.Type)
|
||||
null
|
||||
}
|
||||
|
||||
// TODO: Remove this once checkpoint resume works.
|
||||
StateMachineManager.restoreCheckpointsOnStart = false
|
||||
val node = logElapsedTime("Node startup") { Node(dir, myNetAddr, config, networkMapId, advertisedServices).start() }
|
||||
|
||||
if (listening) {
|
||||
// For demo purposes just extract attachment jars when saved to disk, so the user can explore them.
|
||||
// Buyer will fetch the attachment from the seller.
|
||||
val attachmentsPath = (node.storage.attachments as NodeAttachmentService).let {
|
||||
it.automaticallyExtractAttachments = true
|
||||
it.storePath
|
||||
}
|
||||
// And now construct then start the node object. It takes a little while.
|
||||
val node = logElapsedTime("Node startup") {
|
||||
Node(directory, myNetAddr, config, networkMapId, advertisedServices).start()
|
||||
}
|
||||
|
||||
val buyer = TraderDemoProtocolBuyer(attachmentsPath, node.info.identity)
|
||||
ANSIProgressRenderer.progressTracker = buyer.progressTracker
|
||||
node.smm.add("demo.buyer", buyer).get() // This thread will halt forever here.
|
||||
// What happens next depends on the role. The buyer sits around waiting for a trade to start. The seller role
|
||||
// will contact the buyer and actually make something happen.
|
||||
if (role == Role.BUYER) {
|
||||
runBuyer(node)
|
||||
} else {
|
||||
// Make sure we have the transaction prospectus attachment loaded into our store.
|
||||
if (node.storage.attachments.openAttachment(TraderDemoProtocolSeller.PROSPECTUS_HASH) == null) {
|
||||
TraderDemoProtocolSeller::class.java.getResourceAsStream("bank-of-london-cp.jar").use {
|
||||
val id = node.storage.attachments.importAttachment(it)
|
||||
assertEquals(TraderDemoProtocolSeller.PROSPECTUS_HASH, id)
|
||||
}
|
||||
}
|
||||
|
||||
val otherSide = ArtemisMessagingService.makeRecipient(theirNetAddr)
|
||||
val seller = TraderDemoProtocolSeller(myNetAddr, otherSide)
|
||||
ANSIProgressRenderer.progressTracker = seller.progressTracker
|
||||
node.smm.add("demo.seller", seller).get()
|
||||
node.stop()
|
||||
runSeller(myNetAddr, node, theirNetAddr)
|
||||
}
|
||||
}
|
||||
|
||||
fun setupDirectory(mode: Role): Path {
|
||||
val directory = Paths.get(DIRNAME, mode.name.toLowerCase())
|
||||
Files.createDirectories(directory)
|
||||
return directory
|
||||
}
|
||||
|
||||
fun parseOptions(args: Array<String>, parser: OptionParser): OptionSet {
|
||||
try {
|
||||
return parser.parse(*args)
|
||||
} catch (e: Exception) {
|
||||
println(e.message)
|
||||
println("Please refer to the documentation in docs/build/index.html to learn how to run the demo.")
|
||||
exitProcess(1)
|
||||
}
|
||||
}
|
||||
|
||||
fun runSeller(myNetAddr: HostAndPort, node: Node, theirNetAddr: HostAndPort) {
|
||||
// The seller will sell some commercial paper to the buyer, who will pay with (self issued) cash.
|
||||
//
|
||||
// The CP sale transaction comes with a prospectus PDF, which will tag along for the ride in an
|
||||
// attachment. Make sure we have the transaction prospectus attachment loaded into our store.
|
||||
//
|
||||
// This can also be done via an HTTP upload, but here we short-circuit and do it from code.
|
||||
if (node.storage.attachments.openAttachment(TraderDemoProtocolSeller.PROSPECTUS_HASH) == null) {
|
||||
TraderDemoProtocolSeller::class.java.getResourceAsStream("bank-of-london-cp.jar").use {
|
||||
val id = node.storage.attachments.importAttachment(it)
|
||||
assertEquals(TraderDemoProtocolSeller.PROSPECTUS_HASH, id)
|
||||
}
|
||||
}
|
||||
|
||||
val otherSide = ArtemisMessagingService.makeRecipient(theirNetAddr)
|
||||
val seller = TraderDemoProtocolSeller(myNetAddr, otherSide)
|
||||
ANSIProgressRenderer.progressTracker = seller.progressTracker
|
||||
node.smm.add("demo.seller", seller).get()
|
||||
node.stop()
|
||||
}
|
||||
|
||||
fun runBuyer(node: Node) {
|
||||
// Buyer will fetch the attachment from the seller automatically when it resolves the transaction.
|
||||
// For demo purposes just extract attachment jars when saved to disk, so the user can explore them.
|
||||
val attachmentsPath = (node.storage.attachments as NodeAttachmentService).let {
|
||||
it.automaticallyExtractAttachments = true
|
||||
it.storePath
|
||||
}
|
||||
|
||||
// We use a simple scenario-specific wrapper protocol to make things happen.
|
||||
val buyer = TraderDemoProtocolBuyer(attachmentsPath, node.info.identity)
|
||||
ANSIProgressRenderer.progressTracker = buyer.progressTracker
|
||||
// This thread will halt forever here.
|
||||
node.smm.add("demo.buyer", buyer).get()
|
||||
}
|
||||
|
||||
// We create a couple of ad-hoc test protocols that wrap the two party trade protocol, to give us the demo logic.
|
||||
|
||||
val DEMO_TOPIC = "initiate.demo.trade"
|
||||
|
||||
class TraderDemoProtocolBuyer(private val attachmentsPath: Path, val notary: Party) : ProtocolLogic<Unit>() {
|
||||
companion object {
|
||||
object WAITING_FOR_SELLER_TO_CONNECT : ProgressTracker.Step("Waiting for seller to connect to us")
|
||||
@ -142,8 +212,9 @@ class TraderDemoProtocolBuyer(private val attachmentsPath: Path, val notary: Par
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
// Give us some cash. Note that as nodes do not currently track forward pointers, we can spend the same cash over
|
||||
// and over again and the double spends will never be detected! Fixing that is the next step.
|
||||
// Self issue some cash.
|
||||
//
|
||||
// TODO: At some point this demo should be extended to have a central bank node.
|
||||
(serviceHub.walletService as NodeWalletService).fillWithSomeTestCash(notary, 1500.DOLLARS)
|
||||
|
||||
while (true) {
|
||||
@ -151,18 +222,22 @@ class TraderDemoProtocolBuyer(private val attachmentsPath: Path, val notary: Par
|
||||
// via some other system like an exchange or maybe even a manual messaging system like Bloomberg. But for the
|
||||
// next stage in our building site, we will just auto-generate fake trades to give our nodes something to do.
|
||||
//
|
||||
// As the seller initiates the DVP/two-party trade protocol, here, we will be the buyer.
|
||||
// As the seller initiates the two-party trade protocol, here, we will be the buyer.
|
||||
try {
|
||||
progressTracker.currentStep = WAITING_FOR_SELLER_TO_CONNECT
|
||||
val hostname = receive<HostAndPort>("test.junktrade", 0).validate { it.withDefaultPort(Node.DEFAULT_PORT) }
|
||||
val hostname = receive<HostAndPort>(DEMO_TOPIC, 0).validate { it.withDefaultPort(Node.DEFAULT_PORT) }
|
||||
val newPartnerAddr = ArtemisMessagingService.makeRecipient(hostname)
|
||||
|
||||
// The session ID disambiguates the test trade.
|
||||
val sessionID = random63BitValue()
|
||||
progressTracker.currentStep = STARTING_BUY
|
||||
send("test.junktrade", newPartnerAddr, 0, sessionID)
|
||||
send(DEMO_TOPIC, newPartnerAddr, 0, sessionID)
|
||||
|
||||
val notary = serviceHub.networkMapCache.notaryNodes[0]
|
||||
val buyer = TwoPartyTradeProtocol.Buyer(newPartnerAddr, notary.identity, 1000.DOLLARS,
|
||||
CommercialPaper.State::class.java, sessionID)
|
||||
|
||||
// This invokes the trading protocol and out pops our finished transaction.
|
||||
val tradeTX: SignedTransaction = subProtocol(buyer)
|
||||
|
||||
logger.info("Purchase complete - we are a happy customer! Final transaction is: " +
|
||||
@ -217,7 +292,7 @@ class TraderDemoProtocolSeller(val myAddress: HostAndPort,
|
||||
override fun call() {
|
||||
progressTracker.currentStep = ANNOUNCING
|
||||
|
||||
val sessionID = sendAndReceive<Long>("test.junktrade", otherSide, 0, 0, myAddress).validate { it }
|
||||
val sessionID = sendAndReceive<Long>(DEMO_TOPIC, otherSide, 0, 0, myAddress).validate { it }
|
||||
|
||||
progressTracker.currentStep = SELF_ISSUING
|
||||
|
||||
@ -240,7 +315,7 @@ class TraderDemoProtocolSeller(val myAddress: HostAndPort,
|
||||
val keyPair = generateKeyPair()
|
||||
val party = Party("Bank of London", keyPair.public)
|
||||
|
||||
val issuance = run {
|
||||
val issuance: SignedTransaction = run {
|
||||
val tx = CommercialPaper().generateIssue(party.ref(1, 2, 3), 1100.DOLLARS, Instant.now() + 10.days, notaryNode.identity)
|
||||
|
||||
// TODO: Consider moving these two steps below into generateIssue.
|
||||
@ -248,32 +323,35 @@ class TraderDemoProtocolSeller(val myAddress: HostAndPort,
|
||||
// Attach the prospectus.
|
||||
tx.addAttachment(serviceHub.storageService.attachments.openAttachment(PROSPECTUS_HASH)!!)
|
||||
|
||||
// Timestamp it, all CP must be timestamped.
|
||||
// Requesting timestamping, all CP must be timestamped.
|
||||
tx.setTime(Instant.now(), notaryNode.identity, 30.seconds)
|
||||
|
||||
// Sign it as ourselves.
|
||||
tx.signWith(keyPair)
|
||||
|
||||
// Get the notary to sign it, thus committing the outputs.
|
||||
val notarySig = subProtocol(NotaryProtocol(tx.toWireTransaction()))
|
||||
tx.addSignatureUnchecked(notarySig)
|
||||
tx.toSignedTransaction(true)
|
||||
|
||||
// Commit it to local storage.
|
||||
val stx = tx.toSignedTransaction(true)
|
||||
serviceHub.recordTransactions(listOf(stx))
|
||||
|
||||
stx
|
||||
}
|
||||
|
||||
serviceHub.recordTransactions(listOf(issuance))
|
||||
|
||||
val move = run {
|
||||
val tx = TransactionBuilder()
|
||||
CommercialPaper().generateMove(tx, issuance.tx.outRef(0), ownedBy)
|
||||
tx.signWith(keyPair)
|
||||
val notarySig = subProtocol(NotaryProtocol(tx.toWireTransaction()))
|
||||
tx.addSignatureUnchecked(notarySig)
|
||||
tx.toSignedTransaction(true)
|
||||
// Now make a dummy transaction that moves it to a new key, just to show that resolving dependencies works.
|
||||
val move: SignedTransaction = run {
|
||||
val builder = TransactionBuilder()
|
||||
CommercialPaper().generateMove(builder, issuance.tx.outRef(0), ownedBy)
|
||||
builder.signWith(keyPair)
|
||||
builder.addSignatureUnchecked(subProtocol(NotaryProtocol(builder.toWireTransaction())))
|
||||
val tx = builder.toSignedTransaction(true)
|
||||
serviceHub.recordTransactions(listOf(tx))
|
||||
tx
|
||||
}
|
||||
|
||||
serviceHub.recordTransactions(listOf(move))
|
||||
|
||||
return move.tx.outRef(0)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun printHelp() {
|
||||
println("Please refer to the documentation in docs/build/index.html to learn how to run the demo.")
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user