diff --git a/.gitignore b/.gitignore index f6e29a4216..9afb3bac5d 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,8 @@ lib/dokka.jar buyer seller rate-fix-demo-data +nodeA +nodeB ### JetBrains template # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio diff --git a/core/src/main/kotlin/core/TransactionVerification.kt b/core/src/main/kotlin/core/TransactionVerification.kt index 9950a2fd58..f16a5c577c 100644 --- a/core/src/main/kotlin/core/TransactionVerification.kt +++ b/core/src/main/kotlin/core/TransactionVerification.kt @@ -88,12 +88,32 @@ data class TransactionForVerification(val inStates: List, * Utilities for contract writers to incorporate into their logic. */ + /** + * A set of related inputs and outputs that are connected by some common attributes. An InOutGroup is calculated + * using [groupStates] and is useful for handling cases where a transaction may contain similar but unrelated + * state evolutions, for example, a transaction that moves cash in two different currencies. The numbers must add + * up on both sides of the transaction, but the values must be summed independently per currency. Grouping can + * be used to simplify this logic. + */ data class InOutGroup(val inputs: List, val outputs: List) - // A shortcut to make IDE auto-completion more intuitive for Java users. + /** Simply calls [commands.getTimestampBy] as a shortcut to make code completion more intuitive. */ fun getTimestampBy(timestampingAuthority: Party): TimestampCommand? = commands.getTimestampBy(timestampingAuthority) - // For Java users. + /** + * Given a type and a function that returns a grouping key, associates inputs and outputs together so that they + * can be processed as one. The grouping key is any arbitrary object that can act as a map key (so must implement + * equals and hashCode). + * + * The purpose of this function is to simplify the writing of verification logic for transactions that may contain + * similar but unrelated state evolutions which need to be checked independently. Consider a transaction that + * simultaneously moves both dollars and euros (e.g. is an atomic FX trade). There may be multiple dollar inputs and + * multiple dollar outputs, depending on things like how fragmented the owners wallet is and whether various privacy + * techniques are in use. The quantity of dollars on the output side must sum to the same as on the input side, to + * ensure no money is being lost track of. This summation and checking must be repeated independently for each + * currency. To solve this, you would use groupStates with a type of Cash.State and a selector that returns the + * currency field: the resulting list can then be iterated over to perform the per-currency calculation. + */ fun groupStates(ofType: Class, selector: (T) -> Any): List> { val inputs = inStates.filterIsInstance(ofType) val outputs = outStates.filterIsInstance(ofType) @@ -105,7 +125,7 @@ data class TransactionForVerification(val inStates: List, return groupStatesInternal(inGroups, outGroups) } - // For Kotlin users: this version has nicer syntax and avoids reflection/object creation for the lambda. + /** See the documentation for the reflection-based version of [groupStates] */ inline fun groupStates(selector: (T) -> Any): List> { val inputs = inStates.filterIsInstance() val outputs = outStates.filterIsInstance() diff --git a/src/main/kotlin/core/node/AbstractNode.kt b/src/main/kotlin/core/node/AbstractNode.kt index 338ce50540..d4eb1e826a 100644 --- a/src/main/kotlin/core/node/AbstractNode.kt +++ b/src/main/kotlin/core/node/AbstractNode.kt @@ -72,8 +72,6 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration, lateinit var api: APIServer open fun start(): AbstractNode { - require(timestamperAddress == null || timestamperAddress.advertisedServices.contains(TimestamperService.Type)) - {"Timestamper address must indicate a node that provides timestamping services"} log.info("Node starting up ...") storage = initialiseStorageService(dir) @@ -98,6 +96,10 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration, // given the details, the timestamping node is somewhere else. Otherwise, we do our own timestamping. val tsid = if (timestamperAddress != null) { inNodeTimestampingService = null + require(TimestamperService.Type in timestamperAddress.advertisedServices) { + "Timestamper address must indicate a node that provides timestamping services, actually " + + "has ${timestamperAddress.advertisedServices}" + } timestamperAddress } else { inNodeTimestampingService = NodeTimestamperService(net, storage.myLegalIdentity, storage.myLegalIdentityKey, platformClock) diff --git a/src/main/kotlin/demos/TraderDemo.kt b/src/main/kotlin/demos/TraderDemo.kt index 7e06d7e871..4ba5c54a88 100644 --- a/src/main/kotlin/demos/TraderDemo.kt +++ b/src/main/kotlin/demos/TraderDemo.kt @@ -11,10 +11,11 @@ import core.messaging.SingleMessageRecipient import core.node.Node import core.node.NodeConfiguration import core.node.NodeConfigurationFromConfig -import core.node.services.ArtemisMessagingService import core.node.NodeInfo +import core.node.services.ArtemisMessagingService import core.node.services.NodeAttachmentService import core.node.services.NodeWalletService +import core.node.services.TimestamperService import core.protocols.ProtocolLogic import core.serialization.deserialize import core.utilities.ANSIProgressRenderer @@ -87,7 +88,7 @@ fun main(args: Array) { val addr = HostAndPort.fromString(options.valueOf(timestamperNetAddr)).withDefaultPort(Node.DEFAULT_PORT) val path = Paths.get(options.valueOf(timestamperIdentityFile)) val party = Files.readAllBytes(path).deserialize(includeClassName = true) - NodeInfo(ArtemisMessagingService.makeRecipient(addr), party) + NodeInfo(ArtemisMessagingService.makeRecipient(addr), party, advertisedServices = setOf(TimestamperService.Type)) } else null val node = logElapsedTime("Node startup") { Node(dir, myNetAddr, config, timestamperId).start() }