mirror of
https://github.com/corda/corda.git
synced 2025-06-22 09:08:49 +00:00
Capture file moves to separate Node implementation code into its own gradle module and leave only demo code in top level src folders.
I have to temporarily break\disable the IRS demo to which has a circular dependency. Will fix next.
This commit is contained in:
133
node/src/main/kotlin/api/APIServer.kt
Normal file
133
node/src/main/kotlin/api/APIServer.kt
Normal file
@ -0,0 +1,133 @@
|
||||
package api
|
||||
|
||||
import core.contracts.ContractState
|
||||
import core.contracts.SignedTransaction
|
||||
import core.contracts.StateRef
|
||||
import core.contracts.WireTransaction
|
||||
import core.crypto.DigitalSignature
|
||||
import core.crypto.SecureHash
|
||||
import core.serialization.SerializedBytes
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import javax.ws.rs.GET
|
||||
import javax.ws.rs.Path
|
||||
import javax.ws.rs.Produces
|
||||
import javax.ws.rs.core.MediaType
|
||||
|
||||
/**
|
||||
* Top level interface to external interaction with the distributed ledger.
|
||||
*
|
||||
* Wherever a list is returned by a fetchXXX method that corresponds with an input list, that output list will have optional elements
|
||||
* where a null indicates "missing" and the elements returned will be in the order corresponding with the input list.
|
||||
*
|
||||
*/
|
||||
@Path("")
|
||||
interface APIServer {
|
||||
|
||||
/**
|
||||
* Report current UTC time as understood by the platform.
|
||||
*/
|
||||
@GET
|
||||
@Path("servertime")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
fun serverTime(): LocalDateTime
|
||||
|
||||
/**
|
||||
* Query your "local" states (containing only outputs involving you) and return the hashes & indexes associated with them
|
||||
* to probably be later inflated by fetchLedgerTransactions() or fetchStates() although because immutable you can cache them
|
||||
* to avoid calling fetchLedgerTransactions() many times.
|
||||
*
|
||||
* @param query Some "where clause" like expression.
|
||||
* @return Zero or more matching States.
|
||||
*/
|
||||
fun queryStates(query: StatesQuery): List<StateRef>
|
||||
|
||||
fun fetchStates(states: List<StateRef>): Map<StateRef, ContractState?>
|
||||
|
||||
/**
|
||||
* Query for immutable transactions (results can be cached indefinitely by their id/hash).
|
||||
*
|
||||
* @param txs The hashes (from [StateRef.txhash] returned from [queryStates]) you would like full transactions for.
|
||||
* @return null values indicate missing transactions from the requested list.
|
||||
*/
|
||||
fun fetchTransactions(txs: List<SecureHash>): Map<SecureHash, SignedTransaction?>
|
||||
|
||||
/**
|
||||
* TransactionBuildSteps would be invocations of contract.generateXXX() methods that all share a common TransactionBuilder
|
||||
* and a common contract type (e.g. Cash or CommercialPaper)
|
||||
* which would automatically be passed as the first argument (we'd need that to be a criteria/pattern of the generateXXX methods).
|
||||
*/
|
||||
fun buildTransaction(type: ContractDefRef, steps: List<TransactionBuildStep>): SerializedBytes<WireTransaction>
|
||||
|
||||
/**
|
||||
* Generate a signature for this transaction signed by us.
|
||||
*/
|
||||
fun generateTransactionSignature(tx: SerializedBytes<WireTransaction>): DigitalSignature.WithKey
|
||||
|
||||
/**
|
||||
* Attempt to commit transaction (returned from build transaction) with the necessary signatures for that to be
|
||||
* successful, otherwise exception is thrown.
|
||||
*/
|
||||
fun commitTransaction(tx: SerializedBytes<WireTransaction>, signatures: List<DigitalSignature.WithKey>): SecureHash
|
||||
|
||||
/**
|
||||
* This method would not return until the protocol is finished (hence the "Sync").
|
||||
*
|
||||
* Longer term we'd add an Async version that returns some kind of ProtocolInvocationRef that could be queried and
|
||||
* would appear on some kind of event message that is broadcast informing of progress.
|
||||
*
|
||||
* Will throw exception if protocol fails.
|
||||
*/
|
||||
fun invokeProtocolSync(type: ProtocolRef, args: Map<String, Any?>): Any?
|
||||
|
||||
// fun invokeProtocolAsync(type: ProtocolRef, args: Map<String, Any?>): ProtocolInstanceRef
|
||||
|
||||
/**
|
||||
* Fetch protocols that require a response to some prompt/question by a human (on the "bank" side).
|
||||
*/
|
||||
fun fetchProtocolsRequiringAttention(query: StatesQuery): Map<StateRef, ProtocolRequiringAttention>
|
||||
|
||||
/**
|
||||
* Provide the response that a protocol is waiting for.
|
||||
*
|
||||
* @param protocol Should refer to a previously supplied ProtocolRequiringAttention.
|
||||
* @param stepId Which step of the protocol are we referring too.
|
||||
* @param choice Should be one of the choices presented in the ProtocolRequiringAttention.
|
||||
* @param args Any arguments required.
|
||||
*/
|
||||
fun provideProtocolResponse(protocol: ProtocolInstanceRef, choice: SecureHash, args: Map<String, Any?>)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Encapsulates the contract type. e.g. Cash or CommercialPaper etc.
|
||||
*/
|
||||
interface ContractDefRef {
|
||||
|
||||
}
|
||||
|
||||
data class ContractClassRef(val className: String) : ContractDefRef
|
||||
data class ContractLedgerRef(val hash: SecureHash) : ContractDefRef
|
||||
|
||||
|
||||
/**
|
||||
* Encapsulates the protocol to be instantiated. e.g. TwoPartyTradeProtocol.Buyer.
|
||||
*/
|
||||
interface ProtocolRef {
|
||||
|
||||
}
|
||||
|
||||
data class ProtocolClassRef(val className: String) : ProtocolRef
|
||||
|
||||
data class ProtocolInstanceRef(val protocolInstance: SecureHash, val protocolClass: ProtocolClassRef, val protocolStepId: String)
|
||||
|
||||
/**
|
||||
* Thinking that Instant is OK for short lived protocol deadlines.
|
||||
*/
|
||||
data class ProtocolRequiringAttention(val ref: ProtocolInstanceRef, val prompt: String, val choiceIdsToMessages: Map<SecureHash, String>, val dueBy: Instant)
|
||||
|
||||
|
||||
/**
|
||||
* Encapsulate a generateXXX method call on a contract.
|
||||
*/
|
||||
data class TransactionBuildStep(val generateMethodName: String, val args: Map<String, Any?>)
|
112
node/src/main/kotlin/api/APIServerImpl.kt
Normal file
112
node/src/main/kotlin/api/APIServerImpl.kt
Normal file
@ -0,0 +1,112 @@
|
||||
package api
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import core.*
|
||||
import core.contracts.*
|
||||
import core.crypto.DigitalSignature
|
||||
import core.crypto.SecureHash
|
||||
import core.node.AbstractNode
|
||||
import core.node.subsystems.linearHeadsOfType
|
||||
import core.protocols.ProtocolLogic
|
||||
import core.serialization.SerializedBytes
|
||||
import core.utilities.ANSIProgressRenderer
|
||||
import java.time.LocalDateTime
|
||||
import java.util.*
|
||||
import kotlin.reflect.KParameter
|
||||
import kotlin.reflect.jvm.javaType
|
||||
|
||||
class APIServerImpl(val node: AbstractNode) : APIServer {
|
||||
|
||||
override fun serverTime(): LocalDateTime = LocalDateTime.now(node.services.clock)
|
||||
|
||||
override fun queryStates(query: StatesQuery): List<StateRef> {
|
||||
// We're going to hard code two options here for now and assume that all LinearStates are deals
|
||||
// Would like to maybe move to a model where we take something like a JEXL string, although don't want to develop
|
||||
// something we can't later implement against a persistent store (i.e. need to pick / build a query engine)
|
||||
if (query is StatesQuery.Selection) {
|
||||
if (query.criteria is StatesQuery.Criteria.AllDeals) {
|
||||
val states = node.services.walletService.linearHeads
|
||||
return states.values.map { it.ref }
|
||||
} else if (query.criteria is StatesQuery.Criteria.Deal) {
|
||||
val states = node.services.walletService.linearHeadsOfType<DealState>().filterValues {
|
||||
it.state.ref == query.criteria.ref
|
||||
}
|
||||
return states.values.map { it.ref }
|
||||
}
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override fun fetchStates(states: List<StateRef>): Map<StateRef, ContractState?> {
|
||||
return node.services.walletService.statesForRefs(states)
|
||||
}
|
||||
|
||||
override fun fetchTransactions(txs: List<SecureHash>): Map<SecureHash, SignedTransaction?> {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun buildTransaction(type: ContractDefRef, steps: List<TransactionBuildStep>): SerializedBytes<WireTransaction> {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun generateTransactionSignature(tx: SerializedBytes<WireTransaction>): DigitalSignature.WithKey {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun commitTransaction(tx: SerializedBytes<WireTransaction>, signatures: List<DigitalSignature.WithKey>): SecureHash {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun invokeProtocolSync(type: ProtocolRef, args: Map<String, Any?>): Any? {
|
||||
return invokeProtocolAsync(type, args).get()
|
||||
}
|
||||
|
||||
private fun invokeProtocolAsync(type: ProtocolRef, args: Map<String, Any?>): ListenableFuture<out Any?> {
|
||||
if (type is ProtocolClassRef) {
|
||||
val clazz = Class.forName(type.className)
|
||||
if (ProtocolLogic::class.java.isAssignableFrom(clazz)) {
|
||||
// TODO for security, check annotated as exposed on API? Or have PublicProtocolLogic... etc
|
||||
nextConstructor@ for (constructor in clazz.kotlin.constructors) {
|
||||
val params = HashMap<KParameter, Any?>()
|
||||
for (parameter in constructor.parameters) {
|
||||
if (parameter.isOptional && !args.containsKey(parameter.name)) {
|
||||
// OK to be missing
|
||||
} else if (args.containsKey(parameter.name)) {
|
||||
val value = args[parameter.name]
|
||||
if (value is Any) {
|
||||
// TODO consider supporting more complex test here to support coercing numeric/Kotlin types
|
||||
if (!(parameter.type.javaType as Class<*>).isAssignableFrom(value.javaClass)) {
|
||||
// Not null and not assignable
|
||||
break@nextConstructor
|
||||
}
|
||||
} else if (!parameter.type.isMarkedNullable) {
|
||||
// Null and not nullable
|
||||
break@nextConstructor
|
||||
}
|
||||
params[parameter] = value
|
||||
} else {
|
||||
break@nextConstructor
|
||||
}
|
||||
}
|
||||
// If we get here then we matched every parameter
|
||||
val protocol = constructor.callBy(params) as ProtocolLogic<*>
|
||||
ANSIProgressRenderer.progressTracker = protocol.progressTracker
|
||||
val future = node.smm.add("api-call", protocol)
|
||||
return future
|
||||
}
|
||||
}
|
||||
throw UnsupportedOperationException("Could not find matching protocol and constructor for: $type $args")
|
||||
} else {
|
||||
throw UnsupportedOperationException("Unsupported ProtocolRef type: $type")
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchProtocolsRequiringAttention(query: StatesQuery): Map<StateRef, ProtocolRequiringAttention> {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun provideProtocolResponse(protocol: ProtocolInstanceRef, choice: SecureHash, args: Map<String, Any?>) {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
}
|
17
node/src/main/kotlin/api/Config.kt
Normal file
17
node/src/main/kotlin/api/Config.kt
Normal file
@ -0,0 +1,17 @@
|
||||
package api
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import core.node.ServiceHub
|
||||
import core.utilities.JsonSupport
|
||||
import javax.ws.rs.ext.ContextResolver
|
||||
import javax.ws.rs.ext.Provider
|
||||
|
||||
/**
|
||||
* Primary purpose is to install Kotlin extensions for Jackson ObjectMapper so data classes work
|
||||
* and to organise serializers / deserializers for java.time.* classes as necessary
|
||||
*/
|
||||
@Provider
|
||||
class Config(val services: ServiceHub) : ContextResolver<ObjectMapper> {
|
||||
val defaultObjectMapper = JsonSupport.createDefaultMapper(services.identityService)
|
||||
override fun getContext(type: java.lang.Class<*>) = defaultObjectMapper
|
||||
}
|
30
node/src/main/kotlin/api/Query.kt
Normal file
30
node/src/main/kotlin/api/Query.kt
Normal file
@ -0,0 +1,30 @@
|
||||
package api
|
||||
|
||||
/**
|
||||
* Extremely rudimentary query language which should most likely be replaced with a product
|
||||
*/
|
||||
interface StatesQuery {
|
||||
companion object {
|
||||
fun select(criteria: Criteria): Selection {
|
||||
return Selection(criteria)
|
||||
}
|
||||
|
||||
fun selectAllDeals(): Selection {
|
||||
return select(Criteria.AllDeals)
|
||||
}
|
||||
|
||||
fun selectDeal(ref: String): Selection {
|
||||
return select(Criteria.Deal(ref))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TODO make constructors private
|
||||
data class Selection(val criteria: Criteria) : StatesQuery
|
||||
|
||||
interface Criteria {
|
||||
object AllDeals : Criteria
|
||||
|
||||
data class Deal(val ref: String) : Criteria
|
||||
}
|
||||
}
|
33
node/src/main/kotlin/api/ResponseFilter.kt
Normal file
33
node/src/main/kotlin/api/ResponseFilter.kt
Normal file
@ -0,0 +1,33 @@
|
||||
package api
|
||||
|
||||
import javax.ws.rs.container.ContainerRequestContext
|
||||
import javax.ws.rs.container.ContainerResponseContext
|
||||
import javax.ws.rs.container.ContainerResponseFilter
|
||||
import javax.ws.rs.ext.Provider
|
||||
|
||||
/**
|
||||
* This adds headers needed for cross site scripting on API clients
|
||||
*/
|
||||
@Provider
|
||||
class ResponseFilter : ContainerResponseFilter {
|
||||
override fun filter(requestContext: ContainerRequestContext, responseContext: ContainerResponseContext) {
|
||||
val headers = responseContext.headers
|
||||
|
||||
/**
|
||||
* TODO we need to revisit this for security reasons
|
||||
*
|
||||
* We don't want this scriptable from any web page anywhere, but for demo reasons
|
||||
* we're making this really easy to access pending a proper security approach including
|
||||
* access control and authentication at a network and software level
|
||||
*
|
||||
*/
|
||||
headers.add("Access-Control-Allow-Origin", "*")
|
||||
|
||||
if (requestContext.method == "OPTIONS") {
|
||||
headers.add("Access-Control-Allow-Headers", "Content-Type,Accept,Origin")
|
||||
headers.add("Access-Control-Allow-Methods", "POST,PUT,GET,OPTIONS")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
288
node/src/main/kotlin/core/messaging/StateMachineManager.kt
Normal file
288
node/src/main/kotlin/core/messaging/StateMachineManager.kt
Normal file
@ -0,0 +1,288 @@
|
||||
package core.messaging
|
||||
|
||||
import co.paralleluniverse.fibers.Fiber
|
||||
import co.paralleluniverse.fibers.FiberExecutorScheduler
|
||||
import co.paralleluniverse.io.serialization.kryo.KryoSerializer
|
||||
import com.codahale.metrics.Gauge
|
||||
import com.esotericsoftware.kryo.io.Input
|
||||
import com.google.common.base.Throwables
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import core.node.ServiceHub
|
||||
import core.node.storage.Checkpoint
|
||||
import core.protocols.ProtocolLogic
|
||||
import core.protocols.ProtocolStateMachine
|
||||
import core.protocols.ProtocolStateMachineImpl
|
||||
import core.serialization.SerializedBytes
|
||||
import core.serialization.THREAD_LOCAL_KRYO
|
||||
import core.serialization.createKryo
|
||||
import core.serialization.deserialize
|
||||
import core.then
|
||||
import core.utilities.AffinityExecutor
|
||||
import core.utilities.ProgressTracker
|
||||
import core.utilities.trace
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
import java.util.*
|
||||
import java.util.Collections.synchronizedMap
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
/**
|
||||
* A StateMachineManager is responsible for coordination and persistence of multiple [ProtocolStateMachine] objects.
|
||||
* Each such object represents an instantiation of a (two-party) protocol that has reached a particular point.
|
||||
*
|
||||
* An implementation of this class will persist state machines to long term storage so they can survive process restarts
|
||||
* and, if run with a single-threaded executor, will ensure no two state machines run concurrently with each other
|
||||
* (bad for performance, good for programmer mental health!).
|
||||
*
|
||||
* A "state machine" is a class with a single call method. The call method and any others it invokes are rewritten by
|
||||
* a bytecode rewriting engine called Quasar, to ensure the code can be suspended and resumed at any point.
|
||||
*
|
||||
* The SMM will always invoke the protocol fibers on the given [AffinityExecutor], regardless of which thread actually
|
||||
* starts them via [add].
|
||||
*
|
||||
* TODO: Session IDs should be set up and propagated automatically, on demand.
|
||||
* TODO: Consider the issue of continuation identity more deeply: is it a safe assumption that a serialised
|
||||
* continuation is always unique?
|
||||
* TODO: Think about how to bring the system to a clean stop so it can be upgraded without any serialised stacks on disk
|
||||
* TODO: Timeouts
|
||||
* TODO: Surfacing of exceptions via an API and/or management UI
|
||||
* TODO: Ability to control checkpointing explicitly, for cases where you know replaying a message can't hurt
|
||||
* TODO: Make Kryo (de)serialize markers for heavy objects that are currently in the service hub. This avoids mistakes
|
||||
* where services are temporarily put on the stack.
|
||||
* TODO: Implement stub/skel classes that provide a basic RPC framework on top of this.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class StateMachineManager(val serviceHub: ServiceHub, val executor: AffinityExecutor) {
|
||||
inner class FiberScheduler : FiberExecutorScheduler("Same thread scheduler", executor)
|
||||
|
||||
val scheduler = FiberScheduler()
|
||||
|
||||
// This map is backed by a database and will be used to store serialised state machines to disk, so we can resurrect
|
||||
// them across node restarts.
|
||||
private val checkpointStorage = serviceHub.storageService.checkpointStorage
|
||||
// A list of all the state machines being managed by this class. We expose snapshots of it via the stateMachines
|
||||
// property.
|
||||
private val stateMachines = synchronizedMap(HashMap<ProtocolStateMachineImpl<*>, Checkpoint>())
|
||||
|
||||
// Monitoring support.
|
||||
private val metrics = serviceHub.monitoringService.metrics
|
||||
|
||||
init {
|
||||
metrics.register("Protocols.InFlight", Gauge<Int> { stateMachines.size })
|
||||
}
|
||||
|
||||
private val checkpointingMeter = metrics.meter("Protocols.Checkpointing Rate")
|
||||
private val totalStartedProtocols = metrics.counter("Protocols.Started")
|
||||
private val totalFinishedProtocols = metrics.counter("Protocols.Finished")
|
||||
|
||||
/** Returns a list of all state machines executing the given protocol logic at the top level (subprotocols do not count) */
|
||||
fun <T> findStateMachines(klass: Class<out ProtocolLogic<T>>): List<Pair<ProtocolLogic<T>, ListenableFuture<T>>> {
|
||||
synchronized(stateMachines) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return stateMachines.keys
|
||||
.map { it.logic }
|
||||
.filterIsInstance(klass)
|
||||
.map { it to (it.psm as ProtocolStateMachineImpl<T>).resultFuture }
|
||||
}
|
||||
}
|
||||
|
||||
// Used to work around a small limitation in Quasar.
|
||||
private val QUASAR_UNBLOCKER = run {
|
||||
val field = Fiber::class.java.getDeclaredField("SERIALIZER_BLOCKER")
|
||||
field.isAccessible = true
|
||||
field.get(null)
|
||||
}
|
||||
|
||||
init {
|
||||
Fiber.setDefaultUncaughtExceptionHandler { fiber, throwable ->
|
||||
(fiber as ProtocolStateMachineImpl<*>).logger.error("Caught exception from protocol", throwable)
|
||||
}
|
||||
restoreCheckpoints()
|
||||
}
|
||||
|
||||
/** Reads the database map and resurrects any serialised state machines. */
|
||||
private fun restoreCheckpoints() {
|
||||
for (checkpoint in checkpointStorage.checkpoints) {
|
||||
// Grab the Kryo engine configured by Quasar for its own stuff, and then do our own configuration on top
|
||||
// so we can deserialised the nested stream that holds the fiber.
|
||||
val psm = deserializeFiber(checkpoint.serialisedFiber)
|
||||
initFiber(psm, checkpoint)
|
||||
val awaitingObjectOfType = Class.forName(checkpoint.awaitingObjectOfType)
|
||||
val topic = checkpoint.awaitingTopic
|
||||
|
||||
psm.logger.info("restored ${psm.logic} - was previously awaiting on topic $topic")
|
||||
|
||||
// And now re-wire the deserialised continuation back up to the network service.
|
||||
serviceHub.networkService.runOnNextMessage(topic, executor) { netMsg ->
|
||||
// TODO: See security note below.
|
||||
val obj: Any = THREAD_LOCAL_KRYO.get().readClassAndObject(Input(netMsg.data))
|
||||
if (!awaitingObjectOfType.isInstance(obj))
|
||||
throw ClassCastException("Received message of unexpected type: ${obj.javaClass.name} vs ${awaitingObjectOfType.name}")
|
||||
psm.logger.trace { "<- $topic : message of type ${obj.javaClass.name}" }
|
||||
iterateStateMachine(psm, obj) {
|
||||
try {
|
||||
Fiber.unparkDeserialized(it, scheduler)
|
||||
} catch(e: Throwable) {
|
||||
logError(e, obj, topic, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deserializeFiber(serialisedFiber: SerializedBytes<out ProtocolStateMachine<*>>): ProtocolStateMachineImpl<*> {
|
||||
val deserializer = Fiber.getFiberSerializer(false) as KryoSerializer
|
||||
val kryo = createKryo(deserializer.kryo)
|
||||
return serialisedFiber.deserialize(kryo) as ProtocolStateMachineImpl<*>
|
||||
}
|
||||
|
||||
private fun logError(e: Throwable, obj: Any, topic: String, psm: ProtocolStateMachineImpl<*>) {
|
||||
psm.logger.error("Protocol state machine ${psm.javaClass.name} threw '${Throwables.getRootCause(e)}' " +
|
||||
"when handling a message of type ${obj.javaClass.name} on topic $topic")
|
||||
if (psm.logger.isTraceEnabled) {
|
||||
val s = StringWriter()
|
||||
Throwables.getRootCause(e).printStackTrace(PrintWriter(s))
|
||||
psm.logger.trace("Stack trace of protocol error is: $s")
|
||||
}
|
||||
}
|
||||
|
||||
private fun initFiber(psm: ProtocolStateMachineImpl<*>, checkpoint: Checkpoint?) {
|
||||
stateMachines[psm] = checkpoint
|
||||
psm.resultFuture.then(executor) {
|
||||
psm.logic.progressTracker?.currentStep = ProgressTracker.DONE
|
||||
val finalCheckpoint = stateMachines.remove(psm)
|
||||
if (finalCheckpoint != null) {
|
||||
checkpointStorage.removeCheckpoint(finalCheckpoint)
|
||||
}
|
||||
totalFinishedProtocols.inc()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kicks off a brand new state machine of the given class. It will log with the named logger.
|
||||
* The state machine will be persisted when it suspends, with automated restart if the StateMachineManager is
|
||||
* restarted with checkpointed state machines in the storage service.
|
||||
*/
|
||||
fun <T> add(loggerName: String, logic: ProtocolLogic<T>): ListenableFuture<T> {
|
||||
try {
|
||||
val fiber = ProtocolStateMachineImpl(logic, scheduler, loggerName)
|
||||
// Need to add before iterating in case of immediate completion
|
||||
initFiber(fiber, null)
|
||||
executor.executeASAP {
|
||||
iterateStateMachine(fiber, null) {
|
||||
it.start()
|
||||
}
|
||||
totalStartedProtocols.inc()
|
||||
}
|
||||
return fiber.resultFuture
|
||||
} catch(e: Throwable) {
|
||||
e.printStackTrace()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun replaceCheckpoint(psm: ProtocolStateMachineImpl<*>, newCheckpoint: Checkpoint) {
|
||||
// It's OK for this to be unsynchronised, as the prev/new byte arrays are specific to a continuation instance,
|
||||
// and the underlying map provided by the database layer is expected to be thread safe.
|
||||
val previousCheckpoint = stateMachines.put(psm, newCheckpoint)
|
||||
if (previousCheckpoint != null) {
|
||||
checkpointStorage.removeCheckpoint(previousCheckpoint)
|
||||
}
|
||||
checkpointStorage.addCheckpoint(newCheckpoint)
|
||||
checkpointingMeter.mark()
|
||||
}
|
||||
|
||||
private fun iterateStateMachine(psm: ProtocolStateMachineImpl<*>,
|
||||
obj: Any?,
|
||||
resumeFunc: (ProtocolStateMachineImpl<*>) -> Unit) {
|
||||
executor.checkOnThread()
|
||||
val onSuspend = fun(request: FiberRequest, serialisedFiber: SerializedBytes<ProtocolStateMachineImpl<*>>) {
|
||||
// We have a request to do something: send, receive, or send-and-receive.
|
||||
if (request is FiberRequest.ExpectingResponse<*>) {
|
||||
// Prepare a listener on the network that runs in the background thread when we received a message.
|
||||
checkpointAndSetupMessageHandler(psm, request, serialisedFiber)
|
||||
}
|
||||
// If an object to send was provided (not null), send it now.
|
||||
request.obj?.let {
|
||||
val topic = "${request.topic}.${request.sessionIDForSend}"
|
||||
psm.logger.trace { "-> ${request.destination}/$topic : message of type ${it.javaClass.name}" }
|
||||
serviceHub.networkService.send(topic, it, request.destination!!)
|
||||
}
|
||||
if (request is FiberRequest.NotExpectingResponse) {
|
||||
// We sent a message, but don't expect a response, so re-enter the continuation to let it keep going.
|
||||
iterateStateMachine(psm, null) {
|
||||
try {
|
||||
Fiber.unpark(it, QUASAR_UNBLOCKER)
|
||||
} catch(e: Throwable) {
|
||||
logError(e, request.obj!!, request.topic, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
psm.prepareForResumeWith(serviceHub, obj, onSuspend)
|
||||
|
||||
resumeFunc(psm)
|
||||
}
|
||||
|
||||
private fun checkpointAndSetupMessageHandler(psm: ProtocolStateMachineImpl<*>,
|
||||
request: FiberRequest.ExpectingResponse<*>,
|
||||
serialisedFiber: SerializedBytes<ProtocolStateMachineImpl<*>>) {
|
||||
executor.checkOnThread()
|
||||
val topic = "${request.topic}.${request.sessionIDForReceive}"
|
||||
val newCheckpoint = Checkpoint(serialisedFiber, topic, request.responseType.name)
|
||||
replaceCheckpoint(psm, newCheckpoint)
|
||||
psm.logger.trace { "Waiting for message of type ${request.responseType.name} on $topic" }
|
||||
val consumed = AtomicBoolean()
|
||||
serviceHub.networkService.runOnNextMessage(topic, executor) { netMsg ->
|
||||
// Some assertions to ensure we don't execute on the wrong thread or get executed more than once.
|
||||
executor.checkOnThread()
|
||||
check(netMsg.topic == topic) { "Topic mismatch: ${netMsg.topic} vs $topic" }
|
||||
check(!consumed.getAndSet(true))
|
||||
// TODO: This is insecure: we should not deserialise whatever we find and *then* check.
|
||||
//
|
||||
// We should instead verify as we read the data that it's what we are expecting and throw as early as
|
||||
// possible. We only do it this way for convenience during the prototyping stage. Note that this means
|
||||
// we could simply not require the programmer to specify the expected return type at all, and catch it
|
||||
// at the last moment when we do the downcast. However this would make protocol code harder to read and
|
||||
// make it more difficult to migrate to a more explicit serialisation scheme later.
|
||||
val obj: Any = THREAD_LOCAL_KRYO.get().readClassAndObject(Input(netMsg.data))
|
||||
if (!request.responseType.isInstance(obj))
|
||||
throw IllegalStateException("Expected message of type ${request.responseType.name} but got ${obj.javaClass.name}", request.stackTraceInCaseOfProblems)
|
||||
iterateStateMachine(psm, obj) {
|
||||
try {
|
||||
Fiber.unpark(it, QUASAR_UNBLOCKER)
|
||||
} catch(e: Throwable) {
|
||||
logError(e, obj, topic, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Clean this up
|
||||
open class FiberRequest(val topic: String, val destination: MessageRecipients?,
|
||||
val sessionIDForSend: Long, val sessionIDForReceive: Long, val obj: Any?) {
|
||||
// This is used to identify where we suspended, in case of message mismatch errors and other things where we
|
||||
// don't have the original stack trace because it's in a suspended fiber.
|
||||
val stackTraceInCaseOfProblems = StackSnapshot()
|
||||
|
||||
class ExpectingResponse<R : Any>(
|
||||
topic: String,
|
||||
destination: MessageRecipients?,
|
||||
sessionIDForSend: Long,
|
||||
sessionIDForReceive: Long,
|
||||
obj: Any?,
|
||||
val responseType: Class<R>
|
||||
) : FiberRequest(topic, destination, sessionIDForSend, sessionIDForReceive, obj)
|
||||
|
||||
class NotExpectingResponse(
|
||||
topic: String,
|
||||
destination: MessageRecipients,
|
||||
sessionIDForSend: Long,
|
||||
obj: Any?
|
||||
) : FiberRequest(topic, destination, sessionIDForSend, -1, obj)
|
||||
}
|
||||
}
|
||||
|
||||
class StackSnapshot : Throwable("This is a stack trace to help identify the source of the underlying problem")
|
259
node/src/main/kotlin/core/node/AbstractNode.kt
Normal file
259
node/src/main/kotlin/core/node/AbstractNode.kt
Normal file
@ -0,0 +1,259 @@
|
||||
package core.node
|
||||
|
||||
import api.APIServer
|
||||
import api.APIServerImpl
|
||||
import com.codahale.metrics.MetricRegistry
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import core.crypto.Party
|
||||
import core.messaging.MessagingService
|
||||
import core.messaging.StateMachineManager
|
||||
import core.messaging.runOnNextMessage
|
||||
import core.node.services.*
|
||||
import core.node.subsystems.*
|
||||
import core.node.storage.CheckpointStorage
|
||||
import core.node.storage.PerFileCheckpointStorage
|
||||
import core.node.subsystems.*
|
||||
import core.random63BitValue
|
||||
import core.seconds
|
||||
import core.serialization.deserialize
|
||||
import core.serialization.serialize
|
||||
import core.utilities.AddOrRemove
|
||||
import core.utilities.AffinityExecutor
|
||||
import org.slf4j.Logger
|
||||
import java.nio.file.FileAlreadyExistsException
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.security.KeyPair
|
||||
import java.time.Clock
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* A base node implementation that can be customised either for production (with real implementations that do real
|
||||
* I/O), or a mock implementation suitable for unit test environments.
|
||||
*/
|
||||
// TODO: Where this node is the initial network map service, currently no initialNetworkMapAddress is provided.
|
||||
// In theory the NodeInfo for the node should be passed in, instead, however currently this is constructed by the
|
||||
// AbstractNode. It should be possible to generate the NodeInfo outside of AbstractNode, so it can be passed in.
|
||||
abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration, val initialNetworkMapAddress: NodeInfo?,
|
||||
val advertisedServices: Set<ServiceType>, val platformClock: Clock) {
|
||||
companion object {
|
||||
val PRIVATE_KEY_FILE_NAME = "identity-private-key"
|
||||
val PUBLIC_IDENTITY_FILE_NAME = "identity-public"
|
||||
}
|
||||
|
||||
val networkMapServiceCallTimeout: Duration = Duration.ofSeconds(1)
|
||||
|
||||
// TODO: Persist this, as well as whether the node is registered.
|
||||
/**
|
||||
* Sequence number of changes sent to the network map service, when registering/de-registering this node
|
||||
*/
|
||||
var networkMapSeq: Long = 1
|
||||
|
||||
protected abstract val log: Logger
|
||||
|
||||
// We will run as much stuff in this single thread as possible to keep the risk of thread safety bugs low during the
|
||||
// low-performance prototyping period.
|
||||
protected abstract val serverThread: AffinityExecutor
|
||||
|
||||
// Objects in this list will be scanned by the DataUploadServlet and can be handed new data via HTTP.
|
||||
// Don't mutate this after startup.
|
||||
protected val _servicesThatAcceptUploads = ArrayList<AcceptsFileUpload>()
|
||||
val servicesThatAcceptUploads: List<AcceptsFileUpload> = _servicesThatAcceptUploads
|
||||
|
||||
val services = object : ServiceHub {
|
||||
override val networkService: MessagingService get() = net
|
||||
override val networkMapCache: NetworkMapCache = InMemoryNetworkMapCache()
|
||||
override val storageService: StorageService get() = storage
|
||||
override val walletService: WalletService get() = wallet
|
||||
override val keyManagementService: KeyManagementService get() = keyManagement
|
||||
override val identityService: IdentityService get() = identity
|
||||
override val monitoringService: MonitoringService = MonitoringService(MetricRegistry())
|
||||
override val clock: Clock get() = platformClock
|
||||
}
|
||||
|
||||
val info: NodeInfo by lazy {
|
||||
NodeInfo(net.myAddress, storage.myLegalIdentity, advertisedServices, findMyLocation())
|
||||
}
|
||||
|
||||
protected open fun findMyLocation(): PhysicalLocation? = CityDatabase[configuration.nearestCity]
|
||||
|
||||
lateinit var storage: StorageService
|
||||
lateinit var smm: StateMachineManager
|
||||
lateinit var wallet: WalletService
|
||||
lateinit var keyManagement: E2ETestKeyManagementService
|
||||
var inNodeNetworkMapService: NetworkMapService? = null
|
||||
var inNodeNotaryService: NotaryService? = null
|
||||
lateinit var identity: IdentityService
|
||||
lateinit var net: MessagingService
|
||||
lateinit var api: APIServer
|
||||
|
||||
open fun start(): AbstractNode {
|
||||
log.info("Node starting up ...")
|
||||
|
||||
storage = initialiseStorageService(dir)
|
||||
net = makeMessagingService()
|
||||
smm = StateMachineManager(services, serverThread)
|
||||
wallet = NodeWalletService(services)
|
||||
keyManagement = E2ETestKeyManagementService()
|
||||
makeInterestRatesOracleService()
|
||||
api = APIServerImpl(this)
|
||||
|
||||
// Build services we're advertising
|
||||
if (NetworkMapService.Type in info.advertisedServices) makeNetworkMapService()
|
||||
if (NotaryService.Type in info.advertisedServices) makeNotaryService()
|
||||
|
||||
identity = makeIdentityService()
|
||||
|
||||
// This object doesn't need to be referenced from this class because it registers handlers on the network
|
||||
// service and so that keeps it from being collected.
|
||||
DataVendingService(net, storage)
|
||||
|
||||
startMessagingService()
|
||||
|
||||
require(initialNetworkMapAddress == null || NetworkMapService.Type in initialNetworkMapAddress.advertisedServices)
|
||||
{ "Initial network map address must indicate a node that provides a network map service" }
|
||||
configureNetworkMapCache()
|
||||
|
||||
return this
|
||||
}
|
||||
/**
|
||||
* Register this node with the network map cache, and load network map from a remote service (and register for
|
||||
* updates) if one has been supplied.
|
||||
*/
|
||||
private fun configureNetworkMapCache() {
|
||||
services.networkMapCache.addNode(info)
|
||||
if (initialNetworkMapAddress != null) {
|
||||
// TODO: Return a future so the caller knows these operations may not have completed yet, and can monitor
|
||||
// if needed
|
||||
updateRegistration(initialNetworkMapAddress, AddOrRemove.ADD)
|
||||
services.networkMapCache.addMapService(net, initialNetworkMapAddress, true, null)
|
||||
}
|
||||
if (inNodeNetworkMapService != null) {
|
||||
// Register for updates
|
||||
services.networkMapCache.addMapService(net, info, true, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateRegistration(serviceInfo: NodeInfo, type: AddOrRemove): ListenableFuture<NetworkMapService.RegistrationResponse> {
|
||||
// Register this node against the network
|
||||
val expires = Instant.now() + NetworkMapService.DEFAULT_EXPIRATION_PERIOD
|
||||
val reg = NodeRegistration(info, networkMapSeq++, type, expires)
|
||||
val sessionID = random63BitValue()
|
||||
val request = NetworkMapService.RegistrationRequest(reg.toWire(storage.myLegalIdentityKey.private), net.myAddress, sessionID)
|
||||
val message = net.createMessage(NetworkMapService.REGISTER_PROTOCOL_TOPIC + ".0", request.serialize().bits)
|
||||
val future = SettableFuture.create<NetworkMapService.RegistrationResponse>()
|
||||
val topic = NetworkMapService.REGISTER_PROTOCOL_TOPIC + "." + sessionID
|
||||
|
||||
net.runOnNextMessage(topic, MoreExecutors.directExecutor()) { message ->
|
||||
future.set(message.data.deserialize())
|
||||
}
|
||||
net.send(message, serviceInfo.address)
|
||||
|
||||
return future
|
||||
}
|
||||
|
||||
open protected fun makeNetworkMapService() {
|
||||
val expires = Instant.now() + NetworkMapService.DEFAULT_EXPIRATION_PERIOD
|
||||
val reg = NodeRegistration(info, Long.MAX_VALUE, AddOrRemove.ADD, expires)
|
||||
inNodeNetworkMapService = InMemoryNetworkMapService(net, reg, services.networkMapCache)
|
||||
}
|
||||
|
||||
open protected fun makeNotaryService() {
|
||||
val uniquenessProvider = InMemoryUniquenessProvider()
|
||||
val timestampChecker = TimestampChecker(platformClock, 30.seconds)
|
||||
inNodeNotaryService = NotaryService(net, storage.myLegalIdentity, storage.myLegalIdentityKey, uniquenessProvider, timestampChecker)
|
||||
}
|
||||
|
||||
lateinit var interestRatesService: NodeInterestRates.Service
|
||||
|
||||
open protected fun makeInterestRatesOracleService() {
|
||||
// TODO: Once the service has data, automatically register with the network map service (once built).
|
||||
interestRatesService = NodeInterestRates.Service(this)
|
||||
_servicesThatAcceptUploads += interestRatesService
|
||||
}
|
||||
|
||||
protected open fun makeIdentityService(): IdentityService {
|
||||
val service = InMemoryIdentityService()
|
||||
if (initialNetworkMapAddress != null)
|
||||
service.registerIdentity(initialNetworkMapAddress.identity)
|
||||
service.registerIdentity(storage.myLegalIdentity)
|
||||
|
||||
services.networkMapCache.partyNodes.forEach { service.registerIdentity(it.identity) }
|
||||
|
||||
// TODO: Subscribe to updates to the network map cache
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
open fun stop() {
|
||||
// TODO: We need a good way of handling "nice to have" shutdown events, especially those that deal with the
|
||||
// network, including unsubscribing from updates from remote services. Possibly some sort of parameter to stop()
|
||||
// to indicate "Please shut down gracefully" vs "Shut down now".
|
||||
// Meanwhile, we let the remote service send us updates until the acknowledgment buffer overflows and it
|
||||
// unsubscribes us forcibly, rather than blocking the shutdown process.
|
||||
|
||||
net.stop()
|
||||
}
|
||||
|
||||
protected abstract fun makeMessagingService(): MessagingService
|
||||
|
||||
protected abstract fun startMessagingService()
|
||||
|
||||
protected open fun initialiseStorageService(dir: Path): StorageService {
|
||||
val attachments = makeAttachmentStorage(dir)
|
||||
val checkpointStorage = PerFileCheckpointStorage(dir.resolve("checkpoints"))
|
||||
_servicesThatAcceptUploads += attachments
|
||||
val (identity, keypair) = obtainKeyPair(dir)
|
||||
return constructStorageService(attachments, checkpointStorage, keypair, identity)
|
||||
}
|
||||
|
||||
protected open fun constructStorageService(attachments: NodeAttachmentService, checkpointStorage: CheckpointStorage, keypair: KeyPair, identity: Party) =
|
||||
StorageServiceImpl(attachments, checkpointStorage, keypair, identity)
|
||||
|
||||
private fun obtainKeyPair(dir: Path): Pair<Party, KeyPair> {
|
||||
// Load the private identity key, creating it if necessary. The identity key is a long term well known key that
|
||||
// is distributed to other peers and we use it (or a key signed by it) when we need to do something
|
||||
// "permissioned". The identity file is what gets distributed and contains the node's legal name along with
|
||||
// the public key. Obviously in a real system this would need to be a certificate chain of some kind to ensure
|
||||
// the legal name is actually validated in some way.
|
||||
val privKeyFile = dir.resolve(PRIVATE_KEY_FILE_NAME)
|
||||
val pubIdentityFile = dir.resolve(PUBLIC_IDENTITY_FILE_NAME)
|
||||
|
||||
return if (!Files.exists(privKeyFile)) {
|
||||
log.info("Identity key not found, generating fresh key!")
|
||||
val keypair: KeyPair = generateKeyPair()
|
||||
keypair.serialize().writeToFile(privKeyFile)
|
||||
val myIdentity = Party(configuration.myLegalName, keypair.public)
|
||||
// We include the Party class with the file here to help catch mixups when admins provide files of the
|
||||
// wrong type by mistake.
|
||||
myIdentity.serialize().writeToFile(pubIdentityFile)
|
||||
Pair(myIdentity, keypair)
|
||||
} else {
|
||||
// Check that the identity in the config file matches the identity file we have stored to disk.
|
||||
// This is just a sanity check. It shouldn't fail unless the admin has fiddled with the files and messed
|
||||
// things up for us.
|
||||
val myIdentity = Files.readAllBytes(pubIdentityFile).deserialize<Party>()
|
||||
if (myIdentity.name != configuration.myLegalName)
|
||||
throw ConfigurationException("The legal name in the config file doesn't match the stored identity file:" +
|
||||
"${configuration.myLegalName} vs ${myIdentity.name}")
|
||||
// Load the private key.
|
||||
val keypair = Files.readAllBytes(privKeyFile).deserialize<KeyPair>()
|
||||
Pair(myIdentity, keypair)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun generateKeyPair() = core.crypto.generateKeyPair()
|
||||
|
||||
protected fun makeAttachmentStorage(dir: Path): NodeAttachmentService {
|
||||
val attachmentsDir = dir.resolve("attachments")
|
||||
try {
|
||||
Files.createDirectory(attachmentsDir)
|
||||
} catch (e: FileAlreadyExistsException) {
|
||||
}
|
||||
return NodeAttachmentService(attachmentsDir, services.monitoringService.metrics)
|
||||
}
|
||||
}
|
22
node/src/main/kotlin/core/node/AcceptsFileUpload.kt
Normal file
22
node/src/main/kotlin/core/node/AcceptsFileUpload.kt
Normal file
@ -0,0 +1,22 @@
|
||||
package core.node
|
||||
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* A service that implements AcceptsFileUpload can have new binary data provided to it via an HTTP upload.
|
||||
*
|
||||
* TODO: In future, also accept uploads over the MQ interface too.
|
||||
*/
|
||||
interface AcceptsFileUpload {
|
||||
/** A string that prefixes the URLs, e.g. "attachments" or "interest-rates". Should be OK for URLs. */
|
||||
val dataTypePrefix: String
|
||||
|
||||
/** What file extensions are acceptable for the file to be handed to upload() */
|
||||
val acceptableFileExtensions: List<String>
|
||||
|
||||
/**
|
||||
* Accepts the data in the given input stream, and returns some sort of useful return message that will be sent
|
||||
* back to the user in the response.
|
||||
*/
|
||||
fun upload(data: InputStream): String
|
||||
}
|
174
node/src/main/kotlin/core/node/Node.kt
Normal file
174
node/src/main/kotlin/core/node/Node.kt
Normal file
@ -0,0 +1,174 @@
|
||||
package core.node
|
||||
|
||||
import api.Config
|
||||
//import api.InterestRateSwapAPI
|
||||
import api.ResponseFilter
|
||||
import com.codahale.metrics.JmxReporter
|
||||
import com.google.common.net.HostAndPort
|
||||
import core.messaging.MessagingService
|
||||
import core.node.subsystems.ArtemisMessagingService
|
||||
import core.node.services.ServiceType
|
||||
import core.node.servlets.AttachmentDownloadServlet
|
||||
import core.node.servlets.DataUploadServlet
|
||||
import core.utilities.AffinityExecutor
|
||||
import core.utilities.loggerFor
|
||||
import org.eclipse.jetty.server.Server
|
||||
import org.eclipse.jetty.server.handler.HandlerCollection
|
||||
import org.eclipse.jetty.servlet.ServletContextHandler
|
||||
import org.eclipse.jetty.servlet.ServletHolder
|
||||
import org.eclipse.jetty.webapp.WebAppContext
|
||||
import org.glassfish.jersey.server.ResourceConfig
|
||||
import org.glassfish.jersey.server.ServerProperties
|
||||
import org.glassfish.jersey.servlet.ServletContainer
|
||||
import java.io.RandomAccessFile
|
||||
import java.lang.management.ManagementFactory
|
||||
import java.nio.channels.FileLock
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.StandardOpenOption
|
||||
import java.time.Clock
|
||||
import javax.management.ObjectName
|
||||
|
||||
class ConfigurationException(message: String) : Exception(message)
|
||||
|
||||
// TODO: Split this into a regression testing environment
|
||||
|
||||
/**
|
||||
* A Node manages a standalone server that takes part in the P2P network. It creates the services found in [ServiceHub],
|
||||
* loads important data off disk and starts listening for connections.
|
||||
*
|
||||
* @param dir A [Path] to a location on disk where working files can be found or stored.
|
||||
* @param p2pAddr The host and port that this server will use. It can't find out its own external hostname, so you
|
||||
* have to specify that yourself.
|
||||
* @param configuration This is typically loaded from a .properties file
|
||||
* @param networkMapAddress An external network map service to use. Should only ever be null when creating the first
|
||||
* network map service, while bootstrapping a network.
|
||||
* @param advertisedServices The services this node advertises. This must be a subset of the services it runs,
|
||||
* but nodes are not required to advertise services they run (hence subset).
|
||||
* @param clock The clock used within the node and by all protocols etc
|
||||
*/
|
||||
class Node(dir: Path, val p2pAddr: HostAndPort, configuration: NodeConfiguration,
|
||||
networkMapAddress: NodeInfo?, advertisedServices: Set<ServiceType>,
|
||||
clock: Clock = Clock.systemUTC()) : AbstractNode(dir, configuration, networkMapAddress, advertisedServices, clock) {
|
||||
companion object {
|
||||
/** The port that is used by default if none is specified. As you know, 31337 is the most elite number. */
|
||||
val DEFAULT_PORT = 31337
|
||||
}
|
||||
|
||||
override val log = loggerFor<Node>()
|
||||
|
||||
override val serverThread = AffinityExecutor.ServiceAffinityExecutor("Node thread", 1)
|
||||
|
||||
lateinit var webServer: Server
|
||||
|
||||
// Avoid the lock being garbage collected. We don't really need to release it as the OS will do so for us
|
||||
// when our process shuts down, but we try in stop() anyway just to be nice.
|
||||
private var nodeFileLock: FileLock? = null
|
||||
|
||||
override fun makeMessagingService(): MessagingService = ArtemisMessagingService(dir, p2pAddr, serverThread)
|
||||
|
||||
override fun startMessagingService() {
|
||||
// Start up the MQ service.
|
||||
(net as ArtemisMessagingService).start()
|
||||
}
|
||||
|
||||
private fun initWebServer(): Server {
|
||||
// Note that the web server handlers will all run concurrently, and not on the node thread.
|
||||
|
||||
val port = p2pAddr.port + 1 // TODO: Move this into the node config file.
|
||||
val server = Server(port)
|
||||
|
||||
val handlerCollection = HandlerCollection()
|
||||
|
||||
// Export JMX monitoring statistics and data over REST/JSON.
|
||||
if (configuration.exportJMXto.split(',').contains("http")) {
|
||||
handlerCollection.addHandler(WebAppContext().apply {
|
||||
// Find the jolokia WAR file on the classpath.
|
||||
contextPath = "/monitoring/json"
|
||||
setInitParameter("mimeType", "application/json")
|
||||
val classpath = System.getProperty("java.class.path").split(System.getProperty("path.separator"))
|
||||
war = classpath.first { it.contains("jolokia-agent-war-2") && it.endsWith(".war") }
|
||||
})
|
||||
}
|
||||
|
||||
// API, data upload and download to services (attachments, rates oracles etc)
|
||||
handlerCollection.addHandler(ServletContextHandler().apply {
|
||||
contextPath = "/"
|
||||
setAttribute("node", this@Node)
|
||||
addServlet(DataUploadServlet::class.java, "/upload/*")
|
||||
addServlet(AttachmentDownloadServlet::class.java, "/attachments/*")
|
||||
|
||||
val resourceConfig = ResourceConfig()
|
||||
// Add your API provider classes (annotated for JAX-RS) here
|
||||
resourceConfig.register(Config(services))
|
||||
resourceConfig.register(ResponseFilter())
|
||||
resourceConfig.register(api)
|
||||
//resourceConfig.register(InterestRateSwapAPI(api))
|
||||
// Give the app a slightly better name in JMX rather than a randomly generated one and enable JMX
|
||||
resourceConfig.addProperties(mapOf(ServerProperties.APPLICATION_NAME to "node.api",
|
||||
ServerProperties.MONITORING_STATISTICS_MBEANS_ENABLED to "true"))
|
||||
|
||||
val container = ServletContainer(resourceConfig)
|
||||
val jerseyServlet = ServletHolder(container)
|
||||
addServlet(jerseyServlet, "/api/*")
|
||||
jerseyServlet.initOrder = 0 // Initialise at server start
|
||||
})
|
||||
|
||||
server.handler = handlerCollection
|
||||
server.start()
|
||||
return server
|
||||
}
|
||||
|
||||
override fun start(): Node {
|
||||
alreadyRunningNodeCheck()
|
||||
super.start()
|
||||
webServer = initWebServer()
|
||||
// Begin exporting our own metrics via JMX.
|
||||
JmxReporter.
|
||||
forRegistry(services.monitoringService.metrics).
|
||||
inDomain("com.r3cev.corda").
|
||||
createsObjectNamesWith { type, domain, name ->
|
||||
// Make the JMX hierarchy a bit better organised.
|
||||
val category = name.substringBefore('.')
|
||||
val subName = name.substringAfter('.', "")
|
||||
if (subName == "")
|
||||
ObjectName("$domain:name=$category")
|
||||
else
|
||||
ObjectName("$domain:type=$category,name=$subName")
|
||||
}.
|
||||
build().
|
||||
start()
|
||||
return this
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
webServer.stop()
|
||||
super.stop()
|
||||
nodeFileLock!!.release()
|
||||
serverThread.shutdownNow()
|
||||
}
|
||||
|
||||
private fun alreadyRunningNodeCheck() {
|
||||
// Write out our process ID (which may or may not resemble a UNIX process id - to us it's just a string) to a
|
||||
// file that we'll do our best to delete on exit. But if we don't, it'll be overwritten next time. If it already
|
||||
// exists, we try to take the file lock first before replacing it and if that fails it means we're being started
|
||||
// twice with the same directory: that's a user error and we should bail out.
|
||||
val pidPath = dir.resolve("process-id")
|
||||
val file = pidPath.toFile()
|
||||
if (file.exists()) {
|
||||
val f = RandomAccessFile(file, "rw")
|
||||
val l = f.channel.tryLock()
|
||||
if (l == null) {
|
||||
println("It appears there is already a node running with the specified data directory $dir")
|
||||
println("Shut that other node down and try again. It may have process ID ${file.readText()}")
|
||||
System.exit(1)
|
||||
}
|
||||
nodeFileLock = l
|
||||
}
|
||||
val ourProcessID: String = ManagementFactory.getRuntimeMXBean().name.split("@")[0]
|
||||
Files.write(pidPath, ourProcessID.toByteArray(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)
|
||||
pidPath.toFile().deleteOnExit()
|
||||
if (nodeFileLock == null)
|
||||
nodeFileLock = RandomAccessFile(file, "rw").channel.lock()
|
||||
}
|
||||
}
|
20
node/src/main/kotlin/core/node/NodeConfiguration.kt
Normal file
20
node/src/main/kotlin/core/node/NodeConfiguration.kt
Normal file
@ -0,0 +1,20 @@
|
||||
package core.node
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
interface NodeConfiguration {
|
||||
val myLegalName: String
|
||||
val exportJMXto: String
|
||||
val nearestCity: String
|
||||
}
|
||||
|
||||
// Allow the use of "String by config" syntax. TODO: Make it more flexible.
|
||||
operator fun Config.getValue(receiver: NodeConfigurationFromConfig, metadata: KProperty<*>) = getString(metadata.name)
|
||||
|
||||
class NodeConfigurationFromConfig(val config: Config = ConfigFactory.load()) : NodeConfiguration {
|
||||
override val myLegalName: String by config
|
||||
override val exportJMXto: String by config
|
||||
override val nearestCity: String by config
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package core.node.services
|
||||
|
||||
import core.messaging.Message
|
||||
import core.messaging.MessagingService
|
||||
import core.node.subsystems.TOPIC_DEFAULT_POSTFIX
|
||||
import core.serialization.deserialize
|
||||
import core.serialization.serialize
|
||||
import protocols.AbstractRequestMessage
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
/**
|
||||
* Abstract superclass for services that a node can host, which provides helper functions.
|
||||
*/
|
||||
@ThreadSafe
|
||||
abstract class AbstractNodeService(val net: MessagingService) {
|
||||
|
||||
/**
|
||||
* Register a handler for a message topic. In comparison to using net.addMessageHandler() this manages a lot of
|
||||
* common boilerplate code. Exceptions are caught and passed to the provided consumer.
|
||||
*
|
||||
* @param topic the topic, without the default session ID postfix (".0)
|
||||
* @param handler a function to handle the deserialised request and return a response
|
||||
* @param exceptionConsumer a function to which any thrown exception is passed.
|
||||
*/
|
||||
protected inline fun <reified Q : AbstractRequestMessage, reified R : Any>
|
||||
addMessageHandler(topic: String,
|
||||
crossinline handler: (Q) -> R,
|
||||
crossinline exceptionConsumer: (Message, Exception) -> Unit) {
|
||||
net.addMessageHandler(topic + TOPIC_DEFAULT_POSTFIX, null) { message, r ->
|
||||
try {
|
||||
val req = message.data.deserialize<Q>()
|
||||
val data = handler(req)
|
||||
val msg = net.createMessage(topic + "." + req.sessionID, data.serialize().bits)
|
||||
net.send(msg, req.replyTo)
|
||||
} catch(e: Exception) {
|
||||
exceptionConsumer(message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a handler for a message topic. In comparison to using net.addMessageHandler() this manages a lot of
|
||||
* common boilerplate code. Exceptions are propagated to the messaging layer.
|
||||
*
|
||||
* @param topic the topic, without the default session ID postfix (".0)
|
||||
* @param handler a function to handle the deserialised request and return a response
|
||||
*/
|
||||
protected inline fun <reified Q : AbstractRequestMessage, reified R : Any>
|
||||
addMessageHandler(topic: String,
|
||||
crossinline handler: (Q) -> R) {
|
||||
net.addMessageHandler(topic + TOPIC_DEFAULT_POSTFIX, null) { message, r ->
|
||||
val req = message.data.deserialize<Q>()
|
||||
val data = handler(req)
|
||||
val msg = net.createMessage(topic + "." + req.sessionID, data.serialize().bits)
|
||||
net.send(msg, req.replyTo)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package core.node.services
|
||||
|
||||
import core.crypto.Party
|
||||
import core.contracts.StateRef
|
||||
import core.ThreadBox
|
||||
import core.contracts.WireTransaction
|
||||
import java.util.*
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
/** A dummy Uniqueness provider that stores the whole history of consumed states in memory */
|
||||
@ThreadSafe
|
||||
class InMemoryUniquenessProvider() : UniquenessProvider {
|
||||
/** For each input state store the consuming transaction information */
|
||||
private val committedStates = ThreadBox(HashMap<StateRef, UniquenessProvider.ConsumingTx>())
|
||||
|
||||
// TODO: the uniqueness provider shouldn't be able to see all tx outputs and commands
|
||||
override fun commit(tx: WireTransaction, callerIdentity: Party) {
|
||||
val inputStates = tx.inputs
|
||||
committedStates.locked {
|
||||
val conflictingStates = LinkedHashMap<StateRef, UniquenessProvider.ConsumingTx>()
|
||||
for (inputState in inputStates) {
|
||||
val consumingTx = get(inputState)
|
||||
if (consumingTx != null) conflictingStates[inputState] = consumingTx
|
||||
}
|
||||
if (conflictingStates.isNotEmpty()) {
|
||||
val conflict = UniquenessProvider.Conflict(conflictingStates)
|
||||
throw UniquenessException(conflict)
|
||||
} else {
|
||||
inputStates.forEachIndexed { i, stateRef ->
|
||||
put(stateRef, UniquenessProvider.ConsumingTx(tx.id, i, callerIdentity))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
303
node/src/main/kotlin/core/node/services/NetworkMapService.kt
Normal file
303
node/src/main/kotlin/core/node/services/NetworkMapService.kt
Normal file
@ -0,0 +1,303 @@
|
||||
package core.node.services
|
||||
|
||||
import co.paralleluniverse.common.util.VisibleForTesting
|
||||
import core.crypto.Party
|
||||
import core.ThreadBox
|
||||
import core.crypto.DigitalSignature
|
||||
import core.crypto.SecureHash
|
||||
import core.crypto.SignedData
|
||||
import core.crypto.signWithECDSA
|
||||
import core.messaging.MessageRecipients
|
||||
import core.messaging.MessagingService
|
||||
import core.messaging.SingleMessageRecipient
|
||||
import core.node.NodeInfo
|
||||
import core.node.subsystems.NetworkMapCache
|
||||
import core.node.subsystems.TOPIC_DEFAULT_POSTFIX
|
||||
import core.serialization.SerializedBytes
|
||||
import core.serialization.deserialize
|
||||
import core.serialization.serialize
|
||||
import core.utilities.AddOrRemove
|
||||
import org.slf4j.LoggerFactory
|
||||
import protocols.*
|
||||
import java.security.PrivateKey
|
||||
import java.time.Period
|
||||
import java.time.Instant
|
||||
import java.util.ArrayList
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
|
||||
/**
|
||||
* A network map contains lists of nodes on the network along with information about their identity keys, services
|
||||
* they provide and host names or IP addresses where they can be connected to. This information is cached locally within
|
||||
* nodes, by the [NetworkMapCache]. Currently very basic consensus controls are applied, using signed changes which
|
||||
* replace each other based on a serial number present in the change.
|
||||
*/
|
||||
// TODO: A better architecture for the network map service might be one like the Tor directory authorities, where
|
||||
// several nodes linked by RAFT or Paxos elect a leader and that leader distributes signed documents describing the
|
||||
// network layout. Those documents can then be cached by every node and thus a network map can/ be retrieved given only
|
||||
// a single successful peer connection.
|
||||
//
|
||||
// It may also be that this is replaced or merged with the identity management service; for example if the network has
|
||||
// a concept of identity changes over time, should that include the node for an identity? If so, that is likely to
|
||||
// replace this service.
|
||||
interface NetworkMapService {
|
||||
object Type : ServiceType("corda.network_map")
|
||||
|
||||
companion object {
|
||||
val DEFAULT_EXPIRATION_PERIOD = Period.ofWeeks(4)
|
||||
val FETCH_PROTOCOL_TOPIC = "platform.network_map.fetch"
|
||||
val QUERY_PROTOCOL_TOPIC = "platform.network_map.query"
|
||||
val REGISTER_PROTOCOL_TOPIC = "platform.network_map.register"
|
||||
val SUBSCRIPTION_PROTOCOL_TOPIC = "platform.network_map.subscribe"
|
||||
// Base topic used when pushing out updates to the network map. Consumed, for example, by the map cache.
|
||||
// When subscribing to these updates, remember they must be acknowledged
|
||||
val PUSH_PROTOCOL_TOPIC = "platform.network_map.push"
|
||||
// Base topic for messages acknowledging pushed updates
|
||||
val PUSH_ACK_PROTOCOL_TOPIC = "platform.network_map.push_ack"
|
||||
|
||||
val logger = LoggerFactory.getLogger(NetworkMapService::class.java)
|
||||
}
|
||||
|
||||
val nodes: List<NodeInfo>
|
||||
|
||||
class FetchMapRequest(val subscribe: Boolean, val ifChangedSinceVersion: Int?, replyTo: MessageRecipients, sessionID: Long) : AbstractRequestMessage(replyTo, sessionID)
|
||||
data class FetchMapResponse(val nodes: Collection<NodeRegistration>?, val version: Int)
|
||||
class QueryIdentityRequest(val identity: Party, replyTo: MessageRecipients, sessionID: Long) : AbstractRequestMessage(replyTo, sessionID)
|
||||
data class QueryIdentityResponse(val node: NodeInfo?)
|
||||
class RegistrationRequest(val wireReg: WireNodeRegistration, replyTo: MessageRecipients, sessionID: Long) : AbstractRequestMessage(replyTo, sessionID)
|
||||
data class RegistrationResponse(val success: Boolean)
|
||||
class SubscribeRequest(val subscribe: Boolean, replyTo: MessageRecipients, sessionID: Long) : AbstractRequestMessage(replyTo, sessionID)
|
||||
data class SubscribeResponse(val confirmed: Boolean)
|
||||
data class Update(val wireReg: WireNodeRegistration, val replyTo: MessageRecipients)
|
||||
data class UpdateAcknowledge(val wireRegHash: SecureHash, val replyTo: MessageRecipients)
|
||||
}
|
||||
|
||||
@ThreadSafe
|
||||
class InMemoryNetworkMapService(net: MessagingService, home: NodeRegistration, val cache: NetworkMapCache) : NetworkMapService, AbstractNodeService(net) {
|
||||
private val registeredNodes = ConcurrentHashMap<Party, NodeRegistration>()
|
||||
// Map from subscriber address, to a list of unacknowledged updates
|
||||
private val subscribers = ThreadBox(mutableMapOf<SingleMessageRecipient, MutableList<SecureHash>>())
|
||||
private val mapVersion = AtomicInteger(1)
|
||||
/** Maximum number of unacknowledged updates to send to a node before automatically unregistering them for updates */
|
||||
val maxUnacknowledgedUpdates = 10
|
||||
/**
|
||||
* Maximum credible size for a registration request. Generally requests are around 500-600 bytes, so this gives a
|
||||
* 10 times overhead.
|
||||
*/
|
||||
val maxSizeRegistrationRequestBytes = 5500
|
||||
|
||||
// Filter reduces this to the entries that add a node to the map
|
||||
override val nodes: List<NodeInfo>
|
||||
get() = registeredNodes.mapNotNull { if (it.value.type == AddOrRemove.ADD) it.value.node else null }
|
||||
|
||||
init {
|
||||
// Register the local node with the service
|
||||
val homeIdentity = home.node.identity
|
||||
registeredNodes[homeIdentity] = home
|
||||
|
||||
// Register message handlers
|
||||
addMessageHandler(NetworkMapService.FETCH_PROTOCOL_TOPIC,
|
||||
{ req: NetworkMapService.FetchMapRequest -> processFetchAllRequest(req) }
|
||||
)
|
||||
addMessageHandler(NetworkMapService.QUERY_PROTOCOL_TOPIC,
|
||||
{ req: NetworkMapService.QueryIdentityRequest -> processQueryRequest(req) }
|
||||
)
|
||||
addMessageHandler(NetworkMapService.REGISTER_PROTOCOL_TOPIC,
|
||||
{ req: NetworkMapService.RegistrationRequest -> processRegistrationChangeRequest(req) }
|
||||
)
|
||||
addMessageHandler(NetworkMapService.SUBSCRIPTION_PROTOCOL_TOPIC,
|
||||
{ req: NetworkMapService.SubscribeRequest -> processSubscriptionRequest(req) }
|
||||
)
|
||||
net.addMessageHandler(NetworkMapService.PUSH_ACK_PROTOCOL_TOPIC + TOPIC_DEFAULT_POSTFIX, null) { message, r ->
|
||||
val req = message.data.deserialize<NetworkMapService.UpdateAcknowledge>()
|
||||
processAcknowledge(req)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addSubscriber(subscriber: MessageRecipients) {
|
||||
if (subscriber !is SingleMessageRecipient) throw NodeMapError.InvalidSubscriber()
|
||||
subscribers.locked {
|
||||
if (!containsKey(subscriber)) {
|
||||
put(subscriber, mutableListOf<SecureHash>())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeSubscriber(subscriber: MessageRecipients) {
|
||||
if (subscriber !is SingleMessageRecipient) throw NodeMapError.InvalidSubscriber()
|
||||
subscribers.locked { remove(subscriber) }
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun getUnacknowledgedCount(subscriber: SingleMessageRecipient): Int?
|
||||
= subscribers.locked { get(subscriber)?.count() }
|
||||
|
||||
@VisibleForTesting
|
||||
fun notifySubscribers(wireReg: WireNodeRegistration) {
|
||||
// TODO: Once we have a better established messaging system, we can probably send
|
||||
// to a MessageRecipientGroup that nodes join/leave, rather than the network map
|
||||
// service itself managing the group
|
||||
val update = NetworkMapService.Update(wireReg, net.myAddress).serialize().bits
|
||||
val topic = NetworkMapService.PUSH_PROTOCOL_TOPIC + TOPIC_DEFAULT_POSTFIX
|
||||
val message = net.createMessage(topic, update)
|
||||
|
||||
subscribers.locked {
|
||||
val toRemove = mutableListOf<SingleMessageRecipient>()
|
||||
val hash = SecureHash.sha256(wireReg.raw.bits)
|
||||
forEach { subscriber: Map.Entry<SingleMessageRecipient, MutableList<SecureHash>> ->
|
||||
val unacknowledged = subscriber.value
|
||||
if (unacknowledged.count() < maxUnacknowledgedUpdates) {
|
||||
unacknowledged.add(hash)
|
||||
net.send(message, subscriber.key)
|
||||
} else {
|
||||
toRemove.add(subscriber.key)
|
||||
}
|
||||
}
|
||||
toRemove.forEach { remove(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun processAcknowledge(req: NetworkMapService.UpdateAcknowledge): Unit {
|
||||
subscribers.locked {
|
||||
this[req.replyTo]?.remove(req.wireRegHash)
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun processFetchAllRequest(req: NetworkMapService.FetchMapRequest): NetworkMapService.FetchMapResponse {
|
||||
if (req.subscribe) {
|
||||
addSubscriber(req.replyTo)
|
||||
}
|
||||
val ver = mapVersion.get()
|
||||
if (req.ifChangedSinceVersion == null || req.ifChangedSinceVersion < ver) {
|
||||
val nodes = ArrayList(registeredNodes.values) // Snapshot to avoid attempting to serialise ConcurrentHashMap internals
|
||||
return NetworkMapService.FetchMapResponse(nodes, ver)
|
||||
} else {
|
||||
return NetworkMapService.FetchMapResponse(null, ver)
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun processQueryRequest(req: NetworkMapService.QueryIdentityRequest): NetworkMapService.QueryIdentityResponse {
|
||||
val candidate = registeredNodes[req.identity]
|
||||
|
||||
// If the most recent record we have is of the node being removed from the map, then it's considered
|
||||
// as no match.
|
||||
if (candidate == null || candidate.type == AddOrRemove.REMOVE) {
|
||||
return NetworkMapService.QueryIdentityResponse(null)
|
||||
} else {
|
||||
return NetworkMapService.QueryIdentityResponse(candidate.node)
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun processRegistrationChangeRequest(req: NetworkMapService.RegistrationRequest): NetworkMapService.RegistrationResponse {
|
||||
require(req.wireReg.raw.size < maxSizeRegistrationRequestBytes)
|
||||
val change: NodeRegistration
|
||||
|
||||
try {
|
||||
change = req.wireReg.verified()
|
||||
} catch(e: java.security.SignatureException) {
|
||||
throw NodeMapError.InvalidSignature()
|
||||
}
|
||||
val node = change.node
|
||||
|
||||
var changed: Boolean = false
|
||||
// Update the current value atomically, so that if multiple updates come
|
||||
// in on different threads, there is no risk of a race condition while checking
|
||||
// sequence numbers.
|
||||
registeredNodes.compute(node.identity, { mapKey: Party, existing: NodeRegistration? ->
|
||||
changed = existing == null || existing.serial < change.serial
|
||||
if (changed) {
|
||||
when (change.type) {
|
||||
AddOrRemove.ADD -> change
|
||||
AddOrRemove.REMOVE -> change
|
||||
else -> throw NodeMapError.UnknownChangeType()
|
||||
}
|
||||
} else {
|
||||
existing
|
||||
}
|
||||
})
|
||||
if (changed) {
|
||||
notifySubscribers(req.wireReg)
|
||||
|
||||
// Update the local cache
|
||||
// TODO: Once local messaging is fixed, this should go over the network layer as it does to other
|
||||
// subscribers
|
||||
when (change.type) {
|
||||
AddOrRemove.ADD -> {
|
||||
NetworkMapService.logger.info("Added node ${node.address} to network map")
|
||||
cache.addNode(change.node)
|
||||
}
|
||||
AddOrRemove.REMOVE -> {
|
||||
NetworkMapService.logger.info("Removed node ${node.address} from network map")
|
||||
cache.removeNode(change.node)
|
||||
}
|
||||
}
|
||||
|
||||
mapVersion.incrementAndGet()
|
||||
}
|
||||
return NetworkMapService.RegistrationResponse(changed)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun processSubscriptionRequest(req: NetworkMapService.SubscribeRequest): NetworkMapService.SubscribeResponse {
|
||||
when (req.subscribe) {
|
||||
false -> removeSubscriber(req.replyTo)
|
||||
true -> addSubscriber(req.replyTo)
|
||||
}
|
||||
return NetworkMapService.SubscribeResponse(true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A node registration state in the network map.
|
||||
*
|
||||
* @param node the node being added/removed.
|
||||
* @param serial an increasing value which represents the version of this registration. Not expected to be sequential,
|
||||
* but later versions of the registration must have higher values (or they will be ignored by the map service).
|
||||
* Similar to the serial number on DNS records.
|
||||
* @param type add if the node is being added to the map, or remove if a previous node is being removed (indicated as
|
||||
* going offline).
|
||||
* @param expires when the registration expires. Only used when adding a node to a map.
|
||||
*/
|
||||
// TODO: This might alternatively want to have a node and party, with the node being optional, so registering a node
|
||||
// involves providing both node and paerty, and deregistering a node involves a request with party but no node.
|
||||
class NodeRegistration(val node: NodeInfo, val serial: Long, val type: AddOrRemove, var expires: Instant) {
|
||||
/**
|
||||
* Build a node registration in wire format.
|
||||
*/
|
||||
fun toWire(privateKey: PrivateKey): WireNodeRegistration {
|
||||
val regSerialized = this.serialize()
|
||||
val regSig = privateKey.signWithECDSA(regSerialized.bits, node.identity.owningKey)
|
||||
|
||||
return WireNodeRegistration(regSerialized, regSig)
|
||||
}
|
||||
|
||||
override fun toString() : String = "$node #${serial} (${type})"
|
||||
}
|
||||
|
||||
/**
|
||||
* A node registration and its signature as a pair.
|
||||
*/
|
||||
class WireNodeRegistration(raw: SerializedBytes<NodeRegistration>, sig: DigitalSignature.WithKey) : SignedData<NodeRegistration>(raw, sig) {
|
||||
@Throws(IllegalArgumentException::class)
|
||||
override fun verifyData(data: NodeRegistration) {
|
||||
require(data.node.identity.owningKey == sig.by)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class NodeMapError : Exception() {
|
||||
|
||||
/** Thrown if the signature on the node info does not match the public key for the identity */
|
||||
class InvalidSignature : NodeMapError()
|
||||
|
||||
/** Thrown if the replyTo of a subscription change message is not a single message recipient */
|
||||
class InvalidSubscriber : NodeMapError()
|
||||
|
||||
/** Thrown if a change arrives which is of an unknown type */
|
||||
class UnknownChangeType : NodeMapError()
|
||||
}
|
158
node/src/main/kotlin/core/node/services/NodeAttachmentService.kt
Normal file
158
node/src/main/kotlin/core/node/services/NodeAttachmentService.kt
Normal file
@ -0,0 +1,158 @@
|
||||
package core.node.services
|
||||
|
||||
import com.codahale.metrics.MetricRegistry
|
||||
import com.google.common.annotations.VisibleForTesting
|
||||
import com.google.common.hash.Hashing
|
||||
import com.google.common.hash.HashingInputStream
|
||||
import com.google.common.io.CountingInputStream
|
||||
import core.contracts.Attachment
|
||||
import core.crypto.SecureHash
|
||||
import core.extractZipFile
|
||||
import core.node.AcceptsFileUpload
|
||||
import core.utilities.loggerFor
|
||||
import java.io.FilterInputStream
|
||||
import java.io.InputStream
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardCopyOption
|
||||
import java.util.*
|
||||
import java.util.jar.JarInputStream
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
/**
|
||||
* Stores attachments in the specified local directory, which must exist. Doesn't allow new attachments to be uploaded.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class NodeAttachmentService(val storePath: Path, val metrics: MetricRegistry) : AttachmentStorage, AcceptsFileUpload {
|
||||
private val log = loggerFor<NodeAttachmentService>()
|
||||
|
||||
@VisibleForTesting
|
||||
var checkAttachmentsOnLoad = true
|
||||
|
||||
private val attachmentCount = metrics.counter("Attachments")
|
||||
|
||||
init {
|
||||
attachmentCount.inc(countAttachments())
|
||||
}
|
||||
|
||||
// Just count all non-directories in the attachment store, and assume the admin hasn't dumped any junk there.
|
||||
private fun countAttachments() = Files.list(storePath).filter { Files.isRegularFile(it) }.count()
|
||||
|
||||
/**
|
||||
* If true, newly inserted attachments will be unzipped to a subdirectory of the [storePath]. This is intended for
|
||||
* human browsing convenience: the attachment itself will still be the file (that is, edits to the extracted directory
|
||||
* will not have any effect).
|
||||
*/
|
||||
@Volatile var automaticallyExtractAttachments = false
|
||||
|
||||
init {
|
||||
require(Files.isDirectory(storePath)) { "$storePath must be a directory" }
|
||||
}
|
||||
|
||||
class OnDiskHashMismatch(val file: Path, val actual: SecureHash) : Exception() {
|
||||
override fun toString() = "File $file hashed to $actual: corruption in attachment store?"
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a stream and hashes data as it is read: if the entire stream is consumed, then at the end the hash of
|
||||
* the read data is compared to the [expected] hash and [OnDiskHashMismatch] is thrown by [close] if they didn't
|
||||
* match. The goal of this is to detect cases where attachments in the store have been tampered with or corrupted
|
||||
* and no longer match their file name. It won't always work: if we read a zip for our own uses and skip around
|
||||
* inside it, we haven't read the whole file, so we can't check the hash. But when copying it over the network
|
||||
* this will provide an additional safety check against user error.
|
||||
*/
|
||||
private class HashCheckingStream(val expected: SecureHash.SHA256,
|
||||
val filePath: Path,
|
||||
input: InputStream,
|
||||
private val counter: CountingInputStream = CountingInputStream(input),
|
||||
private val stream: HashingInputStream = HashingInputStream(Hashing.sha256(), counter)) : FilterInputStream(stream) {
|
||||
|
||||
private val expectedSize = Files.size(filePath)
|
||||
|
||||
override fun close() {
|
||||
super.close()
|
||||
if (counter.count != expectedSize) return
|
||||
val actual = SecureHash.SHA256(stream.hash().asBytes())
|
||||
if (actual != expected)
|
||||
throw OnDiskHashMismatch(filePath, actual)
|
||||
}
|
||||
}
|
||||
|
||||
// Deliberately not an inner class to avoid holding a reference to the attachments service.
|
||||
private class AttachmentImpl(override val id: SecureHash,
|
||||
private val path: Path,
|
||||
private val checkOnLoad: Boolean) : Attachment {
|
||||
override fun open(): InputStream {
|
||||
var stream = Files.newInputStream(path)
|
||||
// This is just an optional safety check. If it slows things down too much it can be disabled.
|
||||
if (id is SecureHash.SHA256 && checkOnLoad)
|
||||
stream = HashCheckingStream(id, path, stream)
|
||||
return stream
|
||||
}
|
||||
|
||||
override fun equals(other: Any?) = other is Attachment && other.id == id
|
||||
override fun hashCode(): Int = id.hashCode()
|
||||
}
|
||||
|
||||
override fun openAttachment(id: SecureHash): Attachment? {
|
||||
val path = storePath.resolve(id.toString())
|
||||
if (!Files.exists(path)) return null
|
||||
return AttachmentImpl(id, path, checkAttachmentsOnLoad)
|
||||
}
|
||||
|
||||
// TODO: PLT-147: The attachment should be randomised to prevent brute force guessing and thus privacy leaks.
|
||||
override fun importAttachment(jar: InputStream): SecureHash {
|
||||
require(jar !is JarInputStream)
|
||||
val hs = HashingInputStream(Hashing.sha256(), jar)
|
||||
val tmp = storePath.resolve("tmp.${UUID.randomUUID()}")
|
||||
Files.copy(hs, tmp)
|
||||
checkIsAValidJAR(tmp)
|
||||
val id = SecureHash.SHA256(hs.hash().asBytes())
|
||||
val finalPath = storePath.resolve(id.toString())
|
||||
try {
|
||||
// Move into place atomically or fail if that isn't possible. We don't want a half moved attachment to
|
||||
// be exposed to parallel threads. This gives us thread safety.
|
||||
if (!Files.exists(finalPath)) {
|
||||
log.info("Stored new attachment $id")
|
||||
attachmentCount.inc()
|
||||
} else {
|
||||
log.info("Replacing attachment $id - only bother doing this if you're trying to repair file corruption")
|
||||
}
|
||||
Files.move(tmp, finalPath, StandardCopyOption.ATOMIC_MOVE)
|
||||
} finally {
|
||||
Files.deleteIfExists(tmp)
|
||||
}
|
||||
if (automaticallyExtractAttachments) {
|
||||
val extractTo = storePath.resolve("${id}.jar")
|
||||
try {
|
||||
Files.createDirectory(extractTo)
|
||||
extractZipFile(finalPath, extractTo)
|
||||
} catch(e: java.nio.file.FileAlreadyExistsException) {
|
||||
log.trace("Did not extract attachment jar to directory because it already exists")
|
||||
} catch(e: Exception) {
|
||||
log.error("Failed to extract attachment jar $id, ", e)
|
||||
// TODO: Delete the extractTo directory here.
|
||||
}
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
private fun checkIsAValidJAR(path: Path) {
|
||||
// Just iterate over the entries with verification enabled: should be good enough to catch mistakes.
|
||||
JarInputStream(Files.newInputStream(path), true).use { stream ->
|
||||
while (true) {
|
||||
val cursor = stream.nextJarEntry ?: break
|
||||
val entryPath = Paths.get(cursor.name)
|
||||
// Security check to stop zips trying to escape their rightful place.
|
||||
if (entryPath.isAbsolute || entryPath.normalize() != entryPath || '\\' in cursor.name)
|
||||
throw IllegalArgumentException("Path is either absolute or non-normalised: $entryPath")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Implementations for AcceptsFileUpload
|
||||
override val dataTypePrefix = "attachment"
|
||||
override val acceptableFileExtensions = listOf(".jar", ".zip")
|
||||
override fun upload(data: InputStream) = importAttachment(data).toString()
|
||||
}
|
231
node/src/main/kotlin/core/node/services/NodeInterestRates.kt
Normal file
231
node/src/main/kotlin/core/node/services/NodeInterestRates.kt
Normal file
@ -0,0 +1,231 @@
|
||||
package core.node.services
|
||||
|
||||
import core.*
|
||||
import core.contracts.*
|
||||
import core.crypto.DigitalSignature
|
||||
import core.crypto.Party
|
||||
import core.crypto.signWithECDSA
|
||||
import core.math.CubicSplineInterpolator
|
||||
import core.math.Interpolator
|
||||
import core.math.InterpolatorFactory
|
||||
import core.messaging.Message
|
||||
import core.messaging.MessagingService
|
||||
import core.messaging.send
|
||||
import core.node.AbstractNode
|
||||
import core.node.AcceptsFileUpload
|
||||
import core.serialization.deserialize
|
||||
import org.slf4j.LoggerFactory
|
||||
import protocols.RatesFixProtocol
|
||||
import java.io.InputStream
|
||||
import java.math.BigDecimal
|
||||
import java.security.KeyPair
|
||||
import java.time.LocalDate
|
||||
import java.util.*
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
/**
|
||||
* An interest rates service is an oracle that signs transactions which contain embedded assertions about an interest
|
||||
* rate fix (e.g. LIBOR, EURIBOR ...).
|
||||
*
|
||||
* The oracle has two functions. It can be queried for a fix for the given day. And it can sign a transaction that
|
||||
* includes a fix that it finds acceptable. So to use it you would query the oracle, incorporate its answer into the
|
||||
* transaction you are building, and then (after possibly extra steps) hand the final transaction back to the oracle
|
||||
* for signing.
|
||||
*/
|
||||
object NodeInterestRates {
|
||||
object Type : ServiceType("corda.interest_rates")
|
||||
/**
|
||||
* The Service that wraps [Oracle] and handles messages/network interaction/request scrubbing.
|
||||
*/
|
||||
class Service(node: AbstractNode) : AcceptsFileUpload, AbstractNodeService(node.services.networkService) {
|
||||
val ss = node.services.storageService
|
||||
val oracle = Oracle(ss.myLegalIdentity, ss.myLegalIdentityKey)
|
||||
|
||||
private val logger = LoggerFactory.getLogger(NodeInterestRates.Service::class.java)
|
||||
|
||||
init {
|
||||
addMessageHandler(RatesFixProtocol.TOPIC_SIGN,
|
||||
{ req: RatesFixProtocol.SignRequest -> oracle.sign(req.tx) },
|
||||
{ message, e -> logger.error("Exception during interest rate oracle request processing", e) }
|
||||
)
|
||||
addMessageHandler(RatesFixProtocol.TOPIC_QUERY,
|
||||
{ req: RatesFixProtocol.QueryRequest -> oracle.query(req.queries) },
|
||||
{ message, e -> logger.error("Exception during interest rate oracle request processing", e) }
|
||||
)
|
||||
}
|
||||
|
||||
// File upload support
|
||||
override val dataTypePrefix = "interest-rates"
|
||||
override val acceptableFileExtensions = listOf(".rates", ".txt")
|
||||
|
||||
override fun upload(data: InputStream): String {
|
||||
val fixes = parseFile(data.bufferedReader().readText())
|
||||
// TODO: Save the uploaded fixes to the storage service and reload on construction.
|
||||
|
||||
// This assignment is thread safe because knownFixes is volatile and the oracle code always snapshots
|
||||
// the pointer to the stack before working with the map.
|
||||
oracle.knownFixes = fixes
|
||||
|
||||
return "Accepted ${fixes.size} new interest rate fixes"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An implementation of an interest rate fix oracle which is given data in a simple string format.
|
||||
*
|
||||
* The oracle will try to interpolate the missing value of a tenor for the given fix name and date.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class Oracle(val identity: Party, private val signingKey: KeyPair) {
|
||||
init {
|
||||
require(signingKey.public == identity.owningKey)
|
||||
}
|
||||
|
||||
@Volatile var knownFixes = FixContainer(emptyList<Fix>())
|
||||
set(value) {
|
||||
require(value.size > 0)
|
||||
field = value
|
||||
}
|
||||
|
||||
fun query(queries: List<FixOf>): List<Fix> {
|
||||
require(queries.isNotEmpty())
|
||||
val knownFixes = knownFixes // Snapshot
|
||||
val answers: List<Fix?> = queries.map { knownFixes[it] }
|
||||
val firstNull = answers.indexOf(null)
|
||||
if (firstNull != -1)
|
||||
throw UnknownFix(queries[firstNull])
|
||||
return answers.filterNotNull()
|
||||
}
|
||||
|
||||
fun sign(wtx: WireTransaction): DigitalSignature.LegallyIdentifiable {
|
||||
// Extract the fix commands marked as being signable by us.
|
||||
val fixes: List<Fix> = wtx.commands.
|
||||
filter { identity.owningKey in it.signers && it.value is Fix }.
|
||||
map { it.value as Fix }
|
||||
|
||||
// Reject this signing attempt if there are no commands of the right kind.
|
||||
if (fixes.isEmpty())
|
||||
throw IllegalArgumentException()
|
||||
|
||||
// For each fix, verify that the data is correct.
|
||||
val knownFixes = knownFixes // Snapshot
|
||||
for (fix in fixes) {
|
||||
val known = knownFixes[fix.of]
|
||||
if (known == null || known != fix)
|
||||
throw UnknownFix(fix.of)
|
||||
}
|
||||
|
||||
// It all checks out, so we can return a signature.
|
||||
//
|
||||
// Note that we will happily sign an invalid transaction: we don't bother trying to validate the whole
|
||||
// thing. This is so that later on we can start using tear-offs.
|
||||
return signingKey.signWithECDSA(wtx.serialized, identity)
|
||||
}
|
||||
}
|
||||
|
||||
class UnknownFix(val fix: FixOf) : Exception() {
|
||||
override fun toString() = "Unknown fix: $fix"
|
||||
}
|
||||
|
||||
/** Fix container, for every fix name & date pair stores a tenor to interest rate map - [InterpolatingRateMap] */
|
||||
class FixContainer(val fixes: List<Fix>, val factory: InterpolatorFactory = CubicSplineInterpolator.Factory) {
|
||||
private val container = buildContainer(fixes)
|
||||
val size = fixes.size
|
||||
|
||||
operator fun get(fixOf: FixOf): Fix? {
|
||||
val rates = container[fixOf.name to fixOf.forDay]
|
||||
val fixValue = rates?.getRate(fixOf.ofTenor) ?: return null
|
||||
return Fix(fixOf, fixValue)
|
||||
}
|
||||
|
||||
private fun buildContainer(fixes: List<Fix>): Map<Pair<String, LocalDate>, InterpolatingRateMap> {
|
||||
val tempContainer = HashMap<Pair<String, LocalDate>, HashMap<Tenor, BigDecimal>>()
|
||||
for (fix in fixes) {
|
||||
val fixOf = fix.of
|
||||
val rates = tempContainer.getOrPut(fixOf.name to fixOf.forDay) { HashMap<Tenor, BigDecimal>() }
|
||||
rates[fixOf.ofTenor] = fix.value
|
||||
}
|
||||
|
||||
// TODO: the calendar data needs to be specified for every fix type in the input string
|
||||
val calendar = BusinessCalendar.getInstance("London", "NewYork")
|
||||
|
||||
return tempContainer.mapValues { InterpolatingRateMap(it.key.second, it.value, calendar, factory) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a mapping between tenors and interest rates.
|
||||
* Interpolates missing values using the provided interpolation mechanism.
|
||||
*/
|
||||
class InterpolatingRateMap(val date: LocalDate,
|
||||
val inputRates: Map<Tenor, BigDecimal>,
|
||||
val calendar: BusinessCalendar,
|
||||
val factory: InterpolatorFactory) {
|
||||
|
||||
/** Snapshot of the input */
|
||||
private val rates = HashMap(inputRates)
|
||||
|
||||
/** Number of rates excluding the interpolated ones */
|
||||
val size = inputRates.size
|
||||
|
||||
private val interpolator: Interpolator? by lazy {
|
||||
// Need to convert tenors to doubles for interpolation
|
||||
val numericMap = rates.mapKeys { daysToMaturity(it.key) }.toSortedMap()
|
||||
val keys = numericMap.keys.map { it.toDouble() }.toDoubleArray()
|
||||
val values = numericMap.values.map { it.toDouble() }.toDoubleArray()
|
||||
|
||||
try {
|
||||
factory.create(keys, values)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
null // Not enough data points for interpolation
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the interest rate for a given [Tenor],
|
||||
* or _null_ if the rate is not found and cannot be interpolated
|
||||
*/
|
||||
fun getRate(tenor: Tenor): BigDecimal? {
|
||||
return rates.getOrElse(tenor) {
|
||||
val rate = interpolate(tenor)
|
||||
if (rate != null) rates.put(tenor, rate)
|
||||
return rate
|
||||
}
|
||||
}
|
||||
|
||||
private fun daysToMaturity(tenor: Tenor) = tenor.daysToMaturity(date, calendar)
|
||||
|
||||
private fun interpolate(tenor: Tenor): BigDecimal? {
|
||||
val key = daysToMaturity(tenor).toDouble()
|
||||
val value = interpolator?.interpolate(key) ?: return null
|
||||
return BigDecimal(value)
|
||||
}
|
||||
}
|
||||
|
||||
/** Parses lines containing fixes */
|
||||
fun parseFile(s: String): FixContainer {
|
||||
val fixes = s.lines().
|
||||
map { it.trim() }.
|
||||
// Filter out comment and empty lines.
|
||||
filterNot { it.startsWith("#") || it.isBlank() }.
|
||||
map { parseFix(it) }
|
||||
return FixContainer(fixes)
|
||||
}
|
||||
|
||||
/** Parses a string of the form "LIBOR 16-March-2016 1M = 0.678" into a [Fix] */
|
||||
fun parseFix(s: String): Fix {
|
||||
val (key, value) = s.split('=').map { it.trim() }
|
||||
val of = parseFixOf(key)
|
||||
val rate = BigDecimal(value)
|
||||
return Fix(of, rate)
|
||||
}
|
||||
|
||||
/** Parses a string of the form "LIBOR 16-March-2016 1M" into a [FixOf] */
|
||||
fun parseFixOf(key: String): FixOf {
|
||||
val words = key.split(' ')
|
||||
val tenorString = words.last()
|
||||
val date = words.dropLast(1).last()
|
||||
val name = words.dropLast(2).joinToString(" ")
|
||||
return FixOf(name, LocalDate.parse(date), Tenor(tenorString))
|
||||
}
|
||||
}
|
96
node/src/main/kotlin/core/node/services/NotaryService.kt
Normal file
96
node/src/main/kotlin/core/node/services/NotaryService.kt
Normal file
@ -0,0 +1,96 @@
|
||||
package core.node.services
|
||||
|
||||
import core.crypto.Party
|
||||
import core.contracts.TimestampCommand
|
||||
import core.contracts.WireTransaction
|
||||
import core.crypto.DigitalSignature
|
||||
import core.crypto.SignedData
|
||||
import core.crypto.signWithECDSA
|
||||
import core.messaging.MessagingService
|
||||
import core.noneOrSingle
|
||||
import core.serialization.SerializedBytes
|
||||
import core.serialization.deserialize
|
||||
import core.serialization.serialize
|
||||
import core.utilities.loggerFor
|
||||
import protocols.NotaryError
|
||||
import protocols.NotaryException
|
||||
import protocols.NotaryProtocol
|
||||
import java.security.KeyPair
|
||||
|
||||
/**
|
||||
* A Notary service acts as the final signer of a transaction ensuring two things:
|
||||
* - The (optional) timestamp of the transaction is valid
|
||||
* - None of the referenced input states have previously been consumed by a transaction signed by this Notary
|
||||
*
|
||||
* A transaction has to be signed by a Notary to be considered valid (except for output-only transactions w/o a timestamp)
|
||||
*/
|
||||
class NotaryService(net: MessagingService,
|
||||
val identity: Party,
|
||||
val signingKey: KeyPair,
|
||||
val uniquenessProvider: UniquenessProvider,
|
||||
val timestampChecker: TimestampChecker) : AbstractNodeService(net) {
|
||||
object Type : ServiceType("corda.notary")
|
||||
|
||||
private val logger = loggerFor<NotaryService>()
|
||||
|
||||
init {
|
||||
check(identity.owningKey == signingKey.public)
|
||||
addMessageHandler(NotaryProtocol.TOPIC,
|
||||
{ req: NotaryProtocol.SignRequest -> processRequest(req.txBits, req.callerIdentity) },
|
||||
{ message, e -> logger.error("Exception during notary service request processing", e) }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the timestamp command is valid (if present) and commits the input state, or returns a conflict
|
||||
* if any of the input states have been previously committed
|
||||
*
|
||||
* Note that the transaction is not checked for contract-validity, as that would require fully resolving it
|
||||
* into a [TransactionForVerification], for which the caller would have to reveal the whole transaction history chain.
|
||||
* As a result, the Notary _will commit invalid transactions_ as well, but as it also records the identity of
|
||||
* the caller, it is possible to raise a dispute and verify the validity of the transaction and subsequently
|
||||
* undo the commit of the input states (the exact mechanism still needs to be worked out)
|
||||
*
|
||||
* TODO: the notary service should only be able to see timestamp commands and inputs
|
||||
*/
|
||||
fun processRequest(txBits: SerializedBytes<WireTransaction>, reqIdentity: Party): NotaryProtocol.Result {
|
||||
val wtx = txBits.deserialize()
|
||||
try {
|
||||
validateTimestamp(wtx)
|
||||
commitInputStates(wtx, reqIdentity)
|
||||
} catch(e: NotaryException) {
|
||||
return NotaryProtocol.Result.withError(e.error)
|
||||
}
|
||||
|
||||
val sig = sign(txBits)
|
||||
return NotaryProtocol.Result.noError(sig)
|
||||
}
|
||||
|
||||
private fun validateTimestamp(tx: WireTransaction) {
|
||||
val timestampCmd = try {
|
||||
tx.commands.noneOrSingle { it.value is TimestampCommand } ?: return
|
||||
} catch (e: IllegalArgumentException) {
|
||||
throw NotaryException(NotaryError.MoreThanOneTimestamp())
|
||||
}
|
||||
if (!timestampCmd.signers.contains(identity.owningKey))
|
||||
throw NotaryException(NotaryError.NotForMe())
|
||||
if (!timestampChecker.isValid(timestampCmd.value as TimestampCommand))
|
||||
throw NotaryException(NotaryError.TimestampInvalid())
|
||||
}
|
||||
|
||||
private fun commitInputStates(tx: WireTransaction, reqIdentity: Party) {
|
||||
try {
|
||||
uniquenessProvider.commit(tx, reqIdentity)
|
||||
} catch (e: UniquenessException) {
|
||||
val conflictData = e.error.serialize()
|
||||
val signedConflict = SignedData(conflictData, sign(conflictData))
|
||||
throw NotaryException(NotaryError.Conflict(tx, signedConflict))
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T : Any> sign(bits: SerializedBytes<T>): DigitalSignature.LegallyIdentifiable {
|
||||
return signingKey.signWithECDSA(bits, identity)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,9 @@
|
||||
package core.node.services
|
||||
|
||||
/**
|
||||
* Placeholder interface for regulator services.
|
||||
*/
|
||||
interface RegulatorService {
|
||||
object Type : ServiceType("corda.regulator")
|
||||
|
||||
}
|
26
node/src/main/kotlin/core/node/services/TimestampChecker.kt
Normal file
26
node/src/main/kotlin/core/node/services/TimestampChecker.kt
Normal file
@ -0,0 +1,26 @@
|
||||
package core.node.services
|
||||
|
||||
import core.contracts.TimestampCommand
|
||||
import core.seconds
|
||||
import core.until
|
||||
import java.time.Clock
|
||||
import java.time.Duration
|
||||
|
||||
/**
|
||||
* Checks if the given timestamp falls within the allowed tolerance interval
|
||||
*/
|
||||
class TimestampChecker(val clock: Clock = Clock.systemDefaultZone(),
|
||||
val tolerance: Duration = 30.seconds) {
|
||||
fun isValid(timestampCommand: TimestampCommand): Boolean {
|
||||
val before = timestampCommand.before
|
||||
val after = timestampCommand.after
|
||||
|
||||
val now = clock.instant()
|
||||
|
||||
// We don't need to test for (before == null && after == null) or backwards bounds because the TimestampCommand
|
||||
// constructor already checks that.
|
||||
if (before != null && before until now > tolerance) return false
|
||||
if (after != null && now until after > tolerance) return false
|
||||
return true
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package core.node.servlets
|
||||
|
||||
import core.crypto.SecureHash
|
||||
import core.node.subsystems.StorageService
|
||||
import core.utilities.loggerFor
|
||||
import java.io.FileNotFoundException
|
||||
import javax.servlet.http.HttpServlet
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
/**
|
||||
* Allows the node administrator to either download full attachment zips, or individual files within those zips.
|
||||
*
|
||||
* GET /attachments/123abcdef12121 -> download the zip identified by this hash
|
||||
* GET /attachments/123abcdef12121/foo.txt -> download that file specifically
|
||||
*
|
||||
* Files are always forced to be downloads, they may not be embedded into web pages for security reasons.
|
||||
*
|
||||
* TODO: See if there's a way to prevent access by JavaScript.
|
||||
* TODO: Provide an endpoint that exposes attachment file listings, to make attachments browseable.
|
||||
*/
|
||||
class AttachmentDownloadServlet : HttpServlet() {
|
||||
private val log = loggerFor<AttachmentDownloadServlet>()
|
||||
|
||||
override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
|
||||
val reqPath = req.pathInfo?.substring(1)
|
||||
if (reqPath == null) {
|
||||
resp.sendError(HttpServletResponse.SC_BAD_REQUEST)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val hash = SecureHash.parse(reqPath.substringBefore('/'))
|
||||
val storage = servletContext.getAttribute("storage") as StorageService
|
||||
val attachment = storage.attachments.openAttachment(hash) ?: throw FileNotFoundException()
|
||||
|
||||
// Don't allow case sensitive matches inside the jar, it'd just be confusing.
|
||||
val subPath = reqPath.substringAfter('/', missingDelimiterValue = "").toLowerCase()
|
||||
|
||||
resp.contentType = "application/octet-stream"
|
||||
if (subPath == "") {
|
||||
resp.addHeader("Content-Disposition", "attachment; filename=\"$hash.zip\"")
|
||||
attachment.open().use { it.copyTo(resp.outputStream) }
|
||||
} else {
|
||||
val filename = subPath.split('/').last()
|
||||
resp.addHeader("Content-Disposition", "attachment; filename=\"$filename\"")
|
||||
attachment.extractFile(subPath, resp.outputStream)
|
||||
}
|
||||
resp.outputStream.close()
|
||||
} catch(e: FileNotFoundException) {
|
||||
log.warn("404 Not Found whilst trying to handle attachment download request for ${servletContext.contextPath}/$reqPath")
|
||||
resp.sendError(HttpServletResponse.SC_NOT_FOUND)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
63
node/src/main/kotlin/core/node/servlets/DataUploadServlet.kt
Normal file
63
node/src/main/kotlin/core/node/servlets/DataUploadServlet.kt
Normal file
@ -0,0 +1,63 @@
|
||||
package core.node.servlets
|
||||
|
||||
import core.node.AcceptsFileUpload
|
||||
import core.node.Node
|
||||
import core.utilities.loggerFor
|
||||
import org.apache.commons.fileupload.servlet.ServletFileUpload
|
||||
import java.util.*
|
||||
import javax.servlet.http.HttpServlet
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
/**
|
||||
* Accepts binary streams, finds the right [AcceptsFileUpload] implementor and hands the stream off to it.
|
||||
*/
|
||||
class DataUploadServlet : HttpServlet() {
|
||||
private val log = loggerFor<DataUploadServlet>()
|
||||
|
||||
override fun doPost(req: HttpServletRequest, resp: HttpServletResponse) {
|
||||
val node = servletContext.getAttribute("node") as Node
|
||||
|
||||
@Suppress("DEPRECATION") // Bogus warning due to superclass static method being deprecated.
|
||||
val isMultipart = ServletFileUpload.isMultipartContent(req)
|
||||
|
||||
if (!isMultipart) {
|
||||
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "This end point is for data uploads only.")
|
||||
return
|
||||
}
|
||||
|
||||
val acceptor: AcceptsFileUpload? = findAcceptor(node, req)
|
||||
if (acceptor == null) {
|
||||
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Got a file upload request for an unknown data type")
|
||||
return
|
||||
}
|
||||
|
||||
val upload = ServletFileUpload()
|
||||
val iterator = upload.getItemIterator(req)
|
||||
val messages = ArrayList<String>()
|
||||
while (iterator.hasNext()) {
|
||||
val item = iterator.next()
|
||||
if (item.name != null && !acceptor.acceptableFileExtensions.any { item.name.endsWith(it) }) {
|
||||
resp.sendError(HttpServletResponse.SC_BAD_REQUEST,
|
||||
"${item.name}: Must be have a filename ending in one of: ${acceptor.acceptableFileExtensions}")
|
||||
return
|
||||
}
|
||||
|
||||
log.info("Receiving ${item.name}")
|
||||
|
||||
item.openStream().use {
|
||||
val message = acceptor.upload(it)
|
||||
log.info("${item.name} successfully accepted: $message")
|
||||
messages += message
|
||||
}
|
||||
}
|
||||
|
||||
// Send back the hashes as a convenience for the user.
|
||||
val writer = resp.writer
|
||||
messages.forEach { writer.println(it) }
|
||||
}
|
||||
|
||||
private fun findAcceptor(node: Node, req: HttpServletRequest): AcceptsFileUpload? {
|
||||
return node.servicesThatAcceptUploads.firstOrNull { req.pathInfo.substring(1).substringBefore('/') == it.dataTypePrefix }
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
package core.node.storage
|
||||
|
||||
import core.serialization.SerializedBytes
|
||||
import core.serialization.deserialize
|
||||
import core.serialization.serialize
|
||||
import core.utilities.loggerFor
|
||||
import core.utilities.trace
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.StandardCopyOption
|
||||
import java.util.*
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
|
||||
/**
|
||||
* File-based checkpoint storage, storing checkpoints per file.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class PerFileCheckpointStorage(val storeDir: Path) : CheckpointStorage {
|
||||
|
||||
companion object {
|
||||
private val logger = loggerFor<PerFileCheckpointStorage>()
|
||||
private val fileExtension = ".checkpoint"
|
||||
}
|
||||
|
||||
private val checkpointFiles = Collections.synchronizedMap(IdentityHashMap<Checkpoint, Path>())
|
||||
|
||||
init {
|
||||
logger.trace { "Initialising per file checkpoint storage on $storeDir" }
|
||||
Files.createDirectories(storeDir)
|
||||
Files.list(storeDir)
|
||||
.filter { it.toString().toLowerCase().endsWith(fileExtension) }
|
||||
.forEach {
|
||||
val checkpoint = Files.readAllBytes(it).deserialize<Checkpoint>()
|
||||
checkpointFiles[checkpoint] = it
|
||||
}
|
||||
}
|
||||
|
||||
override fun addCheckpoint(checkpoint: Checkpoint) {
|
||||
val serialisedCheckpoint = checkpoint.serialize()
|
||||
val fileName = "${serialisedCheckpoint.hash.toString().toLowerCase()}$fileExtension"
|
||||
val checkpointFile = storeDir.resolve(fileName)
|
||||
atomicWrite(checkpointFile, serialisedCheckpoint)
|
||||
logger.trace { "Stored $checkpoint to $checkpointFile" }
|
||||
checkpointFiles[checkpoint] = checkpointFile
|
||||
}
|
||||
|
||||
private fun atomicWrite(checkpointFile: Path, serialisedCheckpoint: SerializedBytes<Checkpoint>) {
|
||||
val tempCheckpointFile = checkpointFile.parent.resolve("${checkpointFile.fileName}.tmp")
|
||||
serialisedCheckpoint.writeToFile(tempCheckpointFile)
|
||||
Files.move(tempCheckpointFile, checkpointFile, StandardCopyOption.ATOMIC_MOVE)
|
||||
}
|
||||
|
||||
override fun removeCheckpoint(checkpoint: Checkpoint) {
|
||||
val checkpointFile = checkpointFiles.remove(checkpoint)
|
||||
require(checkpointFile != null) { "Trying to removing unknown checkpoint: $checkpoint" }
|
||||
Files.delete(checkpointFile)
|
||||
logger.trace { "Removed $checkpoint ($checkpointFile)" }
|
||||
}
|
||||
|
||||
override val checkpoints: Iterable<Checkpoint>
|
||||
get() = synchronized(checkpointFiles) {
|
||||
checkpointFiles.keys.toList()
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,312 @@
|
||||
package core.node.subsystems
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import core.RunOnCallerThread
|
||||
import core.ThreadBox
|
||||
import core.messaging.*
|
||||
import core.node.Node
|
||||
import core.utilities.loggerFor
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
import org.apache.activemq.artemis.api.core.TransportConfiguration
|
||||
import org.apache.activemq.artemis.api.core.client.*
|
||||
import org.apache.activemq.artemis.core.config.BridgeConfiguration
|
||||
import org.apache.activemq.artemis.core.config.Configuration
|
||||
import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl
|
||||
import org.apache.activemq.artemis.core.config.impl.SecurityConfiguration
|
||||
import org.apache.activemq.artemis.core.remoting.impl.invm.InVMAcceptorFactory
|
||||
import org.apache.activemq.artemis.core.remoting.impl.invm.InVMConnectorFactory
|
||||
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptorFactory
|
||||
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactory
|
||||
import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants.HOST_PROP_NAME
|
||||
import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants.PORT_PROP_NAME
|
||||
import org.apache.activemq.artemis.core.security.Role
|
||||
import org.apache.activemq.artemis.core.server.embedded.EmbeddedActiveMQ
|
||||
import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager
|
||||
import org.apache.activemq.artemis.spi.core.security.jaas.InVMLoginModule
|
||||
import java.math.BigInteger
|
||||
import java.nio.file.Path
|
||||
import java.security.SecureRandom
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.Executor
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
// TODO: Verify that nobody can connect to us and fiddle with our config over the socket due to the secman.
|
||||
// TODO: Implement a discovery engine that can trigger builds of new connections when another node registers? (later)
|
||||
// TODO: SSL
|
||||
|
||||
/**
|
||||
* This class implements the [MessagingService] API using Apache Artemis, the successor to their ActiveMQ product.
|
||||
* Artemis is a message queue broker and here, we embed the entire server inside our own process. Nodes communicate
|
||||
* with each other using (by default) an Artemis specific protocol, but it supports other protocols like AQMP/1.0
|
||||
* as well.
|
||||
*
|
||||
* The current implementation is skeletal and lacks features like security or firewall tunnelling (that is, you must
|
||||
* be able to receive TCP connections in order to receive messages). It is good enough for local communication within
|
||||
* a fully connected network, trusted network or on localhost.
|
||||
*
|
||||
* @param directory A place where Artemis can stash its message journal and other files.
|
||||
* @param myHostPort What host and port to bind to for receiving inbound connections.
|
||||
* @param defaultExecutor This will be used as the default executor to run message handlers on, if no other is specified.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class ArtemisMessagingService(val directory: Path, val myHostPort: HostAndPort,
|
||||
val defaultExecutor: Executor = RunOnCallerThread) : MessagingService {
|
||||
// In future: can contain onion routing info, etc.
|
||||
private data class Address(val hostAndPort: HostAndPort) : SingleMessageRecipient
|
||||
|
||||
companion object {
|
||||
val log = loggerFor<ArtemisMessagingService>()
|
||||
|
||||
// This is a "property" attached to an Artemis MQ message object, which contains our own notion of "topic".
|
||||
// We should probably try to unify our notion of "topic" (really, just a string that identifies an endpoint
|
||||
// that will handle messages, like a URL) with the terminology used by underlying MQ libraries, to avoid
|
||||
// confusion.
|
||||
val TOPIC_PROPERTY = "platform-topic"
|
||||
|
||||
/** Temp helper until network map is established. */
|
||||
fun makeRecipient(hostAndPort: HostAndPort): SingleMessageRecipient = Address(hostAndPort)
|
||||
fun makeRecipient(hostname: String) = makeRecipient(toHostAndPort(hostname))
|
||||
fun toHostAndPort(hostname: String) = HostAndPort.fromString(hostname).withDefaultPort(Node.DEFAULT_PORT)
|
||||
}
|
||||
|
||||
private lateinit var mq: EmbeddedActiveMQ
|
||||
private lateinit var clientFactory: ClientSessionFactory
|
||||
private lateinit var session: ClientSession
|
||||
private lateinit var inboundConsumer: ClientConsumer
|
||||
|
||||
private class InnerState {
|
||||
var running = false
|
||||
val sendClients = HashMap<Address, ClientProducer>()
|
||||
}
|
||||
|
||||
private val mutex = ThreadBox(InnerState())
|
||||
|
||||
/** A registration to handle messages of different types */
|
||||
inner class Handler(val executor: Executor?, val topic: String,
|
||||
val callback: (Message, MessageHandlerRegistration) -> Unit) : MessageHandlerRegistration
|
||||
|
||||
private val handlers = CopyOnWriteArrayList<Handler>()
|
||||
|
||||
// TODO: This is not robust and needs to be replaced by more intelligently using the message queue server.
|
||||
private val undeliveredMessages = CopyOnWriteArrayList<Message>()
|
||||
|
||||
private fun getSendClient(address: Address): ClientProducer {
|
||||
return mutex.locked {
|
||||
sendClients.getOrPut(address) {
|
||||
if (address != myAddress) {
|
||||
maybeSetupConnection(address.hostAndPort)
|
||||
}
|
||||
session.createProducer(address.hostAndPort.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun start() {
|
||||
// Wire up various bits of configuration. This is so complicated because Artemis is an embedded message queue
|
||||
// server. Thus we're running both a "server" and a "client" in the same JVM process. A future node might be
|
||||
// able to use an external MQ server instead, for instance, if a bank already has an MQ setup and wishes to
|
||||
// reuse it, or if it makes sense for scaling to split the functionality out, or if it makes sense for security.
|
||||
//
|
||||
// But for now, we bundle it all up into one thing.
|
||||
mq = EmbeddedActiveMQ()
|
||||
val config = createArtemisConfig(directory, myHostPort)
|
||||
mq.setConfiguration(config)
|
||||
val secConfig = SecurityConfiguration()
|
||||
val password = BigInteger(128, SecureRandom.getInstanceStrong()).toString(16)
|
||||
secConfig.addUser("internal", password)
|
||||
secConfig.addRole("internal", "internal")
|
||||
secConfig.defaultUser = "internal"
|
||||
config.securityRoles = mapOf(
|
||||
"#" to setOf(Role("internal", true, true, true, true, true, true, true))
|
||||
)
|
||||
val secManager = ActiveMQJAASSecurityManager(InVMLoginModule::class.java.name, secConfig)
|
||||
mq.setSecurityManager(secManager)
|
||||
|
||||
// Currently we cannot find out if something goes wrong during startup :( This is bug ARTEMIS-388 filed by me.
|
||||
// The fix should be in the 1.3.0 release:
|
||||
//
|
||||
// https://issues.apache.org/jira/browse/ARTEMIS-388
|
||||
mq.start()
|
||||
|
||||
// Connect to our in-memory server.
|
||||
clientFactory = ActiveMQClient.createServerLocatorWithoutHA(
|
||||
TransportConfiguration(InVMConnectorFactory::class.java.name)).createSessionFactory()
|
||||
|
||||
// Create a queue on which to receive messages and set up the handler.
|
||||
session = clientFactory.createSession()
|
||||
session.createQueue(myHostPort.toString(), "inbound", false)
|
||||
inboundConsumer = session.createConsumer("inbound").setMessageHandler { message: ClientMessage ->
|
||||
// This code runs for every inbound message.
|
||||
try {
|
||||
if (!message.containsProperty(TOPIC_PROPERTY)) {
|
||||
log.warn("Received message without a ${TOPIC_PROPERTY} property, ignoring")
|
||||
return@setMessageHandler
|
||||
}
|
||||
val topic = message.getStringProperty(TOPIC_PROPERTY)
|
||||
|
||||
val body = ByteArray(message.bodySize).apply { message.bodyBuffer.readBytes(this) }
|
||||
|
||||
val msg = object : Message {
|
||||
override val topic = topic
|
||||
override val data: ByteArray = body
|
||||
override val debugTimestamp: Instant = Instant.ofEpochMilli(message.timestamp)
|
||||
override val debugMessageID: String = message.messageID.toString()
|
||||
override fun serialise(): ByteArray = body
|
||||
override fun toString() = topic + "#" + String(data)
|
||||
}
|
||||
|
||||
deliverMessage(msg)
|
||||
} finally {
|
||||
message.acknowledge()
|
||||
}
|
||||
}
|
||||
session.start()
|
||||
|
||||
mutex.locked { running = true }
|
||||
}
|
||||
|
||||
private fun deliverMessage(msg: Message): Boolean {
|
||||
// Because handlers is a COW list, the loop inside filter will operate on a snapshot. Handlers being added
|
||||
// or removed whilst the filter is executing will not affect anything.
|
||||
val deliverTo = handlers.filter { it.topic.isBlank() || it.topic == msg.topic }
|
||||
|
||||
if (deliverTo.isEmpty()) {
|
||||
// This should probably be downgraded to a trace in future, so the protocol can evolve with new topics
|
||||
// without causing log spam.
|
||||
log.warn("Received message for ${msg.topic} that doesn't have any registered handlers yet")
|
||||
|
||||
// This is a hack; transient messages held in memory isn't crash resistant.
|
||||
// TODO: Use Artemis API more effectively so we don't pop messages off a queue that we aren't ready to use.
|
||||
undeliveredMessages += msg
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
for (handler in deliverTo) {
|
||||
(handler.executor ?: defaultExecutor).execute {
|
||||
try {
|
||||
handler.callback(msg, handler)
|
||||
} catch(e: Exception) {
|
||||
log.error("Caught exception whilst executing message handler for ${msg.topic}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
mutex.locked {
|
||||
for (producer in sendClients.values)
|
||||
producer.close()
|
||||
sendClients.clear()
|
||||
inboundConsumer.close()
|
||||
session.close()
|
||||
mq.stop()
|
||||
|
||||
// We expect to be garbage collected shortly after being stopped, so we don't null anything explicitly here.
|
||||
|
||||
running = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun send(message: Message, target: MessageRecipients) {
|
||||
if (target !is Address)
|
||||
TODO("Only simple sends to single recipients are currently implemented")
|
||||
val artemisMessage = session.createMessage(true).putStringProperty("platform-topic", message.topic).writeBodyBufferBytes(message.data)
|
||||
getSendClient(target).send(artemisMessage)
|
||||
}
|
||||
|
||||
override fun addMessageHandler(topic: String, executor: Executor?,
|
||||
callback: (Message, MessageHandlerRegistration) -> Unit): MessageHandlerRegistration {
|
||||
val handler = Handler(executor, topic, callback)
|
||||
handlers.add(handler)
|
||||
undeliveredMessages.removeIf { deliverMessage(it) }
|
||||
return handler
|
||||
}
|
||||
|
||||
override fun removeMessageHandler(registration: MessageHandlerRegistration) {
|
||||
handlers.remove(registration)
|
||||
}
|
||||
|
||||
override fun createMessage(topic: String, data: ByteArray): Message {
|
||||
// TODO: We could write an object that proxies directly to an underlying MQ message here and avoid copying.
|
||||
return object : Message {
|
||||
override val topic: String get() = topic
|
||||
override val data: ByteArray get() = data
|
||||
override val debugTimestamp: Instant = Instant.now()
|
||||
override fun serialise(): ByteArray = this.serialise()
|
||||
override val debugMessageID: String get() = Instant.now().toEpochMilli().toString()
|
||||
override fun toString() = topic + "#" + String(data)
|
||||
}
|
||||
}
|
||||
|
||||
override val myAddress: SingleMessageRecipient = Address(myHostPort)
|
||||
|
||||
private enum class ConnectionDirection { INBOUND, OUTBOUND }
|
||||
|
||||
private fun maybeSetupConnection(hostAndPort: HostAndPort) {
|
||||
val name = hostAndPort.toString()
|
||||
|
||||
// To make ourselves talk to a remote server, we need a "bridge". Bridges are things inside Artemis that know how
|
||||
// to handle remote machines going away temporarily, retry connections, etc. They're the bit that handles
|
||||
// unreliable peers. Thus, we need one bridge per node we are talking to.
|
||||
//
|
||||
// Each bridge consumes from a queue on our end and forwards messages to a queue on their end. So for each node
|
||||
// we must create a queue, then create and configure a bridge.
|
||||
//
|
||||
// Note that bridges are not two way. A having a bridge to B does not imply that B can connect back to A. This
|
||||
// becomes important for cases like firewall tunnelling and connection proxying where connectivity is not
|
||||
// entirely duplex. The Artemis team may add this functionality in future:
|
||||
//
|
||||
// https://issues.apache.org/jira/browse/ARTEMIS-355
|
||||
if (!session.queueQuery(SimpleString(name)).isExists) {
|
||||
session.createQueue(name, name, true /* durable */)
|
||||
}
|
||||
if (!mq.activeMQServer.configuration.connectorConfigurations.containsKey(name)) {
|
||||
mq.activeMQServer.configuration.addConnectorConfiguration(name, tcpTransport(ConnectionDirection.OUTBOUND,
|
||||
hostAndPort.hostText, hostAndPort.port))
|
||||
mq.activeMQServer.deployBridge(BridgeConfiguration().apply {
|
||||
setName(name)
|
||||
queueName = name
|
||||
forwardingAddress = name
|
||||
staticConnectors = listOf(name)
|
||||
confirmationWindowSize = 100000 // a guess
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun setConfigDirectories(config: Configuration, dir: Path) {
|
||||
config.apply {
|
||||
bindingsDirectory = dir.resolve("bindings").toString()
|
||||
journalDirectory = dir.resolve("journal").toString()
|
||||
largeMessagesDirectory = dir.resolve("largemessages").toString()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createArtemisConfig(directory: Path, hp: HostAndPort): Configuration {
|
||||
val config = ConfigurationImpl()
|
||||
setConfigDirectories(config, directory)
|
||||
// We will be talking to our server purely in memory.
|
||||
config.acceptorConfigurations = setOf(
|
||||
tcpTransport(ConnectionDirection.INBOUND, "0.0.0.0", hp.port),
|
||||
TransportConfiguration(InVMAcceptorFactory::class.java.name)
|
||||
)
|
||||
return config
|
||||
}
|
||||
|
||||
private fun tcpTransport(direction: ConnectionDirection, host: String, port: Int) =
|
||||
TransportConfiguration(
|
||||
when (direction) {
|
||||
ConnectionDirection.INBOUND -> NettyAcceptorFactory::class.java.name
|
||||
ConnectionDirection.OUTBOUND -> NettyConnectorFactory::class.java.name
|
||||
},
|
||||
mapOf(
|
||||
HOST_PROP_NAME to host,
|
||||
PORT_PROP_NAME to port.toInt()
|
||||
)
|
||||
)
|
||||
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
package core.node.subsystems
|
||||
|
||||
import core.contracts.SignedTransaction
|
||||
import core.messaging.MessagingService
|
||||
import core.node.services.AbstractNodeService
|
||||
import core.utilities.loggerFor
|
||||
import protocols.FetchAttachmentsProtocol
|
||||
import protocols.FetchDataProtocol
|
||||
import protocols.FetchTransactionsProtocol
|
||||
import java.io.InputStream
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
/**
|
||||
* This class sets up network message handlers for requests from peers for data keyed by hash. It is a piece of simple
|
||||
* glue that sits between the network layer and the database layer.
|
||||
*
|
||||
* Note that in our data model, to be able to name a thing by hash automatically gives the power to request it. There
|
||||
* are no access control lists. If you want to keep some data private, then you must be careful who you give its name
|
||||
* to, and trust that they will not pass the name onwards. If someone suspects some data might exist but does not have
|
||||
* its name, then the 256-bit search space they'd have to cover makes it physically impossible to enumerate, and as
|
||||
* such the hash of a piece of data can be seen as a type of password allowing access to it.
|
||||
*
|
||||
* Additionally, because nodes do not store invalid transactions, requesting such a transaction will always yield null.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class DataVendingService(net: MessagingService, private val storage: StorageService) : AbstractNodeService(net) {
|
||||
companion object {
|
||||
val logger = loggerFor<DataVendingService>()
|
||||
}
|
||||
|
||||
init {
|
||||
addMessageHandler(FetchTransactionsProtocol.TOPIC,
|
||||
{ req: FetchDataProtocol.Request -> handleTXRequest(req) },
|
||||
{ message, e -> logger.error("Failure processing data vending request.", e) }
|
||||
)
|
||||
addMessageHandler(FetchAttachmentsProtocol.TOPIC,
|
||||
{ req: FetchDataProtocol.Request -> handleAttachmentRequest(req) },
|
||||
{ message, e -> logger.error("Failure processing data vending request.", e) }
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleTXRequest(req: FetchDataProtocol.Request): List<SignedTransaction?> {
|
||||
require(req.hashes.isNotEmpty())
|
||||
return req.hashes.map {
|
||||
val tx = storage.validatedTransactions[it]
|
||||
if (tx == null)
|
||||
logger.info("Got request for unknown tx $it")
|
||||
tx
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAttachmentRequest(req: FetchDataProtocol.Request): List<ByteArray?> {
|
||||
// TODO: Use Artemis message streaming support here, called "large messages". This avoids the need to buffer.
|
||||
require(req.hashes.isNotEmpty())
|
||||
return req.hashes.map {
|
||||
val jar: InputStream? = storage.attachments.openAttachment(it)?.open()
|
||||
if (jar == null) {
|
||||
logger.info("Got request for unknown attachment $it")
|
||||
null
|
||||
} else {
|
||||
jar.readBytes()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package core.node.subsystems
|
||||
|
||||
import core.ThreadBox
|
||||
import core.crypto.generateKeyPair
|
||||
import core.node.subsystems.KeyManagementService
|
||||
import java.security.KeyPair
|
||||
import java.security.PrivateKey
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
/**
|
||||
* A simple in-memory KMS that doesn't bother saving keys to disk. A real implementation would:
|
||||
*
|
||||
* - Probably be accessed via the network layer as an internal node service i.e. via a message queue, so it can run
|
||||
* on a separate/firewalled service.
|
||||
* - Use the protocol framework so requests to fetch keys can be suspended whilst a human signs off on the request.
|
||||
* - Use deterministic key derivation.
|
||||
* - Possibly have some sort of TREZOR-like two-factor authentication ability
|
||||
*
|
||||
* etc
|
||||
*/
|
||||
@ThreadSafe
|
||||
class E2ETestKeyManagementService : KeyManagementService {
|
||||
private class InnerState {
|
||||
val keys = HashMap<PublicKey, PrivateKey>()
|
||||
}
|
||||
|
||||
private val mutex = ThreadBox(InnerState())
|
||||
|
||||
// Accessing this map clones it.
|
||||
override val keys: Map<PublicKey, PrivateKey> get() = mutex.locked { HashMap(keys) }
|
||||
|
||||
override fun freshKey(): KeyPair {
|
||||
val keypair = generateKeyPair()
|
||||
mutex.locked {
|
||||
keys[keypair.public] = keypair.private
|
||||
}
|
||||
return keypair
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package core.node.subsystems
|
||||
|
||||
import core.crypto.Party
|
||||
import core.node.services.IdentityService
|
||||
import java.security.PublicKey
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
/**
|
||||
* Simple identity service which caches parties and provides functionality for efficient lookup.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class InMemoryIdentityService() : IdentityService {
|
||||
private val keyToParties = ConcurrentHashMap<PublicKey, Party>()
|
||||
private val nameToParties = ConcurrentHashMap<String, Party>()
|
||||
|
||||
override fun registerIdentity(party: Party) {
|
||||
keyToParties[party.owningKey] = party
|
||||
nameToParties[party.name] = party
|
||||
}
|
||||
|
||||
override fun partyFromKey(key: PublicKey): Party? = keyToParties[key]
|
||||
override fun partyFromName(name: String): Party? = nameToParties[name]
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
package core.node.subsystems
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import core.contracts.Contract
|
||||
import core.crypto.Party
|
||||
import core.crypto.SecureHash
|
||||
import core.messaging.MessagingService
|
||||
import core.messaging.runOnNextMessage
|
||||
import core.messaging.send
|
||||
import core.node.NodeInfo
|
||||
import core.node.services.*
|
||||
import core.random63BitValue
|
||||
import core.serialization.deserialize
|
||||
import core.serialization.serialize
|
||||
import core.utilities.AddOrRemove
|
||||
import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
import java.util.*
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
/**
|
||||
* Extremely simple in-memory cache of the network map.
|
||||
*/
|
||||
@ThreadSafe
|
||||
open class InMemoryNetworkMapCache() : NetworkMapCache {
|
||||
override val networkMapNodes: List<NodeInfo>
|
||||
get() = get(NetworkMapService.Type)
|
||||
override val regulators: List<NodeInfo>
|
||||
get() = get(RegulatorService.Type)
|
||||
override val notaryNodes: List<NodeInfo>
|
||||
get() = get(NotaryService.Type)
|
||||
override val ratesOracleNodes: List<NodeInfo>
|
||||
get() = get(NodeInterestRates.Type)
|
||||
override val partyNodes: List<NodeInfo>
|
||||
get() = registeredNodes.map { it.value }
|
||||
|
||||
private var registeredForPush = false
|
||||
protected var registeredNodes = Collections.synchronizedMap(HashMap<Party, NodeInfo>())
|
||||
|
||||
override fun get() = registeredNodes.map { it.value }
|
||||
override fun get(serviceType: ServiceType) = registeredNodes.filterValues { it.advertisedServices.contains(serviceType) }.map { it.value }
|
||||
override fun getRecommended(type: ServiceType, contract: Contract, vararg party: Party): NodeInfo? = get(type).firstOrNull()
|
||||
override fun getNodeByLegalName(name: String) = get().singleOrNull { it.identity.name == name }
|
||||
override fun getNodeByPublicKey(publicKey: PublicKey) = get().singleOrNull { it.identity.owningKey == publicKey }
|
||||
|
||||
override fun addMapService(net: MessagingService, service: NodeInfo, subscribe: Boolean,
|
||||
ifChangedSinceVer: Int?): ListenableFuture<Unit> {
|
||||
if (subscribe && !registeredForPush) {
|
||||
// Add handler to the network, for updates received from the remote network map service.
|
||||
net.addMessageHandler(NetworkMapService.PUSH_PROTOCOL_TOPIC + ".0", null) { message, r ->
|
||||
try {
|
||||
val req = message.data.deserialize<NetworkMapService.Update>()
|
||||
val hash = SecureHash.sha256(req.wireReg.serialize().bits)
|
||||
val ackMessage = net.createMessage(NetworkMapService.PUSH_ACK_PROTOCOL_TOPIC + TOPIC_DEFAULT_POSTFIX,
|
||||
NetworkMapService.UpdateAcknowledge(hash, net.myAddress).serialize().bits)
|
||||
net.send(ackMessage, req.replyTo)
|
||||
processUpdatePush(req)
|
||||
} catch(e: NodeMapError) {
|
||||
NetworkMapCache.logger.warn("Failure during node map update due to bad update: ${e.javaClass.name}")
|
||||
} catch(e: Exception) {
|
||||
NetworkMapCache.logger.error("Exception processing update from network map service", e)
|
||||
}
|
||||
}
|
||||
registeredForPush = true
|
||||
}
|
||||
|
||||
// Fetch the network map and register for updates at the same time
|
||||
val sessionID = random63BitValue()
|
||||
val req = NetworkMapService.FetchMapRequest(subscribe, ifChangedSinceVer, net.myAddress, sessionID)
|
||||
|
||||
// Add a message handler for the response, and prepare a future to put the data into.
|
||||
// Note that the message handler will run on the network thread (not this one).
|
||||
val future = SettableFuture.create<Unit>()
|
||||
net.runOnNextMessage("${NetworkMapService.FETCH_PROTOCOL_TOPIC}.$sessionID", MoreExecutors.directExecutor()) { message ->
|
||||
val resp = message.data.deserialize<NetworkMapService.FetchMapResponse>()
|
||||
// We may not receive any nodes back, if the map hasn't changed since the version specified
|
||||
resp.nodes?.forEach { processRegistration(it) }
|
||||
future.set(Unit)
|
||||
}
|
||||
net.send("${NetworkMapService.FETCH_PROTOCOL_TOPIC}.0", req, service.address)
|
||||
|
||||
return future
|
||||
}
|
||||
|
||||
override fun addNode(node: NodeInfo) {
|
||||
registeredNodes[node.identity] = node
|
||||
}
|
||||
|
||||
override fun removeNode(node: NodeInfo) {
|
||||
registeredNodes.remove(node.identity)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribes from updates from the given map service.
|
||||
*
|
||||
* @param service the network map service to listen to updates from.
|
||||
*/
|
||||
override fun deregisterForUpdates(net: MessagingService, service: NodeInfo): ListenableFuture<Unit> {
|
||||
// Fetch the network map and register for updates at the same time
|
||||
val sessionID = random63BitValue()
|
||||
val req = NetworkMapService.SubscribeRequest(false, net.myAddress, sessionID)
|
||||
|
||||
// Add a message handler for the response, and prepare a future to put the data into.
|
||||
// Note that the message handler will run on the network thread (not this one).
|
||||
val future = SettableFuture.create<Unit>()
|
||||
net.runOnNextMessage("${NetworkMapService.SUBSCRIPTION_PROTOCOL_TOPIC}.$sessionID", MoreExecutors.directExecutor()) { message ->
|
||||
val resp = message.data.deserialize<NetworkMapService.SubscribeResponse>()
|
||||
if (resp.confirmed) {
|
||||
future.set(Unit)
|
||||
} else {
|
||||
future.setException(NetworkCacheError.DeregistrationFailed())
|
||||
}
|
||||
}
|
||||
net.send("${NetworkMapService.SUBSCRIPTION_PROTOCOL_TOPIC}.0", req, service.address)
|
||||
|
||||
return future
|
||||
}
|
||||
|
||||
fun processUpdatePush(req: NetworkMapService.Update) {
|
||||
val reg: NodeRegistration
|
||||
try {
|
||||
reg = req.wireReg.verified()
|
||||
} catch(e: SignatureException) {
|
||||
throw NodeMapError.InvalidSignature()
|
||||
}
|
||||
processRegistration(reg)
|
||||
}
|
||||
|
||||
private fun processRegistration(reg: NodeRegistration) {
|
||||
// TODO: Implement filtering by sequence number, so we only accept changes that are
|
||||
// more recent than the latest change we've processed.
|
||||
when (reg.type) {
|
||||
AddOrRemove.ADD -> addNode(reg.node)
|
||||
AddOrRemove.REMOVE -> removeNode(reg.node)
|
||||
}
|
||||
}
|
||||
}
|
186
node/src/main/kotlin/core/node/subsystems/NodeWalletService.kt
Normal file
186
node/src/main/kotlin/core/node/subsystems/NodeWalletService.kt
Normal file
@ -0,0 +1,186 @@
|
||||
package core.node.subsystems
|
||||
|
||||
import com.codahale.metrics.Gauge
|
||||
import contracts.Cash
|
||||
import core.*
|
||||
import core.contracts.*
|
||||
import core.crypto.Party
|
||||
import core.crypto.SecureHash
|
||||
import core.node.ServiceHub
|
||||
import core.utilities.loggerFor
|
||||
import core.utilities.trace
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
/**
|
||||
* This class implements a simple, in memory wallet that tracks states that are owned by us, and also has a convenience
|
||||
* method to auto-generate some self-issued cash states that can be used for test trading. A real wallet would persist
|
||||
* states relevant to us into a database and once such a wallet is implemented, this scaffolding can be removed.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class NodeWalletService(private val services: ServiceHub) : WalletService {
|
||||
private val log = loggerFor<NodeWalletService>()
|
||||
|
||||
// Variables inside InnerState are protected with a lock by the ThreadBox and aren't in scope unless you're
|
||||
// inside mutex.locked {} code block. So we can't forget to take the lock unless we accidentally leak a reference
|
||||
// to wallet somewhere.
|
||||
private class InnerState {
|
||||
var wallet: Wallet = WalletImpl(emptyList<StateAndRef<OwnableState>>())
|
||||
}
|
||||
|
||||
private val mutex = ThreadBox(InnerState())
|
||||
|
||||
override val currentWallet: Wallet get() = mutex.locked { wallet }
|
||||
|
||||
/**
|
||||
* Returns a snapshot of how much cash we have in each currency, ignoring details like issuer. Note: currencies for
|
||||
* which we have no cash evaluate to null, not 0.
|
||||
*/
|
||||
override val cashBalances: Map<Currency, Amount> get() = mutex.locked { wallet }.cashBalances
|
||||
|
||||
/**
|
||||
* Returns a snapshot of the heads of LinearStates
|
||||
*/
|
||||
override val linearHeads: Map<SecureHash, StateAndRef<LinearState>>
|
||||
get() = mutex.locked { wallet }.let { wallet ->
|
||||
wallet.states.filterStatesOfType<LinearState>().associateBy { it.state.thread }.mapValues { it.value }
|
||||
}
|
||||
|
||||
override fun notifyAll(txns: Iterable<WireTransaction>): Wallet {
|
||||
val ourKeys = services.keyManagementService.keys.keys
|
||||
|
||||
// Note how terribly incomplete this all is!
|
||||
//
|
||||
// - We don't notify anyone of anything, there are no event listeners.
|
||||
// - We don't handle or even notice invalidations due to double spends of things in our wallet.
|
||||
// - We have no concept of confidence (for txns where there is no definite finality).
|
||||
// - No notification that keys are used, for the case where we observe a spend of our own states.
|
||||
// - No ability to create complex spends.
|
||||
// - No logging or tracking of how the wallet got into this state.
|
||||
// - No persistence.
|
||||
// - Does tx relevancy calculation and key management need to be interlocked? Probably yes.
|
||||
//
|
||||
// ... and many other things .... (Wallet.java in bitcoinj is several thousand lines long)
|
||||
|
||||
mutex.locked {
|
||||
// Starting from the current wallet, keep applying the transaction updates, calculating a new Wallet each
|
||||
// time, until we get to the result (this is perhaps a bit inefficient, but it's functional and easily
|
||||
// unit tested).
|
||||
wallet = txns.fold(currentWallet) { current, tx -> current.update(tx, ourKeys) }
|
||||
exportCashBalancesViaMetrics(wallet)
|
||||
return wallet
|
||||
}
|
||||
}
|
||||
|
||||
private fun isRelevant(state: ContractState, ourKeys: Set<PublicKey>): Boolean {
|
||||
return if (state is OwnableState) {
|
||||
state.owner in ourKeys
|
||||
} else if (state is LinearState) {
|
||||
// It's potentially of interest to the wallet
|
||||
state.isRelevant(ourKeys)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun Wallet.update(tx: WireTransaction, ourKeys: Set<PublicKey>): Wallet {
|
||||
val ourNewStates = tx.outputs.
|
||||
filter { isRelevant(it, ourKeys) }.
|
||||
map { tx.outRef<ContractState>(it) }
|
||||
|
||||
// Now calculate the states that are being spent by this transaction.
|
||||
val consumed: Set<StateRef> = states.map { it.ref }.intersect(tx.inputs)
|
||||
|
||||
// Is transaction irrelevant?
|
||||
if (consumed.isEmpty() && ourNewStates.isEmpty()) {
|
||||
log.trace { "tx ${tx.id} was irrelevant to this wallet, ignoring" }
|
||||
return this
|
||||
}
|
||||
|
||||
// And calculate the new wallet.
|
||||
val newStates = states.filter { it.ref !in consumed } + ourNewStates
|
||||
|
||||
log.trace {
|
||||
"Applied tx ${tx.id.prefixChars()} to the wallet: consumed ${consumed.size} states and added ${newStates.size}"
|
||||
}
|
||||
|
||||
return WalletImpl(newStates)
|
||||
}
|
||||
|
||||
private class BalanceMetric : Gauge<Long> {
|
||||
@Volatile var pennies = 0L
|
||||
override fun getValue(): Long? = pennies
|
||||
}
|
||||
|
||||
private val balanceMetrics = HashMap<Currency, BalanceMetric>()
|
||||
|
||||
private fun exportCashBalancesViaMetrics(wallet: Wallet) {
|
||||
// This is just for demo purposes. We probably shouldn't expose balances via JMX in a real node as that might
|
||||
// be commercially sensitive info that the sysadmins aren't even meant to know.
|
||||
//
|
||||
// Note: exported as pennies.
|
||||
val m = services.monitoringService.metrics
|
||||
for (balance in wallet.cashBalances) {
|
||||
val metric = balanceMetrics.getOrPut(balance.key) {
|
||||
val newMetric = BalanceMetric()
|
||||
m.register("WalletBalances.${balance.key}Pennies", newMetric)
|
||||
newMetric
|
||||
}
|
||||
metric.pennies = balance.value.pennies
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a random set of between (by default) 3 and 10 cash states that add up to the given amount and adds them
|
||||
* to the wallet.
|
||||
*
|
||||
* The cash is self issued with the current nodes identity, as fetched from the storage service. Thus it
|
||||
* would not be trusted by any sensible market participant and is effectively an IOU. If it had been issued by
|
||||
* the central bank, well ... that'd be a different story altogether.
|
||||
*
|
||||
* TODO: Move this out of NodeWalletService
|
||||
*/
|
||||
fun fillWithSomeTestCash(notary: Party, howMuch: Amount, atLeastThisManyStates: Int = 3,
|
||||
atMostThisManyStates: Int = 10, rng: Random = Random()) {
|
||||
val amounts = calculateRandomlySizedAmounts(howMuch, atLeastThisManyStates, atMostThisManyStates, rng)
|
||||
|
||||
val myIdentity = services.storageService.myLegalIdentity
|
||||
val myKey = services.storageService.myLegalIdentityKey
|
||||
|
||||
// We will allocate one state to one transaction, for simplicities sake.
|
||||
val cash = Cash()
|
||||
val transactions = amounts.map { pennies ->
|
||||
// This line is what makes the cash self issued. We just use zero as our deposit reference: we don't need
|
||||
// this field as there's no other database or source of truth we need to sync with.
|
||||
val depositRef = myIdentity.ref(0)
|
||||
|
||||
val issuance = TransactionBuilder()
|
||||
val freshKey = services.keyManagementService.freshKey()
|
||||
cash.generateIssue(issuance, Amount(pennies, howMuch.currency), depositRef, freshKey.public, notary)
|
||||
issuance.signWith(myKey)
|
||||
|
||||
return@map issuance.toSignedTransaction(true)
|
||||
}
|
||||
|
||||
services.recordTransactions(transactions)
|
||||
}
|
||||
|
||||
private fun calculateRandomlySizedAmounts(howMuch: Amount, min: Int, max: Int, rng: Random): LongArray {
|
||||
val numStates = min + Math.floor(rng.nextDouble() * (max - min)).toInt()
|
||||
val amounts = LongArray(numStates)
|
||||
val baseSize = howMuch.pennies / numStates
|
||||
var filledSoFar = 0L
|
||||
for (i in 0..numStates - 1) {
|
||||
if (i < numStates - 1) {
|
||||
// Adjust the amount a bit up or down, to give more realistic amounts (not all identical).
|
||||
amounts[i] = baseSize + (baseSize / 2 * (rng.nextDouble() - 0.5)).toLong()
|
||||
filledSoFar += baseSize
|
||||
} else {
|
||||
// Handle inexact rounding.
|
||||
amounts[i] = howMuch.pennies - filledSoFar
|
||||
}
|
||||
}
|
||||
return amounts
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package core.node.subsystems
|
||||
|
||||
import core.crypto.Party
|
||||
import core.contracts.SignedTransaction
|
||||
import core.crypto.SecureHash
|
||||
import core.node.services.AttachmentStorage
|
||||
import core.node.storage.CheckpointStorage
|
||||
import core.utilities.RecordingMap
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.security.KeyPair
|
||||
import java.util.*
|
||||
|
||||
open class StorageServiceImpl(override val attachments: AttachmentStorage,
|
||||
override val checkpointStorage: CheckpointStorage,
|
||||
override val myLegalIdentityKey: KeyPair,
|
||||
override val myLegalIdentity: Party = Party("Unit test party", myLegalIdentityKey.public),
|
||||
// This parameter is for unit tests that want to observe operation details.
|
||||
val recordingAs: (String) -> String = { tableName -> "" })
|
||||
: StorageService {
|
||||
protected val tables = HashMap<String, MutableMap<*, *>>()
|
||||
|
||||
private fun <K, V> getMapOriginal(tableName: String): MutableMap<K, V> {
|
||||
synchronized(tables) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return tables.getOrPut(tableName) {
|
||||
recorderWrap(Collections.synchronizedMap(HashMap<K, V>()), tableName)
|
||||
} as MutableMap<K, V>
|
||||
}
|
||||
}
|
||||
|
||||
private fun <K, V> recorderWrap(map: MutableMap<K, V>, tableName: String): MutableMap<K, V> {
|
||||
if (recordingAs(tableName) != "")
|
||||
return RecordingMap(map, LoggerFactory.getLogger("recordingmap.${recordingAs(tableName)}"))
|
||||
else
|
||||
return map
|
||||
}
|
||||
|
||||
override val validatedTransactions: MutableMap<SecureHash, SignedTransaction>
|
||||
get() = getMapOriginal("validated-transactions")
|
||||
|
||||
}
|
31
node/src/main/kotlin/core/node/subsystems/WalletImpl.kt
Normal file
31
node/src/main/kotlin/core/node/subsystems/WalletImpl.kt
Normal file
@ -0,0 +1,31 @@
|
||||
package core.node.subsystems
|
||||
|
||||
import contracts.Cash
|
||||
import core.contracts.Amount
|
||||
import core.contracts.ContractState
|
||||
import core.contracts.StateAndRef
|
||||
import core.contracts.sumOrThrow
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* A wallet (name may be temporary) wraps a set of states that are useful for us to keep track of, for instance,
|
||||
* because we own them. This class represents an immutable, stable state of a wallet: it is guaranteed not to
|
||||
* change out from underneath you, even though the canonical currently-best-known wallet may change as we learn
|
||||
* about new transactions from our peers and generate new transactions that consume states ourselves.
|
||||
*
|
||||
* This concrete implementation references Cash contracts.
|
||||
*/
|
||||
class WalletImpl(override val states: List<StateAndRef<ContractState>>) : Wallet() {
|
||||
|
||||
/**
|
||||
* Returns a map of how much cash we have in each currency, ignoring details like issuer. Note: currencies for
|
||||
* which we have no cash evaluate to null (not present in map), not 0.
|
||||
*/
|
||||
override val cashBalances: Map<Currency, Amount> get() = states.
|
||||
// Select the states we own which are cash, ignore the rest, take the amounts.
|
||||
mapNotNull { (it.state as? Cash.State)?.amount }.
|
||||
// Turn into a Map<Currency, List<Amount>> like { GBP -> (£100, £500, etc), USD -> ($2000, $50) }
|
||||
groupBy { it.currency }.
|
||||
// Collapse to Map<Currency, Amount> by summing all the amounts of the same currency together.
|
||||
mapValues { it.value.sumOrThrow() }
|
||||
}
|
115
node/src/main/kotlin/core/protocols/ProtocolStateMachineImpl.kt
Normal file
115
node/src/main/kotlin/core/protocols/ProtocolStateMachineImpl.kt
Normal file
@ -0,0 +1,115 @@
|
||||
package core.protocols
|
||||
|
||||
import co.paralleluniverse.fibers.Fiber
|
||||
import co.paralleluniverse.fibers.FiberScheduler
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import co.paralleluniverse.io.serialization.kryo.KryoSerializer
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import core.messaging.MessageRecipients
|
||||
import core.messaging.StateMachineManager
|
||||
import core.node.ServiceHub
|
||||
import core.serialization.SerializedBytes
|
||||
import core.serialization.createKryo
|
||||
import core.serialization.serialize
|
||||
import core.utilities.UntrustworthyData
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
/**
|
||||
* A ProtocolStateMachine instance is a suspendable fiber that delegates all actual logic to a [ProtocolLogic] instance.
|
||||
* For any given flow there is only one PSM, even if that protocol invokes subprotocols.
|
||||
*
|
||||
* These classes are created by the [StateMachineManager] when a new protocol is started at the topmost level. If
|
||||
* a protocol invokes a sub-protocol, then it will pass along the PSM to the child. The call method of the topmost
|
||||
* logic element gets to return the value that the entire state machine resolves to.
|
||||
*/
|
||||
class ProtocolStateMachineImpl<R>(val logic: ProtocolLogic<R>, scheduler: FiberScheduler, val loggerName: String) : Fiber<R>("protocol", scheduler), ProtocolStateMachine<R> {
|
||||
|
||||
// These fields shouldn't be serialised, so they are marked @Transient.
|
||||
@Transient private var suspendAction: ((result: StateMachineManager.FiberRequest, serialisedFiber: SerializedBytes<ProtocolStateMachineImpl<*>>) -> Unit)? = null
|
||||
@Transient private var resumeWithObject: Any? = null
|
||||
@Transient lateinit override var serviceHub: ServiceHub
|
||||
|
||||
@Transient private var _logger: Logger? = null
|
||||
override val logger: Logger get() {
|
||||
return _logger ?: run {
|
||||
val l = LoggerFactory.getLogger(loggerName)
|
||||
_logger = l
|
||||
return l
|
||||
}
|
||||
}
|
||||
|
||||
@Transient private var _resultFuture: SettableFuture<R>? = SettableFuture.create<R>()
|
||||
/** This future will complete when the call method returns. */
|
||||
val resultFuture: ListenableFuture<R> get() {
|
||||
return _resultFuture ?: run {
|
||||
val f = SettableFuture.create<R>()
|
||||
_resultFuture = f
|
||||
return f
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
logic.psm = this
|
||||
}
|
||||
|
||||
fun prepareForResumeWith(serviceHub: ServiceHub,
|
||||
withObject: Any?,
|
||||
suspendAction: (StateMachineManager.FiberRequest, SerializedBytes<ProtocolStateMachineImpl<*>>) -> Unit) {
|
||||
this.suspendAction = suspendAction
|
||||
this.resumeWithObject = withObject
|
||||
this.serviceHub = serviceHub
|
||||
}
|
||||
|
||||
@Suspendable @Suppress("UNCHECKED_CAST")
|
||||
override fun run(): R {
|
||||
try {
|
||||
val result = logic.call()
|
||||
if (result != null)
|
||||
_resultFuture?.set(result)
|
||||
return result
|
||||
} catch (e: Throwable) {
|
||||
_resultFuture?.setException(e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable @Suppress("UNCHECKED_CAST")
|
||||
private fun <T : Any> suspendAndExpectReceive(with: StateMachineManager.FiberRequest): UntrustworthyData<T> {
|
||||
suspend(with)
|
||||
val tmp = resumeWithObject ?: throw IllegalStateException("Expected to receive something")
|
||||
resumeWithObject = null
|
||||
return UntrustworthyData(tmp as T)
|
||||
}
|
||||
|
||||
@Suspendable @Suppress("UNCHECKED_CAST")
|
||||
override fun <T : Any> sendAndReceive(topic: String, destination: MessageRecipients, sessionIDForSend: Long, sessionIDForReceive: Long,
|
||||
obj: Any, recvType: Class<T>): UntrustworthyData<T> {
|
||||
val result = StateMachineManager.FiberRequest.ExpectingResponse(topic, destination, sessionIDForSend, sessionIDForReceive, obj, recvType)
|
||||
return suspendAndExpectReceive(result)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun <T : Any> receive(topic: String, sessionIDForReceive: Long, recvType: Class<T>): UntrustworthyData<T> {
|
||||
val result = StateMachineManager.FiberRequest.ExpectingResponse(topic, null, -1, sessionIDForReceive, null, recvType)
|
||||
return suspendAndExpectReceive(result)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun send(topic: String, destination: MessageRecipients, sessionID: Long, obj: Any) {
|
||||
val result = StateMachineManager.FiberRequest.NotExpectingResponse(topic, destination, sessionID, obj)
|
||||
suspend(result)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun suspend(with: StateMachineManager.FiberRequest) {
|
||||
parkAndSerialize { fiber, serializer ->
|
||||
// We don't use the passed-in serializer here, because we need to use our own augmented Kryo.
|
||||
val deserializer = getFiberSerializer(false) as KryoSerializer
|
||||
val kryo = createKryo(deserializer.kryo)
|
||||
suspendAction!!(with, this.serialize(kryo))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
134
node/src/main/kotlin/core/testing/IRSSimulation.kt
Normal file
134
node/src/main/kotlin/core/testing/IRSSimulation.kt
Normal file
@ -0,0 +1,134 @@
|
||||
package core.testing
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import contracts.InterestRateSwap
|
||||
import core.*
|
||||
import core.contracts.SignedTransaction
|
||||
import core.contracts.StateAndRef
|
||||
import core.crypto.SecureHash
|
||||
import core.node.subsystems.linearHeadsOfType
|
||||
import core.utilities.JsonSupport
|
||||
import protocols.TwoPartyDealProtocol
|
||||
import java.time.LocalDate
|
||||
import java.util.*
|
||||
|
||||
|
||||
/**
|
||||
* A simulation in which banks execute interest rate swaps with each other, including the fixing events.
|
||||
*/
|
||||
class IRSSimulation(runAsync: Boolean, latencyInjector: InMemoryMessagingNetwork.LatencyCalculator?) : Simulation(runAsync, latencyInjector) {
|
||||
val om = JsonSupport.createDefaultMapper(MockIdentityService(network.identities))
|
||||
|
||||
init {
|
||||
currentDay = LocalDate.of(2016, 3, 10) // Should be 12th but the actual first fixing date gets rolled backwards.
|
||||
}
|
||||
|
||||
private val executeOnNextIteration = Collections.synchronizedList(LinkedList<() -> Unit>())
|
||||
|
||||
override fun start() {
|
||||
startIRSDealBetween(0, 1).success {
|
||||
// Next iteration is a pause.
|
||||
executeOnNextIteration.add {}
|
||||
executeOnNextIteration.add {
|
||||
// Keep fixing until there's no more left to do.
|
||||
doNextFixing(0, 1)?.addListener(object : Runnable {
|
||||
override fun run() {
|
||||
// Pause for an iteration.
|
||||
executeOnNextIteration.add {}
|
||||
executeOnNextIteration.add {
|
||||
doNextFixing(0, 1)?.addListener(this, RunOnCallerThread)
|
||||
}
|
||||
}
|
||||
}, RunOnCallerThread)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun doNextFixing(i: Int, j: Int): ListenableFuture<*>? {
|
||||
println("Doing a fixing between $i and $j")
|
||||
val node1: SimulatedNode = banks[i]
|
||||
val node2: SimulatedNode = banks[j]
|
||||
|
||||
val sessionID = random63BitValue()
|
||||
val swaps: Map<SecureHash, StateAndRef<InterestRateSwap.State>> = node1.services.walletService.linearHeadsOfType<InterestRateSwap.State>()
|
||||
val theDealRef: StateAndRef<InterestRateSwap.State> = swaps.values.single()
|
||||
|
||||
// Do we have any more days left in this deal's lifetime? If not, return.
|
||||
val nextFixingDate = theDealRef.state.calculation.nextFixingDate() ?: return null
|
||||
extraNodeLabels[node1] = "Fixing event on $nextFixingDate"
|
||||
extraNodeLabels[node2] = "Fixing event on $nextFixingDate"
|
||||
|
||||
// For some reason the first fix is always before the effective date.
|
||||
if (nextFixingDate > currentDay)
|
||||
currentDay = nextFixingDate
|
||||
|
||||
val sideA = TwoPartyDealProtocol.Floater(node2.net.myAddress, sessionID, notary.info,
|
||||
theDealRef, node1.services.keyManagementService.freshKey(), sessionID)
|
||||
val sideB = TwoPartyDealProtocol.Fixer(node1.net.myAddress, notary.info.identity,
|
||||
theDealRef, sessionID)
|
||||
|
||||
linkConsensus(listOf(node1, node2, regulators[0]), sideB)
|
||||
linkProtocolProgress(node1, sideA)
|
||||
linkProtocolProgress(node2, sideB)
|
||||
|
||||
// We have to start the protocols in separate iterations, as adding to the SMM effectively 'iterates' that node
|
||||
// in the simulation, so if we don't do this then the two sides seem to act simultaneously.
|
||||
|
||||
val retFuture = SettableFuture.create<Any>()
|
||||
val futA = node1.smm.add("floater", sideA)
|
||||
executeOnNextIteration += {
|
||||
val futB = node2.smm.add("fixer", sideB)
|
||||
Futures.allAsList(futA, futB).then {
|
||||
retFuture.set(null)
|
||||
}
|
||||
}
|
||||
return retFuture
|
||||
}
|
||||
|
||||
private fun startIRSDealBetween(i: Int, j: Int): ListenableFuture<SignedTransaction> {
|
||||
val node1: SimulatedNode = banks[i]
|
||||
val node2: SimulatedNode = banks[j]
|
||||
|
||||
extraNodeLabels[node1] = "Setting up deal"
|
||||
extraNodeLabels[node2] = "Setting up deal"
|
||||
|
||||
// We load the IRS afresh each time because the leg parts of the structure aren't data classes so they don't
|
||||
// have the convenient copy() method that'd let us make small adjustments. Instead they're partly mutable.
|
||||
// TODO: We should revisit this in post-Excalibur cleanup and fix, e.g. by introducing an interface.
|
||||
val irs = om.readValue<InterestRateSwap.State>(javaClass.getResource("trade.json"))
|
||||
irs.fixedLeg.fixedRatePayer = node1.info.identity
|
||||
irs.floatingLeg.floatingRatePayer = node2.info.identity
|
||||
|
||||
if (irs.fixedLeg.effectiveDate < irs.floatingLeg.effectiveDate)
|
||||
currentDay = irs.fixedLeg.effectiveDate
|
||||
else
|
||||
currentDay = irs.floatingLeg.effectiveDate
|
||||
|
||||
val sessionID = random63BitValue()
|
||||
|
||||
val instigator = TwoPartyDealProtocol.Instigator(node2.net.myAddress, notary.info,
|
||||
irs, node1.services.keyManagementService.freshKey(), sessionID)
|
||||
val acceptor = TwoPartyDealProtocol.Acceptor(node1.net.myAddress, notary.info.identity,
|
||||
irs, sessionID)
|
||||
|
||||
// TODO: Eliminate the need for linkProtocolProgress
|
||||
linkConsensus(listOf(node1, node2, regulators[0]), acceptor)
|
||||
linkProtocolProgress(node1, instigator)
|
||||
linkProtocolProgress(node2, acceptor)
|
||||
|
||||
val instigatorFuture: ListenableFuture<SignedTransaction> = node1.smm.add("instigator", instigator)
|
||||
|
||||
return Futures.transformAsync(Futures.allAsList(instigatorFuture, node2.smm.add("acceptor", acceptor))) {
|
||||
instigatorFuture
|
||||
}
|
||||
}
|
||||
|
||||
override fun iterate() {
|
||||
if (executeOnNextIteration.isNotEmpty())
|
||||
executeOnNextIteration.removeAt(0)()
|
||||
super.iterate()
|
||||
}
|
||||
}
|
267
node/src/main/kotlin/core/testing/InMemoryMessagingNetwork.kt
Normal file
267
node/src/main/kotlin/core/testing/InMemoryMessagingNetwork.kt
Normal file
@ -0,0 +1,267 @@
|
||||
package core.testing
|
||||
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
import core.ThreadBox
|
||||
import core.crypto.sha256
|
||||
import core.messaging.*
|
||||
import core.utilities.loggerFor
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
import kotlin.concurrent.schedule
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
/**
|
||||
* An in-memory network allows you to manufacture [InMemoryMessaging]s for a set of participants. Each
|
||||
* [InMemoryMessaging] maintains a queue of messages it has received, and a background thread that dispatches
|
||||
* messages one by one to registered handlers. Alternatively, a messaging system may be manually pumped, in which
|
||||
* case no thread is created and a caller is expected to force delivery one at a time (this is useful for unit
|
||||
* testing).
|
||||
*/
|
||||
@ThreadSafe
|
||||
class InMemoryMessagingNetwork {
|
||||
private var counter = 0 // -1 means stopped.
|
||||
private val handleEndpointMap = HashMap<Handle, InMemoryMessaging>()
|
||||
// All messages are kept here until the messages are pumped off the queue by a caller to the node class.
|
||||
// Queues are created on-demand when a message is sent to an address: the receiving node doesn't have to have
|
||||
// been created yet. If the node identified by the given handle has gone away/been shut down then messages
|
||||
// stack up here waiting for it to come back. The intent of this is to simulate a reliable messaging network.
|
||||
private val messageQueues = HashMap<Handle, LinkedBlockingQueue<Message>>()
|
||||
|
||||
val endpoints: List<InMemoryMessaging> @Synchronized get() = handleEndpointMap.values.toList()
|
||||
|
||||
/**
|
||||
* Creates a node and returns the new object that identifies its location on the network to senders, and the
|
||||
* [InMemoryMessaging] that the recipient/in-memory node uses to receive messages and send messages itself.
|
||||
*
|
||||
* If [manuallyPumped] is set to true, then you are expected to call the [InMemoryMessaging.pump] method on the [InMemoryMessaging]
|
||||
* in order to cause the delivery of a single message, which will occur on the thread of the caller. If set to false
|
||||
* then this class will set up a background thread to deliver messages asynchronously, if the handler specifies no
|
||||
* executor.
|
||||
*/
|
||||
@Synchronized
|
||||
fun createNode(manuallyPumped: Boolean): Pair<Handle, MessagingServiceBuilder<InMemoryMessaging>> {
|
||||
check(counter >= 0) { "In memory network stopped: please recreate." }
|
||||
val builder = createNodeWithID(manuallyPumped, counter) as Builder
|
||||
counter++
|
||||
val id = builder.id
|
||||
return Pair(id, builder)
|
||||
}
|
||||
|
||||
/** Creates a node at the given address: useful if you want to recreate a node to simulate a restart */
|
||||
fun createNodeWithID(manuallyPumped: Boolean, id: Int): MessagingServiceBuilder<InMemoryMessaging> {
|
||||
return Builder(manuallyPumped, Handle(id))
|
||||
}
|
||||
|
||||
private val _allMessages = PublishSubject.create<Triple<SingleMessageRecipient, Message, MessageRecipients>>()
|
||||
/** A stream of (sender, message, recipients) triples */
|
||||
val allMessages: Observable<Triple<SingleMessageRecipient, Message, MessageRecipients>> = _allMessages
|
||||
|
||||
interface LatencyCalculator {
|
||||
fun between(sender: SingleMessageRecipient, receiver: SingleMessageRecipient): Duration
|
||||
}
|
||||
|
||||
/** This can be set to an object which can inject artificial latency between sender/recipient pairs. */
|
||||
@Volatile var latencyCalculator: LatencyCalculator? = null
|
||||
private val timer = Timer()
|
||||
|
||||
@Synchronized
|
||||
private fun msgSend(from: InMemoryMessaging, message: Message, recipients: MessageRecipients) {
|
||||
val calc = latencyCalculator
|
||||
if (calc != null && recipients is SingleMessageRecipient) {
|
||||
// Inject some artificial latency.
|
||||
timer.schedule(calc.between(from.myAddress, recipients).toMillis()) {
|
||||
msgSendInternal(message, recipients)
|
||||
}
|
||||
} else {
|
||||
msgSendInternal(message, recipients)
|
||||
}
|
||||
_allMessages.onNext(Triple(from.myAddress, message, recipients))
|
||||
}
|
||||
|
||||
private fun msgSendInternal(message: Message, recipients: MessageRecipients) {
|
||||
when (recipients) {
|
||||
is Handle -> getQueueForHandle(recipients).add(message)
|
||||
|
||||
is AllPossibleRecipients -> {
|
||||
// This means all possible recipients _that the network knows about at the time_, not literally everyone
|
||||
// who joins into the indefinite future.
|
||||
for (handle in handleEndpointMap.keys)
|
||||
getQueueForHandle(handle).add(message)
|
||||
}
|
||||
else -> throw IllegalArgumentException("Unknown type of recipient handle")
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun netNodeHasShutdown(handle: Handle) {
|
||||
handleEndpointMap.remove(handle)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun getQueueForHandle(recipients: Handle) = messageQueues.getOrPut(recipients) { LinkedBlockingQueue() }
|
||||
|
||||
val everyoneOnline: AllPossibleRecipients = object : AllPossibleRecipients {}
|
||||
|
||||
fun stop() {
|
||||
val nodes = synchronized(this) {
|
||||
counter = -1
|
||||
handleEndpointMap.values.toList()
|
||||
}
|
||||
|
||||
for (node in nodes)
|
||||
node.stop()
|
||||
|
||||
handleEndpointMap.clear()
|
||||
messageQueues.clear()
|
||||
}
|
||||
|
||||
inner class Builder(val manuallyPumped: Boolean, val id: Handle) : MessagingServiceBuilder<InMemoryMessaging> {
|
||||
override fun start(): ListenableFuture<InMemoryMessaging> {
|
||||
synchronized(this@InMemoryMessagingNetwork) {
|
||||
val node = InMemoryMessaging(manuallyPumped, id)
|
||||
handleEndpointMap[id] = node
|
||||
return Futures.immediateFuture(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Handle(val id: Int) : SingleMessageRecipient {
|
||||
override fun toString() = "In memory node $id"
|
||||
override fun equals(other: Any?) = other is Handle && other.id == id
|
||||
override fun hashCode() = id.hashCode()
|
||||
}
|
||||
|
||||
/**
|
||||
* An [InMemoryMessaging] provides a [MessagingService] that isn't backed by any kind of network or disk storage
|
||||
* system, but just uses regular queues on the heap instead. It is intended for unit testing and developer convenience
|
||||
* when all entities on 'the network' are being simulated in-process.
|
||||
*
|
||||
* An instance can be obtained by creating a builder and then using the start method.
|
||||
*/
|
||||
@ThreadSafe
|
||||
inner class InMemoryMessaging(private val manuallyPumped: Boolean, private val handle: Handle) : MessagingService {
|
||||
inner class Handler(val executor: Executor?, val topic: String,
|
||||
val callback: (Message, MessageHandlerRegistration) -> Unit) : MessageHandlerRegistration
|
||||
|
||||
@Volatile
|
||||
protected var running = true
|
||||
|
||||
protected inner class InnerState {
|
||||
val handlers: MutableList<Handler> = ArrayList()
|
||||
val pendingRedelivery = LinkedList<Message>()
|
||||
}
|
||||
|
||||
protected val state = ThreadBox(InnerState())
|
||||
|
||||
override val myAddress: SingleMessageRecipient = handle
|
||||
|
||||
protected val backgroundThread = if (manuallyPumped) null else
|
||||
thread(isDaemon = true, name = "In-memory message dispatcher") {
|
||||
while (!Thread.currentThread().isInterrupted) {
|
||||
try {
|
||||
pumpInternal(true)
|
||||
} catch(e: InterruptedException) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun addMessageHandler(topic: String, executor: Executor?, callback: (Message, MessageHandlerRegistration) -> Unit): MessageHandlerRegistration {
|
||||
check(running)
|
||||
val (handler, items) = state.locked {
|
||||
val handler = Handler(executor, topic, callback).apply { handlers.add(this) }
|
||||
val items = ArrayList(pendingRedelivery)
|
||||
pendingRedelivery.clear()
|
||||
Pair(handler, items)
|
||||
}
|
||||
for (it in items)
|
||||
msgSend(this, it, handle)
|
||||
return handler
|
||||
}
|
||||
|
||||
override fun removeMessageHandler(registration: MessageHandlerRegistration) {
|
||||
check(running)
|
||||
state.locked { check(handlers.remove(registration as Handler)) }
|
||||
}
|
||||
|
||||
override fun send(message: Message, target: MessageRecipients) {
|
||||
check(running)
|
||||
msgSend(this, message, target)
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
if (backgroundThread != null) {
|
||||
backgroundThread.interrupt()
|
||||
backgroundThread.join()
|
||||
}
|
||||
running = false
|
||||
netNodeHasShutdown(handle)
|
||||
}
|
||||
|
||||
/** Returns the given (topic, data) pair as a newly created message object.*/
|
||||
override fun createMessage(topic: String, data: ByteArray): Message {
|
||||
return object : Message {
|
||||
override val topic: String get() = topic
|
||||
override val data: ByteArray get() = data
|
||||
override val debugTimestamp: Instant = Instant.now()
|
||||
override fun serialise(): ByteArray = this.serialise()
|
||||
override val debugMessageID: String get() = serialise().sha256().prefixChars()
|
||||
|
||||
override fun toString() = topic + "#" + String(data)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delivers a single message from the internal queue. If there are no messages waiting to be delivered and block
|
||||
* is true, waits until one has been provided on a different thread via send. If block is false, the return
|
||||
* result indicates whether a message was delivered or not.
|
||||
*/
|
||||
fun pump(block: Boolean): Boolean {
|
||||
check(manuallyPumped)
|
||||
check(running)
|
||||
return pumpInternal(block)
|
||||
}
|
||||
|
||||
private fun pumpInternal(block: Boolean): Boolean {
|
||||
val q = getQueueForHandle(handle)
|
||||
val message = (if (block) q.take() else q.poll()) ?: return false
|
||||
|
||||
val deliverTo = state.locked {
|
||||
val h = handlers.filter { if (it.topic.isBlank()) true else message.topic == it.topic }
|
||||
|
||||
if (h.isEmpty()) {
|
||||
// Got no handlers for this message yet. Keep the message around and attempt redelivery after a new
|
||||
// handler has been registered. The purpose of this path is to make unit tests that have multi-threading
|
||||
// reliable, as a sender may attempt to send a message to a receiver that hasn't finished setting
|
||||
// up a handler for yet. Most unit tests don't run threaded, but we want to test true parallelism at
|
||||
// least sometimes.
|
||||
pendingRedelivery.add(message)
|
||||
return false
|
||||
}
|
||||
|
||||
h
|
||||
}
|
||||
|
||||
for (handler in deliverTo) {
|
||||
// Now deliver via the requested executor, or on this thread if no executor was provided at registration time.
|
||||
(handler.executor ?: MoreExecutors.directExecutor()).execute {
|
||||
try {
|
||||
handler.callback(message, handler)
|
||||
} catch(e: Exception) {
|
||||
loggerFor<InMemoryMessagingNetwork>().error("Caught exception in handler for $this/${handler.topic}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
24
node/src/main/kotlin/core/testing/MockIdentityService.kt
Normal file
24
node/src/main/kotlin/core/testing/MockIdentityService.kt
Normal file
@ -0,0 +1,24 @@
|
||||
package core.testing
|
||||
|
||||
import core.crypto.Party
|
||||
import core.node.services.IdentityService
|
||||
import java.security.PublicKey
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
/**
|
||||
* Scaffolding: a dummy identity service that just expects to have identities loaded off disk or found elsewhere.
|
||||
* This class allows the provided list of identities to be mutated after construction, so it takes the list lock
|
||||
* when doing lookups and recalculates the mapping each time. The ability to change the list is used by the
|
||||
* MockNetwork code.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class MockIdentityService(val identities: List<Party>) : IdentityService {
|
||||
private val keyToParties: Map<PublicKey, Party>
|
||||
get() = synchronized(identities) { identities.associateBy { it.owningKey } }
|
||||
private val nameToParties: Map<String, Party>
|
||||
get() = synchronized(identities) { identities.associateBy { it.name } }
|
||||
|
||||
override fun registerIdentity(party: Party) { throw UnsupportedOperationException() }
|
||||
override fun partyFromKey(key: PublicKey): Party? = keyToParties[key]
|
||||
override fun partyFromName(name: String): Party? = nameToParties[name]
|
||||
}
|
47
node/src/main/kotlin/core/testing/MockNetworkMapCache.kt
Normal file
47
node/src/main/kotlin/core/testing/MockNetworkMapCache.kt
Normal file
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2016 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
|
||||
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
|
||||
* set forth therein.
|
||||
*
|
||||
* All other rights reserved.
|
||||
*/
|
||||
package core.testing
|
||||
|
||||
import co.paralleluniverse.common.util.VisibleForTesting
|
||||
import core.crypto.Party
|
||||
import core.crypto.DummyPublicKey
|
||||
import core.messaging.SingleMessageRecipient
|
||||
import core.node.subsystems.InMemoryNetworkMapCache
|
||||
import core.node.NodeInfo
|
||||
|
||||
/**
|
||||
* Network map cache with no backing map service.
|
||||
*/
|
||||
class MockNetworkMapCache() : InMemoryNetworkMapCache() {
|
||||
data class MockAddress(val id: String): SingleMessageRecipient
|
||||
|
||||
init {
|
||||
var mockNodeA = NodeInfo(MockAddress("bankC:8080"), Party("Bank C", DummyPublicKey("Bank C")))
|
||||
var mockNodeB = NodeInfo(MockAddress("bankD:8080"), Party("Bank D", DummyPublicKey("Bank D")))
|
||||
registeredNodes[mockNodeA.identity] = mockNodeA
|
||||
registeredNodes[mockNodeB.identity] = mockNodeB
|
||||
}
|
||||
|
||||
/**
|
||||
* Directly add a registration to the internal cache. DOES NOT fire the change listeners, as it's
|
||||
* not a change being received.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
fun addRegistration(node: NodeInfo) {
|
||||
registeredNodes[node.identity] = node
|
||||
}
|
||||
|
||||
/**
|
||||
* Directly remove a registration from the internal cache. DOES NOT fire the change listeners, as it's
|
||||
* not a change being received.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
fun deleteRegistration(identity: Party) : Boolean {
|
||||
return registeredNodes.remove(identity) != null
|
||||
}
|
||||
}
|
147
node/src/main/kotlin/core/testing/MockNode.kt
Normal file
147
node/src/main/kotlin/core/testing/MockNode.kt
Normal file
@ -0,0 +1,147 @@
|
||||
package core.testing
|
||||
|
||||
import com.google.common.jimfs.Jimfs
|
||||
import core.crypto.Party
|
||||
import core.messaging.MessagingService
|
||||
import core.messaging.SingleMessageRecipient
|
||||
import core.node.AbstractNode
|
||||
import core.node.NodeConfiguration
|
||||
import core.node.NodeInfo
|
||||
import core.node.PhysicalLocation
|
||||
import core.node.services.NetworkMapService
|
||||
import core.node.services.NotaryService
|
||||
import core.node.services.ServiceType
|
||||
import core.utilities.AffinityExecutor
|
||||
import core.utilities.loggerFor
|
||||
import org.slf4j.Logger
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.security.KeyPair
|
||||
import java.time.Clock
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* A mock node brings up a suite of in-memory services in a fast manner suitable for unit testing.
|
||||
* Components that do IO are either swapped out for mocks, or pointed to a [Jimfs] in memory filesystem.
|
||||
*
|
||||
* Mock network nodes require manual pumping by default: they will not run asynchronous. This means that
|
||||
* for message exchanges to take place (and associated handlers to run), you must call the [runNetwork]
|
||||
* method.
|
||||
*/
|
||||
class MockNetwork(private val threadPerNode: Boolean = false,
|
||||
private val defaultFactory: Factory = MockNetwork.DefaultFactory) {
|
||||
private var counter = 0
|
||||
val filesystem = Jimfs.newFileSystem(com.google.common.jimfs.Configuration.unix())
|
||||
val messagingNetwork = InMemoryMessagingNetwork()
|
||||
|
||||
val identities = ArrayList<Party>()
|
||||
|
||||
private val _nodes = ArrayList<MockNode>()
|
||||
/** A read only view of the current set of executing nodes. */
|
||||
val nodes: List<MockNode> = _nodes
|
||||
|
||||
init {
|
||||
Files.createDirectory(filesystem.getPath("/nodes"))
|
||||
}
|
||||
|
||||
/** Allows customisation of how nodes are created. */
|
||||
interface Factory {
|
||||
fun create(dir: Path, config: NodeConfiguration, network: MockNetwork, networkMapAddr: NodeInfo?,
|
||||
advertisedServices: Set<ServiceType>, id: Int, keyPair: KeyPair?): MockNode
|
||||
}
|
||||
|
||||
object DefaultFactory : Factory {
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork, networkMapAddr: NodeInfo?,
|
||||
advertisedServices: Set<ServiceType>, id: Int, keyPair: KeyPair?): MockNode {
|
||||
return MockNode(dir, config, network, networkMapAddr, advertisedServices, id, keyPair)
|
||||
}
|
||||
}
|
||||
|
||||
open class MockNode(dir: Path, config: NodeConfiguration, val mockNet: MockNetwork, networkMapAddr: NodeInfo?,
|
||||
advertisedServices: Set<ServiceType>, val id: Int, val keyPair: KeyPair?) : AbstractNode(dir, config, networkMapAddr, advertisedServices, Clock.systemUTC()) {
|
||||
override val log: Logger = loggerFor<MockNode>()
|
||||
override val serverThread: AffinityExecutor =
|
||||
if (mockNet.threadPerNode)
|
||||
AffinityExecutor.ServiceAffinityExecutor("Mock node thread", 1)
|
||||
else
|
||||
AffinityExecutor.SAME_THREAD
|
||||
|
||||
// We only need to override the messaging service here, as currently everything that hits disk does so
|
||||
// through the java.nio API which we are already mocking via Jimfs.
|
||||
|
||||
override fun makeMessagingService(): MessagingService {
|
||||
require(id >= 0) { "Node ID must be zero or positive, was passed: " + id }
|
||||
return mockNet.messagingNetwork.createNodeWithID(!mockNet.threadPerNode, id).start().get()
|
||||
}
|
||||
|
||||
override fun makeIdentityService() = MockIdentityService(mockNet.identities)
|
||||
|
||||
override fun startMessagingService() {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
override fun generateKeyPair(): KeyPair? = keyPair ?: super.generateKeyPair()
|
||||
|
||||
// There is no need to slow down the unit tests by initialising CityDatabase
|
||||
override fun findMyLocation(): PhysicalLocation? = null
|
||||
|
||||
override fun start(): MockNode {
|
||||
super.start()
|
||||
mockNet.identities.add(storage.myLegalIdentity)
|
||||
return this
|
||||
}
|
||||
|
||||
val place: PhysicalLocation get() = info.physicalLocation!!
|
||||
}
|
||||
|
||||
/** Returns a started node, optionally created by the passed factory method */
|
||||
fun createNode(networkMapAddress: NodeInfo? = null, forcedID: Int = -1, nodeFactory: Factory = defaultFactory,
|
||||
legalName: String? = null, keyPair: KeyPair? = null, vararg advertisedServices: ServiceType): MockNode {
|
||||
val newNode = forcedID == -1
|
||||
val id = if (newNode) counter++ else forcedID
|
||||
|
||||
val path = filesystem.getPath("/nodes/$id")
|
||||
if (newNode)
|
||||
Files.createDirectories(path.resolve("attachments"))
|
||||
val config = object : NodeConfiguration {
|
||||
override val myLegalName: String = legalName ?: "Mock Company $id"
|
||||
override val exportJMXto: String = ""
|
||||
override val nearestCity: String = "Atlantis"
|
||||
}
|
||||
val node = nodeFactory.create(path, config, this, networkMapAddress, advertisedServices.toSet(), id, keyPair).start()
|
||||
_nodes.add(node)
|
||||
return node
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks every node in order to process any queued up inbound messages. This may in turn result in nodes
|
||||
* sending more messages to each other, thus, a typical usage is to call runNetwork with the [rounds]
|
||||
* parameter set to -1 (the default) which simply runs as many rounds as necessary to result in network
|
||||
* stability (no nodes sent any messages in the last round).
|
||||
*/
|
||||
fun runNetwork(rounds: Int = -1) {
|
||||
fun pumpAll() = messagingNetwork.endpoints.map { it.pump(false) }
|
||||
if (rounds == -1)
|
||||
while (pumpAll().any { it }) {
|
||||
}
|
||||
else
|
||||
repeat(rounds) { pumpAll() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a two node network, in which the first node runs network map and notary services and the other
|
||||
* doesn't.
|
||||
*/
|
||||
fun createTwoNodes(nodeFactory: Factory = defaultFactory, notaryKeyPair: KeyPair? = null): Pair<MockNode, MockNode> {
|
||||
require(nodes.isEmpty())
|
||||
return Pair(
|
||||
createNode(null, -1, nodeFactory, null, notaryKeyPair, NetworkMapService.Type, NotaryService.Type),
|
||||
createNode(nodes[0].info, -1, nodeFactory, null)
|
||||
)
|
||||
}
|
||||
|
||||
fun createNotaryNode(legalName: String? = null, keyPair: KeyPair? = null) = createNode(null, -1, defaultFactory, legalName, keyPair, NetworkMapService.Type, NotaryService.Type)
|
||||
fun createPartyNode(networkMapAddr: NodeInfo, legalName: String? = null, keyPair: KeyPair? = null) = createNode(networkMapAddr, -1, defaultFactory, legalName, keyPair)
|
||||
|
||||
fun addressToNode(address: SingleMessageRecipient): MockNode = nodes.single { it.net.myAddress == address }
|
||||
}
|
236
node/src/main/kotlin/core/testing/Simulation.kt
Normal file
236
node/src/main/kotlin/core/testing/Simulation.kt
Normal file
@ -0,0 +1,236 @@
|
||||
package core.testing
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import core.node.CityDatabase
|
||||
import core.node.NodeConfiguration
|
||||
import core.node.NodeInfo
|
||||
import core.node.PhysicalLocation
|
||||
import core.node.services.NetworkMapService
|
||||
import core.node.services.NodeInterestRates
|
||||
import core.node.services.NotaryService
|
||||
import core.node.services.ServiceType
|
||||
import core.protocols.ProtocolLogic
|
||||
import core.then
|
||||
import core.utilities.ProgressTracker
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import java.nio.file.Path
|
||||
import java.security.KeyPair
|
||||
import java.time.LocalDate
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Base class for network simulations that are based on the unit test / mock environment.
|
||||
*
|
||||
* Sets up some nodes that can run protocols between each other, and exposes their progress trackers. Provides banks
|
||||
* in a few cities around the world.
|
||||
*/
|
||||
abstract class Simulation(val runAsync: Boolean,
|
||||
val latencyInjector: InMemoryMessagingNetwork.LatencyCalculator?) {
|
||||
init {
|
||||
if (!runAsync && latencyInjector != null)
|
||||
throw IllegalArgumentException("The latency injector is only useful when using manual pumping.")
|
||||
}
|
||||
|
||||
val bankLocations = listOf("London", "Frankfurt", "Rome")
|
||||
|
||||
// This puts together a mock network of SimulatedNodes.
|
||||
|
||||
open class SimulatedNode(dir: Path, config: NodeConfiguration, mockNet: MockNetwork, networkMapAddress: NodeInfo?,
|
||||
advertisedServices: Set<ServiceType>, id: Int, keyPair: KeyPair?) : MockNetwork.MockNode(dir, config, mockNet, networkMapAddress, advertisedServices, id, keyPair) {
|
||||
override fun findMyLocation(): PhysicalLocation? = CityDatabase[configuration.nearestCity]
|
||||
}
|
||||
|
||||
inner class BankFactory : MockNetwork.Factory {
|
||||
var counter = 0
|
||||
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork, networkMapAddr: NodeInfo?,
|
||||
advertisedServices: Set<ServiceType>, id: Int, keyPair: KeyPair?): MockNetwork.MockNode {
|
||||
val letter = 'A' + counter
|
||||
val city = bankLocations[counter++ % bankLocations.size]
|
||||
val cfg = object : NodeConfiguration {
|
||||
// TODO: Set this back to "Bank of $city" after video day.
|
||||
override val myLegalName: String = "Bank $letter"
|
||||
override val exportJMXto: String = ""
|
||||
override val nearestCity: String = city
|
||||
}
|
||||
return SimulatedNode(dir, cfg, network, networkMapAddr, advertisedServices, id, keyPair)
|
||||
}
|
||||
|
||||
fun createAll(): List<SimulatedNode> = bankLocations.
|
||||
map { network.createNode(networkMap.info, nodeFactory = this) as SimulatedNode }
|
||||
}
|
||||
|
||||
val bankFactory = BankFactory()
|
||||
|
||||
object NetworkMapNodeFactory : MockNetwork.Factory {
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork,
|
||||
networkMapAddr: NodeInfo?, advertisedServices: Set<ServiceType>, id: Int, keyPair: KeyPair?): MockNetwork.MockNode {
|
||||
require(advertisedServices.contains(NetworkMapService.Type))
|
||||
val cfg = object : NodeConfiguration {
|
||||
override val myLegalName: String = "Network Map Service Provider"
|
||||
override val exportJMXto: String = ""
|
||||
override val nearestCity: String = "Madrid"
|
||||
}
|
||||
|
||||
return object : SimulatedNode(dir, cfg, network, networkMapAddr, advertisedServices, id, keyPair) {}
|
||||
}
|
||||
}
|
||||
|
||||
object NotaryNodeFactory : MockNetwork.Factory {
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork, networkMapAddr: NodeInfo?,
|
||||
advertisedServices: Set<ServiceType>, id: Int, keyPair: KeyPair?): MockNetwork.MockNode {
|
||||
require(advertisedServices.contains(NotaryService.Type))
|
||||
val cfg = object : NodeConfiguration {
|
||||
override val myLegalName: String = "Notary Service"
|
||||
override val exportJMXto: String = ""
|
||||
override val nearestCity: String = "Zurich"
|
||||
}
|
||||
return SimulatedNode(dir, cfg, network, networkMapAddr, advertisedServices, id, keyPair)
|
||||
}
|
||||
}
|
||||
|
||||
object RatesOracleFactory : MockNetwork.Factory {
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork, networkMapAddr: NodeInfo?,
|
||||
advertisedServices: Set<ServiceType>, id: Int, keyPair: KeyPair?): MockNetwork.MockNode {
|
||||
require(advertisedServices.contains(NodeInterestRates.Type))
|
||||
val cfg = object : NodeConfiguration {
|
||||
override val myLegalName: String = "Rates Service Provider"
|
||||
override val exportJMXto: String = ""
|
||||
override val nearestCity: String = "Madrid"
|
||||
}
|
||||
|
||||
return object : SimulatedNode(dir, cfg, network, networkMapAddr, advertisedServices, id, keyPair) {
|
||||
override fun makeInterestRatesOracleService() {
|
||||
super.makeInterestRatesOracleService()
|
||||
interestRatesService.upload(javaClass.getResourceAsStream("example.rates.txt"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object RegulatorFactory : MockNetwork.Factory {
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork, networkMapAddr: NodeInfo?,
|
||||
advertisedServices: Set<ServiceType>, id: Int, keyPair: KeyPair?): MockNetwork.MockNode {
|
||||
val cfg = object : NodeConfiguration {
|
||||
override val myLegalName: String = "Regulator A"
|
||||
override val exportJMXto: String = ""
|
||||
override val nearestCity: String = "Paris"
|
||||
}
|
||||
|
||||
val n = object : SimulatedNode(dir, cfg, network, networkMapAddr, advertisedServices, id, keyPair) {
|
||||
// TODO: Regulatory nodes don't actually exist properly, this is a last minute demo request.
|
||||
// So we just fire a message at a node that doesn't know how to handle it, and it'll ignore it.
|
||||
// But that's fine for visualisation purposes.
|
||||
}
|
||||
return n
|
||||
}
|
||||
}
|
||||
|
||||
val network = MockNetwork(false)
|
||||
|
||||
val regulators: List<SimulatedNode> = listOf(network.createNode(null, nodeFactory = RegulatorFactory) as SimulatedNode)
|
||||
val networkMap: SimulatedNode
|
||||
= network.createNode(null, nodeFactory = NetworkMapNodeFactory, advertisedServices = NetworkMapService.Type) as SimulatedNode
|
||||
val notary: SimulatedNode
|
||||
= network.createNode(null, nodeFactory = NotaryNodeFactory, advertisedServices = NotaryService.Type) as SimulatedNode
|
||||
val ratesOracle: SimulatedNode
|
||||
= network.createNode(null, nodeFactory = RatesOracleFactory, advertisedServices = NodeInterestRates.Type) as SimulatedNode
|
||||
val serviceProviders: List<SimulatedNode> = listOf(notary, ratesOracle)
|
||||
val banks: List<SimulatedNode> = bankFactory.createAll()
|
||||
|
||||
init {
|
||||
// Now wire up the network maps for each node.
|
||||
for (node in regulators + serviceProviders + banks) {
|
||||
node.services.networkMapCache.addNode(node.info)
|
||||
}
|
||||
}
|
||||
|
||||
private val _allProtocolSteps = PublishSubject.create<Pair<SimulatedNode, ProgressTracker.Change>>()
|
||||
private val _doneSteps = PublishSubject.create<Collection<SimulatedNode>>()
|
||||
val allProtocolSteps: Observable<Pair<SimulatedNode, ProgressTracker.Change>> = _allProtocolSteps
|
||||
val doneSteps: Observable<Collection<SimulatedNode>> = _doneSteps
|
||||
|
||||
private var pumpCursor = 0
|
||||
|
||||
/**
|
||||
* The current simulated date. By default this never changes. If you want it to change, you should do so from
|
||||
* within your overridden [iterate] call. Changes in the current day surface in the [dateChanges] observable.
|
||||
*/
|
||||
var currentDay: LocalDate = LocalDate.now()
|
||||
protected set(value) {
|
||||
field = value
|
||||
_dateChanges.onNext(value)
|
||||
}
|
||||
|
||||
private val _dateChanges = PublishSubject.create<LocalDate>()
|
||||
val dateChanges: Observable<LocalDate> = _dateChanges
|
||||
|
||||
/**
|
||||
* A place for simulations to stash human meaningful text about what the node is "thinking", which might appear
|
||||
* in the UI somewhere.
|
||||
*/
|
||||
val extraNodeLabels = Collections.synchronizedMap(HashMap<SimulatedNode, String>())
|
||||
|
||||
/**
|
||||
* Iterates the simulation by one step.
|
||||
*
|
||||
* The default implementation circles around the nodes, pumping until one of them handles a message. The next call
|
||||
* will carry on from where this one stopped. In an environment where you want to take actions between anything
|
||||
* interesting happening, or control the precise speed at which things operate (beyond the latency injector), this
|
||||
* is a useful way to do things.
|
||||
*/
|
||||
open fun iterate() {
|
||||
// Keep going until one of the nodes has something to do, or we have checked every node.
|
||||
val endpoints = network.messagingNetwork.endpoints
|
||||
var countDown = endpoints.size
|
||||
while (countDown > 0) {
|
||||
val handledMessage = endpoints[pumpCursor].pump(false)
|
||||
if (handledMessage) break
|
||||
// If this node had nothing to do, advance the cursor with wraparound and try again.
|
||||
pumpCursor = (pumpCursor + 1) % endpoints.size
|
||||
countDown--
|
||||
}
|
||||
}
|
||||
|
||||
protected fun linkProtocolProgress(node: SimulatedNode, protocol: ProtocolLogic<*>) {
|
||||
val pt = protocol.progressTracker ?: return
|
||||
pt.changes.subscribe { change: ProgressTracker.Change ->
|
||||
// Runs on node thread.
|
||||
_allProtocolSteps.onNext(Pair(node, change))
|
||||
}
|
||||
// This isn't technically a "change" but it helps with UIs to send this notification of the first step.
|
||||
_allProtocolSteps.onNext(Pair(node, ProgressTracker.Change.Position(pt, pt.steps[1])))
|
||||
}
|
||||
|
||||
protected fun linkConsensus(nodes: Collection<SimulatedNode>, protocol: ProtocolLogic<*>) {
|
||||
protocol.progressTracker?.changes?.subscribe { change: ProgressTracker.Change ->
|
||||
// Runs on node thread.
|
||||
if (protocol.progressTracker!!.currentStep == ProgressTracker.DONE) {
|
||||
_doneSteps.onNext(nodes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open fun start() {
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
network.nodes.forEach { it.stop() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a function that returns a future, iterates that function with arguments like (0, 1), (1, 2), (2, 3) etc
|
||||
* each time the returned future completes.
|
||||
*/
|
||||
fun startTradingCircle(tradeBetween: (indexA: Int, indexB: Int) -> ListenableFuture<*>) {
|
||||
fun next(i: Int, j: Int) {
|
||||
tradeBetween(i, j).then {
|
||||
val ni = (i + 1) % banks.size
|
||||
val nj = (j + 1) % banks.size
|
||||
next(ni, nj)
|
||||
}
|
||||
}
|
||||
next(0, 1)
|
||||
}
|
||||
}
|
54
node/src/main/kotlin/core/testing/TradeSimulation.kt
Normal file
54
node/src/main/kotlin/core/testing/TradeSimulation.kt
Normal file
@ -0,0 +1,54 @@
|
||||
package core.testing
|
||||
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import contracts.CommercialPaper
|
||||
import core.*
|
||||
import core.contracts.DOLLARS
|
||||
import core.contracts.SignedTransaction
|
||||
import core.node.subsystems.NodeWalletService
|
||||
import core.utilities.BriefLogFormatter
|
||||
import protocols.TwoPartyTradeProtocol
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Simulates a never ending series of trades that go pair-wise through the banks (e.g. A and B trade with each other,
|
||||
* then B and C trade with each other, then C and A etc).
|
||||
*/
|
||||
class TradeSimulation(runAsync: Boolean, latencyInjector: InMemoryMessagingNetwork.LatencyCalculator?) : Simulation(runAsync, latencyInjector) {
|
||||
override fun start() {
|
||||
BriefLogFormatter.loggingOn("bank", "core.contract.TransactionGroup", "recordingmap")
|
||||
startTradingCircle { i, j -> tradeBetween(i, j) }
|
||||
}
|
||||
|
||||
private fun tradeBetween(buyerBankIndex: Int, sellerBankIndex: Int): ListenableFuture<MutableList<SignedTransaction>> {
|
||||
val buyer = banks[buyerBankIndex]
|
||||
val seller = banks[sellerBankIndex]
|
||||
|
||||
(buyer.services.walletService as NodeWalletService).fillWithSomeTestCash(notary.info.identity, 1500.DOLLARS)
|
||||
|
||||
val issuance = run {
|
||||
val tx = CommercialPaper().generateIssue(seller.info.identity.ref(1, 2, 3), 1100.DOLLARS, Instant.now() + 10.days, notary.info.identity)
|
||||
tx.setTime(Instant.now(), notary.info.identity, 30.seconds)
|
||||
tx.signWith(notary.storage.myLegalIdentityKey)
|
||||
tx.signWith(seller.storage.myLegalIdentityKey)
|
||||
tx.toSignedTransaction(true)
|
||||
}
|
||||
seller.services.storageService.validatedTransactions[issuance.id] = issuance
|
||||
|
||||
val sessionID = random63BitValue()
|
||||
val buyerProtocol = TwoPartyTradeProtocol.Buyer(seller.net.myAddress, notary.info.identity,
|
||||
1000.DOLLARS, CommercialPaper.State::class.java, sessionID)
|
||||
val sellerProtocol = TwoPartyTradeProtocol.Seller(buyer.net.myAddress, notary.info,
|
||||
issuance.tx.outRef(0), 1000.DOLLARS, seller.storage.myLegalIdentityKey, sessionID)
|
||||
|
||||
linkConsensus(listOf(buyer, seller, notary), sellerProtocol)
|
||||
linkProtocolProgress(buyer, buyerProtocol)
|
||||
linkProtocolProgress(seller, sellerProtocol)
|
||||
|
||||
val buyerFuture = buyer.smm.add("bank.$buyerBankIndex.${TwoPartyTradeProtocol.TRADE_TOPIC}.buyer", buyerProtocol)
|
||||
val sellerFuture = seller.smm.add("bank.$sellerBankIndex.${TwoPartyTradeProtocol.TRADE_TOPIC}.seller", sellerProtocol)
|
||||
|
||||
return Futures.successfulAsList(buyerFuture, sellerFuture)
|
||||
}
|
||||
}
|
149
node/src/main/kotlin/core/utilities/ANSIProgressRenderer.kt
Normal file
149
node/src/main/kotlin/core/utilities/ANSIProgressRenderer.kt
Normal file
@ -0,0 +1,149 @@
|
||||
package core.utilities
|
||||
|
||||
import org.fusesource.jansi.Ansi
|
||||
import org.fusesource.jansi.AnsiConsole
|
||||
import org.fusesource.jansi.AnsiOutputStream
|
||||
import rx.Subscription
|
||||
import java.util.logging.ConsoleHandler
|
||||
import java.util.logging.Formatter
|
||||
import java.util.logging.LogRecord
|
||||
import java.util.logging.Logger
|
||||
|
||||
/**
|
||||
* Knows how to render a [ProgressTracker] to the terminal using coloured, emoji-fied output. Useful when writing small
|
||||
* command line tools, demos, tests etc. Just set the [progressTracker] field and it will go ahead and start drawing
|
||||
* if the terminal supports it. Otherwise it just prints out the name of the step whenever it changes.
|
||||
*
|
||||
* TODO: Thread safety
|
||||
*/
|
||||
object ANSIProgressRenderer {
|
||||
private var installedYet = false
|
||||
private var subscription: Subscription? = null
|
||||
|
||||
private class LineBumpingConsoleHandler : ConsoleHandler() {
|
||||
override fun getFormatter(): Formatter = BriefLogFormatter()
|
||||
|
||||
override fun publish(r: LogRecord?) {
|
||||
if (progressTracker != null) {
|
||||
val ansi = Ansi.ansi()
|
||||
repeat(prevLinesDrawn) { ansi.eraseLine().cursorUp(1).eraseLine() }
|
||||
System.out.print(ansi)
|
||||
System.out.flush()
|
||||
}
|
||||
|
||||
super.publish(r)
|
||||
|
||||
if (progressTracker != null)
|
||||
draw(false)
|
||||
}
|
||||
}
|
||||
|
||||
private var usingANSI = false
|
||||
private var loggerRef: Logger? = null
|
||||
|
||||
var progressTracker: ProgressTracker? = null
|
||||
set(value) {
|
||||
subscription?.unsubscribe()
|
||||
|
||||
field = value
|
||||
if (!installedYet) {
|
||||
AnsiConsole.systemInstall()
|
||||
|
||||
// This line looks weird as hell because the magic code to decide if we really have a TTY or not isn't
|
||||
// actually exposed anywhere as a function (weak sauce). So we have to rely on our knowledge of jansi
|
||||
// implementation details.
|
||||
usingANSI = AnsiConsole.wrapOutputStream(System.out) !is AnsiOutputStream
|
||||
|
||||
if (usingANSI) {
|
||||
loggerRef = Logger.getLogger("").apply {
|
||||
val current = handlers[0]
|
||||
removeHandler(current)
|
||||
val new = LineBumpingConsoleHandler()
|
||||
new.level = current.level
|
||||
addHandler(new)
|
||||
}
|
||||
}
|
||||
|
||||
installedYet = true
|
||||
}
|
||||
|
||||
subscription = value?.changes?.subscribe { draw(true) }
|
||||
}
|
||||
|
||||
// prevMessagePrinted is just for non-ANSI mode.
|
||||
private var prevMessagePrinted: String? = null
|
||||
// prevLinesDraw is just for ANSI mode.
|
||||
private var prevLinesDrawn = 0
|
||||
|
||||
private fun draw(moveUp: Boolean) {
|
||||
val pt = progressTracker!!
|
||||
|
||||
if (!usingANSI) {
|
||||
val currentMessage = pt.currentStepRecursive.label
|
||||
if (currentMessage != prevMessagePrinted) {
|
||||
println(currentMessage)
|
||||
prevMessagePrinted = currentMessage
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Handle the case where the number of steps in a progress tracker is changed during execution.
|
||||
val ansi = Ansi.ansi()
|
||||
if (prevLinesDrawn > 0 && moveUp)
|
||||
ansi.cursorUp(prevLinesDrawn)
|
||||
|
||||
// Put a blank line between any logging and us.
|
||||
ansi.eraseLine()
|
||||
ansi.newline()
|
||||
val newLinesDrawn = 1 + pt.renderLevel(ansi, 0, pt.allSteps)
|
||||
if (newLinesDrawn < prevLinesDrawn) {
|
||||
// If some steps were removed from the progress tracker, we don't want to leave junk hanging around below.
|
||||
val linesToClear = prevLinesDrawn - newLinesDrawn
|
||||
repeat(linesToClear) {
|
||||
ansi.eraseLine()
|
||||
ansi.newline()
|
||||
}
|
||||
ansi.cursorUp(linesToClear)
|
||||
}
|
||||
prevLinesDrawn = newLinesDrawn
|
||||
|
||||
// Need to force a flush here in order to ensure stderr/stdout sync up properly.
|
||||
System.out.print(ansi)
|
||||
System.out.flush()
|
||||
}
|
||||
|
||||
// Returns number of lines rendered.
|
||||
private fun ProgressTracker.renderLevel(ansi: Ansi, indent: Int, allSteps: List<Pair<Int, ProgressTracker.Step>>): Int {
|
||||
with(ansi) {
|
||||
var lines = 0
|
||||
for ((index, step) in steps.withIndex()) {
|
||||
// Don't bother rendering these special steps in some cases.
|
||||
if (step == ProgressTracker.UNSTARTED) continue
|
||||
if (indent > 0 && step == ProgressTracker.DONE) continue
|
||||
|
||||
val marker = when {
|
||||
index < stepIndex -> Emoji.CODE_GREEN_TICK + " "
|
||||
index == stepIndex && step == ProgressTracker.DONE -> Emoji.CODE_GREEN_TICK + " "
|
||||
index == stepIndex -> Emoji.CODE_RIGHT_ARROW + " "
|
||||
else -> " "
|
||||
}
|
||||
a(" ".repeat(indent))
|
||||
a(marker)
|
||||
|
||||
val active = index == stepIndex && step != ProgressTracker.DONE
|
||||
if (active) bold()
|
||||
a(step.label)
|
||||
if (active) boldOff()
|
||||
|
||||
eraseLine(Ansi.Erase.FORWARD)
|
||||
newline()
|
||||
lines++
|
||||
|
||||
val child = childrenFor[step]
|
||||
if (child != null)
|
||||
lines += child.renderLevel(ansi, indent + 1, allSteps)
|
||||
}
|
||||
return lines
|
||||
}
|
||||
}
|
||||
}
|
9
node/src/main/kotlin/core/utilities/AddOrRemove.kt
Normal file
9
node/src/main/kotlin/core/utilities/AddOrRemove.kt
Normal file
@ -0,0 +1,9 @@
|
||||
package core.utilities
|
||||
|
||||
/**
|
||||
* Enum for when adding/removing something, for example adding or removing an entry in a directory.
|
||||
*/
|
||||
enum class AddOrRemove {
|
||||
ADD,
|
||||
REMOVE
|
||||
}
|
119
node/src/main/kotlin/core/utilities/AffinityExecutor.kt
Normal file
119
node/src/main/kotlin/core/utilities/AffinityExecutor.kt
Normal file
@ -0,0 +1,119 @@
|
||||
package core.utilities
|
||||
|
||||
import com.google.common.util.concurrent.Uninterruptibles
|
||||
import java.util.*
|
||||
import java.util.concurrent.*
|
||||
import java.util.function.Supplier
|
||||
|
||||
/**
|
||||
* An extended executor interface that supports thread affinity assertions and short circuiting. This can be useful
|
||||
* for ensuring code runs on the right thread, and also for unit testing.
|
||||
*/
|
||||
interface AffinityExecutor : Executor {
|
||||
/** Returns true if the current thread is equal to the thread this executor is backed by. */
|
||||
val isOnThread: Boolean
|
||||
|
||||
/** Throws an IllegalStateException if the current thread is not one of the threads this executor is backed by. */
|
||||
fun checkOnThread() {
|
||||
if (!isOnThread)
|
||||
throw IllegalStateException("On wrong thread: " + Thread.currentThread())
|
||||
}
|
||||
|
||||
/** If isOnThread() then runnable is invoked immediately, otherwise the closure is queued onto the backing thread. */
|
||||
fun executeASAP(runnable: () -> Unit) {
|
||||
if (isOnThread)
|
||||
runnable()
|
||||
else
|
||||
execute(runnable)
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the given function on the executor, blocking until the result is available. Be careful not to deadlock this
|
||||
* way! Make sure the executor can't possibly be waiting for the calling thread.
|
||||
*/
|
||||
fun <T> fetchFrom(fetcher: () -> T): T {
|
||||
if (isOnThread)
|
||||
return fetcher()
|
||||
else
|
||||
return CompletableFuture.supplyAsync(Supplier { fetcher() }, this).get()
|
||||
}
|
||||
|
||||
/**
|
||||
* Posts a no-op task to the executor and blocks this thread waiting for it to complete. This can be useful in
|
||||
* tests when you want to be sure that a previous task submitted via [execute] has completed.
|
||||
*/
|
||||
fun flush() {
|
||||
fetchFrom { }
|
||||
}
|
||||
|
||||
/**
|
||||
* An executor backed by thread pool (which may often have a single thread) which makes it easy to schedule
|
||||
* tasks in the future and verify code is running on the executor.
|
||||
*/
|
||||
class ServiceAffinityExecutor(threadName: String, numThreads: Int) : AffinityExecutor,
|
||||
ThreadPoolExecutor(numThreads, numThreads, 0L, TimeUnit.MILLISECONDS, LinkedBlockingQueue<Runnable>()) {
|
||||
protected val threads = Collections.synchronizedSet(HashSet<Thread>())
|
||||
private val uncaughtExceptionHandler = Thread.currentThread().uncaughtExceptionHandler
|
||||
|
||||
init {
|
||||
setThreadFactory(fun(runnable: Runnable): Thread {
|
||||
val thread = object : Thread() {
|
||||
override fun run() {
|
||||
try {
|
||||
runnable.run()
|
||||
} finally {
|
||||
threads -= this
|
||||
}
|
||||
}
|
||||
}
|
||||
thread.isDaemon = true
|
||||
thread.name = threadName
|
||||
threads += thread
|
||||
return thread
|
||||
})
|
||||
}
|
||||
|
||||
override fun afterExecute(r: Runnable, t: Throwable?) {
|
||||
if (t != null)
|
||||
uncaughtExceptionHandler.uncaughtException(Thread.currentThread(), t)
|
||||
}
|
||||
|
||||
override val isOnThread: Boolean get() = Thread.currentThread() in threads
|
||||
|
||||
companion object {
|
||||
val logger = loggerFor<ServiceAffinityExecutor>()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An executor useful for unit tests: allows the current thread to block until a command arrives from another
|
||||
* thread, which is then executed. Inbound closures/commands stack up until they are cleared by looping.
|
||||
*
|
||||
* @param alwaysQueue If true, executeASAP will never short-circuit and will always queue up.
|
||||
*/
|
||||
class Gate(private val alwaysQueue: Boolean = false) : AffinityExecutor {
|
||||
private val thisThread = Thread.currentThread()
|
||||
private val commandQ = LinkedBlockingQueue<Runnable>()
|
||||
|
||||
override val isOnThread: Boolean
|
||||
get() = !alwaysQueue && Thread.currentThread() === thisThread
|
||||
|
||||
override fun execute(command: Runnable) {
|
||||
Uninterruptibles.putUninterruptibly(commandQ, command)
|
||||
}
|
||||
|
||||
fun waitAndRun() {
|
||||
val runnable = Uninterruptibles.takeUninterruptibly(commandQ)
|
||||
runnable.run()
|
||||
}
|
||||
|
||||
val taskQueueSize: Int get() = commandQ.size
|
||||
}
|
||||
|
||||
companion object {
|
||||
val SAME_THREAD: AffinityExecutor = object : AffinityExecutor {
|
||||
override val isOnThread: Boolean get() = true
|
||||
override fun execute(command: Runnable) = command.run()
|
||||
}
|
||||
}
|
||||
}
|
129
node/src/main/kotlin/core/utilities/JsonSupport.kt
Normal file
129
node/src/main/kotlin/core/utilities/JsonSupport.kt
Normal file
@ -0,0 +1,129 @@
|
||||
package core.utilities
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.core.JsonParseException
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.core.JsonToken
|
||||
import com.fasterxml.jackson.databind.*
|
||||
import com.fasterxml.jackson.databind.deser.std.NumberDeserializers
|
||||
import com.fasterxml.jackson.databind.deser.std.StringArrayDeserializer
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
||||
import core.contracts.BusinessCalendar
|
||||
import core.crypto.Party
|
||||
import core.crypto.SecureHash
|
||||
import core.node.services.IdentityService
|
||||
import java.math.BigDecimal
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
|
||||
/**
|
||||
* Utilities and serialisers for working with JSON representations of basic types. This adds Jackson support for
|
||||
* the java.time API, some core types, and Kotlin data classes.
|
||||
*/
|
||||
object JsonSupport {
|
||||
fun createDefaultMapper(identities: IdentityService): ObjectMapper {
|
||||
val mapper = ServiceHubObjectMapper(identities)
|
||||
mapper.enable(SerializationFeature.INDENT_OUTPUT);
|
||||
mapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
|
||||
mapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)
|
||||
|
||||
val timeModule = SimpleModule("java.time")
|
||||
timeModule.addSerializer(LocalDate::class.java, ToStringSerializer)
|
||||
timeModule.addDeserializer(LocalDate::class.java, LocalDateDeserializer)
|
||||
timeModule.addKeyDeserializer(LocalDate::class.java, LocalDateKeyDeserializer)
|
||||
timeModule.addSerializer(LocalDateTime::class.java, ToStringSerializer)
|
||||
|
||||
val cordaModule = SimpleModule("core")
|
||||
cordaModule.addSerializer(Party::class.java, PartySerializer)
|
||||
cordaModule.addDeserializer(Party::class.java, PartyDeserializer)
|
||||
cordaModule.addSerializer(BigDecimal::class.java, ToStringSerializer)
|
||||
cordaModule.addDeserializer(BigDecimal::class.java, NumberDeserializers.BigDecimalDeserializer())
|
||||
cordaModule.addSerializer(SecureHash::class.java, SecureHashSerializer)
|
||||
// It's slightly remarkable, but apparently Jackson works out that this is the only possibility
|
||||
// for a SecureHash at the moment and tries to use SHA256 directly even though we only give it SecureHash
|
||||
cordaModule.addDeserializer(SecureHash.SHA256::class.java, SecureHashDeserializer())
|
||||
cordaModule.addDeserializer(BusinessCalendar::class.java, CalendarDeserializer)
|
||||
|
||||
mapper.registerModule(timeModule)
|
||||
mapper.registerModule(cordaModule)
|
||||
mapper.registerModule(KotlinModule())
|
||||
return mapper
|
||||
}
|
||||
|
||||
class ServiceHubObjectMapper(val identities: IdentityService) : ObjectMapper()
|
||||
|
||||
object ToStringSerializer : JsonSerializer<Any>() {
|
||||
override fun serialize(obj: Any, generator: JsonGenerator, provider: SerializerProvider) {
|
||||
generator.writeString(obj.toString())
|
||||
}
|
||||
}
|
||||
|
||||
object LocalDateDeserializer : JsonDeserializer<LocalDate>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext): LocalDate {
|
||||
return try {
|
||||
LocalDate.parse(parser.text)
|
||||
} catch (e: Exception) {
|
||||
throw JsonParseException("Invalid LocalDate ${parser.text}: ${e.message}", parser.currentLocation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object LocalDateKeyDeserializer : KeyDeserializer() {
|
||||
override fun deserializeKey(text: String, p1: DeserializationContext): Any? {
|
||||
return LocalDate.parse(text)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object PartySerializer : JsonSerializer<Party>() {
|
||||
override fun serialize(obj: Party, generator: JsonGenerator, provider: SerializerProvider) {
|
||||
generator.writeString(obj.name)
|
||||
}
|
||||
}
|
||||
|
||||
object PartyDeserializer : JsonDeserializer<Party>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext): Party {
|
||||
if (parser.currentToken == JsonToken.FIELD_NAME) {
|
||||
parser.nextToken()
|
||||
}
|
||||
val mapper = parser.codec as ServiceHubObjectMapper
|
||||
// TODO this needs to use some industry identifier(s) not just these human readable names
|
||||
return mapper.identities.partyFromName(parser.text) ?: throw JsonParseException("Could not find a Party with name: ${parser.text}", parser.currentLocation)
|
||||
}
|
||||
}
|
||||
|
||||
object SecureHashSerializer : JsonSerializer<SecureHash>() {
|
||||
override fun serialize(obj: SecureHash, generator: JsonGenerator, provider: SerializerProvider) {
|
||||
generator.writeString(obj.toString())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implemented as a class so that we can instantiate for T
|
||||
*/
|
||||
class SecureHashDeserializer<T : SecureHash> : JsonDeserializer<T>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext): T {
|
||||
if (parser.currentToken == JsonToken.FIELD_NAME) {
|
||||
parser.nextToken()
|
||||
}
|
||||
try {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return SecureHash.parse(parser.text) as T
|
||||
} catch (e: Exception) {
|
||||
throw JsonParseException("Invalid hash ${parser.text}: ${e.message}", parser.currentLocation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object CalendarDeserializer : JsonDeserializer<BusinessCalendar>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext): BusinessCalendar {
|
||||
return try {
|
||||
val array = StringArrayDeserializer.instance.deserialize(parser, context)
|
||||
BusinessCalendar.getInstance(*array)
|
||||
} catch (e: Exception) {
|
||||
throw JsonParseException("Invalid calendar(s) ${parser.text}: ${e.message}", parser.currentLocation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
297
node/src/main/kotlin/protocols/TwoPartyTradeProtocol.kt
Normal file
297
node/src/main/kotlin/protocols/TwoPartyTradeProtocol.kt
Normal file
@ -0,0 +1,297 @@
|
||||
package protocols
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import contracts.Cash
|
||||
import contracts.sumCashBy
|
||||
import core.*
|
||||
import core.contracts.*
|
||||
import core.crypto.DigitalSignature
|
||||
import core.crypto.Party
|
||||
import core.crypto.signWithECDSA
|
||||
import core.messaging.SingleMessageRecipient
|
||||
import core.messaging.StateMachineManager
|
||||
import core.node.NodeInfo
|
||||
import core.protocols.ProtocolLogic
|
||||
import core.utilities.ProgressTracker
|
||||
import core.utilities.trace
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
|
||||
/**
|
||||
* This asset trading protocol 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 hands the now finalised SignedWireTransaction back to B.
|
||||
*
|
||||
* Assuming no malicious termination, they both end the protocol being in posession 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.
|
||||
*
|
||||
* To initiate the protocol, use either the [runBuyer] or [runSeller] methods, depending on which side of the trade
|
||||
* your node is taking. These methods return a future which will complete once the trade is over and a fully signed
|
||||
* transaction is available: you can either block your thread waiting for the protocol to complete by using
|
||||
* [ListenableFuture.get] or more usefully, register a callback that will be invoked when the time comes.
|
||||
*
|
||||
* To see an example of how to use this class, look at the unit tests.
|
||||
*/
|
||||
object TwoPartyTradeProtocol {
|
||||
val TRADE_TOPIC = "platform.trade"
|
||||
|
||||
fun runSeller(smm: StateMachineManager, notary: NodeInfo,
|
||||
otherSide: SingleMessageRecipient, assetToSell: StateAndRef<OwnableState>, price: Amount,
|
||||
myKeyPair: KeyPair, buyerSessionID: Long): ListenableFuture<SignedTransaction> {
|
||||
val seller = Seller(otherSide, notary, assetToSell, price, myKeyPair, buyerSessionID)
|
||||
return smm.add("${TRADE_TOPIC}.seller", seller)
|
||||
}
|
||||
|
||||
fun runBuyer(smm: StateMachineManager, notaryNode: NodeInfo,
|
||||
otherSide: SingleMessageRecipient, acceptablePrice: Amount, typeToBuy: Class<out OwnableState>,
|
||||
sessionID: Long): ListenableFuture<SignedTransaction> {
|
||||
val buyer = Buyer(otherSide, notaryNode.identity, acceptablePrice, typeToBuy, sessionID)
|
||||
return smm.add("$TRADE_TOPIC.buyer", buyer)
|
||||
}
|
||||
|
||||
class UnacceptablePriceException(val givenPrice: Amount) : Exception()
|
||||
class AssetMismatchException(val expectedTypeName: String, val typeName: String) : Exception() {
|
||||
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 protocol message the seller sends to the buyer.
|
||||
class SellerTradeInfo(
|
||||
val assetForSale: StateAndRef<OwnableState>,
|
||||
val price: Amount,
|
||||
val sellerOwnerKey: PublicKey,
|
||||
val sessionID: Long
|
||||
)
|
||||
|
||||
class SignaturesFromSeller(val sellerSig: DigitalSignature.WithKey,
|
||||
val notarySig: DigitalSignature.WithKey)
|
||||
|
||||
open class Seller(val otherSide: SingleMessageRecipient,
|
||||
val notaryNode: NodeInfo,
|
||||
val assetToSell: StateAndRef<OwnableState>,
|
||||
val price: Amount,
|
||||
val myKeyPair: KeyPair,
|
||||
val buyerSessionID: Long,
|
||||
override val progressTracker: ProgressTracker = Seller.tracker()) : ProtocolLogic<SignedTransaction>() {
|
||||
|
||||
companion object {
|
||||
object AWAITING_PROPOSAL : ProgressTracker.Step("Awaiting transaction proposal")
|
||||
|
||||
object VERIFYING : ProgressTracker.Step("Verifying transaction proposal")
|
||||
|
||||
object SIGNING : ProgressTracker.Step("Signing transaction")
|
||||
|
||||
object NOTARY : ProgressTracker.Step("Getting notary signature")
|
||||
|
||||
object SENDING_SIGS : ProgressTracker.Step("Sending transaction signatures to buyer")
|
||||
|
||||
fun tracker() = ProgressTracker(AWAITING_PROPOSAL, VERIFYING, SIGNING, NOTARY, SENDING_SIGS)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val partialTX: SignedTransaction = receiveAndCheckProposedTransaction()
|
||||
|
||||
// These two steps could be done in parallel, in theory. Our framework doesn't support that yet though.
|
||||
val ourSignature = signWithOurKey(partialTX)
|
||||
val notarySignature = getNotarySignature(partialTX)
|
||||
|
||||
return sendSignatures(partialTX, ourSignature, notarySignature)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun getNotarySignature(stx: SignedTransaction): DigitalSignature.LegallyIdentifiable {
|
||||
progressTracker.currentStep = NOTARY
|
||||
return subProtocol(NotaryProtocol(stx.tx))
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun receiveAndCheckProposedTransaction(): SignedTransaction {
|
||||
progressTracker.currentStep = AWAITING_PROPOSAL
|
||||
|
||||
val sessionID = random63BitValue()
|
||||
|
||||
// Make the first message we'll send to kick off the protocol.
|
||||
val hello = SellerTradeInfo(assetToSell, price, myKeyPair.public, sessionID)
|
||||
|
||||
val maybeSTX = sendAndReceive<SignedTransaction>(TRADE_TOPIC, otherSide, buyerSessionID, sessionID, hello)
|
||||
|
||||
progressTracker.currentStep = VERIFYING
|
||||
|
||||
maybeSTX.validate {
|
||||
progressTracker.nextStep()
|
||||
|
||||
// Check that the tx proposed by the buyer is valid.
|
||||
val missingSigs = it.verify(throwIfSignaturesAreMissing = false)
|
||||
if (missingSigs != setOf(myKeyPair.public, notaryNode.identity.owningKey))
|
||||
throw SignatureException("The set of missing signatures is not as expected: $missingSigs")
|
||||
|
||||
val wtx: WireTransaction = it.tx
|
||||
logger.trace { "Received partially signed transaction: ${it.id}" }
|
||||
|
||||
checkDependencies(it)
|
||||
|
||||
// This verifies that the transaction is contract-valid, even though it is missing signatures.
|
||||
serviceHub.verifyTransaction(wtx.toLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments))
|
||||
|
||||
if (wtx.outputs.sumCashBy(myKeyPair.public) != price)
|
||||
throw IllegalArgumentException("Transaction is not sending us the right amount of cash")
|
||||
|
||||
// There are all sorts of funny games a malicious secondary might play here, 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 protocol state machines on top of the messaging layer.
|
||||
|
||||
return it
|
||||
}
|
||||
}
|
||||
|
||||
@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()
|
||||
subProtocol(ResolveTransactionsProtocol(dependencyTxIDs, otherSide))
|
||||
}
|
||||
|
||||
open fun signWithOurKey(partialTX: SignedTransaction): DigitalSignature.WithKey {
|
||||
progressTracker.currentStep = SIGNING
|
||||
return myKeyPair.signWithECDSA(partialTX.txBits)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun sendSignatures(partialTX: SignedTransaction, ourSignature: DigitalSignature.WithKey,
|
||||
notarySignature: DigitalSignature.LegallyIdentifiable): SignedTransaction {
|
||||
progressTracker.currentStep = SENDING_SIGS
|
||||
val fullySigned = partialTX + ourSignature + notarySignature
|
||||
|
||||
logger.trace { "Built finished transaction, sending back to secondary!" }
|
||||
|
||||
send(TRADE_TOPIC, otherSide, buyerSessionID, SignaturesFromSeller(ourSignature, notarySignature))
|
||||
return fullySigned
|
||||
}
|
||||
}
|
||||
|
||||
open class Buyer(val otherSide: SingleMessageRecipient,
|
||||
val notary: Party,
|
||||
val acceptablePrice: Amount,
|
||||
val typeToBuy: Class<out OwnableState>,
|
||||
val sessionID: Long) : ProtocolLogic<SignedTransaction>() {
|
||||
|
||||
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 SWAPPING_SIGNATURES : ProgressTracker.Step("Swapping signatures with the seller")
|
||||
|
||||
override val progressTracker = ProgressTracker(RECEIVING, VERIFYING, SIGNING, SWAPPING_SIGNATURES)
|
||||
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val tradeRequest = receiveAndValidateTradeRequest()
|
||||
|
||||
progressTracker.currentStep = SIGNING
|
||||
val (ptx, cashSigningPubKeys) = assembleSharedTX(tradeRequest)
|
||||
val stx = signWithOurKeys(cashSigningPubKeys, ptx)
|
||||
|
||||
// exitProcess(0)
|
||||
|
||||
val signatures = swapSignaturesWithSeller(stx, tradeRequest.sessionID)
|
||||
|
||||
logger.trace { "Got signatures from seller, verifying ... " }
|
||||
val fullySigned = stx + signatures.sellerSig + signatures.notarySig
|
||||
fullySigned.verify()
|
||||
|
||||
logger.trace { "Signatures received are valid. Trade complete! :-)" }
|
||||
return fullySigned
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun receiveAndValidateTradeRequest(): SellerTradeInfo {
|
||||
progressTracker.currentStep = RECEIVING
|
||||
// Wait for a trade request to come in on our pre-provided session ID.
|
||||
val maybeTradeRequest = receive<SellerTradeInfo>(TRADE_TOPIC, sessionID)
|
||||
|
||||
progressTracker.currentStep = VERIFYING
|
||||
maybeTradeRequest.validate {
|
||||
// What is the seller trying to sell us?
|
||||
val asset = it.assetForSale.state
|
||||
val assetTypeName = asset.javaClass.name
|
||||
logger.trace { "Got trade request for a $assetTypeName: ${it.assetForSale}" }
|
||||
|
||||
// Check the start message for acceptability.
|
||||
check(it.sessionID > 0)
|
||||
if (it.price > acceptablePrice)
|
||||
throw UnacceptablePriceException(it.price)
|
||||
if (!typeToBuy.isInstance(asset))
|
||||
throw AssetMismatchException(typeToBuy.name, assetTypeName)
|
||||
|
||||
// Check the transaction that contains the state which is being resolved.
|
||||
// We only have a hash here, so if we don't know it already, we have to ask for it.
|
||||
subProtocol(ResolveTransactionsProtocol(setOf(it.assetForSale.ref.txhash), otherSide))
|
||||
|
||||
return it
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun swapSignaturesWithSeller(stx: SignedTransaction, theirSessionID: Long): SignaturesFromSeller {
|
||||
progressTracker.currentStep = SWAPPING_SIGNATURES
|
||||
logger.trace { "Sending partially signed transaction to seller" }
|
||||
|
||||
// TODO: Protect against the seller terminating here and leaving us in the lurch without the final tx.
|
||||
|
||||
return sendAndReceive<SignaturesFromSeller>(TRADE_TOPIC, otherSide, theirSessionID, sessionID, stx).validate { it }
|
||||
}
|
||||
|
||||
private fun signWithOurKeys(cashSigningPubKeys: List<PublicKey>, ptx: TransactionBuilder): SignedTransaction {
|
||||
// Now sign the transaction with whatever keys we need to move the cash.
|
||||
for (k in cashSigningPubKeys) {
|
||||
val priv = serviceHub.keyManagementService.toPrivate(k)
|
||||
ptx.signWith(KeyPair(k, priv))
|
||||
}
|
||||
|
||||
return ptx.toSignedTransaction(checkSufficientSignatures = false)
|
||||
}
|
||||
|
||||
private fun assembleSharedTX(tradeRequest: SellerTradeInfo): Pair<TransactionBuilder, List<PublicKey>> {
|
||||
val ptx = TransactionBuilder()
|
||||
// Add input and output states for the movement of cash, by using the Cash contract to generate the states.
|
||||
val wallet = serviceHub.walletService.currentWallet
|
||||
val cashStates = wallet.statesOfType<Cash.State>()
|
||||
val cashSigningPubKeys = Cash().generateSpend(ptx, tradeRequest.price, tradeRequest.sellerOwnerKey, cashStates)
|
||||
// Add inputs/outputs/a command for the movement of the asset.
|
||||
ptx.addInputState(tradeRequest.assetForSale.ref)
|
||||
// Just pick some new public key for now. This won't be linked with our identity in any way, which is what
|
||||
// we want for privacy reasons: the key is here ONLY to manage and control ownership, it is not intended to
|
||||
// reveal who the owner actually is. The key management service is expected to derive a unique key from some
|
||||
// initial seed in order to provide privacy protection.
|
||||
val freshKey = serviceHub.keyManagementService.freshKey()
|
||||
val (command, state) = tradeRequest.assetForSale.state.withNewOwner(freshKey.public)
|
||||
ptx.addOutputState(state)
|
||||
ptx.addCommand(command, tradeRequest.assetForSale.state.owner)
|
||||
|
||||
// And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt
|
||||
// to have one.
|
||||
val currentTime = serviceHub.clock.instant()
|
||||
ptx.setTime(currentTime, notary, 30.seconds)
|
||||
return Pair(ptx, cashSigningPubKeys)
|
||||
}
|
||||
}
|
||||
}
|
756
node/src/main/resources/core/node/cities.txt
Normal file
756
node/src/main/resources/core/node/cities.txt
Normal file
@ -0,0 +1,756 @@
|
||||
# name longitude latitude
|
||||
Shanghai 121.47 31.23
|
||||
Bombay 72.82 18.96
|
||||
Karachi 67.01 24.86
|
||||
Buenos Aires -58.37 -34.61
|
||||
Delhi 77.21 28.67
|
||||
Istanbul 29 41.1
|
||||
Manila 120.97 14.62
|
||||
Sao Paulo -46.63 -23.53
|
||||
Moscow 37.62 55.75
|
||||
Dhaka 90.39 23.7
|
||||
Soul 126.99 37.56
|
||||
Lagos 3.35 6.5
|
||||
Kinshasa 15.32 -4.31
|
||||
Tokyo 139.77 35.67
|
||||
Mexico City -99.14 19.43
|
||||
Jakarta 106.83 -6.18
|
||||
New York -73.94 40.67
|
||||
Tehran 51.43 35.67
|
||||
Cairo 31.25 30.06
|
||||
Lima -77.05 -12.07
|
||||
Peking 116.4 39.93
|
||||
London -0.1 51.52
|
||||
Bogota -74.09 4.63
|
||||
Lahore 74.35 31.56
|
||||
Rio de Janeiro -43.2 -22.91
|
||||
Bangkok 100.5 13.73
|
||||
Bagdad 44.44 33.33
|
||||
Bangalore 77.56 12.97
|
||||
Santiago -70.64 -33.46
|
||||
Calcutta 88.36 22.57
|
||||
Singapore 103.85 1.3
|
||||
Toronto -79.38 43.65
|
||||
Rangoon 96.15 16.79
|
||||
Ibadan 3.93 7.38
|
||||
Riyadh 46.77 24.65
|
||||
Madras 80.27 13.09
|
||||
Chongqing 106.58 29.57
|
||||
Ho Chi Minh City 106.69 10.78
|
||||
Xian 108.9 34.27
|
||||
Wuhan 114.27 30.58
|
||||
Alexandria 29.95 31.22
|
||||
Saint Petersburg 30.32 59.93
|
||||
Hyderabad 78.48 17.4
|
||||
Chengdu 104.07 30.67
|
||||
Abidjan -4.03 5.33
|
||||
Ankara 32.85 39.93
|
||||
Ahmadabad 72.58 23.03
|
||||
Los Angeles -118.41 34.11
|
||||
Tianjin 117.2 39.13
|
||||
Chattagam 91.81 22.33
|
||||
Sydney 151.21 -33.87
|
||||
Yokohama 139.62 35.47
|
||||
Melbourne 144.96 -37.81
|
||||
Shenyang 123.45 41.8
|
||||
Cape Town 18.46 -33.93
|
||||
Berlin 13.38 52.52
|
||||
Pusan 129.03 35.11
|
||||
Montreal -73.57 45.52
|
||||
Harbin 126.65 45.75
|
||||
Durban 30.99 -29.87
|
||||
Gizeh 31.21 30.01
|
||||
Nanjing 118.78 32.05
|
||||
Casablanca -7.62 33.6
|
||||
Pune 73.84 18.53
|
||||
Addis Abeba 38.74 9.03
|
||||
Pyongyang 125.75 39.02
|
||||
Surat 72.82 21.2
|
||||
Madrid -3.71 40.42
|
||||
Guangzhou 113.25 23.12
|
||||
Jiddah 39.17 21.5
|
||||
Kanpur 80.33 26.47
|
||||
Nairobi 36.82 -1.29
|
||||
Jaipur 75.8 26.92
|
||||
Dar es Salaam 39.28 -6.82
|
||||
Salvador -38.5 -12.97
|
||||
Chicago -87.68 41.84
|
||||
Taiyuan 112.55 37.87
|
||||
al-Mawsil 43.14 36.34
|
||||
Faisalabad 73.11 31.41
|
||||
Changchun 125.35 43.87
|
||||
Izmir 27.15 38.43
|
||||
Taibei 121.45 25.02
|
||||
Osaka 135.5 34.68
|
||||
Lakhnau 80.92 26.85
|
||||
Kiev 30.52 50.43
|
||||
Luanda 13.24 -8.82
|
||||
Inchon 126.64 37.48
|
||||
Rome 12.5 41.89
|
||||
Dakar -17.48 14.72
|
||||
Belo Horizonte -43.94 -19.92
|
||||
Fortaleza -38.59 -3.78
|
||||
Mashhad 59.57 36.27
|
||||
Maracaibo -71.66 10.73
|
||||
Kabul 69.17 34.53
|
||||
Santo Domingo -69.91 18.48
|
||||
Taegu 128.6 35.87
|
||||
Brasilia -47.91 -15.78
|
||||
Umm Durman 32.48 15.65
|
||||
Nagpur 79.08 21.16
|
||||
Surabaya 112.74 -7.24
|
||||
Kano 8.52 12
|
||||
Medellin -75.54 6.29
|
||||
Accra -0.2 5.56
|
||||
Nagoya 136.91 35.15
|
||||
Benin 5.62 6.34
|
||||
Shijiazhuang 114.48 38.05
|
||||
Guayaquil -79.9 -2.21
|
||||
Changsha 112.97 28.2
|
||||
Houston -95.39 29.77
|
||||
Khartoum 32.52 15.58
|
||||
Paris 2.34 48.86
|
||||
Cali -76.52 3.44
|
||||
Algiers 3.04 36.77
|
||||
Jinan 117 36.67
|
||||
Havanna -82.39 23.13
|
||||
Tashkent 69.3 41.31
|
||||
Dalian 121.65 38.92
|
||||
Jilin 126.55 43.85
|
||||
Nanchang 115.88 28.68
|
||||
Zhengzhou 113.67 34.75
|
||||
Vancouver -123.13 49.28
|
||||
Johannesburg 28.04 -26.19
|
||||
Bayrut 35.5 33.89
|
||||
Douala 9.71 4.06
|
||||
Jiulong 114.17 22.32
|
||||
Caracas -66.93 10.54
|
||||
Kaduna 7.44 10.52
|
||||
Bucharest 26.1 44.44
|
||||
Ecatepec -99.05 19.6
|
||||
Sapporo 141.34 43.06
|
||||
Port Harcourt 7.01 4.81
|
||||
Hangzhou 120.17 30.25
|
||||
Rawalpindi 73.04 33.6
|
||||
San'a 44.21 15.38
|
||||
Conakry -13.67 9.55
|
||||
Curitiba -49.29 -25.42
|
||||
al-Basrah 47.82 30.53
|
||||
Brisbane 153.02 -27.46
|
||||
Xinyang 114.07 32.13
|
||||
Medan 98.67 3.59
|
||||
Indore 75.86 22.72
|
||||
Manaus -60.02 -3.12
|
||||
Kumasi -1.63 6.69
|
||||
Hamburg 10 53.55
|
||||
Rabat -6.84 34.02
|
||||
Minsk 27.55 53.91
|
||||
Patna 85.13 25.62
|
||||
Valencia -67.98 10.23
|
||||
Bhopal 77.4 23.24
|
||||
Soweto 27.84 -26.28
|
||||
Warsaw 21.02 52.26
|
||||
Qingdao 120.32 36.07
|
||||
Vienna 16.37 48.22
|
||||
Yaounde 11.52 3.87
|
||||
Dubai 55.33 25.27
|
||||
Thana 72.97 19.2
|
||||
Aleppo 37.17 36.23
|
||||
Bekasi 106.97 -6.22
|
||||
Budapest 19.08 47.51
|
||||
Bamako -7.99 12.65
|
||||
Ludhiana 75.84 30.91
|
||||
Harare 31.05 -17.82
|
||||
Esfahan 51.68 32.68
|
||||
Pretoria 28.22 -25.73
|
||||
Barcelona 2.17 41.4
|
||||
Lubumbashi 27.48 -11.66
|
||||
Bandung 107.6 -6.91
|
||||
Guadalajara -103.35 20.67
|
||||
Tangshan 118.19 39.62
|
||||
Muqdisho 45.33 2.05
|
||||
Phoenix -112.07 33.54
|
||||
Damascus 36.32 33.5
|
||||
Quito -78.5 -0.19
|
||||
Agra 78.01 27.19
|
||||
Urumqi 87.58 43.8
|
||||
Davao 125.63 7.11
|
||||
Santa Cruz -63.21 -17.77
|
||||
Antananarivo 47.51 -18.89
|
||||
Kobe 135.17 34.68
|
||||
Juarez -106.49 31.74
|
||||
Tijuana -117.02 32.53
|
||||
Recife -34.92 -8.08
|
||||
Multan 71.45 30.2
|
||||
Ha Noi 105.84 21.03
|
||||
Gaoxiong 120.27 22.63
|
||||
Belem -48.5 -1.44
|
||||
Cordoba -64.19 -31.4
|
||||
Kampala 32.58 0.32
|
||||
Lome 1.35 6.17
|
||||
Hyderabad 68.37 25.38
|
||||
Suzhou 120.62 31.3
|
||||
Vadodara 73.18 22.31
|
||||
Gujranwala 74.18 32.16
|
||||
Bursa 29.08 40.2
|
||||
Mbuji-Mayi 23.59 -6.13
|
||||
Pimpri 73.8 18.62
|
||||
Karaj 50.97 35.8
|
||||
Kyoto 135.75 35.01
|
||||
Tangerang 106.63 -6.18
|
||||
Aba 7.35 5.1
|
||||
Kharkiv 36.22 49.98
|
||||
Puebla -98.22 19.05
|
||||
Nashik 73.78 20.01
|
||||
Kuala Lumpur 101.71 3.16
|
||||
Philadelphia -75.13 40.01
|
||||
Fukuoka 130.41 33.59
|
||||
Taejon 127.43 36.33
|
||||
Lanzhou 103.68 36.05
|
||||
Mecca 39.82 21.43
|
||||
Shantou 116.67 23.37
|
||||
Koyang 126.93 37.7
|
||||
Hefei 117.28 31.85
|
||||
Novosibirsk 82.93 55.04
|
||||
Porto Alegre -51.22 -30.04
|
||||
Adana 35.32 37
|
||||
Makasar 119.41 -5.14
|
||||
Tabriz 46.3 38.08
|
||||
Narayanganj 90.5 23.62
|
||||
Faridabad 77.3 28.38
|
||||
Fushun 123.88 41.87
|
||||
Phnum Penh 104.92 11.57
|
||||
Luoyang 112.47 34.68
|
||||
Khulna 89.56 22.84
|
||||
Depok 106.83 -6.39
|
||||
Lusaka 28.29 -15.42
|
||||
Ghaziabad 77.41 28.66
|
||||
Handan 114.48 36.58
|
||||
San Antonio -98.51 29.46
|
||||
Kawasaki 139.7 35.53
|
||||
Kwangju 126.91 35.16
|
||||
Peshawar 71.54 34.01
|
||||
Rajkot 70.79 22.31
|
||||
Suwon 127.01 37.26
|
||||
Mandalay 96.09 21.98
|
||||
Almaty 76.92 43.32
|
||||
Munich 11.58 48.14
|
||||
Mirat 77.7 28.99
|
||||
Baotou 110.05 40.6
|
||||
Milan 9.19 45.48
|
||||
Rongcheng 116.34 23.54
|
||||
Kalyan 73.16 19.25
|
||||
Montevideo -56.17 -34.87
|
||||
Xianggangdao 114.14 22.27
|
||||
Yekaterinburg 60.6 56.85
|
||||
Ouagadougou -1.53 12.37
|
||||
Guarulhos -46.49 -23.46
|
||||
Semarang 110.42 -6.97
|
||||
Xuzhou 117.18 34.27
|
||||
Perth 115.84 -31.96
|
||||
Dallas -96.77 32.79
|
||||
Stockholm 18.07 59.33
|
||||
Palembang 104.75 -2.99
|
||||
San Diego -117.14 32.81
|
||||
Goiania -49.26 -16.72
|
||||
Gaziantep 37.39 37.07
|
||||
Nizhniy Novgorod 44 56.33
|
||||
Shiraz 52.57 29.63
|
||||
Rosario -60.67 -32.94
|
||||
Fuzhou 119.3 26.08
|
||||
Nezahualcoyotl -99.03 19.41
|
||||
Saitama 139.64 35.87
|
||||
Shenzhen 114.13 22.53
|
||||
Yerevan 44.52 40.17
|
||||
Tripoli 13.18 32.87
|
||||
Anshan 122.95 41.12
|
||||
Varanasi 83.01 25.32
|
||||
Guiyang 106.72 26.58
|
||||
Baku 49.86 40.39
|
||||
Wuxi 120.3 31.58
|
||||
Prague 14.43 50.08
|
||||
Brazzaville 15.26 -4.25
|
||||
Subang Jaya 101.53 3.15
|
||||
Leon -101.69 21.12
|
||||
Hiroshima 132.44 34.39
|
||||
Amritsar 74.87 31.64
|
||||
Huainan 116.98 32.63
|
||||
Barranquilla -74.8 10.96
|
||||
Monrovia -10.8 6.31
|
||||
'Amman 35.93 31.95
|
||||
Tbilisi 44.79 41.72
|
||||
Abuja 7.49 9.06
|
||||
Aurangabad 75.32 19.89
|
||||
Sofia 23.31 42.69
|
||||
Omsk 73.4 55
|
||||
Monterrey -100.32 25.67
|
||||
Port Elizabeth 25.59 -33.96
|
||||
Navi Mumbai 73.06 19.11
|
||||
Maputo 32.57 -25.95
|
||||
Allahabad 81.84 25.45
|
||||
Samara 50.15 53.2
|
||||
Belgrade 20.5 44.83
|
||||
Campinas -47.08 -22.91
|
||||
Sholapur 75.89 17.67
|
||||
Kazan 49.13 55.75
|
||||
Irbil 44.01 36.18
|
||||
Barquisimeto -69.3 10.05
|
||||
K?benhavn 12.58 55.67
|
||||
Xianyang 108.7 34.37
|
||||
Baoding 115.48 38.87
|
||||
Guatemala -90.55 14.63
|
||||
Maceio -35.75 -9.65
|
||||
Nova Iguacu -43.47 -22.74
|
||||
Kunming 102.7 25.05
|
||||
Taizhong 120.68 24.15
|
||||
Maiduguri 13.16 11.85
|
||||
Datong 113.3 40.08
|
||||
Dublin -6.25 53.33
|
||||
Jabalpur 79.94 23.17
|
||||
Visakhapatnam 83.3 17.73
|
||||
Rostov-na-Donu 39.71 47.24
|
||||
Dnipropetrovs'k 34.98 48.45
|
||||
Shubra-El-Khema 31.25 30.11
|
||||
Srinagar 74.79 34.09
|
||||
Benxi 123.75 41.33
|
||||
Brussels 4.33 50.83
|
||||
al-Madinah 39.59 24.48
|
||||
Adelaide 138.6 -34.93
|
||||
Zapopan -103.4 20.72
|
||||
Chelyabinsk 61.43 55.15
|
||||
Haora 88.33 22.58
|
||||
Calgary -114.06 51.05
|
||||
Sendai 140.89 38.26
|
||||
Tegucigalpa -87.22 14.09
|
||||
Ranchi 85.33 23.36
|
||||
Songnam 127.15 37.44
|
||||
Ilorin 4.55 8.49
|
||||
Fez -5 34.05
|
||||
Ufa 56.04 54.78
|
||||
Klang 101.45 3.04
|
||||
Chandigarh 76.78 30.75
|
||||
Ahvaz 48.72 31.28
|
||||
Koyampattur 76.96 11.01
|
||||
Cologne 6.97 50.95
|
||||
Qom 50.95 34.65
|
||||
Odesa 30.73 46.47
|
||||
Donetsk 37.82 48
|
||||
Jodhpur 73.02 26.29
|
||||
Sao Luis -44.3 -2.5
|
||||
Sao Goncalo -43.07 -22.84
|
||||
Kitakyushu 130.86 33.88
|
||||
Huaibei 116.75 33.95
|
||||
Perm 56.25 58
|
||||
Changzhou 119.97 31.78
|
||||
Maisuru 76.65 12.31
|
||||
Guwahati 91.75 26.19
|
||||
Volgograd 44.48 48.71
|
||||
Konya 32.48 37.88
|
||||
Naples 14.27 40.85
|
||||
Vijayawada 80.63 16.52
|
||||
Ulsan 129.31 35.55
|
||||
San Jose -121.85 37.3
|
||||
Birmingham -1.91 52.48
|
||||
Chiba 140.11 35.61
|
||||
Ciudad Guayana -62.62 8.37
|
||||
Kolwezi 25.66 -10.7
|
||||
Padang 100.35 -0.95
|
||||
Managua -86.27 12.15
|
||||
Mendoza -68.83 -32.89
|
||||
Gwalior 78.17 26.23
|
||||
Biskek 74.57 42.87
|
||||
Kathmandu 85.31 27.71
|
||||
El Alto -68.17 -16.5
|
||||
Niamey 2.12 13.52
|
||||
Kigali 30.06 -1.94
|
||||
Qiqihar 124 47.35
|
||||
Ulaanbaatar 106.91 47.93
|
||||
Krasnoyarsk 93.06 56.02
|
||||
Madurai 78.12 9.92
|
||||
Edmonton -113.54 53.57
|
||||
Asgabat 58.38 37.95
|
||||
al-H?artum Bah?ri 32.52 15.64
|
||||
Arequipa -71.53 -16.39
|
||||
Marrakesh -8 31.63
|
||||
Bandar Lampung 105.27 -5.44
|
||||
Pingdingshan 113.3 33.73
|
||||
Cartagena -75.5 10.4
|
||||
Hubli 75.13 15.36
|
||||
La Paz -68.15 -16.5
|
||||
Wenzhou 120.65 28.02
|
||||
Ottawa -75.71 45.42
|
||||
Johor Bahru 103.75 1.48
|
||||
Mombasa 39.66 -4.04
|
||||
Lilongwe 33.8 -13.97
|
||||
Turin 7.68 45.08
|
||||
Duque de Caxias -43.31 -22.77
|
||||
Abu Dhabi 54.37 24.48
|
||||
Jalandhar 75.57 31.33
|
||||
Warri 5.76 5.52
|
||||
Valencia -0.39 39.48
|
||||
Oslo 10.75 59.91
|
||||
Taian 117.12 36.2
|
||||
ad-Dammam 50.1 26.43
|
||||
Mira Bhayandar 72.85 19.29
|
||||
Salem 78.16 11.67
|
||||
Pietermaritzburg 30.39 -29.61
|
||||
Naucalpan -99.23 19.48
|
||||
H?ims 36.72 34.73
|
||||
Bhubaneswar 85.84 20.27
|
||||
Hamamatsu 137.73 34.72
|
||||
Saratov 46.03 51.57
|
||||
Detroit -83.1 42.38
|
||||
Kirkuk 44.39 35.47
|
||||
Sakai 135.48 34.57
|
||||
Onitsha 6.78 6.14
|
||||
Quetta 67.02 30.21
|
||||
Aligarh 78.06 27.89
|
||||
Voronezh 39.26 51.72
|
||||
Freetown -13.24 8.49
|
||||
Tucuman -65.22 -26.83
|
||||
Bogor 106.79 -6.58
|
||||
Niigata 139.04 37.92
|
||||
Thiruvananthapuram 76.95 8.51
|
||||
Jacksonville -81.66 30.33
|
||||
Bareli 79.41 28.36
|
||||
Cebu 123.9 10.32
|
||||
Kota 75.83 25.18
|
||||
Natal -35.22 -5.8
|
||||
Shihung 126.89 37.46
|
||||
Puchon 126.77 37.48
|
||||
Tiruchchirappalli 78.69 10.81
|
||||
Trujillo -79.03 -8.11
|
||||
Sharjah 55.41 25.37
|
||||
Kermanshah 47.06 34.38
|
||||
Qinhuangdao 119.62 39.93
|
||||
Anyang 114.35 36.08
|
||||
Bhiwandi 73.05 19.3
|
||||
an-Najaf 44.34 32
|
||||
Sao Bernardo do Campo -46.54 -23.71
|
||||
Teresina -42.8 -5.1
|
||||
Nanning 108.32 22.82
|
||||
Antalya 30.71 36.89
|
||||
Campo Grande -54.63 -20.45
|
||||
Indianapolis -86.15 39.78
|
||||
Jaboatao -35.02 -8.11
|
||||
Zaporizhzhya 35.17 47.85
|
||||
Hohhot 111.64 40.82
|
||||
Marseille 5.37 43.31
|
||||
Moradabad 78.76 28.84
|
||||
Zhangjiakou 114.93 40.83
|
||||
Liuzhou 109.25 24.28
|
||||
Nouakchott -15.98 18.09
|
||||
Rajshahi 88.59 24.37
|
||||
Yantai 121.4 37.53
|
||||
Tainan 120.19 23
|
||||
Xining 101.77 36.62
|
||||
Port-au-Prince -72.34 18.54
|
||||
Hegang 130.37 47.4
|
||||
Akure 5.19 7.25
|
||||
N'Djamena 15.05 12.11
|
||||
Guadalupe -100.26 25.68
|
||||
Cracow 19.96 50.06
|
||||
Malang 112.62 -7.98
|
||||
Hengyang 112.62 26.89
|
||||
Athens 23.73 37.98
|
||||
Puyang 114.98 35.7
|
||||
San Francisco -122.45 37.77
|
||||
Jerusalem 35.22 31.78
|
||||
Amsterdam 4.89 52.37
|
||||
?odz 19.46 51.77
|
||||
Merida -89.62 20.97
|
||||
Austin -97.75 30.31
|
||||
Abeokuta 3.35 7.16
|
||||
Xinxiang 113.87 35.32
|
||||
Raipur 81.63 21.24
|
||||
Tunis 10.22 36.84
|
||||
Columbus -82.99 39.99
|
||||
Chihuahua -106.08 28.63
|
||||
L'viv 24 49.83
|
||||
Cotonou 2.44 6.36
|
||||
Pekan Baru 101.43 0.56
|
||||
Blantyre 34.99 -15.79
|
||||
La Plata -57.96 -34.92
|
||||
Bulawayo 28.58 -20.17
|
||||
Tangier -5.81 35.79
|
||||
Kayseri 35.48 38.74
|
||||
Tolyatti 49.51 53.48
|
||||
Foshan 113.12 23.03
|
||||
Ningbo 121.55 29.88
|
||||
Langfang 116.68 39.52
|
||||
Ampang Jaya 101.77 3.15
|
||||
Liaoyang 123.18 41.28
|
||||
Riga 24.13 56.97
|
||||
Changzhi 111.75 35.22
|
||||
Kryvyy Rih 33.35 47.92
|
||||
Libreville 9.45 0.39
|
||||
Chonju 127.14 35.83
|
||||
Fort Worth -97.34 32.75
|
||||
as-Sulaymaniyah 45.43 35.56
|
||||
Osasco -46.78 -23.53
|
||||
Zamboanga 122.08 6.92
|
||||
Tlalnepantla -99.19 19.54
|
||||
Gorakhpur 83.36 26.76
|
||||
San Luis Potosi -100.98 22.16
|
||||
Sevilla -5.98 37.4
|
||||
Zhuzhou 113.15 27.83
|
||||
Zagreb 15.97 45.8
|
||||
Huangshi 115.1 30.22
|
||||
Puente Alto -70.57 -33.61
|
||||
Shaoguan 113.58 24.8
|
||||
Matola 32.46 -25.97
|
||||
Guilin 110.28 25.28
|
||||
Aguascalientes -102.3 21.88
|
||||
Shizuoka 138.39 34.98
|
||||
Benghazi 20.07 32.12
|
||||
Fuxin 121.65 42.01
|
||||
Joao Pessoa -34.86 -7.12
|
||||
Ipoh 101.07 4.6
|
||||
Contagem -44.1 -19.91
|
||||
Dushanbe 68.78 38.57
|
||||
Zhanjiang 110.38 21.2
|
||||
Xingtai 114.49 37.07
|
||||
Okayama 133.92 34.67
|
||||
Yogyakarta 110.37 -7.78
|
||||
Bhilai 81.38 21.21
|
||||
Zigong 104.78 29.4
|
||||
Mudanjiang 129.6 44.58
|
||||
Wahran -0.62 35.7
|
||||
Enugu 7.51 6.44
|
||||
Santo Andre -46.53 -23.65
|
||||
Colombo 79.85 6.93
|
||||
Chimalhuacan -98.96 19.44
|
||||
Shatian 114.19 22.38
|
||||
Memphis -90.01 35.11
|
||||
Kumamoto 130.71 32.8
|
||||
Sao Jose dos Campos -45.88 -23.2
|
||||
Zhangdian 118.06 36.8
|
||||
Acapulco -99.92 16.85
|
||||
Xiangtan 112.9 27.85
|
||||
Quebec -71.23 46.82
|
||||
Dasmarinas 120.93 14.33
|
||||
Zaria 7.71 11.08
|
||||
Nantong 120.82 32.02
|
||||
Charlotte -80.83 35.2
|
||||
Pointe Noire 11.87 -4.77
|
||||
Shaoyang 111.2 27
|
||||
Queretaro -100.4 20.59
|
||||
Hamilton -79.85 43.26
|
||||
Islamabad 73.06 33.72
|
||||
Panjin 122.05 41.18
|
||||
Saltillo -101 25.42
|
||||
Ansan 126.86 37.35
|
||||
Jamshedpur 86.2 22.79
|
||||
Zaragoza -0.89 41.65
|
||||
Cancun -86.83 21.17
|
||||
Dandong 124.4 40.13
|
||||
Frankfurt 8.68 50.12
|
||||
Palermo 13.36 38.12
|
||||
Haikou 110.32 20.05
|
||||
'Adan 45.03 12.79
|
||||
Amravati 77.76 20.95
|
||||
Winnipeg -97.17 49.88
|
||||
Sagamihara 139.38 35.58
|
||||
Zhangzhou 117.67 24.52
|
||||
Gazzah 34.44 31.53
|
||||
Kataka 85.88 20.47
|
||||
El Paso -106.44 31.85
|
||||
Krasnodar 38.98 45.03
|
||||
Kuching 110.34 1.55
|
||||
Wroc?aw 17.03 51.11
|
||||
Asmara 38.94 15.33
|
||||
Zhenjiang 119.43 32.22
|
||||
Baltimore -76.61 39.3
|
||||
Benoni 28.33 -26.15
|
||||
Mersin 34.63 36.81
|
||||
Izhevsk 53.23 56.85
|
||||
Yancheng 120.12 33.39
|
||||
Hermosillo -110.97 29.07
|
||||
Yuanlong 114.02 22.44
|
||||
Uberlandia -48.28 -18.9
|
||||
Ulyanovsk 48.4 54.33
|
||||
Bouake -5.03 7.69
|
||||
Santiago -70.69 19.48
|
||||
Mexicali -115.47 32.65
|
||||
Hai Phong 106.68 20.86
|
||||
Anyang 126.92 37.39
|
||||
Dadiangas 125.25 6.1
|
||||
Morelia -101.18 19.72
|
||||
Oshogbo 4.56 7.77
|
||||
Chongju 127.5 36.64
|
||||
Jos 8.89 9.93
|
||||
al-'Ayn 55.74 24.23
|
||||
Sorocaba -47.47 -23.49
|
||||
Bikaner 73.32 28.03
|
||||
Taizhou 119.9 32.49
|
||||
Antipolo 121.18 14.59
|
||||
Xiamen 118.08 24.45
|
||||
Cochabamba -66.17 -17.38
|
||||
Culiacan -107.39 24.8
|
||||
Yingkou 122.28 40.67
|
||||
Kagoshima 130.56 31.59
|
||||
Siping 124.33 43.17
|
||||
Orumiyeh 45 37.53
|
||||
Luancheng 114.65 37.88
|
||||
Diyarbak?r 40.23 37.92
|
||||
Yaroslavl 39.87 57.62
|
||||
Mixco -90.6 14.64
|
||||
Banjarmasin 114.59 -3.33
|
||||
Chisinau 28.83 47.03
|
||||
Djibouti 43.15 11.59
|
||||
Seattle -122.35 47.62
|
||||
Stuttgart 9.19 48.79
|
||||
Khabarovsk 135.12 48.42
|
||||
Rotterdam 4.48 51.93
|
||||
Jinzhou 121.1 41.12
|
||||
Kisangani 25.19 0.53
|
||||
San Pedro Sula -88.03 15.47
|
||||
Bengbu 117.33 32.95
|
||||
Irkutsk 104.24 52.33
|
||||
Shihezi 86.03 44.3
|
||||
Maracay -67.47 10.33
|
||||
Cucuta -72.51 7.88
|
||||
Bhavnagar 72.13 21.79
|
||||
Port Said 32.29 31.26
|
||||
Denver -104.87 39.77
|
||||
Genoa 8.93 44.42
|
||||
Jiangmen 113.08 22.58
|
||||
Dortmund 7.48 51.51
|
||||
Barnaul 83.75 53.36
|
||||
Washington -77.02 38.91
|
||||
Veracruz -96.14 19.19
|
||||
Ribeirao Preto -47.8 -21.17
|
||||
Vladivostok 131.9 43.13
|
||||
Mar del Plata -57.58 -38
|
||||
Boston -71.02 42.34
|
||||
Eskisehir 30.52 39.79
|
||||
Warangal 79.58 18.01
|
||||
Zahedan 60.83 29.5
|
||||
Essen 7 51.47
|
||||
Dusseldorf 6.79 51.24
|
||||
Kaifeng 114.35 34.85
|
||||
Kingston -76.8 17.99
|
||||
Glasgow -4.27 55.87
|
||||
Funabashi 139.99 35.7
|
||||
Shah Alam 101.56 3.07
|
||||
Maoming 110.87 21.92
|
||||
Hachioji 139.33 35.66
|
||||
Meknes -5.56 33.9
|
||||
Hamhung 127.54 39.91
|
||||
Villa Nueva -90.59 14.53
|
||||
Sargodha 72.67 32.08
|
||||
Las Vegas -115.22 36.21
|
||||
Resht 49.63 37.3
|
||||
Cangzhou 116.87 38.32
|
||||
Tanggu 117.67 39
|
||||
Helsinki 24.94 60.17
|
||||
Malaga -4.42 36.72
|
||||
Milwaukee -87.97 43.06
|
||||
Nashville -86.78 36.17
|
||||
Ife 4.56 7.48
|
||||
Changde 111.68 29.03
|
||||
at-Ta'if 40.38 21.26
|
||||
Surakarta 110.82 -7.57
|
||||
Poznan 16.9 52.4
|
||||
Barcelona -64.72 10.13
|
||||
Bloemfontein 26.23 -29.15
|
||||
Lopez Mateos -99.26 19.57
|
||||
Bangui 18.56 4.36
|
||||
Reynosa -98.28 26.08
|
||||
Xigong 114.25 22.33
|
||||
Cuiaba -56.09 -15.61
|
||||
Shiliguri 88.42 26.73
|
||||
Oklahoma City -97.51 35.47
|
||||
Louisville -85.74 38.22
|
||||
Jiamusi 130.35 46.83
|
||||
Huaiyin 119.03 33.58
|
||||
Welkom 26.73 -27.97
|
||||
Kolhapur 74.22 16.7
|
||||
Ulhasnagar 73.15 19.23
|
||||
Rajpur 88.44 22.44
|
||||
Bremen 8.81 53.08
|
||||
San Salvador -89.19 13.69
|
||||
Maanshan 118.48 31.73
|
||||
Tembisa 28.22 -25.99
|
||||
Banqiao 121.44 25.02
|
||||
Toluca -99.67 19.29
|
||||
Portland -122.66 45.54
|
||||
Gold Coast 153.44 -28.07
|
||||
Kota Kinabalu 116.07 5.97
|
||||
Vilnius 25.27 54.7
|
||||
Agadir -9.61 30.42
|
||||
Ajmer 74.64 26.45
|
||||
Orenburg 55.1 51.78
|
||||
Neijiang 105.05 29.58
|
||||
Salta -65.41 -24.79
|
||||
Guntur 80.44 16.31
|
||||
Novokuznetsk 87.1 53.75
|
||||
Yangzhou 119.43 32.4
|
||||
Durgapur 87.31 23.5
|
||||
Shashi 112.23 30.32
|
||||
Asuncion -57.63 -25.3
|
||||
Aparecida de Goiania -49.24 -16.82
|
||||
Ribeirao das Neves -44.08 -19.76
|
||||
Petaling Jaya 101.62 3.1
|
||||
Sangli-Miraj 74.57 16.86
|
||||
Dehra Dun 78.05 30.34
|
||||
Maturin -63.17 9.75
|
||||
Torreon -103.43 25.55
|
||||
Jiaozuo 113.22 35.25
|
||||
Zhuhai 113.57 22.28
|
||||
Nanded 77.29 19.17
|
||||
Suez 32.54 29.98
|
||||
Tyumen 65.53 57.15
|
||||
Albuquerque -106.62 35.12
|
||||
Cagayan 124.67 8.45
|
||||
Mwanza 32.89 -2.52
|
||||
Petare -66.83 10.52
|
||||
Soledad -74.77 10.92
|
||||
Uijongbu 127.04 37.74
|
||||
Yueyang 113.1 29.38
|
||||
Feira de Santana -38.97 -12.25
|
||||
Ta'izz 44.04 13.6
|
||||
Tucson -110.89 32.2
|
||||
Naberezhnyye Chelny 52.32 55.69
|
||||
Kerman 57.08 30.3
|
||||
Matsuyama 132.77 33.84
|
||||
Garoua 13.39 9.3
|
||||
Tlaquepaque -103.32 20.64
|
||||
Tuxtla Gutierrez -93.12 16.75
|
||||
Jamnagar 70.07 22.47
|
||||
Jammu 74.85 32.71
|
||||
Gulbarga 76.82 17.34
|
||||
Chiclayo -79.84 -6.76
|
||||
Hanover 9.73 52.4
|
||||
Bucaramanga -73.13 7.13
|
||||
Bahawalpur 71.67 29.39
|
||||
Goteborg 12.01 57.72
|
||||
Zhunmen 113.98 22.41
|
||||
Bhatpara 88.42 22.89
|
||||
Ryazan 39.74 54.62
|
||||
Calamba 121.15 14.21
|
||||
Changwon 128.62 35.27
|
||||
Aracaju -37.07 -10.91
|
||||
Zunyi 106.92 27.7
|
||||
Lipetsk 39.62 52.62
|
||||
Dresden 13.74 51.05
|
||||
Saharanpur 77.54 29.97
|
||||
H?amah 36.73 35.15
|
||||
Niyala 24.89 12.06
|
||||
San Nicolas de los Garza -100.3 25.75
|
||||
Higashiosaka 135.59 34.67
|
||||
al-H?illah 44.43 32.48
|
||||
Leipzig 12.4 51.35
|
||||
Xuchang 113.82 34.02
|
||||
Wuhu 118.37 31.35
|
||||
Boma 13.05 -5.85
|
||||
Kananga 22.4 -5.89
|
||||
Mykolayiv 32 46.97
|
||||
Atlanta -84.42 33.76
|
||||
Londrina -51.18 -23.3
|
||||
Tabuk 36.57 28.39
|
||||
Cuautitlan Izcalli -99.25 19.65
|
||||
Nuremberg 11.05 49.45
|
||||
Santa Fe -60.69 -31.6
|
||||
Joinville -48.84 -26.32
|
||||
Zurich 8.55 47.36
|
51
node/src/main/resources/core/testing/example.rates.txt
Normal file
51
node/src/main/resources/core/testing/example.rates.txt
Normal file
@ -0,0 +1,51 @@
|
||||
# Some pretend noddy rate fixes, for the interest rate oracles.
|
||||
|
||||
3M USD 2016-03-16 1M = 0.678
|
||||
3M USD 2016-03-16 2M = 0.655
|
||||
EURIBOR 2016-03-15 1M = 0.123
|
||||
EURIBOR 2016-03-15 2M = 0.111
|
||||
|
||||
3M USD 2016-03-08 3M = 0.0063515
|
||||
3M USD 2016-06-08 3M = 0.0063520
|
||||
3M USD 2016-09-08 3M = 0.0063521
|
||||
3M USD 2016-12-08 3M = 0.0063515
|
||||
3M USD 2017-03-08 3M = 0.0063525
|
||||
3M USD 2017-06-08 3M = 0.0063530
|
||||
3M USD 2017-09-07 3M = 0.0063531
|
||||
3M USD 2017-12-07 3M = 0.0063532
|
||||
3M USD 2018-03-08 3M = 0.0063533
|
||||
3M USD 2018-06-07 3M = 0.0063534
|
||||
3M USD 2018-09-06 3M = 0.0063535
|
||||
3M USD 2018-12-06 3M = 0.0063536
|
||||
3M USD 2019-03-07 3M = 0.0063537
|
||||
3M USD 2019-06-06 3M = 0.0063538
|
||||
3M USD 2019-09-06 3M = 0.0063539
|
||||
3M USD 2019-12-06 3M = 0.0063540
|
||||
3M USD 2020-03-06 3M = 0.0063541
|
||||
3M USD 2020-06-08 3M = 0.0063542
|
||||
3M USD 2020-09-08 3M = 0.0063543
|
||||
3M USD 2020-12-08 3M = 0.0063544
|
||||
3M USD 2021-03-08 3M = 0.0063545
|
||||
3M USD 2021-06-08 3M = 0.0063546
|
||||
3M USD 2021-09-08 3M = 0.0063547
|
||||
3M USD 2021-12-08 3M = 0.0063548
|
||||
3M USD 2022-03-08 3M = 0.0063549
|
||||
3M USD 2022-06-08 3M = 0.0063550
|
||||
3M USD 2022-09-08 3M = 0.0063551
|
||||
3M USD 2022-12-08 3M = 0.0063553
|
||||
3M USD 2023-03-08 3M = 0.0063554
|
||||
3M USD 2023-06-08 3M = 0.0063555
|
||||
3M USD 2023-09-07 3M = 0.0063556
|
||||
3M USD 2023-12-07 3M = 0.0063557
|
||||
3M USD 2024-03-07 3M = 0.0063558
|
||||
3M USD 2024-06-06 3M = 0.0063559
|
||||
3M USD 2024-09-06 3M = 0.0063560
|
||||
3M USD 2024-12-06 3M = 0.0063561
|
||||
3M USD 2025-03-06 3M = 0.0063562
|
||||
3M USD 2025-06-06 3M = 0.0063563
|
||||
3M USD 2025-09-08 3M = 0.0063564
|
||||
3M USD 2025-12-08 3M = 0.0063565
|
||||
3M USD 2026-03-06 3M = 0.0063566
|
||||
3M USD 2026-06-08 3M = 0.0063567
|
||||
3M USD 2026-09-08 3M = 0.0063568
|
||||
3M USD 2026-12-08 3M = 0.0063569
|
104
node/src/main/resources/core/testing/trade.json
Normal file
104
node/src/main/resources/core/testing/trade.json
Normal file
@ -0,0 +1,104 @@
|
||||
{
|
||||
"fixedLeg": {
|
||||
"fixedRatePayer": "Bank A",
|
||||
"notional": {
|
||||
"pennies": 2500000000,
|
||||
"currency": "USD"
|
||||
},
|
||||
"paymentFrequency": "SemiAnnual",
|
||||
"effectiveDate": "2016-03-16",
|
||||
"effectiveDateAdjustment": null,
|
||||
"terminationDate": "2026-03-16",
|
||||
"terminationDateAdjustment": null,
|
||||
"fixedRate": {
|
||||
"ratioUnit": {
|
||||
"value": "0.01676"
|
||||
}
|
||||
},
|
||||
"dayCountBasisDay": "D30",
|
||||
"dayCountBasisYear": "Y360",
|
||||
"rollConvention": "ModifiedFollowing",
|
||||
"dayInMonth": 10,
|
||||
"paymentRule": "InArrears",
|
||||
"paymentDelay": 0,
|
||||
"paymentCalendar": "London",
|
||||
"interestPeriodAdjustment": "Adjusted"
|
||||
},
|
||||
"floatingLeg": {
|
||||
"floatingRatePayer": "Bank B",
|
||||
"notional": {
|
||||
"pennies": 2500000000,
|
||||
"currency": "USD"
|
||||
},
|
||||
"paymentFrequency": "Quarterly",
|
||||
"effectiveDate": "2016-03-12",
|
||||
"effectiveDateAdjustment": null,
|
||||
"terminationDate": "2026-03-12",
|
||||
"terminationDateAdjustment": null,
|
||||
"dayCountBasisDay": "D30",
|
||||
"dayCountBasisYear": "Y360",
|
||||
"rollConvention": "ModifiedFollowing",
|
||||
"fixingRollConvention": "ModifiedFollowing",
|
||||
"dayInMonth": 10,
|
||||
"resetDayInMonth": 10,
|
||||
"paymentRule": "InArrears",
|
||||
"paymentDelay": 0,
|
||||
"paymentCalendar": [ "London" ],
|
||||
"interestPeriodAdjustment": "Adjusted",
|
||||
"fixingPeriod": "TWODAYS",
|
||||
"resetRule": "InAdvance",
|
||||
"fixingsPerPayment": "Quarterly",
|
||||
"fixingCalendar": [ "NewYork" ],
|
||||
"index": "3M USD",
|
||||
"indexSource": "Rates Service Provider",
|
||||
"indexTenor": {
|
||||
"name": "3M"
|
||||
}
|
||||
},
|
||||
"calculation": {
|
||||
"expression": "( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) -(floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))",
|
||||
"floatingLegPaymentSchedule": {
|
||||
},
|
||||
"fixedLegPaymentSchedule": {
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"baseCurrency": "EUR",
|
||||
"eligibleCurrency": "EUR",
|
||||
"eligibleCreditSupport": "Cash in an Eligible Currency",
|
||||
"independentAmounts": {
|
||||
"pennies": 0,
|
||||
"currency": "EUR"
|
||||
},
|
||||
"threshold": {
|
||||
"pennies": 0,
|
||||
"currency": "EUR"
|
||||
},
|
||||
"minimumTransferAmount": {
|
||||
"pennies": 25000000,
|
||||
"currency": "EUR"
|
||||
},
|
||||
"rounding": {
|
||||
"pennies": 1000000,
|
||||
"currency": "EUR"
|
||||
},
|
||||
"valuationDate": "Every Local Business Day",
|
||||
"notificationTime": "2:00pm London",
|
||||
"resolutionTime": "2:00pm London time on the first LocalBusiness Day following the date on which the notice is given ",
|
||||
"interestRate": {
|
||||
"oracle": "Rates Service Provider",
|
||||
"tenor": {
|
||||
"name": "6M"
|
||||
},
|
||||
"ratioUnit": null,
|
||||
"name": "EONIA"
|
||||
},
|
||||
"addressForTransfers": "",
|
||||
"exposure": {},
|
||||
"localBusinessDay": [ "London" , "NewYork" ],
|
||||
"dailyInterestAmount": "(CashAmount * InterestRate ) / (fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360",
|
||||
"tradeID": "tradeXXX",
|
||||
"hashLegalDocs": "put hash here"
|
||||
},
|
||||
"programRef": "1E6BBA305D445341F0026E51B6C7F3ACB834AFC6C2510C0EF7BC0477235EFECF"
|
||||
}
|
79
node/src/test/java/core/crypto/Base58Test.java
Normal file
79
node/src/test/java/core/crypto/Base58Test.java
Normal file
@ -0,0 +1,79 @@
|
||||
package core.crypto;
|
||||
|
||||
import org.junit.*;
|
||||
|
||||
import java.math.*;
|
||||
import java.util.*;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* From the bitcoinj library
|
||||
*/
|
||||
public class Base58Test {
|
||||
@Test
|
||||
public void testEncode() throws Exception {
|
||||
byte[] testbytes = "Hello World".getBytes();
|
||||
assertEquals("JxF12TrwUP45BMd", Base58.encode(testbytes));
|
||||
|
||||
BigInteger bi = BigInteger.valueOf(3471844090L);
|
||||
assertEquals("16Ho7Hs", Base58.encode(bi.toByteArray()));
|
||||
|
||||
byte[] zeroBytes1 = new byte[1];
|
||||
assertEquals("1", Base58.encode(zeroBytes1));
|
||||
|
||||
byte[] zeroBytes7 = new byte[7];
|
||||
assertEquals("1111111", Base58.encode(zeroBytes7));
|
||||
|
||||
// test empty encode
|
||||
assertEquals("", Base58.encode(new byte[0]));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecode() throws Exception {
|
||||
byte[] testbytes = "Hello World".getBytes();
|
||||
byte[] actualbytes = Base58.decode("JxF12TrwUP45BMd");
|
||||
assertTrue(new String(actualbytes), Arrays.equals(testbytes, actualbytes));
|
||||
|
||||
assertTrue("1", Arrays.equals(Base58.decode("1"), new byte[1]));
|
||||
assertTrue("1111", Arrays.equals(Base58.decode("1111"), new byte[4]));
|
||||
|
||||
try {
|
||||
Base58.decode("This isn't valid base58");
|
||||
fail();
|
||||
} catch (AddressFormatException e) {
|
||||
// expected
|
||||
}
|
||||
|
||||
Base58.decodeChecked("4stwEBjT6FYyVV");
|
||||
|
||||
// Checksum should fail.
|
||||
try {
|
||||
Base58.decodeChecked("4stwEBjT6FYyVW");
|
||||
fail();
|
||||
} catch (AddressFormatException e) {
|
||||
// expected
|
||||
}
|
||||
|
||||
// Input is too short.
|
||||
try {
|
||||
Base58.decodeChecked("4s");
|
||||
fail();
|
||||
} catch (AddressFormatException e) {
|
||||
// expected
|
||||
}
|
||||
|
||||
// Test decode of empty String.
|
||||
assertEquals(0, Base58.decode("").length);
|
||||
|
||||
// Now check we can correctly decode the case where the high bit of the first byte is not zero, so BigInteger
|
||||
// sign extends. Fix for a bug that stopped us parsing keys exported using sipas patch.
|
||||
Base58.decodeChecked("93VYUMzRG9DdbRP72uQXjaWibbQwygnvaCu9DumcqDjGybD864T");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecodeToBigInteger() {
|
||||
byte[] input = Base58.decode("129");
|
||||
assertEquals(new BigInteger(1, input), Base58.decodeToBigInteger("129"));
|
||||
}
|
||||
}
|
458
node/src/test/kotlin/contracts/CashTests.kt
Normal file
458
node/src/test/kotlin/contracts/CashTests.kt
Normal file
@ -0,0 +1,458 @@
|
||||
import contracts.Cash
|
||||
import contracts.DummyContract
|
||||
import contracts.InsufficientBalanceException
|
||||
import core.*
|
||||
import core.contracts.*
|
||||
import core.crypto.Party
|
||||
import core.crypto.SecureHash
|
||||
import core.serialization.OpaqueBytes
|
||||
import core.testutils.*
|
||||
import org.junit.Test
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertNotEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class CashTests {
|
||||
val inState = Cash.State(
|
||||
deposit = MEGA_CORP.ref(1),
|
||||
amount = 1000.DOLLARS,
|
||||
owner = DUMMY_PUBKEY_1,
|
||||
notary = DUMMY_NOTARY
|
||||
)
|
||||
val outState = inState.copy(owner = DUMMY_PUBKEY_2)
|
||||
|
||||
fun Cash.State.editDepositRef(ref: Byte) = copy(deposit = deposit.copy(reference = OpaqueBytes.of(ref)))
|
||||
|
||||
@Test
|
||||
fun trivial() {
|
||||
transaction {
|
||||
input { inState }
|
||||
this `fails requirement` "the amounts balance"
|
||||
|
||||
tweak {
|
||||
output { outState.copy(amount = 2000.DOLLARS) }
|
||||
this `fails requirement` "the amounts balance"
|
||||
}
|
||||
tweak {
|
||||
output { outState }
|
||||
// No command arguments
|
||||
this `fails requirement` "required contracts.Cash.Commands.Move command"
|
||||
}
|
||||
tweak {
|
||||
output { outState }
|
||||
arg(DUMMY_PUBKEY_2) { Cash.Commands.Move() }
|
||||
this `fails requirement` "the owning keys are the same as the signing keys"
|
||||
}
|
||||
tweak {
|
||||
output { outState }
|
||||
output { outState `issued by` MINI_CORP }
|
||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
this `fails requirement` "at least one cash input"
|
||||
}
|
||||
// Simple reallocation works.
|
||||
tweak {
|
||||
output { outState }
|
||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
this.accepts()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun issueMoney() {
|
||||
// Check we can't "move" money into existence.
|
||||
transaction {
|
||||
input { DummyContract.State(notary = DUMMY_NOTARY) }
|
||||
output { outState }
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
|
||||
this `fails requirement` "there is at least one cash input"
|
||||
}
|
||||
|
||||
// Check we can issue money only as long as the issuer institution is a command signer, i.e. any recognised
|
||||
// institution is allowed to issue as much cash as they want.
|
||||
transaction {
|
||||
output { outState }
|
||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Issue() }
|
||||
this `fails requirement` "output deposits are owned by a command signer"
|
||||
}
|
||||
transaction {
|
||||
output {
|
||||
Cash.State(
|
||||
amount = 1000.DOLLARS,
|
||||
owner = DUMMY_PUBKEY_1,
|
||||
deposit = MINI_CORP.ref(12, 34),
|
||||
notary = DUMMY_NOTARY
|
||||
)
|
||||
}
|
||||
tweak {
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Issue(0) }
|
||||
this `fails requirement` "has a nonce"
|
||||
}
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
this.accepts()
|
||||
}
|
||||
|
||||
// Test generation works.
|
||||
val ptx = TransactionBuilder()
|
||||
Cash().generateIssue(ptx, 100.DOLLARS, MINI_CORP.ref(12, 34), owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY)
|
||||
assertTrue(ptx.inputStates().isEmpty())
|
||||
val s = ptx.outputStates()[0] as Cash.State
|
||||
assertEquals(100.DOLLARS, s.amount)
|
||||
assertEquals(MINI_CORP, s.deposit.party)
|
||||
assertEquals(DUMMY_PUBKEY_1, s.owner)
|
||||
assertTrue(ptx.commands()[0].value is Cash.Commands.Issue)
|
||||
assertEquals(MINI_CORP_PUBKEY, ptx.commands()[0].signers[0])
|
||||
|
||||
// Test issuance from the issuance definition
|
||||
val issuanceDef = Cash.IssuanceDefinition(MINI_CORP.ref(12, 34), USD)
|
||||
val templatePtx = TransactionBuilder()
|
||||
Cash().generateIssue(templatePtx, issuanceDef, 100.DOLLARS.pennies, owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY)
|
||||
assertTrue(templatePtx.inputStates().isEmpty())
|
||||
assertEquals(ptx.outputStates()[0], templatePtx.outputStates()[0])
|
||||
|
||||
// We can consume $1000 in a transaction and output $2000 as long as it's signed by an issuer.
|
||||
transaction {
|
||||
input { inState }
|
||||
output { inState.copy(amount = inState.amount * 2) }
|
||||
|
||||
// Move fails: not allowed to summon money.
|
||||
tweak {
|
||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
this `fails requirement` "at issuer MegaCorp the amounts balance"
|
||||
}
|
||||
|
||||
// Issue works.
|
||||
tweak {
|
||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
this.accepts()
|
||||
}
|
||||
}
|
||||
|
||||
// Can't use an issue command to lower the amount.
|
||||
transaction {
|
||||
input { inState }
|
||||
output { inState.copy(amount = inState.amount / 2) }
|
||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
this `fails requirement` "output values sum to more than the inputs"
|
||||
}
|
||||
|
||||
// Can't have an issue command that doesn't actually issue money.
|
||||
transaction {
|
||||
input { inState }
|
||||
output { inState }
|
||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
this `fails requirement` "output values sum to more than the inputs"
|
||||
}
|
||||
|
||||
// Can't have any other commands if we have an issue command (because the issue command overrules them)
|
||||
transaction {
|
||||
input { inState }
|
||||
output { inState.copy(amount = inState.amount * 2) }
|
||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
tweak {
|
||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
this `fails requirement` "there is only a single issue command"
|
||||
}
|
||||
tweak {
|
||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
this `fails requirement` "there is only a single issue command"
|
||||
}
|
||||
tweak {
|
||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(inState.amount / 2) }
|
||||
this `fails requirement` "there is only a single issue command"
|
||||
}
|
||||
this.accepts()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMergeSplit() {
|
||||
// Splitting value works.
|
||||
transaction {
|
||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
tweak {
|
||||
input { inState }
|
||||
for (i in 1..4) output { inState.copy(amount = inState.amount / 4) }
|
||||
this.accepts()
|
||||
}
|
||||
// Merging 4 inputs into 2 outputs works.
|
||||
tweak {
|
||||
for (i in 1..4) input { inState.copy(amount = inState.amount / 4) }
|
||||
output { inState.copy(amount = inState.amount / 2) }
|
||||
output { inState.copy(amount = inState.amount / 2) }
|
||||
this.accepts()
|
||||
}
|
||||
// Merging 2 inputs into 1 works.
|
||||
tweak {
|
||||
input { inState.copy(amount = inState.amount / 2) }
|
||||
input { inState.copy(amount = inState.amount / 2) }
|
||||
output { inState }
|
||||
this.accepts()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun zeroSizedValues() {
|
||||
transaction {
|
||||
input { inState }
|
||||
input { inState.copy(amount = 0.DOLLARS) }
|
||||
this `fails requirement` "zero sized inputs"
|
||||
}
|
||||
transaction {
|
||||
input { inState }
|
||||
output { inState }
|
||||
output { inState.copy(amount = 0.DOLLARS) }
|
||||
this `fails requirement` "zero sized outputs"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun trivialMismatches() {
|
||||
// Can't change issuer.
|
||||
transaction {
|
||||
input { inState }
|
||||
output { outState `issued by` MINI_CORP }
|
||||
this `fails requirement` "at issuer MegaCorp the amounts balance"
|
||||
}
|
||||
// Can't change deposit reference when splitting.
|
||||
transaction {
|
||||
input { inState }
|
||||
output { outState.editDepositRef(0).copy(amount = inState.amount / 2) }
|
||||
output { outState.editDepositRef(1).copy(amount = inState.amount / 2) }
|
||||
this `fails requirement` "for deposit [01] at issuer MegaCorp the amounts balance"
|
||||
}
|
||||
// Can't mix currencies.
|
||||
transaction {
|
||||
input { inState }
|
||||
output { outState.copy(amount = 800.DOLLARS) }
|
||||
output { outState.copy(amount = 200.POUNDS) }
|
||||
this `fails requirement` "the amounts balance"
|
||||
}
|
||||
transaction {
|
||||
input { inState }
|
||||
input {
|
||||
inState.copy(
|
||||
amount = 150.POUNDS,
|
||||
owner = DUMMY_PUBKEY_2
|
||||
)
|
||||
}
|
||||
output { outState.copy(amount = 1150.DOLLARS) }
|
||||
this `fails requirement` "the amounts balance"
|
||||
}
|
||||
// Can't have superfluous input states from different issuers.
|
||||
transaction {
|
||||
input { inState }
|
||||
input { inState `issued by` MINI_CORP }
|
||||
output { outState }
|
||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
this `fails requirement` "at issuer MiniCorp the amounts balance"
|
||||
}
|
||||
// Can't combine two different deposits at the same issuer.
|
||||
transaction {
|
||||
input { inState }
|
||||
input { inState.editDepositRef(3) }
|
||||
output { outState.copy(amount = inState.amount * 2).editDepositRef(3) }
|
||||
this `fails requirement` "for deposit [01]"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun exitLedger() {
|
||||
// Single input/output straightforward case.
|
||||
transaction {
|
||||
input { inState }
|
||||
output { outState.copy(amount = inState.amount - 200.DOLLARS) }
|
||||
|
||||
tweak {
|
||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(100.DOLLARS) }
|
||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
this `fails requirement` "the amounts balance"
|
||||
}
|
||||
|
||||
tweak {
|
||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS) }
|
||||
this `fails requirement` "required contracts.Cash.Commands.Move command"
|
||||
|
||||
tweak {
|
||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
this.accepts()
|
||||
}
|
||||
}
|
||||
}
|
||||
// Multi-issuer case.
|
||||
transaction {
|
||||
input { inState }
|
||||
input { inState `issued by` MINI_CORP }
|
||||
|
||||
output { inState.copy(amount = inState.amount - 200.DOLLARS) `issued by` MINI_CORP }
|
||||
output { inState.copy(amount = inState.amount - 200.DOLLARS) }
|
||||
|
||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
|
||||
this `fails requirement` "at issuer MegaCorp the amounts balance"
|
||||
|
||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS) }
|
||||
this `fails requirement` "at issuer MiniCorp the amounts balance"
|
||||
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS) }
|
||||
this.accepts()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun multiIssuer() {
|
||||
transaction {
|
||||
// Gather 2000 dollars from two different issuers.
|
||||
input { inState }
|
||||
input { inState `issued by` MINI_CORP }
|
||||
|
||||
// Can't merge them together.
|
||||
tweak {
|
||||
output { inState.copy(owner = DUMMY_PUBKEY_2, amount = 2000.DOLLARS) }
|
||||
this `fails requirement` "at issuer MegaCorp the amounts balance"
|
||||
}
|
||||
// Missing MiniCorp deposit
|
||||
tweak {
|
||||
output { inState.copy(owner = DUMMY_PUBKEY_2) }
|
||||
output { inState.copy(owner = DUMMY_PUBKEY_2) }
|
||||
this `fails requirement` "at issuer MegaCorp the amounts balance"
|
||||
}
|
||||
|
||||
// This works.
|
||||
output { inState.copy(owner = DUMMY_PUBKEY_2) }
|
||||
output { inState.copy(owner = DUMMY_PUBKEY_2) `issued by` MINI_CORP }
|
||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
this.accepts()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun multiCurrency() {
|
||||
// Check we can do an atomic currency trade tx.
|
||||
transaction {
|
||||
val pounds = Cash.State(MINI_CORP.ref(3, 4, 5), 658.POUNDS, DUMMY_PUBKEY_2, DUMMY_NOTARY)
|
||||
input { inState `owned by` DUMMY_PUBKEY_1 }
|
||||
input { pounds }
|
||||
output { inState `owned by` DUMMY_PUBKEY_2 }
|
||||
output { pounds `owned by` DUMMY_PUBKEY_1 }
|
||||
arg(DUMMY_PUBKEY_1, DUMMY_PUBKEY_2) { Cash.Commands.Move() }
|
||||
|
||||
this.accepts()
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Spend tx generation
|
||||
|
||||
val OUR_PUBKEY_1 = DUMMY_PUBKEY_1
|
||||
val THEIR_PUBKEY_1 = DUMMY_PUBKEY_2
|
||||
|
||||
fun makeCash(amount: Amount, corp: Party, depositRef: Byte = 1) =
|
||||
StateAndRef(
|
||||
Cash.State(corp.ref(depositRef), amount, OUR_PUBKEY_1, DUMMY_NOTARY),
|
||||
StateRef(SecureHash.randomSHA256(), Random().nextInt(32))
|
||||
)
|
||||
|
||||
val WALLET = listOf(
|
||||
makeCash(100.DOLLARS, MEGA_CORP),
|
||||
makeCash(400.DOLLARS, MEGA_CORP),
|
||||
makeCash(80.DOLLARS, MINI_CORP),
|
||||
makeCash(80.SWISS_FRANCS, MINI_CORP, 2)
|
||||
)
|
||||
|
||||
fun makeSpend(amount: Amount, dest: PublicKey): WireTransaction {
|
||||
val tx = TransactionBuilder()
|
||||
Cash().generateSpend(tx, amount, dest, WALLET)
|
||||
return tx.toWireTransaction()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun generateSimpleDirectSpend() {
|
||||
val wtx = makeSpend(100.DOLLARS, THEIR_PUBKEY_1)
|
||||
assertEquals(WALLET[0].ref, wtx.inputs[0])
|
||||
assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1), wtx.outputs[0])
|
||||
assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun generateSimpleSpendWithParties() {
|
||||
val tx = TransactionBuilder()
|
||||
Cash().generateSpend(tx, 80.DOLLARS, ALICE_PUBKEY, WALLET, setOf(MINI_CORP))
|
||||
assertEquals(WALLET[2].ref, tx.inputStates()[0])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun generateSimpleSpendWithChange() {
|
||||
val wtx = makeSpend(10.DOLLARS, THEIR_PUBKEY_1)
|
||||
assertEquals(WALLET[0].ref, wtx.inputs[0])
|
||||
assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 10.DOLLARS), wtx.outputs[0])
|
||||
assertEquals(WALLET[0].state.copy(amount = 90.DOLLARS), wtx.outputs[1])
|
||||
assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun generateSpendWithTwoInputs() {
|
||||
val wtx = makeSpend(500.DOLLARS, THEIR_PUBKEY_1)
|
||||
assertEquals(WALLET[0].ref, wtx.inputs[0])
|
||||
assertEquals(WALLET[1].ref, wtx.inputs[1])
|
||||
assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS), wtx.outputs[0])
|
||||
assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun generateSpendMixedDeposits() {
|
||||
val wtx = makeSpend(580.DOLLARS, THEIR_PUBKEY_1)
|
||||
assertEquals(WALLET[0].ref, wtx.inputs[0])
|
||||
assertEquals(WALLET[1].ref, wtx.inputs[1])
|
||||
assertEquals(WALLET[2].ref, wtx.inputs[2])
|
||||
assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS), wtx.outputs[0])
|
||||
assertEquals(WALLET[2].state.copy(owner = THEIR_PUBKEY_1), wtx.outputs[1])
|
||||
assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun generateSpendInsufficientBalance() {
|
||||
val e: InsufficientBalanceException = assertFailsWith("balance") {
|
||||
makeSpend(1000.DOLLARS, THEIR_PUBKEY_1)
|
||||
}
|
||||
assertEquals((1000 - 580).DOLLARS, e.amountMissing)
|
||||
|
||||
assertFailsWith(InsufficientBalanceException::class) {
|
||||
makeSpend(81.SWISS_FRANCS, THEIR_PUBKEY_1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm that aggregation of states is correctly modelled.
|
||||
*/
|
||||
@Test
|
||||
fun aggregation() {
|
||||
val fiveThousandDollarsFromMega = Cash.State(MEGA_CORP.ref(2), 5000.DOLLARS, MEGA_CORP_PUBKEY, DUMMY_NOTARY)
|
||||
val twoThousandDollarsFromMega = Cash.State(MEGA_CORP.ref(2), 2000.DOLLARS, MINI_CORP_PUBKEY, DUMMY_NOTARY)
|
||||
val oneThousandDollarsFromMini = Cash.State(MINI_CORP.ref(3), 1000.DOLLARS, MEGA_CORP_PUBKEY, DUMMY_NOTARY)
|
||||
|
||||
// Obviously it must be possible to aggregate states with themselves
|
||||
assertEquals(fiveThousandDollarsFromMega.issuanceDef, fiveThousandDollarsFromMega.issuanceDef)
|
||||
|
||||
// Owner is not considered when calculating whether it is possible to aggregate states
|
||||
assertEquals(fiveThousandDollarsFromMega.issuanceDef, twoThousandDollarsFromMega.issuanceDef)
|
||||
|
||||
// States cannot be aggregated if the deposit differs
|
||||
assertNotEquals(fiveThousandDollarsFromMega.issuanceDef, oneThousandDollarsFromMini.issuanceDef)
|
||||
assertNotEquals(twoThousandDollarsFromMega.issuanceDef, oneThousandDollarsFromMini.issuanceDef)
|
||||
|
||||
// States cannot be aggregated if the currency differs
|
||||
assertNotEquals(oneThousandDollarsFromMini.issuanceDef,
|
||||
Cash.State(MINI_CORP.ref(3), 1000.POUNDS, MEGA_CORP_PUBKEY, DUMMY_NOTARY).issuanceDef)
|
||||
|
||||
// States cannot be aggregated if the reference differs
|
||||
assertNotEquals(fiveThousandDollarsFromMega.issuanceDef, fiveThousandDollarsFromMega.copy(deposit = MEGA_CORP.ref(1)).issuanceDef)
|
||||
assertNotEquals(fiveThousandDollarsFromMega.copy(deposit = MEGA_CORP.ref(1)).issuanceDef, fiveThousandDollarsFromMega.issuanceDef)
|
||||
}
|
||||
}
|
246
node/src/test/kotlin/contracts/CommercialPaperTests.kt
Normal file
246
node/src/test/kotlin/contracts/CommercialPaperTests.kt
Normal file
@ -0,0 +1,246 @@
|
||||
package contracts
|
||||
|
||||
import core.*
|
||||
import core.contracts.*
|
||||
import core.crypto.SecureHash
|
||||
import core.testutils.*
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import java.time.Instant
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
interface ICommercialPaperTestTemplate {
|
||||
open fun getPaper(): ICommercialPaperState
|
||||
open fun getIssueCommand(): CommandData
|
||||
open fun getRedeemCommand(): CommandData
|
||||
open fun getMoveCommand(): CommandData
|
||||
}
|
||||
|
||||
class JavaCommercialPaperTest() : ICommercialPaperTestTemplate {
|
||||
override fun getPaper(): ICommercialPaperState = JavaCommercialPaper.State(
|
||||
MEGA_CORP.ref(123),
|
||||
MEGA_CORP_PUBKEY,
|
||||
1000.DOLLARS,
|
||||
TEST_TX_TIME + 7.days,
|
||||
DUMMY_NOTARY
|
||||
)
|
||||
|
||||
override fun getIssueCommand(): CommandData = JavaCommercialPaper.Commands.Issue()
|
||||
override fun getRedeemCommand(): CommandData = JavaCommercialPaper.Commands.Redeem()
|
||||
override fun getMoveCommand(): CommandData = JavaCommercialPaper.Commands.Move()
|
||||
}
|
||||
|
||||
class KotlinCommercialPaperTest() : ICommercialPaperTestTemplate {
|
||||
override fun getPaper(): ICommercialPaperState = CommercialPaper.State(
|
||||
issuance = MEGA_CORP.ref(123),
|
||||
owner = MEGA_CORP_PUBKEY,
|
||||
faceValue = 1000.DOLLARS,
|
||||
maturityDate = TEST_TX_TIME + 7.days,
|
||||
notary = DUMMY_NOTARY
|
||||
)
|
||||
|
||||
override fun getIssueCommand(): CommandData = CommercialPaper.Commands.Issue()
|
||||
override fun getRedeemCommand(): CommandData = CommercialPaper.Commands.Redeem()
|
||||
override fun getMoveCommand(): CommandData = CommercialPaper.Commands.Move()
|
||||
}
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class CommercialPaperTestsGeneric {
|
||||
companion object {
|
||||
@Parameterized.Parameters @JvmStatic
|
||||
fun data() = listOf(JavaCommercialPaperTest(), KotlinCommercialPaperTest())
|
||||
}
|
||||
|
||||
@Parameterized.Parameter
|
||||
lateinit var thisTest: ICommercialPaperTestTemplate
|
||||
|
||||
val attachments = MockStorageService().attachments
|
||||
|
||||
@Test
|
||||
fun ok() {
|
||||
trade().verify()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `not matured at redemption`() {
|
||||
trade(redemptionTime = TEST_TX_TIME + 2.days).expectFailureOfTx(3, "must have matured")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `key mismatch at issue`() {
|
||||
transactionGroup {
|
||||
transaction {
|
||||
output { thisTest.getPaper() }
|
||||
arg(DUMMY_PUBKEY_1) { thisTest.getIssueCommand() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
expectFailureOfTx(1, "signed by the claimed issuer")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `face value is not zero`() {
|
||||
transactionGroup {
|
||||
transaction {
|
||||
output { thisTest.getPaper().withFaceValue(0.DOLLARS) }
|
||||
arg(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
expectFailureOfTx(1, "face value is not zero")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `maturity date not in the past`() {
|
||||
transactionGroup {
|
||||
transaction {
|
||||
output { thisTest.getPaper().withMaturityDate(TEST_TX_TIME - 10.days) }
|
||||
arg(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
expectFailureOfTx(1, "maturity date is not in the past")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `issue cannot replace an existing state`() {
|
||||
transactionGroup {
|
||||
roots {
|
||||
transaction(thisTest.getPaper() label "paper")
|
||||
}
|
||||
transaction {
|
||||
input("paper")
|
||||
output { thisTest.getPaper() }
|
||||
arg(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
expectFailureOfTx(1, "there is no input state")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `did not receive enough money at redemption`() {
|
||||
trade(aliceGetsBack = 700.DOLLARS).expectFailureOfTx(3, "received amount equals the face value")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `paper must be destroyed by redemption`() {
|
||||
trade(destroyPaperAtRedemption = false).expectFailureOfTx(3, "must be destroyed")
|
||||
}
|
||||
|
||||
fun cashOutputsToWallet(vararg states: Cash.State): Pair<LedgerTransaction, List<StateAndRef<Cash.State>>> {
|
||||
val ltx = LedgerTransaction(emptyList(), emptyList(), listOf(*states), emptyList(), SecureHash.randomSHA256())
|
||||
return Pair(ltx, states.mapIndexed { index, state -> StateAndRef(state, StateRef(ltx.id, index)) })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `issue move and then redeem`() {
|
||||
// MiniCorp issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself.
|
||||
val issueTX: LedgerTransaction = run {
|
||||
val ptx = CommercialPaper().generateIssue(MINI_CORP.ref(123), 10000.DOLLARS, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply {
|
||||
setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds)
|
||||
signWith(MINI_CORP_KEY)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}
|
||||
val stx = ptx.toSignedTransaction()
|
||||
stx.verifyToLedgerTransaction(MockIdentityService, attachments)
|
||||
}
|
||||
|
||||
val (alicesWalletTX, alicesWallet) = cashOutputsToWallet(
|
||||
3000.DOLLARS.CASH `owned by` ALICE_PUBKEY,
|
||||
3000.DOLLARS.CASH `owned by` ALICE_PUBKEY,
|
||||
3000.DOLLARS.CASH `owned by` ALICE_PUBKEY
|
||||
)
|
||||
|
||||
// Alice pays $9000 to MiniCorp to own some of their debt.
|
||||
val moveTX: LedgerTransaction = run {
|
||||
val ptx = TransactionBuilder()
|
||||
Cash().generateSpend(ptx, 9000.DOLLARS, MINI_CORP_PUBKEY, alicesWallet)
|
||||
CommercialPaper().generateMove(ptx, issueTX.outRef(0), ALICE_PUBKEY)
|
||||
ptx.signWith(MINI_CORP_KEY)
|
||||
ptx.signWith(ALICE_KEY)
|
||||
ptx.signWith(DUMMY_NOTARY_KEY)
|
||||
ptx.toSignedTransaction().verifyToLedgerTransaction(MockIdentityService, attachments)
|
||||
}
|
||||
|
||||
// Won't be validated.
|
||||
val (corpWalletTX, corpWallet) = cashOutputsToWallet(
|
||||
9000.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY,
|
||||
4000.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY
|
||||
)
|
||||
|
||||
fun makeRedeemTX(time: Instant): LedgerTransaction {
|
||||
val ptx = TransactionBuilder()
|
||||
ptx.setTime(time, DUMMY_NOTARY, 30.seconds)
|
||||
CommercialPaper().generateRedeem(ptx, moveTX.outRef(1), corpWallet)
|
||||
ptx.signWith(ALICE_KEY)
|
||||
ptx.signWith(MINI_CORP_KEY)
|
||||
ptx.signWith(DUMMY_NOTARY_KEY)
|
||||
return ptx.toSignedTransaction().verifyToLedgerTransaction(MockIdentityService, attachments)
|
||||
}
|
||||
|
||||
val tooEarlyRedemption = makeRedeemTX(TEST_TX_TIME + 10.days)
|
||||
val validRedemption = makeRedeemTX(TEST_TX_TIME + 31.days)
|
||||
|
||||
val e = assertFailsWith(TransactionVerificationException::class) {
|
||||
TransactionGroup(setOf(issueTX, moveTX, tooEarlyRedemption), setOf(corpWalletTX, alicesWalletTX)).verify()
|
||||
}
|
||||
assertTrue(e.cause!!.message!!.contains("paper must have matured"))
|
||||
|
||||
TransactionGroup(setOf(issueTX, moveTX, validRedemption), setOf(corpWalletTX, alicesWalletTX)).verify()
|
||||
}
|
||||
|
||||
// Generate a trade lifecycle with various parameters.
|
||||
fun trade(redemptionTime: Instant = TEST_TX_TIME + 8.days,
|
||||
aliceGetsBack: Amount = 1000.DOLLARS,
|
||||
destroyPaperAtRedemption: Boolean = true): TransactionGroupDSL<ICommercialPaperState> {
|
||||
val someProfits = 1200.DOLLARS
|
||||
return transactionGroupFor() {
|
||||
roots {
|
||||
transaction(900.DOLLARS.CASH `owned by` ALICE_PUBKEY label "alice's $900")
|
||||
transaction(someProfits.CASH `owned by` MEGA_CORP_PUBKEY label "some profits")
|
||||
}
|
||||
|
||||
// Some CP is issued onto the ledger by MegaCorp.
|
||||
transaction("Issuance") {
|
||||
output("paper") { thisTest.getPaper() }
|
||||
arg(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
// The CP is sold to alice for her $900, $100 less than the face value. At 10% interest after only 7 days,
|
||||
// that sounds a bit too good to be true!
|
||||
transaction("Trade") {
|
||||
input("paper")
|
||||
input("alice's $900")
|
||||
output("borrowed $900") { 900.DOLLARS.CASH `owned by` MEGA_CORP_PUBKEY }
|
||||
output("alice's paper") { "paper".output `owned by` ALICE_PUBKEY }
|
||||
arg(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||
arg(MEGA_CORP_PUBKEY) { thisTest.getMoveCommand() }
|
||||
}
|
||||
|
||||
// Time passes, and Alice redeem's her CP for $1000, netting a $100 profit. MegaCorp has received $1200
|
||||
// as a single payment from somewhere and uses it to pay Alice off, keeping the remaining $200 as change.
|
||||
transaction("Redemption") {
|
||||
input("alice's paper")
|
||||
input("some profits")
|
||||
|
||||
output("Alice's profit") { aliceGetsBack.CASH `owned by` ALICE_PUBKEY }
|
||||
output("Change") { (someProfits - aliceGetsBack).CASH `owned by` MEGA_CORP_PUBKEY }
|
||||
if (!destroyPaperAtRedemption)
|
||||
output { "paper".output }
|
||||
|
||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
arg(ALICE_PUBKEY) { thisTest.getRedeemCommand() }
|
||||
|
||||
timestamp(redemptionTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
160
node/src/test/kotlin/contracts/CrowdFundTests.kt
Normal file
160
node/src/test/kotlin/contracts/CrowdFundTests.kt
Normal file
@ -0,0 +1,160 @@
|
||||
package contracts
|
||||
|
||||
import core.*
|
||||
import core.contracts.*
|
||||
import core.crypto.SecureHash
|
||||
import core.testutils.*
|
||||
import org.junit.Test
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class CrowdFundTests {
|
||||
val CF_1 = CrowdFund.State(
|
||||
campaign = CrowdFund.Campaign(
|
||||
owner = MINI_CORP_PUBKEY,
|
||||
name = "kickstart me",
|
||||
target = 1000.DOLLARS,
|
||||
closingTime = TEST_TX_TIME + 7.days
|
||||
),
|
||||
closed = false,
|
||||
pledges = ArrayList<CrowdFund.Pledge>(),
|
||||
notary = DUMMY_NOTARY
|
||||
)
|
||||
|
||||
val attachments = MockStorageService().attachments
|
||||
|
||||
@Test
|
||||
fun `key mismatch at issue`() {
|
||||
transactionGroup {
|
||||
transaction {
|
||||
output { CF_1 }
|
||||
arg(DUMMY_PUBKEY_1) { CrowdFund.Commands.Register() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
expectFailureOfTx(1, "the transaction is signed by the owner of the crowdsourcing")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `closing time not in the future`() {
|
||||
transactionGroup {
|
||||
transaction {
|
||||
output { CF_1.copy(campaign = CF_1.campaign.copy(closingTime = TEST_TX_TIME - 1.days)) }
|
||||
arg(MINI_CORP_PUBKEY) { CrowdFund.Commands.Register() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
expectFailureOfTx(1, "the output registration has a closing time in the future")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ok() {
|
||||
raiseFunds().verify()
|
||||
}
|
||||
|
||||
private fun raiseFunds(): TransactionGroupDSL<CrowdFund.State> {
|
||||
return transactionGroupFor {
|
||||
roots {
|
||||
transaction(1000.DOLLARS.CASH `owned by` ALICE_PUBKEY label "alice's $1000")
|
||||
}
|
||||
|
||||
// 1. Create the funding opportunity
|
||||
transaction {
|
||||
output("funding opportunity") { CF_1 }
|
||||
arg(MINI_CORP_PUBKEY) { CrowdFund.Commands.Register() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
// 2. Place a pledge
|
||||
transaction {
|
||||
input ("funding opportunity")
|
||||
input("alice's $1000")
|
||||
output ("pledged opportunity") {
|
||||
CF_1.copy(
|
||||
pledges = CF_1.pledges + CrowdFund.Pledge(ALICE_PUBKEY, 1000.DOLLARS)
|
||||
)
|
||||
}
|
||||
output { 1000.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY }
|
||||
arg(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||
arg(ALICE_PUBKEY) { CrowdFund.Commands.Pledge() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
// 3. Close the opportunity, assuming the target has been met
|
||||
transaction {
|
||||
input ("pledged opportunity")
|
||||
output ("funded and closed") { "pledged opportunity".output.copy(closed = true) }
|
||||
arg(MINI_CORP_PUBKEY) { CrowdFund.Commands.Close() }
|
||||
timestamp(time = TEST_TX_TIME + 8.days)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cashOutputsToWallet(vararg states: Cash.State): Pair<LedgerTransaction, List<StateAndRef<Cash.State>>> {
|
||||
val ltx = LedgerTransaction(emptyList(), emptyList(), listOf(*states), emptyList(), SecureHash.randomSHA256())
|
||||
return Pair(ltx, states.mapIndexed { index, state -> StateAndRef(state, StateRef(ltx.id, index)) })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `raise more funds using output-state generation functions`() {
|
||||
// MiniCorp registers a crowdfunding of $1,000, to close in 7 days.
|
||||
val registerTX: LedgerTransaction = run {
|
||||
// craftRegister returns a partial transaction
|
||||
val ptx = CrowdFund().generateRegister(MINI_CORP.ref(123), 1000.DOLLARS, "crowd funding", TEST_TX_TIME + 7.days, DUMMY_NOTARY).apply {
|
||||
setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds)
|
||||
signWith(MINI_CORP_KEY)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}
|
||||
ptx.toSignedTransaction().verifyToLedgerTransaction(MockIdentityService, attachments)
|
||||
}
|
||||
|
||||
// let's give Alice some funds that she can invest
|
||||
val (aliceWalletTX, aliceWallet) = cashOutputsToWallet(
|
||||
200.DOLLARS.CASH `owned by` ALICE_PUBKEY,
|
||||
500.DOLLARS.CASH `owned by` ALICE_PUBKEY,
|
||||
300.DOLLARS.CASH `owned by` ALICE_PUBKEY
|
||||
)
|
||||
|
||||
// Alice pays $1000 to MiniCorp to fund their campaign.
|
||||
val pledgeTX: LedgerTransaction = run {
|
||||
val ptx = TransactionBuilder()
|
||||
CrowdFund().generatePledge(ptx, registerTX.outRef(0), ALICE_PUBKEY)
|
||||
Cash().generateSpend(ptx, 1000.DOLLARS, MINI_CORP_PUBKEY, aliceWallet)
|
||||
ptx.setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds)
|
||||
ptx.signWith(ALICE_KEY)
|
||||
ptx.signWith(DUMMY_NOTARY_KEY)
|
||||
// this verify passes - the transaction contains an output cash, necessary to verify the fund command
|
||||
ptx.toSignedTransaction().verifyToLedgerTransaction(MockIdentityService, attachments)
|
||||
}
|
||||
|
||||
// Won't be validated.
|
||||
val (miniCorpWalletTx, miniCorpWallet) = cashOutputsToWallet(
|
||||
900.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY,
|
||||
400.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY
|
||||
)
|
||||
// MiniCorp closes their campaign.
|
||||
fun makeFundedTX(time: Instant): LedgerTransaction {
|
||||
val ptx = TransactionBuilder()
|
||||
ptx.setTime(time, DUMMY_NOTARY, 30.seconds)
|
||||
CrowdFund().generateClose(ptx, pledgeTX.outRef(0), miniCorpWallet)
|
||||
ptx.signWith(MINI_CORP_KEY)
|
||||
ptx.signWith(DUMMY_NOTARY_KEY)
|
||||
return ptx.toSignedTransaction().verifyToLedgerTransaction(MockIdentityService, attachments)
|
||||
}
|
||||
|
||||
val tooEarlyClose = makeFundedTX(TEST_TX_TIME + 6.days)
|
||||
val validClose = makeFundedTX(TEST_TX_TIME + 8.days)
|
||||
|
||||
val e = assertFailsWith(TransactionVerificationException::class) {
|
||||
TransactionGroup(setOf(registerTX, pledgeTX, tooEarlyClose), setOf(miniCorpWalletTx, aliceWalletTX)).verify()
|
||||
}
|
||||
assertTrue(e.cause!!.message!!.contains("the closing date has past"))
|
||||
|
||||
// This verification passes
|
||||
TransactionGroup(setOf(registerTX, pledgeTX, validClose), setOf(aliceWalletTX)).verify()
|
||||
}
|
||||
}
|
733
node/src/test/kotlin/contracts/IRSTests.kt
Normal file
733
node/src/test/kotlin/contracts/IRSTests.kt
Normal file
@ -0,0 +1,733 @@
|
||||
package contracts
|
||||
|
||||
import core.*
|
||||
import core.contracts.*
|
||||
import core.testutils.*
|
||||
import org.junit.Test
|
||||
import java.math.BigDecimal
|
||||
import java.time.LocalDate
|
||||
import java.util.*
|
||||
|
||||
fun createDummyIRS(irsSelect: Int): InterestRateSwap.State {
|
||||
return when (irsSelect) {
|
||||
1 -> {
|
||||
|
||||
val fixedLeg = InterestRateSwap.FixedLeg(
|
||||
fixedRatePayer = MEGA_CORP,
|
||||
notional = 15900000.DOLLARS,
|
||||
paymentFrequency = Frequency.SemiAnnual,
|
||||
effectiveDate = LocalDate.of(2016, 3, 10),
|
||||
effectiveDateAdjustment = null,
|
||||
terminationDate = LocalDate.of(2026, 3, 10),
|
||||
terminationDateAdjustment = null,
|
||||
fixedRate = FixedRate(PercentageRatioUnit("1.677")),
|
||||
dayCountBasisDay = DayCountBasisDay.D30,
|
||||
dayCountBasisYear = DayCountBasisYear.Y360,
|
||||
rollConvention = DateRollConvention.ModifiedFollowing,
|
||||
dayInMonth = 10,
|
||||
paymentRule = PaymentRule.InArrears,
|
||||
paymentDelay = 0,
|
||||
paymentCalendar = BusinessCalendar.getInstance("London", "NewYork"),
|
||||
interestPeriodAdjustment = AccrualAdjustment.Adjusted
|
||||
)
|
||||
|
||||
val floatingLeg = InterestRateSwap.FloatingLeg(
|
||||
floatingRatePayer = MINI_CORP,
|
||||
notional = 15900000.DOLLARS,
|
||||
paymentFrequency = Frequency.Quarterly,
|
||||
effectiveDate = LocalDate.of(2016, 3, 10),
|
||||
effectiveDateAdjustment = null,
|
||||
terminationDate = LocalDate.of(2026, 3, 10),
|
||||
terminationDateAdjustment = null,
|
||||
dayCountBasisDay = DayCountBasisDay.D30,
|
||||
dayCountBasisYear = DayCountBasisYear.Y360,
|
||||
rollConvention = DateRollConvention.ModifiedFollowing,
|
||||
fixingRollConvention = DateRollConvention.ModifiedFollowing,
|
||||
dayInMonth = 10,
|
||||
resetDayInMonth = 10,
|
||||
paymentRule = PaymentRule.InArrears,
|
||||
paymentDelay = 0,
|
||||
paymentCalendar = BusinessCalendar.getInstance("London", "NewYork"),
|
||||
interestPeriodAdjustment = AccrualAdjustment.Adjusted,
|
||||
fixingPeriod = DateOffset.TWODAYS,
|
||||
resetRule = PaymentRule.InAdvance,
|
||||
fixingsPerPayment = Frequency.Quarterly,
|
||||
fixingCalendar = BusinessCalendar.getInstance("London"),
|
||||
index = "LIBOR",
|
||||
indexSource = "TEL3750",
|
||||
indexTenor = Tenor("3M")
|
||||
)
|
||||
|
||||
val calculation = InterestRateSwap.Calculation (
|
||||
|
||||
// TODO: this seems to fail quite dramatically
|
||||
//expression = "fixedLeg.notional * fixedLeg.fixedRate",
|
||||
|
||||
// TODO: How I want it to look
|
||||
//expression = "( fixedLeg.notional * (fixedLeg.fixedRate)) - (floatingLeg.notional * (rateSchedule.get(context.getDate('currentDate'))))",
|
||||
|
||||
// How it's ended up looking, which I think is now broken but it's a WIP.
|
||||
expression = Expression("( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) -" +
|
||||
"(floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))"),
|
||||
|
||||
floatingLegPaymentSchedule = HashMap(),
|
||||
fixedLegPaymentSchedule = HashMap()
|
||||
)
|
||||
|
||||
val EUR = currency("EUR")
|
||||
|
||||
val common = InterestRateSwap.Common(
|
||||
baseCurrency = EUR,
|
||||
eligibleCurrency = EUR,
|
||||
eligibleCreditSupport = "Cash in an Eligible Currency",
|
||||
independentAmounts = Amount(0, EUR),
|
||||
threshold = Amount(0, EUR),
|
||||
minimumTransferAmount = Amount(250000 * 100, EUR),
|
||||
rounding = Amount(10000 * 100, EUR),
|
||||
valuationDate = "Every Local Business Day",
|
||||
notificationTime = "2:00pm London",
|
||||
resolutionTime = "2:00pm London time on the first LocalBusiness Day following the date on which the notice is given ",
|
||||
interestRate = ReferenceRate("T3270", Tenor("6M"), "EONIA"),
|
||||
addressForTransfers = "",
|
||||
exposure = UnknownType(),
|
||||
localBusinessDay = BusinessCalendar.getInstance("London"),
|
||||
tradeID = "trade1",
|
||||
hashLegalDocs = "put hash here",
|
||||
dailyInterestAmount = Expression("(CashAmount * InterestRate ) / (fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360")
|
||||
)
|
||||
|
||||
InterestRateSwap.State(fixedLeg = fixedLeg, floatingLeg = floatingLeg, calculation = calculation, common = common, notary = DUMMY_NOTARY)
|
||||
}
|
||||
2 -> {
|
||||
// 10y swap, we pay 1.3% fixed 30/360 semi, rec 3m usd libor act/360 Q on 25m notional (mod foll/adj on both sides)
|
||||
// I did a mock up start date 10/03/2015 – 10/03/2025 so you have 5 cashflows on float side that have been preset the rest are unknown
|
||||
|
||||
val fixedLeg = InterestRateSwap.FixedLeg(
|
||||
fixedRatePayer = MEGA_CORP,
|
||||
notional = 25000000.DOLLARS,
|
||||
paymentFrequency = Frequency.SemiAnnual,
|
||||
effectiveDate = LocalDate.of(2015, 3, 10),
|
||||
effectiveDateAdjustment = null,
|
||||
terminationDate = LocalDate.of(2025, 3, 10),
|
||||
terminationDateAdjustment = null,
|
||||
fixedRate = FixedRate(PercentageRatioUnit("1.3")),
|
||||
dayCountBasisDay = DayCountBasisDay.D30,
|
||||
dayCountBasisYear = DayCountBasisYear.Y360,
|
||||
rollConvention = DateRollConvention.ModifiedFollowing,
|
||||
dayInMonth = 10,
|
||||
paymentRule = PaymentRule.InArrears,
|
||||
paymentDelay = 0,
|
||||
paymentCalendar = BusinessCalendar.getInstance(),
|
||||
interestPeriodAdjustment = AccrualAdjustment.Adjusted
|
||||
)
|
||||
|
||||
val floatingLeg = InterestRateSwap.FloatingLeg(
|
||||
floatingRatePayer = MINI_CORP,
|
||||
notional = 25000000.DOLLARS,
|
||||
paymentFrequency = Frequency.Quarterly,
|
||||
effectiveDate = LocalDate.of(2015, 3, 10),
|
||||
effectiveDateAdjustment = null,
|
||||
terminationDate = LocalDate.of(2025, 3, 10),
|
||||
terminationDateAdjustment = null,
|
||||
dayCountBasisDay = DayCountBasisDay.DActual,
|
||||
dayCountBasisYear = DayCountBasisYear.Y360,
|
||||
rollConvention = DateRollConvention.ModifiedFollowing,
|
||||
fixingRollConvention = DateRollConvention.ModifiedFollowing,
|
||||
dayInMonth = 10,
|
||||
resetDayInMonth = 10,
|
||||
paymentRule = PaymentRule.InArrears,
|
||||
paymentDelay = 0,
|
||||
paymentCalendar = BusinessCalendar.getInstance(),
|
||||
interestPeriodAdjustment = AccrualAdjustment.Adjusted,
|
||||
fixingPeriod = DateOffset.TWODAYS,
|
||||
resetRule = PaymentRule.InAdvance,
|
||||
fixingsPerPayment = Frequency.Quarterly,
|
||||
fixingCalendar = BusinessCalendar.getInstance(),
|
||||
index = "USD LIBOR",
|
||||
indexSource = "TEL3750",
|
||||
indexTenor = Tenor("3M")
|
||||
)
|
||||
|
||||
val calculation = InterestRateSwap.Calculation (
|
||||
|
||||
// TODO: this seems to fail quite dramatically
|
||||
//expression = "fixedLeg.notional * fixedLeg.fixedRate",
|
||||
|
||||
// TODO: How I want it to look
|
||||
//expression = "( fixedLeg.notional * (fixedLeg.fixedRate)) - (floatingLeg.notional * (rateSchedule.get(context.getDate('currentDate'))))",
|
||||
|
||||
// How it's ended up looking, which I think is now broken but it's a WIP.
|
||||
expression = Expression("( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) -" +
|
||||
"(floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))"),
|
||||
|
||||
floatingLegPaymentSchedule = HashMap(),
|
||||
fixedLegPaymentSchedule = HashMap()
|
||||
)
|
||||
|
||||
val EUR = currency("EUR")
|
||||
|
||||
val common = InterestRateSwap.Common(
|
||||
baseCurrency = EUR,
|
||||
eligibleCurrency = EUR,
|
||||
eligibleCreditSupport = "Cash in an Eligible Currency",
|
||||
independentAmounts = Amount(0, EUR),
|
||||
threshold = Amount(0, EUR),
|
||||
minimumTransferAmount = Amount(250000 * 100, EUR),
|
||||
rounding = Amount(10000 * 100, EUR),
|
||||
valuationDate = "Every Local Business Day",
|
||||
notificationTime = "2:00pm London",
|
||||
resolutionTime = "2:00pm London time on the first LocalBusiness Day following the date on which the notice is given ",
|
||||
interestRate = ReferenceRate("T3270", Tenor("6M"), "EONIA"),
|
||||
addressForTransfers = "",
|
||||
exposure = UnknownType(),
|
||||
localBusinessDay = BusinessCalendar.getInstance("London"),
|
||||
tradeID = "trade2",
|
||||
hashLegalDocs = "put hash here",
|
||||
dailyInterestAmount = Expression("(CashAmount * InterestRate ) / (fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360")
|
||||
)
|
||||
|
||||
return InterestRateSwap.State(fixedLeg = fixedLeg, floatingLeg = floatingLeg, calculation = calculation, common = common, notary = DUMMY_NOTARY)
|
||||
|
||||
}
|
||||
else -> TODO("IRS number $irsSelect not defined")
|
||||
}
|
||||
}
|
||||
|
||||
class IRSTests {
|
||||
|
||||
val attachments = MockStorageService().attachments
|
||||
|
||||
val exampleIRS = createDummyIRS(1)
|
||||
|
||||
val inState = InterestRateSwap.State(
|
||||
exampleIRS.fixedLeg,
|
||||
exampleIRS.floatingLeg,
|
||||
exampleIRS.calculation,
|
||||
exampleIRS.common,
|
||||
DUMMY_NOTARY
|
||||
)
|
||||
|
||||
val outState = inState.copy()
|
||||
|
||||
@Test
|
||||
fun ok() {
|
||||
trade().verify()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ok with groups`() {
|
||||
tradegroups().verify()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an IRS txn - we'll need it for a few things.
|
||||
*/
|
||||
fun generateIRSTxn(irsSelect: Int): LedgerTransaction {
|
||||
val dummyIRS = createDummyIRS(irsSelect)
|
||||
val genTX: LedgerTransaction = run {
|
||||
val gtx = InterestRateSwap().generateAgreement(
|
||||
fixedLeg = dummyIRS.fixedLeg,
|
||||
floatingLeg = dummyIRS.floatingLeg,
|
||||
calculation = dummyIRS.calculation,
|
||||
common = dummyIRS.common,
|
||||
notary = DUMMY_NOTARY).apply {
|
||||
setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds)
|
||||
signWith(MEGA_CORP_KEY)
|
||||
signWith(MINI_CORP_KEY)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}
|
||||
gtx.toSignedTransaction().verifyToLedgerTransaction(MockIdentityService, attachments)
|
||||
}
|
||||
return genTX
|
||||
}
|
||||
|
||||
/**
|
||||
* Just make sure it's sane.
|
||||
*/
|
||||
@Test
|
||||
fun pprintIRS() {
|
||||
val irs = singleIRS()
|
||||
println(irs.prettyPrint())
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility so I don't have to keep typing this
|
||||
*/
|
||||
fun singleIRS(irsSelector: Int = 1): InterestRateSwap.State {
|
||||
return generateIRSTxn(irsSelector).outputs.filterIsInstance<InterestRateSwap.State>().single()
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the generate. No explicit exception as if something goes wrong, we'll find out anyway.
|
||||
*/
|
||||
@Test
|
||||
fun generateIRS() {
|
||||
// Tests aren't allowed to return things
|
||||
generateIRSTxn(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing a simple IRS, add a few fixings and then display as CSV
|
||||
*/
|
||||
@Test
|
||||
fun `IRS Export test`() {
|
||||
// No transactions etc required - we're just checking simple maths and export functionallity
|
||||
val irs = singleIRS(2)
|
||||
|
||||
var newCalculation = irs.calculation
|
||||
|
||||
val fixings = mapOf(LocalDate.of(2015, 3, 6) to "0.6",
|
||||
LocalDate.of(2015, 6, 8) to "0.75",
|
||||
LocalDate.of(2015, 9, 8) to "0.8",
|
||||
LocalDate.of(2015, 12, 8) to "0.55",
|
||||
LocalDate.of(2016, 3, 8) to "0.644")
|
||||
|
||||
for (it in fixings) {
|
||||
newCalculation = newCalculation.applyFixing(it.key, FixedRate(PercentageRatioUnit(it.value)))
|
||||
}
|
||||
|
||||
val newIRS = InterestRateSwap.State(irs.fixedLeg, irs.floatingLeg, newCalculation, irs.common, DUMMY_NOTARY)
|
||||
println(newIRS.exportIRSToCSV())
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure it has a schedule and the schedule has some unfixed rates
|
||||
*/
|
||||
@Test
|
||||
fun `next fixing date`() {
|
||||
val irs = singleIRS(1)
|
||||
println(irs.calculation.nextFixingDate())
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate through all the fix dates and add something
|
||||
*/
|
||||
@Test
|
||||
fun generateIRSandFixSome() {
|
||||
var previousTXN = generateIRSTxn(1)
|
||||
var currentIRS = previousTXN.outputs.filterIsInstance<InterestRateSwap.State>().single()
|
||||
println(currentIRS.prettyPrint())
|
||||
while (true) {
|
||||
val nextFixingDate = currentIRS.calculation.nextFixingDate() ?: break
|
||||
println("\n\n\n ***** Applying a fixing to $nextFixingDate \n\n\n")
|
||||
var fixTX: LedgerTransaction = run {
|
||||
val tx = TransactionBuilder()
|
||||
val fixing = Pair(nextFixingDate, FixedRate("0.052".percent))
|
||||
InterestRateSwap().generateFix(tx, previousTXN.outRef(0), fixing)
|
||||
with(tx) {
|
||||
setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds)
|
||||
signWith(MEGA_CORP_KEY)
|
||||
signWith(MINI_CORP_KEY)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}
|
||||
tx.toSignedTransaction().verifyToLedgerTransaction(MockIdentityService, attachments)
|
||||
}
|
||||
currentIRS = previousTXN.outputs.filterIsInstance<InterestRateSwap.State>().single()
|
||||
println(currentIRS.prettyPrint())
|
||||
previousTXN = fixTX
|
||||
}
|
||||
}
|
||||
|
||||
// Move these later as they aren't IRS specific.
|
||||
@Test
|
||||
fun `test some rate objects 100 * FixedRate(5%)`() {
|
||||
val r1 = FixedRate(PercentageRatioUnit("5"))
|
||||
assert(100 * r1 == 5)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `expression calculation testing`() {
|
||||
val dummyIRS = singleIRS()
|
||||
val stuffToPrint: ArrayList<String> = arrayListOf(
|
||||
"fixedLeg.notional.pennies",
|
||||
"fixedLeg.fixedRate.ratioUnit",
|
||||
"fixedLeg.fixedRate.ratioUnit.value",
|
||||
"floatingLeg.notional.pennies",
|
||||
"fixedLeg.fixedRate",
|
||||
"currentBusinessDate",
|
||||
"calculation.floatingLegPaymentSchedule.get(currentBusinessDate)",
|
||||
"fixedLeg.notional.currency.currencyCode",
|
||||
"fixedLeg.notional.pennies * 10",
|
||||
"fixedLeg.notional.pennies * fixedLeg.fixedRate.ratioUnit.value",
|
||||
"(fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360 ",
|
||||
"(fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value))"
|
||||
// "calculation.floatingLegPaymentSchedule.get(context.getDate('currentDate')).rate"
|
||||
// "calculation.floatingLegPaymentSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value",
|
||||
//"( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) - (floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))",
|
||||
// "( fixedLeg.notional * fixedLeg.fixedRate )"
|
||||
)
|
||||
|
||||
for (i in stuffToPrint) {
|
||||
println(i)
|
||||
var z = dummyIRS.evaluateCalculation(LocalDate.of(2016, 9, 12), Expression(i))
|
||||
println(z.javaClass)
|
||||
println(z)
|
||||
println("-----------")
|
||||
}
|
||||
// This does not throw an exception in the test itself; it evaluates the above and they will throw if they do not pass.
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generates a typical transactional history for an IRS.
|
||||
*/
|
||||
fun trade(): TransactionGroupDSL<InterestRateSwap.State> {
|
||||
|
||||
val ld = LocalDate.of(2016, 3, 8)
|
||||
val bd = BigDecimal("0.0063518")
|
||||
|
||||
val txgroup: TransactionGroupDSL<InterestRateSwap.State> = transactionGroupFor() {
|
||||
transaction("Agreement") {
|
||||
output("irs post agreement") { singleIRS() }
|
||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
transaction("Fix") {
|
||||
input("irs post agreement")
|
||||
output("irs post first fixing") {
|
||||
"irs post agreement".output.copy(
|
||||
"irs post agreement".output.fixedLeg,
|
||||
"irs post agreement".output.floatingLeg,
|
||||
"irs post agreement".output.calculation.applyFixing(ld, FixedRate(RatioUnit(bd))),
|
||||
"irs post agreement".output.common
|
||||
)
|
||||
}
|
||||
arg(ORACLE_PUBKEY) {
|
||||
InterestRateSwap.Commands.Fix()
|
||||
}
|
||||
arg(ORACLE_PUBKEY) {
|
||||
Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)
|
||||
}
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
}
|
||||
return txgroup
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure failure occurs when there are inbound states for an agreement command`() {
|
||||
transaction {
|
||||
input() { singleIRS() }
|
||||
output("irs post agreement") { singleIRS() }
|
||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails requirement` "There are no in states for an agreement"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure failure occurs when no events in fix schedule`() {
|
||||
val irs = singleIRS()
|
||||
val emptySchedule = HashMap<LocalDate, FixedRatePaymentEvent>()
|
||||
transaction {
|
||||
output() {
|
||||
irs.copy(calculation = irs.calculation.copy(fixedLegPaymentSchedule = emptySchedule))
|
||||
}
|
||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails requirement` "There are events in the fix schedule"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure failure occurs when no events in floating schedule`() {
|
||||
val irs = singleIRS()
|
||||
val emptySchedule = HashMap<LocalDate, FloatingRatePaymentEvent>()
|
||||
transaction {
|
||||
output() {
|
||||
irs.copy(calculation = irs.calculation.copy(floatingLegPaymentSchedule = emptySchedule))
|
||||
}
|
||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails requirement` "There are events in the float schedule"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure notionals are non zero`() {
|
||||
val irs = singleIRS()
|
||||
transaction {
|
||||
output() {
|
||||
irs.copy(irs.fixedLeg.copy(notional = irs.fixedLeg.notional.copy(pennies = 0)))
|
||||
}
|
||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails requirement` "All notionals must be non zero"
|
||||
}
|
||||
|
||||
transaction {
|
||||
output() {
|
||||
irs.copy(irs.fixedLeg.copy(notional = irs.floatingLeg.notional.copy(pennies = 0)))
|
||||
}
|
||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails requirement` "All notionals must be non zero"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure positive rate on fixed leg`() {
|
||||
val irs = singleIRS()
|
||||
val modifiedIRS = irs.copy(fixedLeg = irs.fixedLeg.copy(fixedRate = FixedRate(PercentageRatioUnit("-0.1"))))
|
||||
transaction {
|
||||
output() {
|
||||
modifiedIRS
|
||||
}
|
||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails requirement` "The fixed leg rate must be positive"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This will be modified once we adapt the IRS to be cross currency
|
||||
*/
|
||||
@Test
|
||||
fun `ensure same currency notionals`() {
|
||||
val irs = singleIRS()
|
||||
val modifiedIRS = irs.copy(fixedLeg = irs.fixedLeg.copy(notional = Amount(irs.fixedLeg.notional.pennies, Currency.getInstance("JPY"))))
|
||||
transaction {
|
||||
output() {
|
||||
modifiedIRS
|
||||
}
|
||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails requirement` "The currency of the notionals must be the same"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure notional amounts are equal`() {
|
||||
val irs = singleIRS()
|
||||
val modifiedIRS = irs.copy(fixedLeg = irs.fixedLeg.copy(notional = Amount(irs.floatingLeg.notional.pennies + 1, irs.floatingLeg.notional.currency)))
|
||||
transaction {
|
||||
output() {
|
||||
modifiedIRS
|
||||
}
|
||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails requirement` "All leg notionals must be the same"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure trade date and termination date checks are done pt1`() {
|
||||
val irs = singleIRS()
|
||||
val modifiedIRS1 = irs.copy(fixedLeg = irs.fixedLeg.copy(terminationDate = irs.fixedLeg.effectiveDate.minusDays(1)))
|
||||
transaction {
|
||||
output() {
|
||||
modifiedIRS1
|
||||
}
|
||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails requirement` "The effective date is before the termination date for the fixed leg"
|
||||
}
|
||||
|
||||
val modifiedIRS2 = irs.copy(floatingLeg = irs.floatingLeg.copy(terminationDate = irs.floatingLeg.effectiveDate.minusDays(1)))
|
||||
transaction {
|
||||
output() {
|
||||
modifiedIRS2
|
||||
}
|
||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails requirement` "The effective date is before the termination date for the floating leg"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure trade date and termination date checks are done pt2`() {
|
||||
val irs = singleIRS()
|
||||
|
||||
val modifiedIRS3 = irs.copy(floatingLeg = irs.floatingLeg.copy(terminationDate = irs.fixedLeg.terminationDate.minusDays(1)))
|
||||
transaction {
|
||||
output() {
|
||||
modifiedIRS3
|
||||
}
|
||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails requirement` "The termination dates are aligned"
|
||||
}
|
||||
|
||||
|
||||
val modifiedIRS4 = irs.copy(floatingLeg = irs.floatingLeg.copy(effectiveDate = irs.fixedLeg.effectiveDate.minusDays(1)))
|
||||
transaction {
|
||||
output() {
|
||||
modifiedIRS4
|
||||
}
|
||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails requirement` "The effective dates are aligned"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `various fixing tests`() {
|
||||
|
||||
val ld = LocalDate.of(2016, 3, 8)
|
||||
val bd = BigDecimal("0.0063518")
|
||||
|
||||
transaction {
|
||||
output("irs post agreement") { singleIRS() }
|
||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
val oldIRS = singleIRS(1)
|
||||
val newIRS = oldIRS.copy(oldIRS.fixedLeg,
|
||||
oldIRS.floatingLeg,
|
||||
oldIRS.calculation.applyFixing(ld, FixedRate(RatioUnit(bd))),
|
||||
oldIRS.common)
|
||||
|
||||
transaction {
|
||||
input() {
|
||||
oldIRS
|
||||
|
||||
}
|
||||
|
||||
// Templated tweak for reference. A corrent fixing applied should be ok
|
||||
tweak {
|
||||
arg(ORACLE_PUBKEY) {
|
||||
InterestRateSwap.Commands.Fix()
|
||||
}
|
||||
timestamp(TEST_TX_TIME)
|
||||
arg(ORACLE_PUBKEY) {
|
||||
Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)
|
||||
}
|
||||
output() { newIRS }
|
||||
this.accepts()
|
||||
}
|
||||
|
||||
// This test makes sure that verify confirms the fixing was applied and there is a difference in the old and new
|
||||
tweak {
|
||||
arg(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
arg(ORACLE_PUBKEY) { Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd) }
|
||||
output() { oldIRS }
|
||||
this`fails requirement` "There is at least one difference in the IRS floating leg payment schedules"
|
||||
}
|
||||
|
||||
// This tests tries to sneak in a change to another fixing (which may or may not be the latest one)
|
||||
tweak {
|
||||
arg(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
arg(ORACLE_PUBKEY) {
|
||||
Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)
|
||||
}
|
||||
|
||||
val firstResetKey = newIRS.calculation.floatingLegPaymentSchedule.keys.first()
|
||||
val firstResetValue = newIRS.calculation.floatingLegPaymentSchedule[firstResetKey]
|
||||
var modifiedFirstResetValue = firstResetValue!!.copy(notional = Amount(firstResetValue.notional.pennies, Currency.getInstance("JPY")))
|
||||
|
||||
output() {
|
||||
newIRS.copy(
|
||||
newIRS.fixedLeg,
|
||||
newIRS.floatingLeg,
|
||||
newIRS.calculation.copy(floatingLegPaymentSchedule = newIRS.calculation.floatingLegPaymentSchedule.plus(
|
||||
Pair(firstResetKey, modifiedFirstResetValue))),
|
||||
newIRS.common
|
||||
)
|
||||
}
|
||||
this`fails requirement` "There is only one change in the IRS floating leg payment schedule"
|
||||
}
|
||||
|
||||
// This tests modifies the payment currency for the fixing
|
||||
tweak {
|
||||
arg(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
arg(ORACLE_PUBKEY) { Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd) }
|
||||
|
||||
val latestReset = newIRS.calculation.floatingLegPaymentSchedule.filter { it.value.rate is FixedRate }.maxBy { it.key }
|
||||
var modifiedLatestResetValue = latestReset!!.value.copy(notional = Amount(latestReset.value.notional.pennies, Currency.getInstance("JPY")))
|
||||
|
||||
output() {
|
||||
newIRS.copy(
|
||||
newIRS.fixedLeg,
|
||||
newIRS.floatingLeg,
|
||||
newIRS.calculation.copy(floatingLegPaymentSchedule = newIRS.calculation.floatingLegPaymentSchedule.plus(
|
||||
Pair(latestReset.key, modifiedLatestResetValue))),
|
||||
newIRS.common
|
||||
)
|
||||
}
|
||||
this`fails requirement` "The fix payment has the same currency as the notional"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This returns an example of transactions that are grouped by TradeId and then a fixing applied.
|
||||
* It's important to make the tradeID different for two reasons, the hashes will be the same and all sorts of confusion will
|
||||
* result and the grouping won't work either.
|
||||
* In reality, the only fields that should be in common will be the next fixing date and the reference rate.
|
||||
*/
|
||||
fun tradegroups(): TransactionGroupDSL<InterestRateSwap.State> {
|
||||
val ld1 = LocalDate.of(2016, 3, 8)
|
||||
val bd1 = BigDecimal("0.0063518")
|
||||
|
||||
val irs = singleIRS()
|
||||
|
||||
val txgroup: TransactionGroupDSL<InterestRateSwap.State> = transactionGroupFor() {
|
||||
transaction("Agreement") {
|
||||
output("irs post agreement1") {
|
||||
irs.copy(
|
||||
irs.fixedLeg,
|
||||
irs.floatingLeg,
|
||||
irs.calculation,
|
||||
irs.common.copy(tradeID = "t1")
|
||||
|
||||
)
|
||||
}
|
||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
transaction("Agreement") {
|
||||
output("irs post agreement2") {
|
||||
irs.copy(
|
||||
irs.fixedLeg,
|
||||
irs.floatingLeg,
|
||||
irs.calculation,
|
||||
irs.common.copy(tradeID = "t2")
|
||||
|
||||
)
|
||||
}
|
||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
transaction("Fix") {
|
||||
input("irs post agreement1")
|
||||
input("irs post agreement2")
|
||||
output("irs post first fixing1") {
|
||||
"irs post agreement1".output.copy(
|
||||
"irs post agreement1".output.fixedLeg,
|
||||
"irs post agreement1".output.floatingLeg,
|
||||
"irs post agreement1".output.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))),
|
||||
"irs post agreement1".output.common.copy(tradeID = "t1")
|
||||
)
|
||||
}
|
||||
output("irs post first fixing2") {
|
||||
"irs post agreement2".output.copy(
|
||||
"irs post agreement2".output.fixedLeg,
|
||||
"irs post agreement2".output.floatingLeg,
|
||||
"irs post agreement2".output.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))),
|
||||
"irs post agreement2".output.common.copy(tradeID = "t2")
|
||||
)
|
||||
}
|
||||
|
||||
arg(ORACLE_PUBKEY) {
|
||||
InterestRateSwap.Commands.Fix()
|
||||
}
|
||||
arg(ORACLE_PUBKEY) {
|
||||
Fix(FixOf("ICE LIBOR", ld1, Tenor("3M")), bd1)
|
||||
}
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
}
|
||||
return txgroup
|
||||
}
|
||||
}
|
||||
|
||||
|
130
node/src/test/kotlin/core/MockServices.kt
Normal file
130
node/src/test/kotlin/core/MockServices.kt
Normal file
@ -0,0 +1,130 @@
|
||||
package core
|
||||
|
||||
import com.codahale.metrics.MetricRegistry
|
||||
import core.contracts.Attachment
|
||||
import core.crypto.SecureHash
|
||||
import core.crypto.generateKeyPair
|
||||
import core.crypto.sha256
|
||||
import core.messaging.MessagingService
|
||||
import core.node.ServiceHub
|
||||
import core.node.storage.Checkpoint
|
||||
import core.node.storage.CheckpointStorage
|
||||
import core.node.subsystems.*
|
||||
import core.node.services.AttachmentStorage
|
||||
import core.node.services.IdentityService
|
||||
import core.node.services.NetworkMapService
|
||||
import core.testing.MockNetworkMapCache
|
||||
import core.testutils.MockIdentityService
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.security.KeyPair
|
||||
import java.security.PrivateKey
|
||||
import java.security.PublicKey
|
||||
import java.time.Clock
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import java.util.jar.JarInputStream
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
class MockKeyManagementService(vararg initialKeys: KeyPair) : KeyManagementService {
|
||||
override val keys: MutableMap<PublicKey, PrivateKey>
|
||||
|
||||
init {
|
||||
keys = initialKeys.map { it.public to it.private }.toMap(HashMap())
|
||||
}
|
||||
|
||||
val nextKeys = LinkedList<KeyPair>()
|
||||
|
||||
override fun freshKey(): KeyPair {
|
||||
val k = nextKeys.poll() ?: generateKeyPair()
|
||||
keys[k.public] = k.private
|
||||
return k
|
||||
}
|
||||
}
|
||||
|
||||
class MockAttachmentStorage : AttachmentStorage {
|
||||
val files = HashMap<SecureHash, ByteArray>()
|
||||
|
||||
override fun openAttachment(id: SecureHash): Attachment? {
|
||||
val f = files[id] ?: return null
|
||||
return object : Attachment {
|
||||
override fun open(): InputStream = ByteArrayInputStream(f)
|
||||
override val id: SecureHash = id
|
||||
}
|
||||
}
|
||||
|
||||
override fun importAttachment(jar: InputStream): SecureHash {
|
||||
// JIS makes read()/readBytes() return bytes of the current file, but we want to hash the entire container here.
|
||||
require(jar !is JarInputStream)
|
||||
|
||||
val bytes = run {
|
||||
val s = ByteArrayOutputStream()
|
||||
jar.copyTo(s)
|
||||
s.close()
|
||||
s.toByteArray()
|
||||
}
|
||||
val sha256 = bytes.sha256()
|
||||
if (files.containsKey(sha256))
|
||||
throw FileAlreadyExistsException(File("!! MOCK FILE NAME"))
|
||||
files[sha256] = bytes
|
||||
return sha256
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class MockCheckpointStorage : CheckpointStorage {
|
||||
|
||||
private val _checkpoints = ConcurrentLinkedQueue<Checkpoint>()
|
||||
override val checkpoints: Iterable<Checkpoint>
|
||||
get() = _checkpoints.toList()
|
||||
|
||||
override fun addCheckpoint(checkpoint: Checkpoint) {
|
||||
_checkpoints.add(checkpoint)
|
||||
}
|
||||
|
||||
override fun removeCheckpoint(checkpoint: Checkpoint) {
|
||||
require(_checkpoints.remove(checkpoint))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ThreadSafe
|
||||
class MockStorageService : StorageServiceImpl(MockAttachmentStorage(), MockCheckpointStorage(), generateKeyPair())
|
||||
|
||||
class MockServices(
|
||||
customWallet: WalletService? = null,
|
||||
val keyManagement: KeyManagementService? = null,
|
||||
val net: MessagingService? = null,
|
||||
val identity: IdentityService? = MockIdentityService,
|
||||
val storage: StorageService? = MockStorageService(),
|
||||
val mapCache: NetworkMapCache? = MockNetworkMapCache(),
|
||||
val mapService: NetworkMapService? = null,
|
||||
val overrideClock: Clock? = Clock.systemUTC()
|
||||
) : ServiceHub {
|
||||
override val walletService: WalletService = customWallet ?: NodeWalletService(this)
|
||||
|
||||
override val keyManagementService: KeyManagementService
|
||||
get() = keyManagement ?: throw UnsupportedOperationException()
|
||||
override val identityService: IdentityService
|
||||
get() = identity ?: throw UnsupportedOperationException()
|
||||
override val networkService: MessagingService
|
||||
get() = net ?: throw UnsupportedOperationException()
|
||||
override val networkMapCache: NetworkMapCache
|
||||
get() = mapCache ?: throw UnsupportedOperationException()
|
||||
override val storageService: StorageService
|
||||
get() = storage ?: throw UnsupportedOperationException()
|
||||
override val clock: Clock
|
||||
get() = overrideClock ?: throw UnsupportedOperationException()
|
||||
|
||||
override val monitoringService: MonitoringService = MonitoringService(MetricRegistry())
|
||||
|
||||
init {
|
||||
if (net != null && storage != null) {
|
||||
// Creating this class is sufficient, we don't have to store it anywhere, because it registers a listener
|
||||
// on the networking service, so that will keep it from being collected.
|
||||
DataVendingService(net, storage)
|
||||
}
|
||||
}
|
||||
}
|
152
node/src/test/kotlin/core/TransactionGroupTests.kt
Normal file
152
node/src/test/kotlin/core/TransactionGroupTests.kt
Normal file
@ -0,0 +1,152 @@
|
||||
package core
|
||||
|
||||
import contracts.Cash
|
||||
import core.contracts.*
|
||||
import core.testutils.*
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertNotEquals
|
||||
|
||||
class TransactionGroupTests {
|
||||
val A_THOUSAND_POUNDS = Cash.State(MINI_CORP.ref(1, 2, 3), 1000.POUNDS, MINI_CORP_PUBKEY, DUMMY_NOTARY)
|
||||
|
||||
@Test
|
||||
fun success() {
|
||||
transactionGroup {
|
||||
roots {
|
||||
transaction(A_THOUSAND_POUNDS label "£1000")
|
||||
}
|
||||
|
||||
transaction {
|
||||
input("£1000")
|
||||
output("alice's £1000") { A_THOUSAND_POUNDS `owned by` ALICE_PUBKEY }
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
}
|
||||
|
||||
transaction {
|
||||
input("alice's £1000")
|
||||
arg(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Exit(1000.POUNDS) }
|
||||
}
|
||||
|
||||
verify()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun conflict() {
|
||||
transactionGroup {
|
||||
val t = transaction {
|
||||
output("cash") { A_THOUSAND_POUNDS }
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
}
|
||||
|
||||
val conflict1 = transaction {
|
||||
input("cash")
|
||||
val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` BOB_PUBKEY
|
||||
output { HALF }
|
||||
output { HALF }
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
}
|
||||
|
||||
verify()
|
||||
|
||||
// Alice tries to double spend back to herself.
|
||||
val conflict2 = transaction {
|
||||
input("cash")
|
||||
val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` ALICE_PUBKEY
|
||||
output { HALF }
|
||||
output { HALF }
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
}
|
||||
|
||||
assertNotEquals(conflict1, conflict2)
|
||||
|
||||
val e = assertFailsWith(TransactionConflictException::class) {
|
||||
verify()
|
||||
}
|
||||
assertEquals(StateRef(t.id, 0), e.conflictRef)
|
||||
assertEquals(setOf(conflict1.id, conflict2.id), setOf(e.tx1.id, e.tx2.id))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun disconnected() {
|
||||
// Check that if we have a transaction in the group that doesn't connect to anything else, it's rejected.
|
||||
val tg = transactionGroup {
|
||||
transaction {
|
||||
output("cash") { A_THOUSAND_POUNDS }
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
}
|
||||
|
||||
transaction {
|
||||
input("cash")
|
||||
output { A_THOUSAND_POUNDS `owned by` BOB_PUBKEY }
|
||||
}
|
||||
}
|
||||
|
||||
// We have to do this manually without the DSL because transactionGroup { } won't let us create a tx that
|
||||
// points nowhere.
|
||||
val input = generateStateRef()
|
||||
tg.txns += TransactionBuilder().apply {
|
||||
addInputState(input)
|
||||
addOutputState(A_THOUSAND_POUNDS)
|
||||
addCommand(Cash.Commands.Move(), BOB_PUBKEY)
|
||||
}.toWireTransaction()
|
||||
|
||||
val e = assertFailsWith(TransactionResolutionException::class) {
|
||||
tg.verify()
|
||||
}
|
||||
assertEquals(e.hash, input.txhash)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun duplicatedInputs() {
|
||||
// Check that a transaction cannot refer to the same input more than once.
|
||||
transactionGroup {
|
||||
roots {
|
||||
transaction(A_THOUSAND_POUNDS label "£1000")
|
||||
}
|
||||
|
||||
transaction {
|
||||
input("£1000")
|
||||
input("£1000")
|
||||
output { A_THOUSAND_POUNDS.copy(amount = A_THOUSAND_POUNDS.amount * 2) }
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
}
|
||||
|
||||
assertFailsWith(TransactionConflictException::class) {
|
||||
verify()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun signGroup() {
|
||||
val signedTxns: List<SignedTransaction> = transactionGroup {
|
||||
transaction {
|
||||
output("£1000") { A_THOUSAND_POUNDS }
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
}
|
||||
|
||||
transaction {
|
||||
input("£1000")
|
||||
output("alice's £1000") { A_THOUSAND_POUNDS `owned by` ALICE_PUBKEY }
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
}
|
||||
|
||||
transaction {
|
||||
input("alice's £1000")
|
||||
arg(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Exit(1000.POUNDS) }
|
||||
}
|
||||
}.signAll()
|
||||
|
||||
// Now go through the conversion -> verification path with them.
|
||||
val ltxns = signedTxns.map {
|
||||
it.verifyToLedgerTransaction(MockIdentityService, MockStorageService().attachments)
|
||||
}.toSet()
|
||||
TransactionGroup(ltxns, emptySet()).verify()
|
||||
}
|
||||
}
|
121
node/src/test/kotlin/core/messaging/AttachmentTests.kt
Normal file
121
node/src/test/kotlin/core/messaging/AttachmentTests.kt
Normal file
@ -0,0 +1,121 @@
|
||||
package core.messaging
|
||||
|
||||
import core.contracts.Attachment
|
||||
import core.crypto.SecureHash
|
||||
import core.crypto.sha256
|
||||
import core.node.NodeConfiguration
|
||||
import core.node.NodeInfo
|
||||
import core.node.services.NetworkMapService
|
||||
import core.node.services.NodeAttachmentService
|
||||
import core.node.services.NotaryService
|
||||
import core.node.services.ServiceType
|
||||
import core.serialization.OpaqueBytes
|
||||
import core.testing.MockNetwork
|
||||
import core.testutils.rootCauseExceptions
|
||||
import core.utilities.BriefLogFormatter
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import protocols.FetchAttachmentsProtocol
|
||||
import protocols.FetchDataProtocol
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.StandardOpenOption
|
||||
import java.security.KeyPair
|
||||
import java.util.jar.JarOutputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class AttachmentTests {
|
||||
lateinit var network: MockNetwork
|
||||
|
||||
init {
|
||||
BriefLogFormatter.init()
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
network = MockNetwork()
|
||||
}
|
||||
|
||||
fun fakeAttachment(): ByteArray {
|
||||
val bs = ByteArrayOutputStream()
|
||||
val js = JarOutputStream(bs)
|
||||
js.putNextEntry(ZipEntry("file1.txt"))
|
||||
js.writer().apply { append("Some useful content"); flush() }
|
||||
js.closeEntry()
|
||||
js.close()
|
||||
return bs.toByteArray()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `download and store`() {
|
||||
val (n0, n1) = network.createTwoNodes()
|
||||
|
||||
// Insert an attachment into node zero's store directly.
|
||||
val id = n0.storage.attachments.importAttachment(ByteArrayInputStream(fakeAttachment()))
|
||||
|
||||
// Get node one to run a protocol to fetch it and insert it.
|
||||
val f1 = n1.smm.add("tests.fetch1", FetchAttachmentsProtocol(setOf(id), n0.net.myAddress))
|
||||
network.runNetwork()
|
||||
assertEquals(0, f1.get().fromDisk.size)
|
||||
|
||||
// Verify it was inserted into node one's store.
|
||||
val attachment = n1.storage.attachments.openAttachment(id)!!
|
||||
assertEquals(id, attachment.open().readBytes().sha256())
|
||||
|
||||
// Shut down node zero and ensure node one can still resolve the attachment.
|
||||
n0.stop()
|
||||
|
||||
val response: FetchDataProtocol.Result<Attachment> = n1.smm.add("tests.fetch1", FetchAttachmentsProtocol(setOf(id), n0.net.myAddress)).get()
|
||||
assertEquals(attachment, response.fromDisk[0])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `missing`() {
|
||||
val (n0, n1) = network.createTwoNodes()
|
||||
|
||||
// Get node one to fetch a non-existent attachment.
|
||||
val hash = SecureHash.randomSHA256()
|
||||
val f1 = n1.smm.add("tests.fetch2", FetchAttachmentsProtocol(setOf(hash), n0.net.myAddress))
|
||||
network.runNetwork()
|
||||
val e = assertFailsWith<FetchDataProtocol.HashNotFound> { rootCauseExceptions { f1.get() } }
|
||||
assertEquals(hash, e.requested)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun maliciousResponse() {
|
||||
// Make a node that doesn't do sanity checking at load time.
|
||||
val n0 = network.createNode(null, -1, object : MockNetwork.Factory {
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork, networkMapAddr: NodeInfo?,
|
||||
advertisedServices: Set<ServiceType>, id: Int, keyPair: KeyPair?): MockNetwork.MockNode {
|
||||
return object : MockNetwork.MockNode(dir, config, network, networkMapAddr, advertisedServices, id, keyPair) {
|
||||
override fun start(): MockNetwork.MockNode {
|
||||
super.start()
|
||||
(storage.attachments as NodeAttachmentService).checkAttachmentsOnLoad = false
|
||||
return this
|
||||
}
|
||||
}
|
||||
}
|
||||
}, null, null, NetworkMapService.Type, NotaryService.Type)
|
||||
val n1 = network.createNode(n0.info)
|
||||
|
||||
// Insert an attachment into node zero's store directly.
|
||||
val id = n0.storage.attachments.importAttachment(ByteArrayInputStream(fakeAttachment()))
|
||||
|
||||
// Corrupt its store.
|
||||
val writer = Files.newByteChannel(network.filesystem.getPath("/nodes/0/attachments/$id"), StandardOpenOption.WRITE)
|
||||
writer.write(ByteBuffer.wrap(OpaqueBytes.of(99, 99, 99, 99).bits))
|
||||
writer.close()
|
||||
|
||||
// Get n1 to fetch the attachment. Should receive corrupted bytes.
|
||||
val f1 = n1.smm.add("tests.fetch1", FetchAttachmentsProtocol(setOf(id), n0.net.myAddress))
|
||||
network.runNetwork()
|
||||
assertFailsWith<FetchDataProtocol.DownloadedVsRequestedDataMismatch> {
|
||||
rootCauseExceptions { f1.get() }
|
||||
}
|
||||
}
|
||||
}
|
120
node/src/test/kotlin/core/messaging/InMemoryMessagingTests.kt
Normal file
120
node/src/test/kotlin/core/messaging/InMemoryMessagingTests.kt
Normal file
@ -0,0 +1,120 @@
|
||||
@file:Suppress("UNUSED_VARIABLE")
|
||||
|
||||
package core.messaging
|
||||
|
||||
import core.serialization.deserialize
|
||||
import core.testing.MockNetwork
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFails
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class InMemoryMessagingTests {
|
||||
lateinit var network: MockNetwork
|
||||
|
||||
init {
|
||||
// BriefLogFormatter.initVerbose()
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
network = MockNetwork()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun topicStringValidation() {
|
||||
TopicStringValidator.check("this.is.ok")
|
||||
TopicStringValidator.check("this.is.OkAlso")
|
||||
assertFails {
|
||||
TopicStringValidator.check("this.is.not-ok")
|
||||
}
|
||||
assertFails {
|
||||
TopicStringValidator.check("")
|
||||
}
|
||||
assertFails {
|
||||
TopicStringValidator.check("this.is not ok") // Spaces
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun basics() {
|
||||
val node1 = network.createNode()
|
||||
val node2 = network.createNode()
|
||||
val node3 = network.createNode()
|
||||
|
||||
val bits = "test-content".toByteArray()
|
||||
var finalDelivery: Message? = null
|
||||
|
||||
with(node2) {
|
||||
node2.net.addMessageHandler { msg, registration ->
|
||||
node2.net.send(msg, node3.info.address)
|
||||
}
|
||||
}
|
||||
|
||||
with(node3) {
|
||||
node2.net.addMessageHandler { msg, registration ->
|
||||
finalDelivery = msg
|
||||
}
|
||||
}
|
||||
|
||||
// Node 1 sends a message and it should end up in finalDelivery, after we run the network
|
||||
node1.net.send(node1.net.createMessage("test.topic", bits), node2.info.address)
|
||||
|
||||
network.runNetwork(rounds = 1)
|
||||
|
||||
assertTrue(Arrays.equals(finalDelivery!!.data, bits))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun broadcast() {
|
||||
val node1 = network.createNode()
|
||||
val node2 = network.createNode()
|
||||
val node3 = network.createNode()
|
||||
|
||||
val bits = "test-content".toByteArray()
|
||||
|
||||
var counter = 0
|
||||
listOf(node1, node2, node3).forEach { it.net.addMessageHandler { msg, registration -> counter++ } }
|
||||
node1.net.send(node2.net.createMessage("test.topic", bits), network.messagingNetwork.everyoneOnline)
|
||||
network.runNetwork(rounds = 1)
|
||||
assertEquals(3, counter)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun downAndUp() {
|
||||
// Test (re)delivery of messages to nodes that aren't created yet, or were stopped and then restarted.
|
||||
// The purpose of this functionality is to simulate a reliable messaging system that keeps trying until
|
||||
// messages are delivered.
|
||||
val node1 = network.createNode()
|
||||
var node2 = network.createNode()
|
||||
|
||||
node1.net.send("test.topic", "hello!", node2.info.address)
|
||||
network.runNetwork(rounds = 1) // No handler registered, so the message goes into a holding area.
|
||||
var runCount = 0
|
||||
node2.net.addMessageHandler("test.topic") { msg, registration ->
|
||||
if (msg.data.deserialize<String>() == "hello!")
|
||||
runCount++
|
||||
}
|
||||
network.runNetwork(rounds = 1) // Try again now the handler is registered
|
||||
assertEquals(1, runCount)
|
||||
|
||||
// Shut node2 down for a while. Node 1 keeps sending it messages though.
|
||||
node2.stop()
|
||||
|
||||
node1.net.send("test.topic", "are you there?", node2.info.address)
|
||||
node1.net.send("test.topic", "wake up!", node2.info.address)
|
||||
|
||||
// Now re-create node2 with the same address as last time, and re-register a message handler.
|
||||
// Check that the messages that were sent whilst it was gone are still there, waiting for it.
|
||||
node2 = network.createNode(null, node2.id)
|
||||
node2.net.addMessageHandler("test.topic") { a, b -> runCount++ }
|
||||
network.runNetwork(rounds = 1)
|
||||
assertEquals(2, runCount)
|
||||
network.runNetwork(rounds = 1)
|
||||
assertEquals(3, runCount)
|
||||
network.runNetwork(rounds = 1)
|
||||
assertEquals(3, runCount)
|
||||
}
|
||||
}
|
@ -0,0 +1,434 @@
|
||||
package core.messaging
|
||||
|
||||
import contracts.Cash
|
||||
import contracts.CommercialPaper
|
||||
import core.*
|
||||
import core.contracts.*
|
||||
import core.crypto.Party
|
||||
import core.crypto.SecureHash
|
||||
import core.node.NodeConfiguration
|
||||
import core.node.NodeInfo
|
||||
import core.node.ServiceHub
|
||||
import core.node.services.NodeAttachmentService
|
||||
import core.node.services.ServiceType
|
||||
import core.node.storage.CheckpointStorage
|
||||
import core.node.subsystems.NodeWalletService
|
||||
import core.node.subsystems.StorageServiceImpl
|
||||
import core.node.subsystems.Wallet
|
||||
import core.node.subsystems.WalletImpl
|
||||
import core.testing.InMemoryMessagingNetwork
|
||||
import core.testing.MockNetwork
|
||||
import core.testutils.*
|
||||
import core.utilities.BriefLogFormatter
|
||||
import core.utilities.RecordingMap
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import protocols.TwoPartyTradeProtocol
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.file.Path
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
import java.util.concurrent.ExecutionException
|
||||
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 TwoPartyTradeProtocolTests {
|
||||
lateinit var net: MockNetwork
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
net = MockNetwork(false)
|
||||
net.identities += MockIdentityService.identities
|
||||
BriefLogFormatter.loggingOn("platform.trade", "core.contract.TransactionGroup", "recordingmap")
|
||||
}
|
||||
|
||||
@After
|
||||
fun after() {
|
||||
BriefLogFormatter.loggingOff("platform.trade", "core.contract.TransactionGroup", "recordingmap")
|
||||
}
|
||||
|
||||
@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.
|
||||
net = MockNetwork(true)
|
||||
transactionGroupFor<ContractState> {
|
||||
val notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||
val aliceNode = net.createPartyNode(notaryNode.info, ALICE.name, ALICE_KEY)
|
||||
val bobNode = net.createPartyNode(notaryNode.info, BOB.name, BOB_KEY)
|
||||
|
||||
(bobNode.wallet as NodeWalletService).fillWithSomeTestCash(DUMMY_NOTARY, 2000.DOLLARS)
|
||||
val alicesFakePaper = fillUpForSeller(false, aliceNode.storage.myLegalIdentity.owningKey,
|
||||
notaryNode.info.identity, null).second
|
||||
|
||||
insertFakeTransactions(alicesFakePaper, aliceNode.services, aliceNode.storage.myLegalIdentityKey, notaryNode.storage.myLegalIdentityKey)
|
||||
|
||||
val buyerSessionID = random63BitValue()
|
||||
|
||||
val aliceResult = TwoPartyTradeProtocol.runSeller(
|
||||
aliceNode.smm,
|
||||
notaryNode.info,
|
||||
bobNode.net.myAddress,
|
||||
lookup("alice's paper"),
|
||||
1000.DOLLARS,
|
||||
ALICE_KEY,
|
||||
buyerSessionID
|
||||
)
|
||||
val bobResult = TwoPartyTradeProtocol.runBuyer(
|
||||
bobNode.smm,
|
||||
notaryNode.info,
|
||||
aliceNode.net.myAddress,
|
||||
1000.DOLLARS,
|
||||
CommercialPaper.State::class.java,
|
||||
buyerSessionID
|
||||
)
|
||||
|
||||
// TODO: Verify that the result was inserted into the transaction database.
|
||||
// assertEquals(bobResult.get(), aliceNode.storage.validatedTransactions[aliceResult.get().id])
|
||||
assertEquals(aliceResult.get(), bobResult.get())
|
||||
|
||||
aliceNode.stop()
|
||||
bobNode.stop()
|
||||
|
||||
assertThat(aliceNode.storage.checkpointStorage.checkpoints).isEmpty()
|
||||
assertThat(bobNode.storage.checkpointStorage.checkpoints).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shutdown and restore`() {
|
||||
transactionGroupFor<ContractState> {
|
||||
val notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||
val aliceNode = net.createPartyNode(notaryNode.info, ALICE.name, ALICE_KEY)
|
||||
var bobNode = net.createPartyNode(notaryNode.info, BOB.name, BOB_KEY)
|
||||
|
||||
val aliceAddr = aliceNode.net.myAddress
|
||||
val bobAddr = bobNode.net.myAddress as InMemoryMessagingNetwork.Handle
|
||||
val networkMapAddr = notaryNode.info
|
||||
|
||||
net.runNetwork() // Clear network map registration messages
|
||||
|
||||
(bobNode.wallet as NodeWalletService).fillWithSomeTestCash(DUMMY_NOTARY, 2000.DOLLARS)
|
||||
val alicesFakePaper = fillUpForSeller(false, aliceNode.storage.myLegalIdentity.owningKey,
|
||||
notaryNode.info.identity, null).second
|
||||
insertFakeTransactions(alicesFakePaper, aliceNode.services, aliceNode.storage.myLegalIdentityKey)
|
||||
|
||||
val buyerSessionID = random63BitValue()
|
||||
|
||||
val aliceFuture = TwoPartyTradeProtocol.runSeller(
|
||||
aliceNode.smm,
|
||||
notaryNode.info,
|
||||
bobAddr,
|
||||
lookup("alice's paper"),
|
||||
1000.DOLLARS,
|
||||
ALICE_KEY,
|
||||
buyerSessionID
|
||||
)
|
||||
TwoPartyTradeProtocol.runBuyer(
|
||||
bobNode.smm,
|
||||
notaryNode.info,
|
||||
aliceAddr,
|
||||
1000.DOLLARS,
|
||||
CommercialPaper.State::class.java,
|
||||
buyerSessionID
|
||||
)
|
||||
|
||||
// Everything is on this thread so we can now step through the protocol one step at a time.
|
||||
// Seller Alice already sent a message to Buyer Bob. Pump once:
|
||||
fun pumpAlice() = (aliceNode.net as InMemoryMessagingNetwork.InMemoryMessaging).pump(false)
|
||||
|
||||
fun pumpBob() = (bobNode.net as InMemoryMessagingNetwork.InMemoryMessaging).pump(false)
|
||||
|
||||
pumpBob()
|
||||
|
||||
// Bob sends a couple of queries for the dependencies back to Alice. Alice reponds.
|
||||
pumpAlice()
|
||||
pumpBob()
|
||||
pumpAlice()
|
||||
pumpBob()
|
||||
|
||||
// OK, now Bob has sent the partial transaction back to Alice and is waiting for Alice's signature.
|
||||
assertThat(bobNode.storage.checkpointStorage.checkpoints).hasSize(1)
|
||||
|
||||
// .. and let's imagine that Bob's computer has a power cut. He now has nothing now beyond what was on disk.
|
||||
bobNode.stop()
|
||||
|
||||
// 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.
|
||||
assertTrue(pumpAlice())
|
||||
|
||||
// ... 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 = net.createNode(networkMapAddr, bobAddr.id, object : MockNetwork.Factory {
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork, networkMapAddr: NodeInfo?,
|
||||
advertisedServices: Set<ServiceType>, id: Int, keyPair: KeyPair?): MockNetwork.MockNode {
|
||||
return MockNetwork.MockNode(dir, config, network, networkMapAddr, advertisedServices, bobAddr.id, BOB_KEY)
|
||||
}
|
||||
}, BOB.name, BOB_KEY)
|
||||
|
||||
// Find the future representing the result of this state machine again.
|
||||
var bobFuture = bobNode.smm.findStateMachines(TwoPartyTradeProtocol.Buyer::class.java).single().second
|
||||
|
||||
// And off we go again.
|
||||
net.runNetwork()
|
||||
|
||||
// Bob is now finished and has the same transaction as Alice.
|
||||
assertEquals(bobFuture.get(), aliceFuture.get())
|
||||
|
||||
assertThat(bobNode.smm.findStateMachines(TwoPartyTradeProtocol.Buyer::class.java)).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
// 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(networkMapAddr: NodeInfo?, name: String, keyPair: KeyPair): MockNetwork.MockNode {
|
||||
// Create a node in the mock network ...
|
||||
return net.createNode(networkMapAddr, -1, object : MockNetwork.Factory {
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork, networkMapAddr: NodeInfo?,
|
||||
advertisedServices: Set<ServiceType>, id: Int, keyPair: KeyPair?): MockNetwork.MockNode {
|
||||
return object : MockNetwork.MockNode(dir, config, network, networkMapAddr, advertisedServices, id, keyPair) {
|
||||
// That constructs the storage service object in a customised way ...
|
||||
override fun constructStorageService(attachments: NodeAttachmentService, checkpointStorage: CheckpointStorage, keypair: KeyPair, identity: Party): StorageServiceImpl {
|
||||
// To use RecordingMaps instead of ordinary HashMaps.
|
||||
return StorageServiceImpl(attachments, checkpointStorage, keypair, identity, { tableName -> name })
|
||||
}
|
||||
}
|
||||
}
|
||||
}, name, keyPair)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun checkDependenciesOfSaleAssetAreResolved() {
|
||||
transactionGroupFor<ContractState> {
|
||||
val notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||
val aliceNode = makeNodeWithTracking(notaryNode.info, ALICE.name, ALICE_KEY)
|
||||
val bobNode = makeNodeWithTracking(notaryNode.info, BOB.name, BOB_KEY)
|
||||
|
||||
// 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.storage.attachments.importAttachment(ByteArrayInputStream(stream.toByteArray()))
|
||||
|
||||
val bobsFakeCash = fillUpForBuyer(false, bobNode.keyManagement.freshKey().public).second
|
||||
val bobsSignedTxns = insertFakeTransactions(bobsFakeCash, bobNode.services)
|
||||
val alicesFakePaper = fillUpForSeller(false, aliceNode.storage.myLegalIdentity.owningKey,
|
||||
notaryNode.info.identity, attachmentID).second
|
||||
val alicesSignedTxns = insertFakeTransactions(alicesFakePaper, aliceNode.services, aliceNode.storage.myLegalIdentityKey)
|
||||
|
||||
val buyerSessionID = random63BitValue()
|
||||
|
||||
net.runNetwork() // Clear network map registration messages
|
||||
|
||||
TwoPartyTradeProtocol.runSeller(
|
||||
aliceNode.smm,
|
||||
notaryNode.info,
|
||||
bobNode.net.myAddress,
|
||||
lookup("alice's paper"),
|
||||
1000.DOLLARS,
|
||||
ALICE_KEY,
|
||||
buyerSessionID
|
||||
)
|
||||
TwoPartyTradeProtocol.runBuyer(
|
||||
bobNode.smm,
|
||||
notaryNode.info,
|
||||
aliceNode.net.myAddress,
|
||||
1000.DOLLARS,
|
||||
CommercialPaper.State::class.java,
|
||||
buyerSessionID
|
||||
)
|
||||
|
||||
net.runNetwork()
|
||||
|
||||
run {
|
||||
val records = (bobNode.storage.validatedTransactions as RecordingMap).records
|
||||
// Check Bobs's database accesses as Bob's cash transactions are downloaded by Alice.
|
||||
val expected = listOf(
|
||||
// Buyer Bob is told about Alice's commercial paper, but doesn't know it ..
|
||||
RecordingMap.Get(alicesFakePaper[0].id),
|
||||
// He asks and gets the tx, validates it, sees it's a self issue with no dependencies, stores.
|
||||
RecordingMap.Put(alicesFakePaper[0].id, alicesSignedTxns.values.first()),
|
||||
// Alice gets Bob's proposed transaction and doesn't know his two cash states. She asks, Bob answers.
|
||||
RecordingMap.Get(bobsFakeCash[1].id),
|
||||
RecordingMap.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.
|
||||
RecordingMap.Get(bobsFakeCash[0].id)
|
||||
)
|
||||
assertEquals(expected, records)
|
||||
|
||||
// Bob has downloaded the attachment.
|
||||
bobNode.storage.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.storage.validatedTransactions as RecordingMap).records
|
||||
val expected = listOf(
|
||||
// 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
|
||||
RecordingMap.Get(alicesFakePaper[0].id),
|
||||
// Seller Alice gets a proposed tx which depends on Bob's two cash txns and her own tx.
|
||||
RecordingMap.Get(bobsFakeCash[1].id),
|
||||
RecordingMap.Get(bobsFakeCash[2].id),
|
||||
RecordingMap.Get(alicesFakePaper[0].id),
|
||||
// Alice notices that Bob's cash txns depend on a third tx she also doesn't know.
|
||||
RecordingMap.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
|
||||
RecordingMap.Put(bobsFakeCash[1].id, bobsSignedTxns[bobsFakeCash[1].id]),
|
||||
RecordingMap.Put(bobsFakeCash[2].id, bobsSignedTxns[bobsFakeCash[2].id]),
|
||||
RecordingMap.Put(bobsFakeCash[0].id, bobsSignedTxns[bobsFakeCash[0].id]),
|
||||
// Now she verifies the transaction is contract-valid (not signature valid) which means
|
||||
// looking up the states again.
|
||||
RecordingMap.Get(bobsFakeCash[1].id),
|
||||
RecordingMap.Get(bobsFakeCash[2].id),
|
||||
RecordingMap.Get(alicesFakePaper[0].id),
|
||||
// Alice needs to look up the input states to find out which Notary they point to
|
||||
RecordingMap.Get(bobsFakeCash[1].id),
|
||||
RecordingMap.Get(bobsFakeCash[2].id),
|
||||
RecordingMap.Get(alicesFakePaper[0].id)
|
||||
)
|
||||
assertEquals(expected, records)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dependency with error on buyer side`() {
|
||||
transactionGroupFor<ContractState> {
|
||||
runWithError(true, false, "at least one cash input")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dependency with error on seller side`() {
|
||||
transactionGroupFor<ContractState> {
|
||||
runWithError(false, true, "must be timestamped")
|
||||
}
|
||||
}
|
||||
|
||||
private fun TransactionGroupDSL<ContractState>.runWithError(bobError: Boolean, aliceError: Boolean,
|
||||
expectedMessageSubstring: String) {
|
||||
val notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||
val aliceNode = net.createPartyNode(notaryNode.info, ALICE.name, ALICE_KEY)
|
||||
val bobNode = net.createPartyNode(notaryNode.info, BOB.name, BOB_KEY)
|
||||
|
||||
val aliceAddr = aliceNode.net.myAddress
|
||||
val bobAddr = bobNode.net.myAddress as InMemoryMessagingNetwork.Handle
|
||||
|
||||
val bobKey = bobNode.keyManagement.freshKey()
|
||||
val bobsBadCash = fillUpForBuyer(bobError, bobKey.public).second
|
||||
val alicesFakePaper = fillUpForSeller(aliceError, aliceNode.storage.myLegalIdentity.owningKey, notaryNode.info.identity, null).second
|
||||
|
||||
insertFakeTransactions(bobsBadCash, bobNode.services, bobNode.storage.myLegalIdentityKey, bobNode.storage.myLegalIdentityKey)
|
||||
insertFakeTransactions(alicesFakePaper, aliceNode.services, aliceNode.storage.myLegalIdentityKey)
|
||||
|
||||
val buyerSessionID = random63BitValue()
|
||||
|
||||
net.runNetwork() // Clear network map registration messages
|
||||
|
||||
val aliceResult = TwoPartyTradeProtocol.runSeller(
|
||||
aliceNode.smm,
|
||||
notaryNode.info,
|
||||
bobAddr,
|
||||
lookup("alice's paper"),
|
||||
1000.DOLLARS,
|
||||
ALICE_KEY,
|
||||
buyerSessionID
|
||||
)
|
||||
val bobResult = TwoPartyTradeProtocol.runBuyer(
|
||||
bobNode.smm,
|
||||
notaryNode.info,
|
||||
aliceAddr,
|
||||
1000.DOLLARS,
|
||||
CommercialPaper.State::class.java,
|
||||
buyerSessionID
|
||||
)
|
||||
|
||||
net.runNetwork()
|
||||
|
||||
val e = assertFailsWith<ExecutionException> {
|
||||
if (bobError)
|
||||
aliceResult.get()
|
||||
else
|
||||
bobResult.get()
|
||||
}
|
||||
assertTrue(e.cause is TransactionVerificationException)
|
||||
assertTrue(e.cause!!.cause!!.message!!.contains(expectedMessageSubstring))
|
||||
}
|
||||
|
||||
private fun TransactionGroupDSL<ContractState>.insertFakeTransactions(wtxToSign: List<WireTransaction>,
|
||||
services: ServiceHub,
|
||||
vararg extraKeys: KeyPair): Map<SecureHash, SignedTransaction> {
|
||||
val signed: List<SignedTransaction> = signAll(wtxToSign, *extraKeys)
|
||||
services.recordTransactions(signed, skipRecordingMap = true)
|
||||
return signed.associateBy { it.id }
|
||||
}
|
||||
|
||||
private fun TransactionGroupDSL<ContractState>.fillUpForBuyer(withError: Boolean, owner: PublicKey = BOB_PUBKEY): Pair<Wallet, List<WireTransaction>> {
|
||||
// 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 {
|
||||
// Issued money to itself.
|
||||
output("elbonian money 1") { 800.DOLLARS.CASH `issued by` MEGA_CORP `owned by` MEGA_CORP_PUBKEY }
|
||||
output("elbonian money 2") { 1000.DOLLARS.CASH `issued by` MEGA_CORP `owned by` MEGA_CORP_PUBKEY }
|
||||
if (!withError)
|
||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
// Bob gets some cash onto the ledger from BoE
|
||||
val bc1 = transaction {
|
||||
input("elbonian money 1")
|
||||
output("bob cash 1") { 800.DOLLARS.CASH `issued by` MEGA_CORP `owned by` owner }
|
||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
}
|
||||
|
||||
val bc2 = transaction {
|
||||
input("elbonian money 2")
|
||||
output("bob cash 2") { 300.DOLLARS.CASH `issued by` MEGA_CORP `owned by` owner }
|
||||
output { 700.DOLLARS.CASH `issued by` MEGA_CORP `owned by` MEGA_CORP_PUBKEY } // Change output.
|
||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
}
|
||||
|
||||
val wallet = WalletImpl(listOf<StateAndRef<Cash.State>>(lookup("bob cash 1"), lookup("bob cash 2")))
|
||||
return Pair(wallet, listOf(eb1, bc1, bc2))
|
||||
}
|
||||
|
||||
private fun TransactionGroupDSL<ContractState>.fillUpForSeller(withError: Boolean, owner: PublicKey, notary: Party, attachmentID: SecureHash?): Pair<Wallet, List<WireTransaction>> {
|
||||
val ap = transaction {
|
||||
output("alice's paper") {
|
||||
CommercialPaper.State(MEGA_CORP.ref(1, 2, 3), owner, 1200.DOLLARS, TEST_TX_TIME + 7.days, notary)
|
||||
}
|
||||
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
||||
if (!withError)
|
||||
arg(notary.owningKey) { TimestampCommand(TEST_TX_TIME, 30.seconds) }
|
||||
if (attachmentID != null)
|
||||
attachment(attachmentID)
|
||||
}
|
||||
|
||||
val wallet = WalletImpl(listOf<StateAndRef<Cash.State>>(lookup("alice's paper")))
|
||||
return Pair(wallet, listOf(ap))
|
||||
}
|
||||
}
|
258
node/src/test/kotlin/core/node/AttachmentClassLoaderTests.kt
Normal file
258
node/src/test/kotlin/core/node/AttachmentClassLoaderTests.kt
Normal file
@ -0,0 +1,258 @@
|
||||
package core.node
|
||||
|
||||
import contracts.DUMMY_PROGRAM_ID
|
||||
import contracts.DummyContract
|
||||
import core.*
|
||||
import core.contracts.*
|
||||
import core.crypto.Party
|
||||
import core.crypto.SecureHash
|
||||
import core.node.services.AttachmentStorage
|
||||
import core.serialization.*
|
||||
import core.testutils.DUMMY_NOTARY
|
||||
import core.testutils.MEGA_CORP
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.junit.Test
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.net.URLClassLoader
|
||||
import java.util.jar.JarOutputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
interface DummyContractBackdoor {
|
||||
fun generateInitial(owner: PartyAndReference, magicNumber: Int, notary: Party): TransactionBuilder
|
||||
fun inspectState(state: ContractState): Int
|
||||
}
|
||||
|
||||
class AttachmentClassLoaderTests {
|
||||
companion object {
|
||||
val ISOLATED_CONTRACTS_JAR_PATH = AttachmentClassLoaderTests::class.java.getResource("isolated.jar")
|
||||
}
|
||||
|
||||
fun importJar(storage: AttachmentStorage) = ISOLATED_CONTRACTS_JAR_PATH.openStream().use { storage.importAttachment(it) }
|
||||
|
||||
// These ClassLoaders work together to load 'AnotherDummyContract' in a disposable way, such that even though
|
||||
// the class may be on the unit test class path (due to default IDE settings, etc), it won't be loaded into the
|
||||
// regular app classloader but rather than ClassLoaderForTests. This helps keep our environment clean and
|
||||
// ensures we have precise control over where it's loaded.
|
||||
object FilteringClassLoader : ClassLoader() {
|
||||
override fun loadClass(name: String, resolve: Boolean): Class<*>? {
|
||||
if ("AnotherDummyContract" in name) {
|
||||
return null
|
||||
} else
|
||||
return super.loadClass(name, resolve)
|
||||
}
|
||||
}
|
||||
|
||||
class ClassLoaderForTests : URLClassLoader(arrayOf(ISOLATED_CONTRACTS_JAR_PATH), FilteringClassLoader)
|
||||
|
||||
@Test
|
||||
fun `dynamically load AnotherDummyContract from isolated contracts jar`() {
|
||||
val child = ClassLoaderForTests()
|
||||
|
||||
val contractClass = Class.forName("contracts.isolated.AnotherDummyContract", true, child)
|
||||
val contract = contractClass.newInstance() as Contract
|
||||
|
||||
assertEquals(SecureHash.sha256("https://anotherdummy.org"), contract.legalContractReference)
|
||||
}
|
||||
|
||||
fun fakeAttachment(filepath: String, content: String): ByteArray {
|
||||
val bs = ByteArrayOutputStream()
|
||||
val js = JarOutputStream(bs)
|
||||
js.putNextEntry(ZipEntry(filepath))
|
||||
js.writer().apply { append(content); flush() }
|
||||
js.closeEntry()
|
||||
js.close()
|
||||
return bs.toByteArray()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test MockAttachmentStorage open as jar`() {
|
||||
val storage = MockAttachmentStorage()
|
||||
val key = importJar(storage)
|
||||
val attachment = storage.openAttachment(key)!!
|
||||
|
||||
val jar = attachment.openAsJAR()
|
||||
|
||||
assert(jar.nextEntry != null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test overlapping file exception`() {
|
||||
val storage = MockAttachmentStorage()
|
||||
|
||||
val att0 = importJar(storage)
|
||||
val att1 = storage.importAttachment(ByteArrayInputStream(fakeAttachment("file.txt", "some data")))
|
||||
val att2 = storage.importAttachment(ByteArrayInputStream(fakeAttachment("file.txt", "some other data")))
|
||||
|
||||
assertFailsWith(AttachmentsClassLoader.OverlappingAttachments::class) {
|
||||
AttachmentsClassLoader(arrayOf(att0, att1, att2).map { storage.openAttachment(it)!! })
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `basic`() {
|
||||
val storage = MockAttachmentStorage()
|
||||
|
||||
val att0 = importJar(storage)
|
||||
val att1 = storage.importAttachment(ByteArrayInputStream(fakeAttachment("file1.txt", "some data")))
|
||||
val att2 = storage.importAttachment(ByteArrayInputStream(fakeAttachment("file2.txt", "some other data")))
|
||||
|
||||
val cl = AttachmentsClassLoader(arrayOf(att0, att1, att2).map { storage.openAttachment(it)!! })
|
||||
val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"))
|
||||
assertEquals("some data", txt)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loading class AnotherDummyContract`() {
|
||||
val storage = MockAttachmentStorage()
|
||||
|
||||
val att0 = importJar(storage)
|
||||
val att1 = storage.importAttachment(ByteArrayInputStream(fakeAttachment("file1.txt", "some data")))
|
||||
val att2 = storage.importAttachment(ByteArrayInputStream(fakeAttachment("file2.txt", "some other data")))
|
||||
|
||||
val cl = AttachmentsClassLoader(arrayOf(att0, att1, att2).map { storage.openAttachment(it)!! }, FilteringClassLoader)
|
||||
val contractClass = Class.forName("contracts.isolated.AnotherDummyContract", true, cl)
|
||||
val contract = contractClass.newInstance() as Contract
|
||||
assertEquals(cl, contract.javaClass.classLoader)
|
||||
assertEquals(SecureHash.sha256("https://anotherdummy.org"), contract.legalContractReference)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `verify that contract DummyContract is in classPath`() {
|
||||
val contractClass = Class.forName("contracts.DummyContract")
|
||||
val contract = contractClass.newInstance() as Contract
|
||||
|
||||
assertNotNull(contract)
|
||||
}
|
||||
|
||||
fun createContract2Cash(): Contract {
|
||||
val cl = ClassLoaderForTests()
|
||||
val contractClass = Class.forName("contracts.isolated.AnotherDummyContract", true, cl)
|
||||
return contractClass.newInstance() as Contract
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `testing Kryo with ClassLoader (with top level class name)`() {
|
||||
val contract = createContract2Cash()
|
||||
|
||||
val bytes = contract.serialize()
|
||||
|
||||
val storage = MockAttachmentStorage()
|
||||
|
||||
val att0 = importJar(storage)
|
||||
val att1 = storage.importAttachment(ByteArrayInputStream(fakeAttachment("file1.txt", "some data")))
|
||||
val att2 = storage.importAttachment(ByteArrayInputStream(fakeAttachment("file2.txt", "some other data")))
|
||||
|
||||
val cl = AttachmentsClassLoader(arrayOf(att0, att1, att2).map { storage.openAttachment(it)!! }, FilteringClassLoader)
|
||||
|
||||
val kryo = createKryo()
|
||||
kryo.classLoader = cl
|
||||
|
||||
val state2 = bytes.deserialize(kryo)
|
||||
assert(state2.javaClass.classLoader is AttachmentsClassLoader)
|
||||
assertNotNull(state2)
|
||||
}
|
||||
|
||||
// top level wrapper
|
||||
class Data(val contract: Contract)
|
||||
|
||||
@Test
|
||||
fun `testing Kryo with ClassLoader (without top level class name)`() {
|
||||
val data = Data(createContract2Cash())
|
||||
|
||||
assertNotNull(data.contract)
|
||||
|
||||
val bytes = data.serialize()
|
||||
|
||||
val storage = MockAttachmentStorage()
|
||||
|
||||
val att0 = importJar(storage)
|
||||
val att1 = storage.importAttachment(ByteArrayInputStream(fakeAttachment("file1.txt", "some data")))
|
||||
val att2 = storage.importAttachment(ByteArrayInputStream(fakeAttachment("file2.txt", "some other data")))
|
||||
|
||||
val cl = AttachmentsClassLoader(arrayOf(att0, att1, att2).map { storage.openAttachment(it)!! }, FilteringClassLoader)
|
||||
|
||||
val kryo = createKryo()
|
||||
kryo.classLoader = cl
|
||||
|
||||
val state2 = bytes.deserialize(kryo)
|
||||
assertEquals(cl, state2.contract.javaClass.classLoader)
|
||||
assertNotNull(state2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test serialization of WireTransaction with statically loaded contract`() {
|
||||
val tx = DUMMY_PROGRAM_ID.generateInitial(MEGA_CORP.ref(0), 42, DUMMY_NOTARY)
|
||||
val wireTransaction = tx.toWireTransaction()
|
||||
val bytes = wireTransaction.serialize()
|
||||
val copiedWireTransaction = bytes.deserialize()
|
||||
|
||||
assertEquals(1, copiedWireTransaction.outputs.size)
|
||||
assertEquals(42, (copiedWireTransaction.outputs[0] as DummyContract.State).magicNumber)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test serialization of WireTransaction with dynamically loaded contract`() {
|
||||
val child = ClassLoaderForTests()
|
||||
val contractClass = Class.forName("contracts.isolated.AnotherDummyContract", true, child)
|
||||
val contract = contractClass.newInstance() as DummyContractBackdoor
|
||||
val tx = contract.generateInitial(MEGA_CORP.ref(0), 42, DUMMY_NOTARY)
|
||||
val storage = MockAttachmentStorage()
|
||||
val kryo = createKryo()
|
||||
|
||||
// todo - think about better way to push attachmentStorage down to serializer
|
||||
kryo.attachmentStorage = storage
|
||||
|
||||
val attachmentRef = importJar(storage)
|
||||
|
||||
tx.addAttachment(storage.openAttachment(attachmentRef)!!)
|
||||
|
||||
val wireTransaction = tx.toWireTransaction()
|
||||
|
||||
val bytes = wireTransaction.serialize(kryo)
|
||||
|
||||
val kryo2 = createKryo()
|
||||
// use empty attachmentStorage
|
||||
kryo2.attachmentStorage = storage
|
||||
|
||||
val copiedWireTransaction = bytes.deserialize(kryo2)
|
||||
|
||||
assertEquals(1, copiedWireTransaction.outputs.size)
|
||||
val contract2 = copiedWireTransaction.outputs[0].contract as DummyContractBackdoor
|
||||
assertEquals(42, contract2.inspectState(copiedWireTransaction.outputs[0]))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test deserialize of WireTransaction where contract cannot be found`() {
|
||||
val child = ClassLoaderForTests()
|
||||
val contractClass = Class.forName("contracts.isolated.AnotherDummyContract", true, child)
|
||||
val contract = contractClass.newInstance() as DummyContractBackdoor
|
||||
val tx = contract.generateInitial(MEGA_CORP.ref(0), 42, DUMMY_NOTARY)
|
||||
val storage = MockAttachmentStorage()
|
||||
val kryo = createKryo()
|
||||
|
||||
// todo - think about better way to push attachmentStorage down to serializer
|
||||
kryo.attachmentStorage = storage
|
||||
|
||||
val attachmentRef = importJar(storage)
|
||||
|
||||
tx.addAttachment(storage.openAttachment(attachmentRef)!!)
|
||||
|
||||
val wireTransaction = tx.toWireTransaction()
|
||||
|
||||
val bytes = wireTransaction.serialize(kryo)
|
||||
|
||||
val kryo2 = createKryo()
|
||||
// use empty attachmentStorage
|
||||
kryo2.attachmentStorage = MockAttachmentStorage()
|
||||
|
||||
val e = assertFailsWith(MissingAttachmentsException::class) {
|
||||
bytes.deserialize(kryo2)
|
||||
}
|
||||
assertEquals(attachmentRef, e.ids.single())
|
||||
}
|
||||
}
|
95
node/src/test/kotlin/core/node/NodeAttachmentStorageTest.kt
Normal file
95
node/src/test/kotlin/core/node/NodeAttachmentStorageTest.kt
Normal file
@ -0,0 +1,95 @@
|
||||
package core.node
|
||||
|
||||
import com.codahale.metrics.MetricRegistry
|
||||
import com.google.common.jimfs.Configuration
|
||||
import com.google.common.jimfs.Jimfs
|
||||
import core.crypto.SecureHash
|
||||
import core.node.services.NodeAttachmentService
|
||||
import core.use
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.file.FileSystem
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.StandardOpenOption
|
||||
import java.util.jar.JarEntry
|
||||
import java.util.jar.JarOutputStream
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class NodeAttachmentStorageTest {
|
||||
// Use an in memory file system for testing attachment storage.
|
||||
lateinit var fs: FileSystem
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
fs = Jimfs.newFileSystem(Configuration.unix())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `insert and retrieve`() {
|
||||
val testJar = makeTestJar()
|
||||
val expectedHash = SecureHash.sha256(Files.readAllBytes(testJar))
|
||||
|
||||
val storage = NodeAttachmentService(fs.getPath("/"), MetricRegistry())
|
||||
val id = testJar.use { storage.importAttachment(it) }
|
||||
assertEquals(expectedHash, id)
|
||||
|
||||
assertNull(storage.openAttachment(SecureHash.randomSHA256()))
|
||||
val stream = storage.openAttachment(expectedHash)!!.openAsJAR()
|
||||
val e1 = stream.nextJarEntry!!
|
||||
assertEquals("test1.txt", e1.name)
|
||||
assertEquals(stream.readBytes().toString(Charset.defaultCharset()), "This is some useful content")
|
||||
val e2 = stream.nextJarEntry!!
|
||||
assertEquals("test2.txt", e2.name)
|
||||
assertEquals(stream.readBytes().toString(Charset.defaultCharset()), "Some more useful content")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `duplicates not allowed`() {
|
||||
val testJar = makeTestJar()
|
||||
val storage = NodeAttachmentService(fs.getPath("/"), MetricRegistry())
|
||||
testJar.use { storage.importAttachment(it) }
|
||||
assertFailsWith<java.nio.file.FileAlreadyExistsException> {
|
||||
testJar.use { storage.importAttachment(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `corrupt entry throws exception`() {
|
||||
val testJar = makeTestJar()
|
||||
val storage = NodeAttachmentService(fs.getPath("/"), MetricRegistry())
|
||||
val id = testJar.use { storage.importAttachment(it) }
|
||||
|
||||
// Corrupt the file in the store.
|
||||
Files.write(fs.getPath("/", id.toString()), "arggghhhh".toByteArray(), StandardOpenOption.WRITE)
|
||||
|
||||
val e = assertFailsWith<NodeAttachmentService.OnDiskHashMismatch> {
|
||||
storage.openAttachment(id)!!.open().use { it.readBytes() }
|
||||
}
|
||||
assertEquals(e.file, storage.storePath.resolve(id.toString()))
|
||||
|
||||
// But if we skip around and read a single entry, no exception is thrown.
|
||||
storage.openAttachment(id)!!.openAsJAR().use {
|
||||
it.nextJarEntry
|
||||
it.readBytes()
|
||||
}
|
||||
}
|
||||
|
||||
private var counter = 0
|
||||
private fun makeTestJar(): Path {
|
||||
counter++
|
||||
val f = fs.getPath("$counter.jar")
|
||||
JarOutputStream(Files.newOutputStream(f)).use {
|
||||
it.putNextEntry(JarEntry("test1.txt"))
|
||||
it.write("This is some useful content".toByteArray())
|
||||
it.closeEntry()
|
||||
it.putNextEntry(JarEntry("test2.txt"))
|
||||
it.write("Some more useful content".toByteArray())
|
||||
it.closeEntry()
|
||||
}
|
||||
return f
|
||||
}
|
||||
}
|
@ -0,0 +1,191 @@
|
||||
package core.node.services
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import core.*
|
||||
import core.crypto.SecureHash
|
||||
import core.crypto.signWithECDSA
|
||||
import core.node.NodeInfo
|
||||
import core.protocols.ProtocolLogic
|
||||
import core.serialization.serialize
|
||||
import core.testing.MockNetwork
|
||||
import core.utilities.AddOrRemove
|
||||
import core.utilities.BriefLogFormatter
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.security.PrivateKey
|
||||
import java.time.Instant
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class InMemoryNetworkMapServiceTest {
|
||||
lateinit var network: MockNetwork
|
||||
|
||||
init {
|
||||
BriefLogFormatter.init()
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
network = MockNetwork()
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform basic tests of registering, de-registering and fetching the full network map.
|
||||
*/
|
||||
@Test
|
||||
fun success() {
|
||||
val (mapServiceNode, registerNode) = network.createTwoNodes()
|
||||
val service = mapServiceNode.inNodeNetworkMapService!! as InMemoryNetworkMapService
|
||||
|
||||
// Confirm the service contains only its own node
|
||||
assertEquals(1, service.nodes.count())
|
||||
assertNull(service.processQueryRequest(NetworkMapService.QueryIdentityRequest(registerNode.info.identity, mapServiceNode.info.address, Long.MIN_VALUE)).node)
|
||||
|
||||
// Register the second node
|
||||
var seq = 1L
|
||||
val expires = Instant.now() + NetworkMapService.DEFAULT_EXPIRATION_PERIOD
|
||||
val nodeKey = registerNode.storage.myLegalIdentityKey
|
||||
val addChange = NodeRegistration(registerNode.info, seq++, AddOrRemove.ADD, expires)
|
||||
val addWireChange = addChange.toWire(nodeKey.private)
|
||||
service.processRegistrationChangeRequest(NetworkMapService.RegistrationRequest(addWireChange, mapServiceNode.info.address, Long.MIN_VALUE))
|
||||
assertEquals(2, service.nodes.count())
|
||||
assertEquals(mapServiceNode.info, service.processQueryRequest(NetworkMapService.QueryIdentityRequest(mapServiceNode.info.identity, mapServiceNode.info.address, Long.MIN_VALUE)).node)
|
||||
|
||||
// Re-registering should be a no-op
|
||||
service.processRegistrationChangeRequest(NetworkMapService.RegistrationRequest(addWireChange, mapServiceNode.info.address, Long.MIN_VALUE))
|
||||
assertEquals(2, service.nodes.count())
|
||||
|
||||
// Confirm that de-registering the node succeeds and drops it from the node lists
|
||||
var removeChange = NodeRegistration(registerNode.info, seq, AddOrRemove.REMOVE, expires)
|
||||
val removeWireChange = removeChange.toWire(nodeKey.private)
|
||||
assert(service.processRegistrationChangeRequest(NetworkMapService.RegistrationRequest(removeWireChange, mapServiceNode.info.address, Long.MIN_VALUE)).success)
|
||||
assertNull(service.processQueryRequest(NetworkMapService.QueryIdentityRequest(registerNode.info.identity, mapServiceNode.info.address, Long.MIN_VALUE)).node)
|
||||
|
||||
// Trying to de-register a node that doesn't exist should fail
|
||||
assert(!service.processRegistrationChangeRequest(NetworkMapService.RegistrationRequest(removeWireChange, mapServiceNode.info.address, Long.MIN_VALUE)).success)
|
||||
}
|
||||
|
||||
class TestAcknowledgePSM(val server: NodeInfo, val hash: SecureHash)
|
||||
: ProtocolLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val req = NetworkMapService.UpdateAcknowledge(hash, serviceHub.networkService.myAddress)
|
||||
send(NetworkMapService.PUSH_ACK_PROTOCOL_TOPIC, server.address, 0, req)
|
||||
}
|
||||
}
|
||||
|
||||
class TestFetchPSM(val server: NodeInfo, val subscribe: Boolean, val ifChangedSinceVersion: Int? = null)
|
||||
: ProtocolLogic<Collection<NodeRegistration>?>() {
|
||||
@Suspendable
|
||||
override fun call(): Collection<NodeRegistration>? {
|
||||
val sessionID = random63BitValue()
|
||||
val req = NetworkMapService.FetchMapRequest(subscribe, ifChangedSinceVersion, serviceHub.networkService.myAddress, sessionID)
|
||||
return sendAndReceive<NetworkMapService.FetchMapResponse>(
|
||||
NetworkMapService.FETCH_PROTOCOL_TOPIC, server.address, 0, sessionID, req)
|
||||
.validate { it.nodes }
|
||||
}
|
||||
}
|
||||
|
||||
class TestRegisterPSM(val server: NodeInfo, val reg: NodeRegistration, val privateKey: PrivateKey)
|
||||
: ProtocolLogic<NetworkMapService.RegistrationResponse>() {
|
||||
@Suspendable
|
||||
override fun call(): NetworkMapService.RegistrationResponse {
|
||||
val sessionID = random63BitValue()
|
||||
val req = NetworkMapService.RegistrationRequest(reg.toWire(privateKey), serviceHub.networkService.myAddress, sessionID)
|
||||
|
||||
return sendAndReceive<NetworkMapService.RegistrationResponse>(
|
||||
NetworkMapService.REGISTER_PROTOCOL_TOPIC, server.address, 0, sessionID, req)
|
||||
.validate { it }
|
||||
}
|
||||
}
|
||||
|
||||
class TestSubscribePSM(val server: NodeInfo, val subscribe: Boolean)
|
||||
: ProtocolLogic<NetworkMapService.SubscribeResponse>() {
|
||||
@Suspendable
|
||||
override fun call(): NetworkMapService.SubscribeResponse {
|
||||
val sessionID = random63BitValue()
|
||||
val req = NetworkMapService.SubscribeRequest(subscribe, serviceHub.networkService.myAddress, sessionID)
|
||||
|
||||
return sendAndReceive<NetworkMapService.SubscribeResponse>(
|
||||
NetworkMapService.SUBSCRIPTION_PROTOCOL_TOPIC, server.address, 0, sessionID, req)
|
||||
.validate { it }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun successWithNetwork() {
|
||||
val (mapServiceNode, registerNode) = network.createTwoNodes()
|
||||
|
||||
// Confirm there's a network map service on node 0
|
||||
assertNotNull(mapServiceNode.inNodeNetworkMapService)
|
||||
|
||||
// Confirm all nodes have registered themselves
|
||||
var fetchPsm = registerNode.smm.add(NetworkMapService.FETCH_PROTOCOL_TOPIC, TestFetchPSM(mapServiceNode.info, false))
|
||||
network.runNetwork()
|
||||
assertEquals(2, fetchPsm.get()?.count())
|
||||
|
||||
// Forcibly deregister the second node
|
||||
val nodeKey = registerNode.storage.myLegalIdentityKey
|
||||
val expires = Instant.now() + NetworkMapService.DEFAULT_EXPIRATION_PERIOD
|
||||
val seq = 2L
|
||||
val reg = NodeRegistration(registerNode.info, seq, AddOrRemove.REMOVE, expires)
|
||||
val registerPsm = registerNode.smm.add(NetworkMapService.REGISTER_PROTOCOL_TOPIC, TestRegisterPSM(mapServiceNode.info, reg, nodeKey.private))
|
||||
network.runNetwork()
|
||||
assertTrue(registerPsm.get().success)
|
||||
|
||||
// Now only map service node should be registered
|
||||
fetchPsm = registerNode.smm.add(NetworkMapService.FETCH_PROTOCOL_TOPIC, TestFetchPSM(mapServiceNode.info, false))
|
||||
network.runNetwork()
|
||||
assertEquals(mapServiceNode.info, fetchPsm.get()?.filter { it.type == AddOrRemove.ADD }?.map { it.node }?.single())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun subscribeWithNetwork() {
|
||||
val (mapServiceNode, registerNode) = network.createTwoNodes()
|
||||
val service = (mapServiceNode.inNodeNetworkMapService as InMemoryNetworkMapService)
|
||||
|
||||
// Test subscribing to updates
|
||||
val subscribePsm = registerNode.smm.add(NetworkMapService.SUBSCRIPTION_PROTOCOL_TOPIC,
|
||||
TestSubscribePSM(mapServiceNode.info, true))
|
||||
network.runNetwork()
|
||||
subscribePsm.get()
|
||||
|
||||
// Check the unacknowledged count is zero
|
||||
assertEquals(0, service.getUnacknowledgedCount(registerNode.info.address))
|
||||
|
||||
// Fire off an update
|
||||
val nodeKey = registerNode.storage.myLegalIdentityKey
|
||||
var seq = 1L
|
||||
val expires = Instant.now() + NetworkMapService.DEFAULT_EXPIRATION_PERIOD
|
||||
var reg = NodeRegistration(registerNode.info, seq++, AddOrRemove.ADD, expires)
|
||||
var wireReg = reg.toWire(nodeKey.private)
|
||||
service.notifySubscribers(wireReg)
|
||||
|
||||
// Check the unacknowledged count is one
|
||||
assertEquals(1, service.getUnacknowledgedCount(registerNode.info.address))
|
||||
|
||||
// Send in an acknowledgment and verify the count goes down
|
||||
val hash = SecureHash.sha256(wireReg.raw.bits)
|
||||
val acknowledgePsm = registerNode.smm.add(NetworkMapService.PUSH_ACK_PROTOCOL_TOPIC,
|
||||
TestAcknowledgePSM(mapServiceNode.info, hash))
|
||||
network.runNetwork()
|
||||
acknowledgePsm.get()
|
||||
|
||||
assertEquals(0, service.getUnacknowledgedCount(registerNode.info.address))
|
||||
|
||||
// Intentionally fill the pending acknowledgements to verify it doesn't drop subscribers before the limit
|
||||
// is hit. On the last iteration overflow the pending list, and check the node is unsubscribed
|
||||
for (i in 0..service.maxUnacknowledgedUpdates) {
|
||||
reg = NodeRegistration(registerNode.info, seq++, AddOrRemove.ADD, expires)
|
||||
wireReg = reg.toWire(nodeKey.private)
|
||||
service.notifySubscribers(wireReg)
|
||||
if (i < service.maxUnacknowledgedUpdates) {
|
||||
assertEquals(i + 1, service.getUnacknowledgedCount(registerNode.info.address))
|
||||
} else {
|
||||
assertNull(service.getUnacknowledgedCount(registerNode.info.address))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
110
node/src/test/kotlin/core/node/services/NodeInterestRatesTest.kt
Normal file
110
node/src/test/kotlin/core/node/services/NodeInterestRatesTest.kt
Normal file
@ -0,0 +1,110 @@
|
||||
package core.node.services
|
||||
|
||||
import contracts.Cash
|
||||
import core.contracts.DOLLARS
|
||||
import core.contracts.Fix
|
||||
import core.contracts.TransactionBuilder
|
||||
import core.bd
|
||||
import core.testing.MockNetwork
|
||||
import core.testutils.*
|
||||
import core.utilities.BriefLogFormatter
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import protocols.RatesFixProtocol
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class NodeInterestRatesTest {
|
||||
val TEST_DATA = NodeInterestRates.parseFile("""
|
||||
LIBOR 2016-03-16 1M = 0.678
|
||||
LIBOR 2016-03-16 2M = 0.685
|
||||
LIBOR 2016-03-16 1Y = 0.890
|
||||
LIBOR 2016-03-16 2Y = 0.962
|
||||
EURIBOR 2016-03-15 1M = 0.123
|
||||
EURIBOR 2016-03-15 2M = 0.111
|
||||
""".trimIndent())
|
||||
|
||||
val oracle = NodeInterestRates.Oracle(MEGA_CORP, MEGA_CORP_KEY).apply { knownFixes = TEST_DATA }
|
||||
|
||||
@Test fun `query successfully`() {
|
||||
val q = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
|
||||
val res = oracle.query(listOf(q))
|
||||
assertEquals(1, res.size)
|
||||
assertEquals("0.678".bd, res[0].value)
|
||||
assertEquals(q, res[0].of)
|
||||
}
|
||||
|
||||
@Test fun `query with one success and one missing`() {
|
||||
val q1 = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
|
||||
val q2 = NodeInterestRates.parseFixOf("LIBOR 2016-03-15 1M")
|
||||
val e = assertFailsWith<NodeInterestRates.UnknownFix> { oracle.query(listOf(q1, q2)) }
|
||||
assertEquals(e.fix, q2)
|
||||
}
|
||||
|
||||
@Test fun `query successfully with interpolated rate`() {
|
||||
val q = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 5M")
|
||||
val res = oracle.query(listOf(q))
|
||||
assertEquals(1, res.size)
|
||||
Assert.assertEquals(0.7316228, res[0].value.toDouble(), 0.0000001)
|
||||
assertEquals(q, res[0].of)
|
||||
}
|
||||
|
||||
@Test fun `rate missing and unable to interpolate`() {
|
||||
val q = NodeInterestRates.parseFixOf("EURIBOR 2016-03-15 3M")
|
||||
assertFailsWith<NodeInterestRates.UnknownFix> { oracle.query(listOf(q)) }
|
||||
}
|
||||
|
||||
@Test fun `empty query`() {
|
||||
assertFailsWith<IllegalArgumentException> { oracle.query(emptyList()) }
|
||||
}
|
||||
|
||||
@Test fun `refuse to sign with no relevant commands`() {
|
||||
val tx = makeTX()
|
||||
assertFailsWith<IllegalArgumentException> { oracle.sign(tx.toWireTransaction()) }
|
||||
tx.addCommand(Cash.Commands.Move(), ALICE_PUBKEY)
|
||||
assertFailsWith<IllegalArgumentException> { oracle.sign(tx.toWireTransaction()) }
|
||||
}
|
||||
|
||||
@Test fun `sign successfully`() {
|
||||
val tx = makeTX()
|
||||
val fix = oracle.query(listOf(NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M"))).first()
|
||||
tx.addCommand(fix, oracle.identity.owningKey)
|
||||
|
||||
// Sign successfully.
|
||||
val signature = oracle.sign(tx.toWireTransaction())
|
||||
tx.checkAndAddSignature(signature)
|
||||
}
|
||||
|
||||
@Test fun `do not sign with unknown fix`() {
|
||||
val tx = makeTX()
|
||||
val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
|
||||
val badFix = Fix(fixOf, "0.6789".bd)
|
||||
tx.addCommand(badFix, oracle.identity.owningKey)
|
||||
|
||||
val e1 = assertFailsWith<NodeInterestRates.UnknownFix> { oracle.sign(tx.toWireTransaction()) }
|
||||
assertEquals(fixOf, e1.fix)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun network() {
|
||||
val net = MockNetwork()
|
||||
val (n1, n2) = net.createTwoNodes()
|
||||
n2.interestRatesService.oracle.knownFixes = TEST_DATA
|
||||
|
||||
val tx = TransactionBuilder()
|
||||
val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
|
||||
val protocol = RatesFixProtocol(tx, n2.info, fixOf, "0.675".bd, "0.1".bd)
|
||||
BriefLogFormatter.initVerbose("rates")
|
||||
val future = n1.smm.add("rates", protocol)
|
||||
|
||||
net.runNetwork()
|
||||
future.get()
|
||||
|
||||
// We should now have a valid signature over our tx from the oracle.
|
||||
val fix = tx.toSignedTransaction(true).tx.commands.map { it.value as Fix }.first()
|
||||
assertEquals(fixOf, fix.of)
|
||||
assertEquals("0.678".bd, fix.value)
|
||||
}
|
||||
|
||||
private fun makeTX() = TransactionBuilder(outputs = mutableListOf(1000.DOLLARS.CASH `owned by` ALICE_PUBKEY))
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
package core.node.services
|
||||
|
||||
import core.contracts.TransactionBuilder
|
||||
import core.seconds
|
||||
import core.testing.MockNetwork
|
||||
import core.testutils.DUMMY_NOTARY
|
||||
import core.testutils.DUMMY_NOTARY_KEY
|
||||
import core.testutils.issueState
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import protocols.NotaryError
|
||||
import protocols.NotaryException
|
||||
import protocols.NotaryProtocol
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.ExecutionException
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class NotaryServiceTests {
|
||||
lateinit var net: MockNetwork
|
||||
lateinit var notaryNode: MockNetwork.MockNode
|
||||
lateinit var clientNode: MockNetwork.MockNode
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
// TODO: Move into MockNetwork
|
||||
net = MockNetwork()
|
||||
notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||
clientNode = net.createPartyNode(networkMapAddr = notaryNode.info)
|
||||
net.runNetwork() // Clear network map registration messages
|
||||
}
|
||||
|
||||
@Test fun `should sign a unique transaction with a valid timestamp`() {
|
||||
val inputState = issueState(clientNode)
|
||||
val tx = TransactionBuilder().withItems(inputState)
|
||||
tx.setTime(Instant.now(), DUMMY_NOTARY, 30.seconds)
|
||||
var wtx = tx.toWireTransaction()
|
||||
|
||||
val protocol = NotaryProtocol(wtx, NotaryProtocol.tracker())
|
||||
val future = clientNode.smm.add(NotaryProtocol.TOPIC, protocol)
|
||||
net.runNetwork()
|
||||
|
||||
val signature = future.get()
|
||||
signature.verifyWithECDSA(wtx.serialized)
|
||||
}
|
||||
|
||||
@Test fun `should sign a unique transaction without a timestamp`() {
|
||||
val inputState = issueState(clientNode)
|
||||
val wtx = TransactionBuilder().withItems(inputState).toWireTransaction()
|
||||
|
||||
val protocol = NotaryProtocol(wtx, NotaryProtocol.tracker())
|
||||
val future = clientNode.smm.add(NotaryProtocol.TOPIC, protocol)
|
||||
net.runNetwork()
|
||||
|
||||
val signature = future.get()
|
||||
signature.verifyWithECDSA(wtx.serialized)
|
||||
}
|
||||
|
||||
@Test fun `should report error for transaction with an invalid timestamp`() {
|
||||
val inputState = issueState(clientNode)
|
||||
val tx = TransactionBuilder().withItems(inputState)
|
||||
tx.setTime(Instant.now().plusSeconds(3600), DUMMY_NOTARY, 30.seconds)
|
||||
var wtx = tx.toWireTransaction()
|
||||
|
||||
val protocol = NotaryProtocol(wtx, NotaryProtocol.tracker())
|
||||
val future = clientNode.smm.add(NotaryProtocol.TOPIC, protocol)
|
||||
net.runNetwork()
|
||||
|
||||
val ex = assertFailsWith(ExecutionException::class) { future.get() }
|
||||
val error = (ex.cause as NotaryException).error
|
||||
assertTrue(error is NotaryError.TimestampInvalid)
|
||||
}
|
||||
|
||||
@Test fun `should report conflict for a duplicate transaction`() {
|
||||
val inputState = issueState(clientNode)
|
||||
val wtx = TransactionBuilder().withItems(inputState).toWireTransaction()
|
||||
|
||||
val firstSpend = NotaryProtocol(wtx)
|
||||
val secondSpend = NotaryProtocol(wtx)
|
||||
clientNode.smm.add("${NotaryProtocol.TOPIC}.first", firstSpend)
|
||||
val future = clientNode.smm.add("${NotaryProtocol.TOPIC}.second", secondSpend)
|
||||
net.runNetwork()
|
||||
|
||||
val ex = assertFailsWith(ExecutionException::class) { future.get() }
|
||||
val notaryError = (ex.cause as NotaryException).error as NotaryError.Conflict
|
||||
assertEquals(notaryError.tx, wtx)
|
||||
notaryError.conflict.verified()
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package core.node.services
|
||||
|
||||
import core.contracts.TimestampCommand
|
||||
import core.seconds
|
||||
import org.junit.Test
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class TimestampCheckerTests {
|
||||
val clock = Clock.fixed(Instant.now(), ZoneId.systemDefault())
|
||||
val timestampChecker = TimestampChecker(clock, tolerance = 30.seconds)
|
||||
|
||||
@Test
|
||||
fun `should return true for valid timestamp`() {
|
||||
val now = clock.instant()
|
||||
val timestampPast = TimestampCommand(now - 60.seconds, now - 29.seconds)
|
||||
val timestampFuture = TimestampCommand(now + 29.seconds, now + 60.seconds)
|
||||
assertTrue { timestampChecker.isValid(timestampPast) }
|
||||
assertTrue { timestampChecker.isValid(timestampFuture) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return false for invalid timestamp`() {
|
||||
val now = clock.instant()
|
||||
val timestampPast = TimestampCommand(now - 60.seconds, now - 31.seconds)
|
||||
val timestampFuture = TimestampCommand(now + 31.seconds, now + 60.seconds)
|
||||
assertFalse { timestampChecker.isValid(timestampPast) }
|
||||
assertFalse { timestampChecker.isValid(timestampFuture) }
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package core.node.services
|
||||
|
||||
import core.contracts.TransactionBuilder
|
||||
import core.testutils.MEGA_CORP
|
||||
import core.testutils.generateStateRef
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class UniquenessProviderTests {
|
||||
val identity = MEGA_CORP
|
||||
|
||||
@Test fun `should commit a transaction with unused inputs without exception`() {
|
||||
val provider = InMemoryUniquenessProvider()
|
||||
val inputState = generateStateRef()
|
||||
val tx = TransactionBuilder().withItems(inputState).toWireTransaction()
|
||||
provider.commit(tx, identity)
|
||||
}
|
||||
|
||||
@Test fun `should report a conflict for a transaction with previously used inputs`() {
|
||||
val provider = InMemoryUniquenessProvider()
|
||||
val inputState = generateStateRef()
|
||||
|
||||
val tx1 = TransactionBuilder().withItems(inputState).toWireTransaction()
|
||||
provider.commit(tx1, identity)
|
||||
|
||||
val tx2 = TransactionBuilder().withItems(inputState).toWireTransaction()
|
||||
val ex = assertFailsWith<UniquenessException> { provider.commit(tx2, identity) }
|
||||
|
||||
val consumingTx = ex.error.stateHistory[inputState]!!
|
||||
assertEquals(consumingTx.id, tx1.id)
|
||||
assertEquals(consumingTx.inputIndex, tx1.inputs.indexOf(inputState))
|
||||
assertEquals(consumingTx.requestingParty, identity)
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
package core.node.storage
|
||||
|
||||
import com.google.common.jimfs.Configuration.unix
|
||||
import com.google.common.jimfs.Jimfs
|
||||
import com.google.common.primitives.Ints
|
||||
import core.serialization.SerializedBytes
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.nio.file.Files
|
||||
|
||||
class PerFileCheckpointStorageTests {
|
||||
|
||||
val fileSystem = Jimfs.newFileSystem(unix())
|
||||
val storeDir = fileSystem.getPath("store")
|
||||
lateinit var checkpointStorage: PerFileCheckpointStorage
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
newCheckpointStorage()
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
fileSystem.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `add new checkpoint`() {
|
||||
val checkpoint = newCheckpoint()
|
||||
checkpointStorage.addCheckpoint(checkpoint)
|
||||
assertThat(checkpointStorage.checkpoints).containsExactly(checkpoint)
|
||||
newCheckpointStorage()
|
||||
assertThat(checkpointStorage.checkpoints).containsExactly(checkpoint)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `remove checkpoint`() {
|
||||
val checkpoint = newCheckpoint()
|
||||
checkpointStorage.addCheckpoint(checkpoint)
|
||||
checkpointStorage.removeCheckpoint(checkpoint)
|
||||
assertThat(checkpointStorage.checkpoints).isEmpty()
|
||||
newCheckpointStorage()
|
||||
assertThat(checkpointStorage.checkpoints).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `remove unknown checkpoint`() {
|
||||
val checkpoint = newCheckpoint()
|
||||
assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
|
||||
checkpointStorage.removeCheckpoint(checkpoint)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `add two checkpoints then remove first one`() {
|
||||
val firstCheckpoint = newCheckpoint()
|
||||
checkpointStorage.addCheckpoint(firstCheckpoint)
|
||||
val secondCheckpoint = newCheckpoint()
|
||||
checkpointStorage.addCheckpoint(secondCheckpoint)
|
||||
checkpointStorage.removeCheckpoint(firstCheckpoint)
|
||||
assertThat(checkpointStorage.checkpoints).containsExactly(secondCheckpoint)
|
||||
newCheckpointStorage()
|
||||
assertThat(checkpointStorage.checkpoints).containsExactly(secondCheckpoint)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `add checkpoint and then remove after 'restart'`() {
|
||||
val originalCheckpoint = newCheckpoint()
|
||||
checkpointStorage.addCheckpoint(originalCheckpoint)
|
||||
newCheckpointStorage()
|
||||
val reconstructedCheckpoint = checkpointStorage.checkpoints.single()
|
||||
assertThat(reconstructedCheckpoint).isEqualTo(originalCheckpoint).isNotSameAs(originalCheckpoint)
|
||||
checkpointStorage.removeCheckpoint(reconstructedCheckpoint)
|
||||
assertThat(checkpointStorage.checkpoints).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `non-checkpoint files are ignored`() {
|
||||
val checkpoint = newCheckpoint()
|
||||
checkpointStorage.addCheckpoint(checkpoint)
|
||||
Files.write(storeDir.resolve("random-non-checkpoint-file"), "this is not a checkpoint!!".toByteArray())
|
||||
newCheckpointStorage()
|
||||
assertThat(checkpointStorage.checkpoints).containsExactly(checkpoint)
|
||||
}
|
||||
|
||||
private fun newCheckpointStorage() {
|
||||
checkpointStorage = PerFileCheckpointStorage(storeDir)
|
||||
}
|
||||
|
||||
private var checkpointCount = 1
|
||||
private fun newCheckpoint() = Checkpoint(SerializedBytes(Ints.toByteArray(checkpointCount++)), "topic", "javaType")
|
||||
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package core.node.subsystems
|
||||
|
||||
import core.messaging.Message
|
||||
import core.messaging.MessageRecipients
|
||||
import core.testutils.freeLocalHostAndPort
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import java.util.concurrent.TimeUnit.MILLISECONDS
|
||||
import java.util.concurrent.TimeUnit.SECONDS
|
||||
|
||||
class ArtemisMessagingServiceTests {
|
||||
|
||||
@Rule @JvmField val temporaryFolder = TemporaryFolder()
|
||||
|
||||
val topic = "platform.self"
|
||||
lateinit var messagingNetwork: ArtemisMessagingService
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
messagingNetwork = ArtemisMessagingService(temporaryFolder.newFolder().toPath(), freeLocalHostAndPort())
|
||||
messagingNetwork.start()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
messagingNetwork.stop()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sending message to self`() {
|
||||
val receivedMessages = LinkedBlockingQueue<Message>()
|
||||
|
||||
messagingNetwork.addMessageHandler(topic) { message, r ->
|
||||
receivedMessages.add(message)
|
||||
}
|
||||
|
||||
sendMessage("first msg", messagingNetwork.myAddress)
|
||||
|
||||
assertThat(String(receivedMessages.poll(2, SECONDS).data)).isEqualTo("first msg")
|
||||
assertThat(receivedMessages.poll(200, MILLISECONDS)).isNull()
|
||||
}
|
||||
|
||||
private fun sendMessage(body: String, address: MessageRecipients) {
|
||||
messagingNetwork.send(messagingNetwork.createMessage(topic, body.toByteArray()), address)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package core.node.subsystems
|
||||
|
||||
import core.testing.MockNetwork
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class InMemoryNetworkMapCacheTest {
|
||||
lateinit var network: MockNetwork
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
network = MockNetwork()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun registerWithNetwork() {
|
||||
val (n0, n1) = network.createTwoNodes()
|
||||
|
||||
val future = n1.services.networkMapCache.addMapService(n1.net, n0.info, false, null)
|
||||
network.runNetwork()
|
||||
future.get()
|
||||
}
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
package core.node.subsystems
|
||||
|
||||
import contracts.Cash
|
||||
import core.*
|
||||
import core.contracts.DOLLARS
|
||||
import core.contracts.TransactionBuilder
|
||||
import core.contracts.USD
|
||||
import core.contracts.verifyToLedgerTransaction
|
||||
import core.node.ServiceHub
|
||||
import core.testutils.*
|
||||
import core.utilities.BriefLogFormatter
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class NodeWalletServiceTest {
|
||||
val kms = MockKeyManagementService(ALICE_KEY)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
BriefLogFormatter.loggingOn(NodeWalletService::class)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
BriefLogFormatter.loggingOff(NodeWalletService::class)
|
||||
}
|
||||
|
||||
fun make(): Pair<NodeWalletService, ServiceHub> {
|
||||
val services = MockServices(keyManagement = kms)
|
||||
return Pair(services.walletService as NodeWalletService, services)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun splits() {
|
||||
val (wallet, services) = make()
|
||||
|
||||
kms.nextKeys += Array(3) { ALICE_KEY }
|
||||
// Fix the PRNG so that we get the same splits every time.
|
||||
wallet.fillWithSomeTestCash(DUMMY_NOTARY, 100.DOLLARS, 3, 3, Random(0L))
|
||||
|
||||
val w = wallet.currentWallet
|
||||
assertEquals(3, w.states.size)
|
||||
|
||||
val state = w.states[0].state as Cash.State
|
||||
assertEquals(services.storageService.myLegalIdentity, state.deposit.party)
|
||||
assertEquals(services.storageService.myLegalIdentityKey.public, state.deposit.party.owningKey)
|
||||
assertEquals(29.01.DOLLARS, state.amount)
|
||||
assertEquals(ALICE_PUBKEY, state.owner)
|
||||
|
||||
assertEquals(33.34.DOLLARS, (w.states[2].state as Cash.State).amount)
|
||||
assertEquals(35.61.DOLLARS, (w.states[1].state as Cash.State).amount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun basics() {
|
||||
val (wallet, services) = make()
|
||||
|
||||
// A tx that sends us money.
|
||||
val freshKey = services.keyManagementService.freshKey()
|
||||
val usefulTX = TransactionBuilder().apply {
|
||||
Cash().generateIssue(this, 100.DOLLARS, MEGA_CORP.ref(1), freshKey.public, DUMMY_NOTARY)
|
||||
signWith(MEGA_CORP_KEY)
|
||||
}.toSignedTransaction()
|
||||
val myOutput = usefulTX.verifyToLedgerTransaction(MockIdentityService, MockStorageService().attachments).outRef<Cash.State>(0)
|
||||
|
||||
// A tx that spends our money.
|
||||
val spendTX = TransactionBuilder().apply {
|
||||
Cash().generateSpend(this, 80.DOLLARS, BOB_PUBKEY, listOf(myOutput))
|
||||
signWith(freshKey)
|
||||
}.toSignedTransaction()
|
||||
|
||||
// A tx that doesn't send us anything.
|
||||
val irrelevantTX = TransactionBuilder().apply {
|
||||
Cash().generateIssue(this, 100.DOLLARS, MEGA_CORP.ref(1), BOB_KEY.public, DUMMY_NOTARY)
|
||||
signWith(MEGA_CORP_KEY)
|
||||
}.toSignedTransaction()
|
||||
|
||||
assertNull(wallet.cashBalances[USD])
|
||||
wallet.notify(usefulTX.tx)
|
||||
assertEquals(100.DOLLARS, wallet.cashBalances[USD])
|
||||
wallet.notify(irrelevantTX.tx)
|
||||
assertEquals(100.DOLLARS, wallet.cashBalances[USD])
|
||||
wallet.notify(spendTX.tx)
|
||||
assertEquals(20.DOLLARS, wallet.cashBalances[USD])
|
||||
|
||||
// TODO: Flesh out these tests as needed.
|
||||
}
|
||||
}
|
34
node/src/test/kotlin/core/serialization/KryoTests.kt
Normal file
34
node/src/test/kotlin/core/serialization/KryoTests.kt
Normal file
@ -0,0 +1,34 @@
|
||||
package core.serialization
|
||||
|
||||
import com.esotericsoftware.kryo.Kryo
|
||||
import org.junit.Test
|
||||
import java.time.Instant
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class KryoTests {
|
||||
data class Person(val name: String, val birthday: Instant?)
|
||||
|
||||
private val kryo: Kryo = createKryo()
|
||||
|
||||
@Test
|
||||
fun ok() {
|
||||
val april_17th = Instant.parse("1984-04-17T00:30:00.00Z")
|
||||
val mike = Person("mike", april_17th)
|
||||
val bits = mike.serialize(kryo)
|
||||
with(bits.deserialize<Person>(kryo)) {
|
||||
assertEquals("mike", name)
|
||||
assertEquals(april_17th, birthday)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nullables() {
|
||||
val bob = Person("bob", null)
|
||||
val bits = bob.serialize(kryo)
|
||||
with(bits.deserialize<Person>(kryo)) {
|
||||
assertEquals("bob", name)
|
||||
assertNull(birthday)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
package core.serialization
|
||||
|
||||
import contracts.Cash
|
||||
import core.*
|
||||
import core.contracts.*
|
||||
import core.testutils.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.security.SignatureException
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class TransactionSerializationTests {
|
||||
// Simple TX that takes 1000 pounds from me and sends 600 to someone else (with 400 change).
|
||||
// It refers to a fake TX/state that we don't bother creating here.
|
||||
val depositRef = MINI_CORP.ref(1)
|
||||
val outputState = Cash.State(depositRef, 600.POUNDS, DUMMY_PUBKEY_1, DUMMY_NOTARY)
|
||||
val changeState = Cash.State(depositRef, 400.POUNDS, TestUtils.keypair.public, DUMMY_NOTARY)
|
||||
|
||||
val fakeStateRef = generateStateRef()
|
||||
lateinit var tx: TransactionBuilder
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
tx = TransactionBuilder().withItems(
|
||||
fakeStateRef, outputState, changeState, Command(Cash.Commands.Move(), arrayListOf(TestUtils.keypair.public))
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun signWireTX() {
|
||||
tx.signWith(TestUtils.keypair)
|
||||
val signedTX = tx.toSignedTransaction()
|
||||
|
||||
// Now check that the signature we just made verifies.
|
||||
signedTX.verifySignatures()
|
||||
|
||||
// Corrupt the data and ensure the signature catches the problem.
|
||||
signedTX.txBits.bits[5] = 0
|
||||
assertFailsWith(SignatureException::class) {
|
||||
signedTX.verifySignatures()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun wrongKeys() {
|
||||
// Can't convert if we don't have signatures for all commands
|
||||
assertFailsWith(IllegalStateException::class) {
|
||||
tx.toSignedTransaction()
|
||||
}
|
||||
|
||||
tx.signWith(TestUtils.keypair)
|
||||
val signedTX = tx.toSignedTransaction()
|
||||
|
||||
// Cannot construct with an empty sigs list.
|
||||
assertFailsWith(IllegalStateException::class) {
|
||||
signedTX.copy(sigs = emptyList())
|
||||
}
|
||||
|
||||
// If the signature was replaced in transit, we don't like it.
|
||||
assertFailsWith(SignatureException::class) {
|
||||
val tx2 = TransactionBuilder().withItems(fakeStateRef, outputState, changeState,
|
||||
Command(Cash.Commands.Move(), TestUtils.keypair2.public))
|
||||
tx2.signWith(TestUtils.keypair2)
|
||||
|
||||
signedTX.copy(sigs = tx2.toSignedTransaction().sigs).verify()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun timestamp() {
|
||||
tx.setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds)
|
||||
tx.signWith(TestUtils.keypair)
|
||||
tx.signWith(DUMMY_NOTARY_KEY)
|
||||
val stx = tx.toSignedTransaction()
|
||||
val ltx = stx.verifyToLedgerTransaction(MockIdentityService, MockStorageService().attachments)
|
||||
assertEquals(tx.commands().map { it.value }, ltx.commands.map { it.value })
|
||||
assertEquals(tx.inputStates(), ltx.inputs)
|
||||
assertEquals(tx.outputStates(), ltx.outputs)
|
||||
assertEquals(TEST_TX_TIME, ltx.commands.getTimestampBy(DUMMY_NOTARY)!!.midpoint)
|
||||
}
|
||||
}
|
387
node/src/test/kotlin/core/testutils/TestUtils.kt
Normal file
387
node/src/test/kotlin/core/testutils/TestUtils.kt
Normal file
@ -0,0 +1,387 @@
|
||||
@file:Suppress("UNUSED_PARAMETER", "UNCHECKED_CAST")
|
||||
|
||||
package core.testutils
|
||||
|
||||
import com.google.common.base.Throwables
|
||||
import com.google.common.net.HostAndPort
|
||||
import contracts.*
|
||||
import core.*
|
||||
import core.contracts.*
|
||||
import core.crypto.*
|
||||
import core.node.AbstractNode
|
||||
import core.serialization.serialize
|
||||
import core.testing.MockIdentityService
|
||||
import core.visualiser.GraphVisualiser
|
||||
import java.net.ServerSocket
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.fail
|
||||
|
||||
/** If an exception is thrown by the body, rethrows the root cause exception. */
|
||||
inline fun <R> rootCauseExceptions(body: () -> R): R {
|
||||
try {
|
||||
return body()
|
||||
} catch(e: Exception) {
|
||||
throw Throwables.getRootCause(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun freeLocalHostAndPort(): HostAndPort {
|
||||
val freePort = ServerSocket(0).use { it.localPort }
|
||||
return HostAndPort.fromParts("localhost", freePort)
|
||||
}
|
||||
|
||||
object TestUtils {
|
||||
val keypair = generateKeyPair()
|
||||
val keypair2 = generateKeyPair()
|
||||
val keypair3 = generateKeyPair()
|
||||
}
|
||||
|
||||
// A dummy time at which we will be pretending test transactions are created.
|
||||
val TEST_TX_TIME = Instant.parse("2015-04-17T12:00:00.00Z")
|
||||
|
||||
// A few dummy values for testing.
|
||||
val MEGA_CORP_KEY = TestUtils.keypair
|
||||
val MEGA_CORP_PUBKEY = MEGA_CORP_KEY.public
|
||||
|
||||
val MINI_CORP_KEY = TestUtils.keypair2
|
||||
val MINI_CORP_PUBKEY = MINI_CORP_KEY.public
|
||||
|
||||
val ORACLE_KEY = TestUtils.keypair3
|
||||
val ORACLE_PUBKEY = ORACLE_KEY.public
|
||||
|
||||
val DUMMY_PUBKEY_1 = DummyPublicKey("x1")
|
||||
val DUMMY_PUBKEY_2 = DummyPublicKey("x2")
|
||||
|
||||
val ALICE_KEY = generateKeyPair()
|
||||
val ALICE_PUBKEY = ALICE_KEY.public
|
||||
val ALICE = Party("Alice", ALICE_PUBKEY)
|
||||
|
||||
val BOB_KEY = generateKeyPair()
|
||||
val BOB_PUBKEY = BOB_KEY.public
|
||||
val BOB = Party("Bob", BOB_PUBKEY)
|
||||
|
||||
val MEGA_CORP = Party("MegaCorp", MEGA_CORP_PUBKEY)
|
||||
val MINI_CORP = Party("MiniCorp", MINI_CORP_PUBKEY)
|
||||
|
||||
val DUMMY_NOTARY_KEY = generateKeyPair()
|
||||
val DUMMY_NOTARY = Party("Notary Service", DUMMY_NOTARY_KEY.public)
|
||||
|
||||
val ALL_TEST_KEYS = listOf(MEGA_CORP_KEY, MINI_CORP_KEY, ALICE_KEY, BOB_KEY, DUMMY_NOTARY_KEY)
|
||||
|
||||
val MockIdentityService = MockIdentityService(listOf(MEGA_CORP, MINI_CORP, DUMMY_NOTARY))
|
||||
|
||||
// In a real system this would be a persistent map of hash to bytecode and we'd instantiate the object as needed inside
|
||||
// a sandbox. For unit tests we just have a hard-coded list.
|
||||
val TEST_PROGRAM_MAP: Map<Contract, Class<out Contract>> = mapOf(
|
||||
CASH_PROGRAM_ID to Cash::class.java,
|
||||
CP_PROGRAM_ID to CommercialPaper::class.java,
|
||||
JavaCommercialPaper.JCP_PROGRAM_ID to JavaCommercialPaper::class.java,
|
||||
CROWDFUND_PROGRAM_ID to CrowdFund::class.java,
|
||||
DUMMY_PROGRAM_ID to DummyContract::class.java,
|
||||
IRS_PROGRAM_ID to InterestRateSwap::class.java
|
||||
)
|
||||
|
||||
fun generateState(notary: Party = DUMMY_NOTARY) = DummyContract.State(Random().nextInt(), notary)
|
||||
fun generateStateRef() = StateRef(SecureHash.randomSHA256(), 0)
|
||||
|
||||
fun issueState(node: AbstractNode, notary: Party = DUMMY_NOTARY): StateRef {
|
||||
val tx = DummyContract().generateInitial(node.info.identity.ref(0), Random().nextInt(), DUMMY_NOTARY)
|
||||
tx.signWith(node.storage.myLegalIdentityKey)
|
||||
tx.signWith(DUMMY_NOTARY_KEY)
|
||||
val stx = tx.toSignedTransaction()
|
||||
node.services.recordTransactions(listOf(stx))
|
||||
return StateRef(stx.id, 0)
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Defines a simple DSL for building pseudo-transactions (not the same as the wire protocol) for testing purposes.
|
||||
//
|
||||
// Define a transaction like this:
|
||||
//
|
||||
// transaction {
|
||||
// input { someExpression }
|
||||
// output { someExpression }
|
||||
// arg { someExpression }
|
||||
//
|
||||
// tweak {
|
||||
// ... same thing but works with a copy of the parent, can add inputs/outputs/args just within this scope.
|
||||
// }
|
||||
//
|
||||
// contract.accepts() -> should pass
|
||||
// contract `fails requirement` "some substring of the error message"
|
||||
// }
|
||||
//
|
||||
// TODO: Make it impossible to forget to test either a failure or an accept for each transaction{} block
|
||||
|
||||
infix fun Cash.State.`owned by`(owner: PublicKey) = copy(owner = owner)
|
||||
infix fun Cash.State.`issued by`(party: Party) = copy(deposit = deposit.copy(party = party))
|
||||
infix fun CommercialPaper.State.`owned by`(owner: PublicKey) = this.copy(owner = owner)
|
||||
infix fun ICommercialPaperState.`owned by`(new_owner: PublicKey) = this.withOwner(new_owner)
|
||||
|
||||
// Allows you to write 100.DOLLARS.CASH
|
||||
val Amount.CASH: Cash.State get() = Cash.State(MINI_CORP.ref(1, 2, 3), this, NullPublicKey, DUMMY_NOTARY)
|
||||
|
||||
class LabeledOutput(val label: String?, val state: ContractState) {
|
||||
override fun toString() = state.toString() + (if (label != null) " ($label)" else "")
|
||||
override fun equals(other: Any?) = other is LabeledOutput && state.equals(other.state)
|
||||
override fun hashCode(): Int = state.hashCode()
|
||||
}
|
||||
|
||||
infix fun ContractState.label(label: String) = LabeledOutput(label, this)
|
||||
|
||||
abstract class AbstractTransactionForTest {
|
||||
protected val attachments = ArrayList<SecureHash>()
|
||||
protected val outStates = ArrayList<LabeledOutput>()
|
||||
protected val commands = ArrayList<Command>()
|
||||
|
||||
open fun output(label: String? = null, s: () -> ContractState) = LabeledOutput(label, s()).apply { outStates.add(this) }
|
||||
|
||||
protected fun commandsToAuthenticatedObjects(): List<AuthenticatedObject<CommandData>> {
|
||||
return commands.map { AuthenticatedObject(it.signers, it.signers.mapNotNull { MockIdentityService.partyFromKey(it) }, it.value) }
|
||||
}
|
||||
|
||||
fun attachment(attachmentID: SecureHash) {
|
||||
attachments.add(attachmentID)
|
||||
}
|
||||
|
||||
fun arg(vararg key: PublicKey, c: () -> CommandData) {
|
||||
val keys = listOf(*key)
|
||||
commands.add(Command(c(), keys))
|
||||
}
|
||||
|
||||
fun timestamp(time: Instant) {
|
||||
val data = TimestampCommand(time, 30.seconds)
|
||||
timestamp(data)
|
||||
}
|
||||
|
||||
fun timestamp(data: TimestampCommand) {
|
||||
commands.add(Command(data, DUMMY_NOTARY.owningKey))
|
||||
}
|
||||
|
||||
// Forbid patterns like: transaction { ... transaction { ... } }
|
||||
@Deprecated("Cannot nest transactions, use tweak", level = DeprecationLevel.ERROR)
|
||||
fun transaction(body: TransactionForTest.() -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
// Corresponds to the args to Contract.verify
|
||||
open class TransactionForTest : AbstractTransactionForTest() {
|
||||
private val inStates = arrayListOf<ContractState>()
|
||||
fun input(s: () -> ContractState) = inStates.add(s())
|
||||
|
||||
protected fun runCommandsAndVerify(time: Instant) {
|
||||
val cmds = commandsToAuthenticatedObjects()
|
||||
val tx = TransactionForVerification(inStates, outStates.map { it.state }, emptyList(), cmds, SecureHash.randomSHA256())
|
||||
tx.verify()
|
||||
}
|
||||
|
||||
fun accepts(time: Instant = TEST_TX_TIME) = runCommandsAndVerify(time)
|
||||
fun rejects(withMessage: String? = null, time: Instant = TEST_TX_TIME) {
|
||||
val r = try {
|
||||
runCommandsAndVerify(time)
|
||||
false
|
||||
} catch (e: Exception) {
|
||||
val m = e.message
|
||||
if (m == null)
|
||||
fail("Threw exception without a message")
|
||||
else
|
||||
if (withMessage != null && !m.toLowerCase().contains(withMessage.toLowerCase())) throw AssertionError("Error was actually: $m", e)
|
||||
true
|
||||
}
|
||||
if (!r) throw AssertionError("Expected exception but didn't get one")
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to confirm that the test, when (implicitly) run against the .verify() method, fails with the text of the message
|
||||
*/
|
||||
infix fun `fails requirement`(msg: String) = rejects(msg)
|
||||
|
||||
fun fails_requirement(msg: String) = this.`fails requirement`(msg)
|
||||
|
||||
// Use this to create transactions where the output of this transaction is automatically used as an input of
|
||||
// the next.
|
||||
fun chain(vararg outputLabels: String, body: TransactionForTest.() -> Unit): TransactionForTest {
|
||||
val states = outStates.mapNotNull {
|
||||
val l = it.label
|
||||
if (l != null && outputLabels.contains(l))
|
||||
it.state
|
||||
else
|
||||
null
|
||||
}
|
||||
val tx = TransactionForTest()
|
||||
tx.inStates.addAll(states)
|
||||
tx.body()
|
||||
return tx
|
||||
}
|
||||
|
||||
// Allow customisation of partial transactions.
|
||||
fun tweak(body: TransactionForTest.() -> Unit): TransactionForTest {
|
||||
val tx = TransactionForTest()
|
||||
tx.inStates.addAll(inStates)
|
||||
tx.outStates.addAll(outStates)
|
||||
tx.commands.addAll(commands)
|
||||
tx.body()
|
||||
return tx
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return """transaction {
|
||||
inputs: $inStates
|
||||
outputs: $outStates
|
||||
commands $commands
|
||||
}"""
|
||||
}
|
||||
|
||||
override fun equals(other: Any?) = this === other || (other is TransactionForTest && inStates == other.inStates && outStates == other.outStates && commands == other.commands)
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = inStates.hashCode()
|
||||
result += 31 * result + outStates.hashCode()
|
||||
result += 31 * result + commands.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
fun transaction(body: TransactionForTest.() -> Unit) = TransactionForTest().apply { body() }
|
||||
|
||||
class TransactionGroupDSL<T : ContractState>(private val stateType: Class<T>) {
|
||||
open inner class WireTransactionDSL : AbstractTransactionForTest() {
|
||||
private val inStates = ArrayList<StateRef>()
|
||||
|
||||
fun input(label: String) {
|
||||
inStates.add(label.outputRef)
|
||||
}
|
||||
|
||||
fun toWireTransaction() = WireTransaction(inStates, attachments, outStates.map { it.state }, commands)
|
||||
}
|
||||
|
||||
val String.output: T get() = labelToOutputs[this] ?: throw IllegalArgumentException("State with label '$this' was not found")
|
||||
val String.outputRef: StateRef get() = labelToRefs[this] ?: throw IllegalArgumentException("Unknown label \"$this\"")
|
||||
|
||||
fun <C : ContractState> lookup(label: String) = StateAndRef(label.output as C, label.outputRef)
|
||||
|
||||
private inner class InternalWireTransactionDSL : WireTransactionDSL() {
|
||||
fun finaliseAndInsertLabels(): WireTransaction {
|
||||
val wtx = toWireTransaction()
|
||||
for ((index, labelledState) in outStates.withIndex()) {
|
||||
if (labelledState.label != null) {
|
||||
labelToRefs[labelledState.label] = StateRef(wtx.id, index)
|
||||
if (stateType.isInstance(labelledState.state)) {
|
||||
labelToOutputs[labelledState.label] = labelledState.state as T
|
||||
}
|
||||
outputsToLabels[labelledState.state] = labelledState.label
|
||||
}
|
||||
}
|
||||
return wtx
|
||||
}
|
||||
}
|
||||
|
||||
private val rootTxns = ArrayList<WireTransaction>()
|
||||
private val labelToRefs = HashMap<String, StateRef>()
|
||||
private val labelToOutputs = HashMap<String, T>()
|
||||
private val outputsToLabels = HashMap<ContractState, String>()
|
||||
|
||||
fun labelForState(state: T): String? = outputsToLabels[state]
|
||||
|
||||
inner class Roots {
|
||||
fun transaction(vararg outputStates: LabeledOutput) {
|
||||
val outs = outputStates.map { it.state }
|
||||
val wtx = WireTransaction(emptyList(), emptyList(), outs, emptyList())
|
||||
for ((index, state) in outputStates.withIndex()) {
|
||||
val label = state.label!!
|
||||
labelToRefs[label] = StateRef(wtx.id, index)
|
||||
outputsToLabels[state.state] = label
|
||||
labelToOutputs[label] = state.state as T
|
||||
}
|
||||
rootTxns.add(wtx)
|
||||
}
|
||||
|
||||
@Deprecated("Does not nest ", level = DeprecationLevel.ERROR)
|
||||
fun roots(body: Roots.() -> Unit) {
|
||||
}
|
||||
|
||||
@Deprecated("Use the vararg form of transaction inside roots", level = DeprecationLevel.ERROR)
|
||||
fun transaction(body: WireTransactionDSL.() -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
fun roots(body: Roots.() -> Unit) = Roots().apply { body() }
|
||||
|
||||
val txns = ArrayList<WireTransaction>()
|
||||
private val txnToLabelMap = HashMap<SecureHash, String>()
|
||||
|
||||
fun transaction(label: String? = null, body: WireTransactionDSL.() -> Unit): WireTransaction {
|
||||
val forTest = InternalWireTransactionDSL()
|
||||
forTest.body()
|
||||
val wtx = forTest.finaliseAndInsertLabels()
|
||||
txns.add(wtx)
|
||||
if (label != null)
|
||||
txnToLabelMap[wtx.id] = label
|
||||
return wtx
|
||||
}
|
||||
|
||||
fun labelForTransaction(tx: WireTransaction): String? = txnToLabelMap[tx.id]
|
||||
fun labelForTransaction(tx: LedgerTransaction): String? = txnToLabelMap[tx.id]
|
||||
|
||||
@Deprecated("Does not nest ", level = DeprecationLevel.ERROR)
|
||||
fun transactionGroup(body: TransactionGroupDSL<T>.() -> Unit) {
|
||||
}
|
||||
|
||||
fun toTransactionGroup() = TransactionGroup(
|
||||
txns.map { it.toLedgerTransaction(MockIdentityService, MockStorageService().attachments) }.toSet(),
|
||||
rootTxns.map { it.toLedgerTransaction(MockIdentityService, MockStorageService().attachments) }.toSet()
|
||||
)
|
||||
|
||||
class Failed(val index: Int, cause: Throwable) : Exception("Transaction $index didn't verify", cause)
|
||||
|
||||
fun verify() {
|
||||
val group = toTransactionGroup()
|
||||
try {
|
||||
group.verify()
|
||||
} catch (e: TransactionVerificationException) {
|
||||
// Let the developer know the index of the transaction that failed.
|
||||
val wtx: WireTransaction = txns.find { it.id == e.tx.origHash }!!
|
||||
throw Failed(txns.indexOf(wtx) + 1, e)
|
||||
}
|
||||
}
|
||||
|
||||
fun expectFailureOfTx(index: Int, message: String): Exception {
|
||||
val e = assertFailsWith(Failed::class) {
|
||||
verify()
|
||||
}
|
||||
assertEquals(index, e.index)
|
||||
if (!e.cause!!.message!!.contains(message))
|
||||
throw AssertionError("Exception should have said '$message' but was actually: ${e.cause.message}", e.cause)
|
||||
return e
|
||||
}
|
||||
|
||||
fun visualise() {
|
||||
@Suppress("CAST_NEVER_SUCCEEDS")
|
||||
GraphVisualiser(this as TransactionGroupDSL<ContractState>).display()
|
||||
}
|
||||
|
||||
fun signAll(txnsToSign: List<WireTransaction> = txns, vararg extraKeys: KeyPair): List<SignedTransaction> {
|
||||
return txnsToSign.map { wtx ->
|
||||
val allPubKeys = wtx.commands.flatMap { it.signers }.toMutableSet()
|
||||
val bits = wtx.serialize()
|
||||
require(bits == wtx.serialized)
|
||||
val sigs = ArrayList<DigitalSignature.WithKey>()
|
||||
for (key in ALL_TEST_KEYS + extraKeys) {
|
||||
if (allPubKeys.contains(key.public)) {
|
||||
sigs += key.signWithECDSA(bits)
|
||||
allPubKeys -= key.public
|
||||
}
|
||||
}
|
||||
SignedTransaction(bits, sigs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified T : ContractState> transactionGroupFor(body: TransactionGroupDSL<T>.() -> Unit) = TransactionGroupDSL<T>(T::class.java).apply { this.body() }
|
||||
fun transactionGroup(body: TransactionGroupDSL<ContractState>.() -> Unit) = TransactionGroupDSL(ContractState::class.java).apply { this.body() }
|
98
node/src/test/kotlin/core/utilities/AffinityExecutorTests.kt
Normal file
98
node/src/test/kotlin/core/utilities/AffinityExecutorTests.kt
Normal file
@ -0,0 +1,98 @@
|
||||
package core.utilities
|
||||
|
||||
import org.junit.After
|
||||
import org.junit.Test
|
||||
import java.util.*
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFails
|
||||
import kotlin.test.assertNotEquals
|
||||
|
||||
class AffinityExecutorTests {
|
||||
@Test fun `AffinityExecutor SAME_THREAD executes on calling thread`() {
|
||||
assert(AffinityExecutor.SAME_THREAD.isOnThread)
|
||||
|
||||
run {
|
||||
val thatThread = CompletableFuture<Thread>()
|
||||
AffinityExecutor.SAME_THREAD.execute { thatThread.complete(Thread.currentThread()) }
|
||||
assertEquals(Thread.currentThread(), thatThread.get())
|
||||
}
|
||||
run {
|
||||
val thatThread = CompletableFuture<Thread>()
|
||||
AffinityExecutor.SAME_THREAD.executeASAP { thatThread.complete(Thread.currentThread()) }
|
||||
assertEquals(Thread.currentThread(), thatThread.get())
|
||||
}
|
||||
}
|
||||
|
||||
var executor: AffinityExecutor.ServiceAffinityExecutor? = null
|
||||
|
||||
@After fun shutdown() {
|
||||
executor?.shutdown()
|
||||
}
|
||||
|
||||
@Test fun `single threaded affinity executor runs on correct thread`() {
|
||||
val thisThread = Thread.currentThread()
|
||||
val executor = AffinityExecutor.ServiceAffinityExecutor("test thread", 1)
|
||||
assert(!executor.isOnThread)
|
||||
assertFails { executor.checkOnThread() }
|
||||
|
||||
val thread = AtomicReference<Thread>()
|
||||
executor.execute {
|
||||
assertNotEquals(thisThread, Thread.currentThread())
|
||||
executor.checkOnThread()
|
||||
thread.set(Thread.currentThread())
|
||||
}
|
||||
val thread2 = AtomicReference<Thread>()
|
||||
executor.execute {
|
||||
thread2.set(Thread.currentThread())
|
||||
executor.checkOnThread()
|
||||
}
|
||||
executor.flush()
|
||||
assertEquals(thread2.get(), thread.get())
|
||||
}
|
||||
|
||||
@Test fun `pooled executor`() {
|
||||
val executor = AffinityExecutor.ServiceAffinityExecutor("test2", 3)
|
||||
assert(!executor.isOnThread)
|
||||
|
||||
val latch = CountDownLatch(1)
|
||||
val threads = Collections.synchronizedList(ArrayList<Thread>())
|
||||
|
||||
fun blockAThread() {
|
||||
executor.execute {
|
||||
assert(executor.isOnThread)
|
||||
threads += Thread.currentThread()
|
||||
latch.await()
|
||||
}
|
||||
}
|
||||
blockAThread()
|
||||
blockAThread()
|
||||
executor.flush()
|
||||
assertEquals(2, threads.size)
|
||||
val numThreads = executor.fetchFrom {
|
||||
assert(executor.isOnThread)
|
||||
threads += Thread.currentThread()
|
||||
threads.distinct().size
|
||||
}
|
||||
assertEquals(3, numThreads)
|
||||
latch.countDown()
|
||||
executor.flush()
|
||||
}
|
||||
|
||||
@Test fun `exceptions are reported to the specified handler`() {
|
||||
val exception = AtomicReference<Throwable?>()
|
||||
// Run in a separate thread to avoid messing with any default exception handlers in the unit test thread.
|
||||
thread {
|
||||
Thread.currentThread().setUncaughtExceptionHandler { thread, throwable -> exception.set(throwable) }
|
||||
val executor = AffinityExecutor.ServiceAffinityExecutor("test3", 1)
|
||||
executor.execute {
|
||||
throw Exception("foo")
|
||||
}
|
||||
executor.flush()
|
||||
}.join()
|
||||
assertEquals("foo", exception.get()?.message)
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package core.utilities
|
||||
|
||||
import core.indexOfOrThrow
|
||||
import core.noneOrSingle
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class CollectionExtensionTests {
|
||||
@Test
|
||||
fun `noneOrSingle returns a single item`() {
|
||||
val collection = listOf(1)
|
||||
assertEquals(collection.noneOrSingle(), 1)
|
||||
assertEquals(collection.noneOrSingle { it == 1 }, 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `noneOrSingle returns null if item not found`() {
|
||||
val collection = emptyList<Int>()
|
||||
assertEquals(collection.noneOrSingle(), null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `noneOrSingle throws if more than one item found`() {
|
||||
val collection = listOf(1, 2)
|
||||
assertFailsWith<IllegalArgumentException> { collection.noneOrSingle() }
|
||||
assertFailsWith<IllegalArgumentException> { collection.noneOrSingle { it > 0 } }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `indexOfOrThrow returns index of the given item`() {
|
||||
val collection = listOf(1, 2)
|
||||
assertEquals(collection.indexOfOrThrow(1), 0)
|
||||
assertEquals(collection.indexOfOrThrow(2), 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `indexOfOrThrow throws if the given item is not found`() {
|
||||
val collection = listOf(1)
|
||||
assertFailsWith<IllegalArgumentException> { collection.indexOfOrThrow(2) }
|
||||
}
|
||||
}
|
91
node/src/test/kotlin/core/visualiser/GraphStream.kt
Normal file
91
node/src/test/kotlin/core/visualiser/GraphStream.kt
Normal file
@ -0,0 +1,91 @@
|
||||
package core.visualiser
|
||||
|
||||
import org.graphstream.graph.Edge
|
||||
import org.graphstream.graph.Element
|
||||
import org.graphstream.graph.Graph
|
||||
import org.graphstream.graph.Node
|
||||
import org.graphstream.graph.implementations.SingleGraph
|
||||
import org.graphstream.ui.layout.Layout
|
||||
import org.graphstream.ui.layout.springbox.implementations.SpringBox
|
||||
import org.graphstream.ui.swingViewer.DefaultView
|
||||
import org.graphstream.ui.view.Viewer
|
||||
import org.graphstream.ui.view.ViewerListener
|
||||
import java.util.*
|
||||
import javax.swing.JFrame
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
// Some utilities to make the GraphStream API a bit nicer to work with. For some reason GS likes to use a non-type safe
|
||||
// string->value map type API for configuring common things. We fix it up here:
|
||||
|
||||
class GSPropertyDelegate<T>(private val prefix: String) {
|
||||
operator fun getValue(thisRef: Element, property: KProperty<*>): T = thisRef.getAttribute("$prefix.${property.name}")
|
||||
operator fun setValue(thisRef: Element, property: KProperty<*>, value: T) = thisRef.setAttribute("$prefix.${property.name}", value)
|
||||
}
|
||||
|
||||
var Node.label: String by GSPropertyDelegate<String>("ui")
|
||||
var Graph.stylesheet: String by GSPropertyDelegate<String>("ui")
|
||||
var Edge.weight: Double by GSPropertyDelegate<Double>("layout")
|
||||
|
||||
// Do this one by hand as 'class' is a reserved word.
|
||||
var Node.styleClass: String
|
||||
set(value) = setAttribute("ui.class", value)
|
||||
get() = getAttribute("ui.class")
|
||||
|
||||
fun createGraph(name: String, styles: String): SingleGraph {
|
||||
System.setProperty("org.graphstream.ui.renderer", "org.graphstream.ui.j2dviewer.J2DGraphRenderer");
|
||||
return SingleGraph(name).apply {
|
||||
stylesheet = styles
|
||||
setAttribute("ui.quality")
|
||||
setAttribute("ui.antialias")
|
||||
setAttribute("layout.quality", 0)
|
||||
setAttribute("layout.force", 0.9)
|
||||
}
|
||||
}
|
||||
|
||||
class MyViewer(graph: Graph) : Viewer(graph, Viewer.ThreadingModel.GRAPH_IN_ANOTHER_THREAD) {
|
||||
override fun enableAutoLayout(layoutAlgorithm: Layout) {
|
||||
super.enableAutoLayout(layoutAlgorithm)
|
||||
|
||||
// Setting shortNap to 1 stops things bouncing around horribly at the start.
|
||||
optLayout.setNaps(50, 1)
|
||||
}
|
||||
}
|
||||
|
||||
fun runGraph(graph: SingleGraph, nodeOnClick: (Node) -> Unit) {
|
||||
// Use a bit of custom code here instead of calling graph.display() so we can maximize the window.
|
||||
val viewer = MyViewer(graph)
|
||||
val view: DefaultView = object : DefaultView(viewer, Viewer.DEFAULT_VIEW_ID, Viewer.newGraphRenderer()) {
|
||||
override fun openInAFrame(on: Boolean) {
|
||||
super.openInAFrame(on)
|
||||
if (frame != null) {
|
||||
frame.extendedState = frame.extendedState or JFrame.MAXIMIZED_BOTH
|
||||
}
|
||||
}
|
||||
}
|
||||
viewer.addView(view)
|
||||
|
||||
var loop: Boolean = true
|
||||
val viewerPipe = viewer.newViewerPipe()
|
||||
viewerPipe.addViewerListener(object : ViewerListener {
|
||||
override fun buttonPushed(id: String?) {
|
||||
}
|
||||
|
||||
override fun buttonReleased(id: String?) {
|
||||
val node = graph.getNode<Node>(id)
|
||||
nodeOnClick(node)
|
||||
}
|
||||
|
||||
override fun viewClosed(viewName: String?) {
|
||||
loop = false
|
||||
}
|
||||
})
|
||||
|
||||
view.openInAFrame(true)
|
||||
// Seed determined through trial and error: it gives a reasonable layout for the Wednesday demo.
|
||||
val springBox = SpringBox(false, Random(-103468310429824593L))
|
||||
viewer.enableAutoLayout(springBox)
|
||||
|
||||
while (loop) {
|
||||
viewerPipe.blockingPump()
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
package core.visualiser
|
||||
|
||||
import core.contracts.CommandData
|
||||
import core.contracts.ContractState
|
||||
import core.crypto.SecureHash
|
||||
import core.testutils.TransactionGroupDSL
|
||||
import org.graphstream.graph.Edge
|
||||
import org.graphstream.graph.Node
|
||||
import org.graphstream.graph.implementations.SingleGraph
|
||||
import kotlin.reflect.memberProperties
|
||||
|
||||
class GraphVisualiser(val dsl: TransactionGroupDSL<in ContractState>) {
|
||||
companion object {
|
||||
val css = GraphVisualiser::class.java.getResourceAsStream("graph.css").bufferedReader().readText()
|
||||
}
|
||||
|
||||
fun convert(): SingleGraph {
|
||||
val tg = dsl.toTransactionGroup()
|
||||
val graph = createGraph("Transaction group", css)
|
||||
|
||||
// Map all the transactions, including the bogus non-verified ones (with no inputs) to graph nodes.
|
||||
for ((txIndex, tx) in (tg.transactions + tg.nonVerifiedRoots).withIndex()) {
|
||||
val txNode = graph.addNode<Node>("tx$txIndex")
|
||||
if (tx !in tg.nonVerifiedRoots)
|
||||
txNode.label = dsl.labelForTransaction(tx).let { it ?: "TX ${tx.id.prefixChars()}" }
|
||||
txNode.styleClass = "tx"
|
||||
|
||||
// Now create a vertex for each output state.
|
||||
for (outIndex in tx.outputs.indices) {
|
||||
val node = graph.addNode<Node>(tx.outRef<ContractState>(outIndex).ref.toString())
|
||||
val state = tx.outputs[outIndex]
|
||||
node.label = stateToLabel(state)
|
||||
node.styleClass = stateToCSSClass(state) + ",state"
|
||||
node.setAttribute("state", state)
|
||||
val edge = graph.addEdge<Edge>("tx$txIndex-out$outIndex", txNode, node, true)
|
||||
edge.weight = 0.7
|
||||
}
|
||||
|
||||
// And a vertex for each command.
|
||||
for ((index, cmd) in tx.commands.withIndex()) {
|
||||
val node = graph.addNode<Node>(SecureHash.randomSHA256().prefixChars())
|
||||
node.label = commandToTypeName(cmd.value)
|
||||
node.styleClass = "command"
|
||||
val edge = graph.addEdge<Edge>("tx$txIndex-cmd-$index", node, txNode)
|
||||
edge.weight = 0.4
|
||||
}
|
||||
}
|
||||
// And now all states and transactions were mapped to graph nodes, hook up the input edges.
|
||||
for ((txIndex, tx) in tg.transactions.withIndex()) {
|
||||
for ((inputIndex, ref) in tx.inputs.withIndex()) {
|
||||
val edge = graph.addEdge<Edge>("tx$txIndex-in$inputIndex", ref.toString(), "tx$txIndex", true)
|
||||
edge.weight = 1.2
|
||||
}
|
||||
}
|
||||
return graph
|
||||
}
|
||||
|
||||
private fun stateToLabel(state: ContractState): String {
|
||||
return dsl.labelForState(state) ?: stateToTypeName(state)
|
||||
}
|
||||
|
||||
private fun commandToTypeName(state: CommandData) = state.javaClass.canonicalName.removePrefix("contracts.").replace('$', '.')
|
||||
private fun stateToTypeName(state: ContractState) = state.javaClass.canonicalName.removePrefix("contracts.").removeSuffix(".State")
|
||||
private fun stateToCSSClass(state: ContractState) = stateToTypeName(state).replace('.', '_').toLowerCase()
|
||||
|
||||
fun display() {
|
||||
runGraph(convert(), nodeOnClick = { node ->
|
||||
val state: ContractState? = node.getAttribute("state")
|
||||
if (state != null) {
|
||||
val props: List<Pair<String, Any?>> = state.javaClass.kotlin.memberProperties.map { it.name to it.getter.call(state) }
|
||||
StateViewer.show(props)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
37
node/src/test/kotlin/core/visualiser/StateViewer.form
Normal file
37
node/src/test/kotlin/core/visualiser/StateViewer.form
Normal file
@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="core.visualiser.StateViewer">
|
||||
<grid id="27dc6" binding="root" layout-manager="BorderLayout" hgap="15" vgap="15">
|
||||
<constraints>
|
||||
<xy x="20" y="20" width="500" height="400"/>
|
||||
</constraints>
|
||||
<properties>
|
||||
<background color="-1"/>
|
||||
</properties>
|
||||
<border type="empty">
|
||||
<size top="15" left="15" bottom="15" right="15"/>
|
||||
</border>
|
||||
<children>
|
||||
<component id="c1614" class="javax.swing.JLabel">
|
||||
<constraints border-constraint="North"/>
|
||||
<properties>
|
||||
<font style="1"/>
|
||||
<text value="State viewer"/>
|
||||
</properties>
|
||||
</component>
|
||||
<scrollpane id="2974d">
|
||||
<constraints border-constraint="Center"/>
|
||||
<properties/>
|
||||
<border type="none"/>
|
||||
<children>
|
||||
<component id="8f1af" class="javax.swing.JTable" binding="propsTable">
|
||||
<constraints/>
|
||||
<properties>
|
||||
<autoResizeMode value="3"/>
|
||||
<showHorizontalLines value="false"/>
|
||||
</properties>
|
||||
</component>
|
||||
</children>
|
||||
</scrollpane>
|
||||
</children>
|
||||
</grid>
|
||||
</form>
|
109
node/src/test/kotlin/core/visualiser/StateViewer.java
Normal file
109
node/src/test/kotlin/core/visualiser/StateViewer.java
Normal file
@ -0,0 +1,109 @@
|
||||
package core.visualiser;
|
||||
|
||||
import kotlin.*;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.table.*;
|
||||
import java.awt.*;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
|
||||
public class StateViewer {
|
||||
private JPanel root;
|
||||
private JTable propsTable;
|
||||
|
||||
public static void main(String[] args) {
|
||||
JFrame frame = new JFrame("StateViewer");
|
||||
List<Pair<String, Object>> props = new ArrayList<>();
|
||||
props.add(new Pair<>("a", 123));
|
||||
props.add(new Pair<>("things", "bar"));
|
||||
frame.setContentPane(new StateViewer(props).root);
|
||||
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
frame.pack();
|
||||
frame.setVisible(true);
|
||||
frame.setSize(800, 600);
|
||||
}
|
||||
|
||||
public static void show(List<Pair<String, Object>> props) {
|
||||
JFrame frame = new JFrame("StateViewer");
|
||||
StateViewer viewer = new StateViewer(props);
|
||||
frame.setContentPane(viewer.root);
|
||||
frame.pack();
|
||||
frame.setSize(600, 300);
|
||||
|
||||
viewer.propsTable.getColumnModel().getColumn(0).setMinWidth(150);
|
||||
viewer.propsTable.getColumnModel().getColumn(0).setMaxWidth(150);
|
||||
|
||||
frame.setVisible(true);
|
||||
}
|
||||
|
||||
public StateViewer(List<Pair<String, Object>> props) {
|
||||
propsTable.setModel(new AbstractTableModel() {
|
||||
@Override
|
||||
public int getRowCount() {
|
||||
return props.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnCount() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getColumnName(int column) {
|
||||
if (column == 0)
|
||||
return "Attribute";
|
||||
else if (column == 1)
|
||||
return "Value";
|
||||
else
|
||||
return "?";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValueAt(int rowIndex, int columnIndex) {
|
||||
if (columnIndex == 0)
|
||||
return props.get(rowIndex).getFirst();
|
||||
else
|
||||
return props.get(rowIndex).getSecond();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
// GUI initializer generated by IntelliJ IDEA GUI Designer
|
||||
// >>> IMPORTANT!! <<<
|
||||
// DO NOT EDIT OR ADD ANY CODE HERE!
|
||||
$$$setupUI$$$();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method generated by IntelliJ IDEA GUI Designer
|
||||
* >>> IMPORTANT!! <<<
|
||||
* DO NOT edit this method OR call it in your code!
|
||||
*
|
||||
* @noinspection ALL
|
||||
*/
|
||||
private void $$$setupUI$$$() {
|
||||
root = new JPanel();
|
||||
root.setLayout(new BorderLayout(15, 15));
|
||||
root.setBackground(new Color(-1));
|
||||
root.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15), null));
|
||||
final JLabel label1 = new JLabel();
|
||||
label1.setFont(new Font(label1.getFont().getName(), Font.BOLD, label1.getFont().getSize()));
|
||||
label1.setText("State viewer");
|
||||
root.add(label1, BorderLayout.NORTH);
|
||||
final JScrollPane scrollPane1 = new JScrollPane();
|
||||
root.add(scrollPane1, BorderLayout.CENTER);
|
||||
propsTable = new JTable();
|
||||
propsTable.setAutoResizeMode(3);
|
||||
propsTable.setShowHorizontalLines(false);
|
||||
scrollPane1.setViewportView(propsTable);
|
||||
}
|
||||
|
||||
/**
|
||||
* @noinspection ALL
|
||||
*/
|
||||
public JComponent $$$getRootComponent$$$() {
|
||||
return root;
|
||||
}
|
||||
}
|
BIN
node/src/test/resources/core/node/isolated.jar
Normal file
BIN
node/src/test/resources/core/node/isolated.jar
Normal file
Binary file not shown.
41
node/src/test/resources/core/visualiser/graph.css
Normal file
41
node/src/test/resources/core/visualiser/graph.css
Normal file
@ -0,0 +1,41 @@
|
||||
node.tx {
|
||||
size: 10px;
|
||||
fill-color: blue;
|
||||
shape: rounded-box;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
node.state {
|
||||
size: 25px;
|
||||
fill-color: beige;
|
||||
stroke-width: 2px;
|
||||
stroke-color: black;
|
||||
stroke-mode: plain;
|
||||
}
|
||||
|
||||
node {
|
||||
text-background-mode: rounded-box;
|
||||
text-background-color: darkslategrey;
|
||||
text-padding: 5px;
|
||||
text-offset: 10px;
|
||||
text-color: white;
|
||||
text-alignment: under;
|
||||
text-size: 16;
|
||||
}
|
||||
|
||||
node.command {
|
||||
text-size: 12;
|
||||
size: 8px;
|
||||
fill-color: white;
|
||||
stroke-width: 2px;
|
||||
stroke-color: black;
|
||||
stroke-mode: plain;
|
||||
}
|
||||
|
||||
node.cash {
|
||||
fill-color: red;
|
||||
}
|
||||
|
||||
graph {
|
||||
padding: 100px;
|
||||
}
|
Reference in New Issue
Block a user