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:
Matthew Nesbit
2016-05-14 17:57:41 +01:00
parent 058ac986bd
commit 988e00099d
82 changed files with 143 additions and 63 deletions

View 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?>)

View 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()
}
}

View 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
}

View 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
}
}

View 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")
}
}
}

View 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")

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

View 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
}

View 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()
}
}

View 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
}

View File

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

View File

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

View 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()
}

View 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()
}

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

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

View File

@ -0,0 +1,9 @@
package core.node.services
/**
* Placeholder interface for regulator services.
*/
interface RegulatorService {
object Type : ServiceType("corda.regulator")
}

View 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
}
}

View File

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

View 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 }
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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
}
}

View File

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

View 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() }
}

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

View 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()
}
}

View 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
}
}
}

View 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]
}

View 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
}
}

View 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 }
}

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

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

View 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
}
}
}

View 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
}

View 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()
}
}
}

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

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

View 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

View 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

View 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"
}

View 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"));
}
}

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

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

View 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()
}
}

View 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
}
}

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

View 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()
}
}

View 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() }
}
}
}

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

View File

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

View 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())
}
}

View 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
}
}

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View 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() }

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

View File

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

View 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()
}
}

View File

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

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

View 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;
}
}

Binary file not shown.

View 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;
}