From 9f7ae4c61d3845f62882c160fbc968b3adf0d5e9 Mon Sep 17 00:00:00 2001 From: "rick.parker" Date: Fri, 18 Mar 2016 14:40:39 +0000 Subject: [PATCH 1/5] Basic JSON API with servertime call exposed at GET /api/servertime Global Clock as part of ServiceHub to offer source of time in transactions, protocols, time stamping service etc (can be replace for demos and testing with a Clock that can be externally manipulated) Edited with Mike's feedback Edited with Mike's feedback --- core/src/main/kotlin/core/Structures.kt | 17 +++ src/main/kotlin/api/APIServer.kt | 141 ++++++++++++++++++ src/main/kotlin/api/APIServerImpl.kt | 108 ++++++++++++++ src/main/kotlin/api/Config.kt | 104 ++++++++++++- src/main/kotlin/api/Query.kt | 41 +++++ .../core/messaging/NetworkMapService.kt | 14 ++ src/main/kotlin/core/node/AbstractNode.kt | 10 +- src/main/kotlin/core/node/Node.kt | 32 ++-- .../core/node/services/NodeWalletService.kt | 25 +++- .../kotlin/core/node/services/Services.kt | 28 +++- src/main/kotlin/core/testing/MockNode.kt | 3 +- .../protocols/ResolveTransactionsProtocol.kt | 2 +- src/test/kotlin/core/MockServices.kt | 10 +- 13 files changed, 502 insertions(+), 33 deletions(-) create mode 100644 src/main/kotlin/api/APIServer.kt create mode 100644 src/main/kotlin/api/APIServerImpl.kt create mode 100644 src/main/kotlin/api/Query.kt diff --git a/core/src/main/kotlin/core/Structures.kt b/core/src/main/kotlin/core/Structures.kt index 56a8a0d326..dbfaa196b3 100644 --- a/core/src/main/kotlin/core/Structures.kt +++ b/core/src/main/kotlin/core/Structures.kt @@ -47,6 +47,23 @@ interface OwnableState : ContractState { fun withNewOwner(newOwner: PublicKey): Pair } +/** + * A state that evolves by superseding itself, all of which share the common "thread" + * + * This simplifies the job of tracking the current version of certain types of state in e.g. a wallet + */ +interface LinearState: ContractState { + /** Unique thread id within the wallets of all parties */ + val thread: SecureHash + + /** Human readable well known reference (e.g. trade reference) */ + // TODO we will push this down out of here once we have something more sophisticated and a more powerful query API + val ref: String + + /** true if this should be tracked by our wallet(s) */ + fun isRelevant(ourKeys: Set): Boolean +} + /** Returns the SHA-256 hash of the serialised contents of this state (not cached!) */ fun ContractState.hash(): SecureHash = SecureHash.sha256(serialize().bits) diff --git a/src/main/kotlin/api/APIServer.kt b/src/main/kotlin/api/APIServer.kt new file mode 100644 index 0000000000..afcada41c4 --- /dev/null +++ b/src/main/kotlin/api/APIServer.kt @@ -0,0 +1,141 @@ +/* + * 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 api + +import core.ContractState +import core.SignedTransaction +import core.StateRef +import core.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 + + fun fetchStates(states: List): Map + + /** + * 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): Map + + /** + * 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): SerializedBytes + + /** + * Generate a signature for this transaction signed by us. + */ + fun generateTransactionSignature(tx: SerializedBytes): 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, signatures: List): 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): Any? + + // fun invokeProtocolAsync(type: ProtocolRef, args: Map): ProtocolInstanceRef + + /** + * Fetch protocols that require a response to some prompt/question by a human (on the "bank" side). + */ + fun fetchProtocolsRequiringAttention(query: StatesQuery): Map + + /** + * 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) + +} + +/** + * 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, val dueBy: Instant) + + +/** + * Encapsulate a generateXXX method call on a contract. + */ +data class TransactionBuildStep(val generateMethodName: String, val args: Map) diff --git a/src/main/kotlin/api/APIServerImpl.kt b/src/main/kotlin/api/APIServerImpl.kt new file mode 100644 index 0000000000..476ebf4a92 --- /dev/null +++ b/src/main/kotlin/api/APIServerImpl.kt @@ -0,0 +1,108 @@ +package api + +import com.google.common.util.concurrent.ListenableFuture +import core.* +import core.crypto.DigitalSignature +import core.crypto.SecureHash +import core.node.AbstractNode +import core.protocols.ProtocolLogic +import core.serialization.SerializedBytes +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 { + // 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.linearHeadsInstanceOf(LinearState::class.java) { + it.ref == query.criteria.ref + } + return states.values.map { it.ref } + } + } + return emptyList() + } + + override fun fetchStates(states: List): Map { + return node.services.walletService.statesForRefs(states) + } + + override fun fetchTransactions(txs: List): Map { + throw UnsupportedOperationException() + } + + override fun buildTransaction(type: ContractDefRef, steps: List): SerializedBytes { + throw UnsupportedOperationException() + } + + override fun generateTransactionSignature(tx: SerializedBytes): DigitalSignature.WithKey { + throw UnsupportedOperationException() + } + + override fun commitTransaction(tx: SerializedBytes, signatures: List): SecureHash { + throw UnsupportedOperationException() + } + + override fun invokeProtocolSync(type: ProtocolRef, args: Map): Any? { + return invokeProtocolAsync(type, args).get() + } + + private fun invokeProtocolAsync(type: ProtocolRef, args: Map): ListenableFuture { + 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() + 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) { + 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<*> + 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 { + throw UnsupportedOperationException() + } + + override fun provideProtocolResponse(protocol: ProtocolInstanceRef, choice: SecureHash, args: Map) { + throw UnsupportedOperationException() + } + +} diff --git a/src/main/kotlin/api/Config.kt b/src/main/kotlin/api/Config.kt index 0238958800..7d841139d1 100644 --- a/src/main/kotlin/api/Config.kt +++ b/src/main/kotlin/api/Config.kt @@ -1,11 +1,21 @@ package api 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.BusinessCalendar +import core.Party +import core.crypto.SecureHash +import core.node.services.ServiceHub +import java.math.BigDecimal import java.time.LocalDate +import java.time.LocalDateTime import javax.ws.rs.ext.ContextResolver import javax.ws.rs.ext.Provider @@ -14,24 +24,44 @@ import javax.ws.rs.ext.Provider * and to organise serializers / deserializers for java.time.* classes as necessary */ @Provider -class Config: ContextResolver { +class Config(val services: ServiceHub): ContextResolver { - val defaultObjectMapper = createDefaultMapper() + val defaultObjectMapper = createDefaultMapper(services) override fun getContext(type: java.lang.Class<*>): ObjectMapper { return defaultObjectMapper } + class ServiceHubObjectMapper(var serviceHub: ServiceHub): ObjectMapper() { + + } + companion object { - private fun createDefaultMapper(): ObjectMapper { - val mapper = ObjectMapper() + private fun createDefaultMapper(services: ServiceHub): ObjectMapper { + val mapper = ServiceHubObjectMapper(services) mapper.enable(SerializationFeature.INDENT_OUTPUT); + mapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) mapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS) - mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); // Although we shouldn't really use java.util.* but instead java.time.* + 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 } @@ -45,7 +75,69 @@ class Config: ContextResolver { object LocalDateDeserializer: JsonDeserializer() { override fun deserialize(parser: JsonParser, context: DeserializationContext): LocalDate { - return LocalDate.parse(parser.text) + 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() { + override fun serialize(obj: Party, generator: JsonGenerator, provider: SerializerProvider) { + generator.writeString(obj.name) + } + } + + object PartyDeserializer: JsonDeserializer() { + 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 + val nodeForPartyName = mapper.serviceHub.networkMapService.nodeForPartyName(parser.text) ?: throw JsonParseException("Could not find a Party with name: ${parser.text}", parser.currentLocation) + return nodeForPartyName.identity + } + } + + object SecureHashSerializer: JsonSerializer() { + 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: JsonDeserializer() { + override fun deserialize(parser: JsonParser, context: DeserializationContext): T { + if(parser.currentToken == JsonToken.FIELD_NAME) { + parser.nextToken() + } + return try { + return SecureHash.parse(parser.text) as T + } catch (e: Exception) { + throw JsonParseException("Invalid hash ${parser.text}: ${e.message}", parser.currentLocation) + } + } + } + + object CalendarDeserializer: JsonDeserializer() { + 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) + } } } } \ No newline at end of file diff --git a/src/main/kotlin/api/Query.kt b/src/main/kotlin/api/Query.kt new file mode 100644 index 0000000000..960a4dfb38 --- /dev/null +++ b/src/main/kotlin/api/Query.kt @@ -0,0 +1,41 @@ +/* + * 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 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 + } + +} diff --git a/src/main/kotlin/core/messaging/NetworkMapService.kt b/src/main/kotlin/core/messaging/NetworkMapService.kt index 81b2b48399..9936acdda8 100644 --- a/src/main/kotlin/core/messaging/NetworkMapService.kt +++ b/src/main/kotlin/core/messaging/NetworkMapService.kt @@ -9,6 +9,7 @@ package core.messaging import core.Party +import core.crypto.DummyPublicKey import java.util.* /** Info about a network node that has is operated by some sort of verified identity. */ @@ -25,9 +26,22 @@ data class LegallyIdentifiableNode(val address: SingleMessageRecipient, val iden */ interface NetworkMapService { val timestampingNodes: List + val partyNodes: List + + fun nodeForPartyName(name: String): LegallyIdentifiableNode? = partyNodes.singleOrNull { it.identity.name == name } } // TODO: Move this to the test tree once a real network map is implemented and this scaffolding is no longer needed. class MockNetworkMapService : NetworkMapService { + + data class MockAddress(val id: String): SingleMessageRecipient + override val timestampingNodes = Collections.synchronizedList(ArrayList()) + override val partyNodes = Collections.synchronizedList(ArrayList()) + + init { + partyNodes.add(LegallyIdentifiableNode(MockAddress("excalibur:8080"), Party("Excalibur", DummyPublicKey("Excalibur")))) + partyNodes.add(LegallyIdentifiableNode(MockAddress("another:8080"), Party("ANOther", DummyPublicKey("ANOther")))) + + } } diff --git a/src/main/kotlin/core/node/AbstractNode.kt b/src/main/kotlin/core/node/AbstractNode.kt index 3e637f8603..7d6eec0523 100644 --- a/src/main/kotlin/core/node/AbstractNode.kt +++ b/src/main/kotlin/core/node/AbstractNode.kt @@ -16,6 +16,8 @@ package core.node +import api.APIServer +import api.APIServerImpl import com.codahale.metrics.MetricRegistry import contracts.* import core.* @@ -30,6 +32,7 @@ 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.util.* import java.util.concurrent.Executors @@ -37,7 +40,7 @@ import java.util.concurrent.Executors * 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. */ -abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration, val timestamperAddress: LegallyIdentifiableNode?) { +abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration, val timestamperAddress: LegallyIdentifiableNode?, val platformClock: Clock) { companion object { val PRIVATE_KEY_FILE_NAME = "identity-private-key" val PUBLIC_IDENTITY_FILE_NAME = "identity-public" @@ -62,6 +65,7 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration, 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 legallyIdentifableAddress: LegallyIdentifiableNode get() = LegallyIdentifiableNode(net.myAddress, storage.myLegalIdentity) @@ -89,6 +93,7 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration, var inNodeTimestampingService: NodeTimestamperService? = null lateinit var identity: IdentityService lateinit var net: MessagingService + lateinit var api: APIServer open fun start(): AbstractNode { log.info("Node starting up ...") @@ -99,6 +104,7 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration, wallet = NodeWalletService(services) keyManagement = E2ETestKeyManagementService() makeInterestRateOracleService() + api = APIServerImpl(this) // Insert a network map entry for the timestamper: this is all temp scaffolding and will go away. If we are // given the details, the timestamping node is somewhere else. Otherwise, we do our own timestamping. @@ -106,7 +112,7 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration, inNodeTimestampingService = null timestamperAddress } else { - inNodeTimestampingService = NodeTimestamperService(net, storage.myLegalIdentity, storage.myLegalIdentityKey) + inNodeTimestampingService = NodeTimestamperService(net, storage.myLegalIdentity, storage.myLegalIdentityKey, platformClock) LegallyIdentifiableNode(net.myAddress, storage.myLegalIdentity) } (services.networkMapService as MockNetworkMapService).timestampingNodes.add(tsid) diff --git a/src/main/kotlin/core/node/Node.kt b/src/main/kotlin/core/node/Node.kt index 9c0546e7a5..04a0daa289 100644 --- a/src/main/kotlin/core/node/Node.kt +++ b/src/main/kotlin/core/node/Node.kt @@ -22,6 +22,7 @@ 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 @@ -30,8 +31,8 @@ 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 -import kotlin.reflect.KClass class ConfigurationException(message: String) : Exception(message) @@ -46,9 +47,11 @@ class ConfigurationException(message: String) : Exception(message) * have to specify that yourself. * @param configuration This is typically loaded from a .properties file * @param timestamperAddress If null, this node will become a timestamping node, otherwise, it will use that one. + * @param clock The clock used within the node and by all protocols etc */ class Node(dir: Path, val p2pAddr: HostAndPort, configuration: NodeConfiguration, - timestamperAddress: LegallyIdentifiableNode?) : AbstractNode(dir, configuration, timestamperAddress) { + timestamperAddress: LegallyIdentifiableNode?, + clock: Clock = Clock.systemUTC()) : AbstractNode(dir, configuration, timestamperAddress, 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 @@ -88,14 +91,18 @@ class Node(dir: Path, val p2pAddr: HostAndPort, configuration: NodeConfiguration addServlet(DataUploadServlet::class.java, "/upload/*") addServlet(AttachmentDownloadServlet::class.java, "/attachments/*") - setAttribute("services", services) - val jerseyServlet = addServlet(ServletContainer::class.java, "/api/*") - // Give the app a slightly better name in JMX rather than a randomly generated one - jerseyServlet.setInitParameter(ServerProperties.APPLICATION_NAME, "node.api") - jerseyServlet.setInitParameter(ServerProperties.MONITORING_STATISTICS_MBEANS_ENABLED, "true") - jerseyServlet.initOrder = 0 // Initialise at server start + val resourceConfig = ResourceConfig() // Add your API provider classes (annotated for JAX-RS) here - setProviders(jerseyServlet, Config::class) + resourceConfig.register(Config(services)) + resourceConfig.register(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 @@ -103,11 +110,6 @@ class Node(dir: Path, val p2pAddr: HostAndPort, configuration: NodeConfiguration return server } - private fun setProviders(jerseyServlet: ServletHolder, vararg providerClasses: KClass) { - val providerClassNames = providerClasses.map { it.java.canonicalName }.joinToString() - jerseyServlet.setInitParameter(ServerProperties.PROVIDER_CLASSNAMES, providerClassNames) - } - override fun start(): Node { alreadyRunningNodeCheck() super.start() @@ -161,4 +163,4 @@ class Node(dir: Path, val p2pAddr: HostAndPort, configuration: NodeConfiguration if (nodeFileLock == null) nodeFileLock = RandomAccessFile(file, "rw").channel.lock() } -} \ No newline at end of file +} diff --git a/src/main/kotlin/core/node/services/NodeWalletService.kt b/src/main/kotlin/core/node/services/NodeWalletService.kt index 3dfe1cb98b..1841be036b 100644 --- a/src/main/kotlin/core/node/services/NodeWalletService.kt +++ b/src/main/kotlin/core/node/services/NodeWalletService.kt @@ -11,6 +11,7 @@ package core.node.services import com.codahale.metrics.Gauge import contracts.Cash import core.* +import core.crypto.SecureHash import core.utilities.loggerFor import core.utilities.trace import java.security.PublicKey @@ -42,6 +43,14 @@ class NodeWalletService(private val services: ServiceHub) : WalletService { */ override val cashBalances: Map get() = mutex.locked { wallet }.cashBalances + /** + * Returns a snapshot of the heads of LinearStates + */ + override val linearHeads: Map> + get() = mutex.locked { wallet }.let { wallet -> + wallet.states.filter { it.state is LinearState }.associateBy { (it.state as LinearState).thread }.mapValues { it.value as StateAndRef } + } + override fun notifyAll(txns: Iterable): Wallet { val ourKeys = services.keyManagementService.keys.keys @@ -68,11 +77,21 @@ class NodeWalletService(private val services: ServiceHub) : WalletService { } } + private fun isRelevant(state: ContractState, ourKeys: Set): 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): Wallet { val ourNewStates = tx.outputs. - filterIsInstance(). - filter { it.owner in ourKeys }. - map { tx.outRef(it) } + filter { isRelevant(it, ourKeys) }. + map { tx.outRef(it) } // Now calculate the states that are being spent by this transaction. val consumed: Set = states.map { it.ref }.intersect(tx.inputs) diff --git a/src/main/kotlin/core/node/services/Services.kt b/src/main/kotlin/core/node/services/Services.kt index 6974af1c4e..e9aae810d0 100644 --- a/src/main/kotlin/core/node/services/Services.kt +++ b/src/main/kotlin/core/node/services/Services.kt @@ -18,6 +18,7 @@ import java.io.InputStream import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey +import java.time.Clock import java.util.* /** @@ -31,7 +32,7 @@ import java.util.* * 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. */ -data class Wallet(val states: List>) { +data class Wallet(val states: List>) { @Suppress("UNCHECKED_CAST") inline fun statesOfType() = states.filter { it.state is T } as List> @@ -67,6 +68,20 @@ interface WalletService { */ val cashBalances: Map + /** + * Returns a snapshot of the heads of LinearStates + */ + val linearHeads: Map> + + fun linearHeadsInstanceOf(clazz: Class, predicate: (T) -> Boolean = { true } ): Map> { + return linearHeads.filterValues { clazz.isInstance(it.state) }.filterValues { predicate(it.state as T) } + } + + fun statesForRefs(refs: List): Map { + val refsToStates = currentWallet.states.associateBy { it.ref } + return refs.associateBy( { it }, { refsToStates[it]?.state } ) + } + /** * Possibly update the wallet by marking as spent states that these transactions consume, and adding any relevant * new states that they create. You should only insert transactions that have been successfully verified here! @@ -175,6 +190,7 @@ interface ServiceHub { val networkService: MessagingService val networkMapService: NetworkMapService val monitoringService: MonitoringService + val clock: Clock /** * Given a [LedgerTransaction], looks up all its dependencies in the local database, uses the identity service to map @@ -188,4 +204,14 @@ interface ServiceHub { val ltxns = dependencies.map { it.verifyToLedgerTransaction(identityService, storageService.attachments) } TransactionGroup(setOf(ltx), ltxns.toSet()).verify(storageService.contractPrograms) } + + /** + * Use this for storing transactions to StorageService and WalletService + * + * TODO Need to come up with a way for preventing transactions being written other than by this method + */ + fun recordTransactions(txs: List) { + storageService.validatedTransactions.putAll(txs.groupBy { it.id }.mapValues { it.value.first() }) + walletService.notifyAll(txs.map { it.tx }) + } } diff --git a/src/main/kotlin/core/testing/MockNode.kt b/src/main/kotlin/core/testing/MockNode.kt index 818ca45568..c0f6cfc913 100644 --- a/src/main/kotlin/core/testing/MockNode.kt +++ b/src/main/kotlin/core/testing/MockNode.kt @@ -20,6 +20,7 @@ import core.utilities.loggerFor import org.slf4j.Logger import java.nio.file.Files import java.nio.file.Path +import java.time.Clock import java.util.* import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -48,7 +49,7 @@ class MockNetwork(private val threadPerNode: Boolean = false) { } open class MockNode(dir: Path, config: NodeConfiguration, val mockNet: MockNetwork, - withTimestamper: LegallyIdentifiableNode?, val forcedID: Int = -1) : AbstractNode(dir, config, withTimestamper) { + withTimestamper: LegallyIdentifiableNode?, val forcedID: Int = -1) : AbstractNode(dir, config, withTimestamper, Clock.systemUTC()) { override val log: Logger = loggerFor() override val serverThread: ExecutorService = if (mockNet.threadPerNode) diff --git a/src/main/kotlin/protocols/ResolveTransactionsProtocol.kt b/src/main/kotlin/protocols/ResolveTransactionsProtocol.kt index 8e6a638c9d..dbda1e645d 100644 --- a/src/main/kotlin/protocols/ResolveTransactionsProtocol.kt +++ b/src/main/kotlin/protocols/ResolveTransactionsProtocol.kt @@ -75,7 +75,7 @@ class ResolveTransactionsProtocol(private val txHashes: Set, // It may seem tempting to write transactions to the database as we receive them, instead of all at once // here at the end. Doing it this way avoids cases where a transaction is in the database but its // dependencies aren't, or an unvalidated and possibly broken tx is there. - serviceHub.storageService.validatedTransactions.putAll(downloadedSignedTxns.associateBy { it.id }) + serviceHub.recordTransactions(downloadedSignedTxns) } @Suspendable diff --git a/src/test/kotlin/core/MockServices.kt b/src/test/kotlin/core/MockServices.kt index 277b5a656e..c6567f2a9f 100644 --- a/src/test/kotlin/core/MockServices.kt +++ b/src/test/kotlin/core/MockServices.kt @@ -14,14 +14,11 @@ import core.messaging.MessagingService import core.messaging.MockNetworkMapService import core.messaging.NetworkMapService import core.node.services.* -import core.node.AbstractNode -import core.node.services.StorageServiceImpl import core.serialization.SerializedBytes import core.serialization.deserialize import core.testutils.TEST_KEYS_TO_CORP_MAP import core.testutils.TEST_PROGRAM_MAP import core.testutils.TEST_TX_TIME -import org.slf4j.LoggerFactory import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.File @@ -76,6 +73,8 @@ class MockKeyManagementService(vararg initialKeys: KeyPair) : KeyManagementServi } class MockWalletService(val states: List>) : WalletService { + override val linearHeads: Map> + get() = TODO("Use NodeWalletService instead") override val cashBalances: Map get() = TODO("Use NodeWalletService instead") @@ -132,7 +131,8 @@ class MockServices( val net: MessagingService? = null, val identity: IdentityService? = MockIdentityService, val storage: StorageService? = MockStorageService(), - val networkMap: NetworkMapService? = MockNetworkMapService() + val networkMap: NetworkMapService? = MockNetworkMapService(), + val overrideClock: Clock? = Clock.systemUTC() ) : ServiceHub { override val walletService: WalletService get() = wallet ?: throw UnsupportedOperationException() @@ -146,6 +146,8 @@ class MockServices( get() = networkMap ?: throw UnsupportedOperationException() override val storageService: StorageService get() = storage ?: throw UnsupportedOperationException() + override val clock: Clock + get() = overrideClock ?: throw UnsupportedOperationException() override val monitoringService: MonitoringService = MonitoringService(MetricRegistry()) From 4167b044a13e832aa9449d54915c6a09d442a14e Mon Sep 17 00:00:00 2001 From: Richard Green Date: Wed, 23 Mar 2016 12:17:30 +0000 Subject: [PATCH 2/5] Field name request from client --- core/src/main/kotlin/core/FinanceTypes.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/core/FinanceTypes.kt b/core/src/main/kotlin/core/FinanceTypes.kt index f987f0a4bc..c03c3b52c9 100644 --- a/core/src/main/kotlin/core/FinanceTypes.kt +++ b/core/src/main/kotlin/core/FinanceTypes.kt @@ -225,7 +225,7 @@ fun LocalDate.isWorkingDay(accordingToCalendar: BusinessCalendar): Boolean = acc * typical feature of financial contracts, in which a business may not want a payment event to fall on a day when * no staff are around to handle problems. */ -open class BusinessCalendar private constructor(val holidayDates: List) { +open class BusinessCalendar private constructor(val calendars:String, val holidayDates: List) { class UnknownCalendar(name: String): Exception("$name not found") companion object { @@ -239,7 +239,7 @@ open class BusinessCalendar private constructor(val holidayDates: List Date: Wed, 23 Mar 2016 13:42:52 +0000 Subject: [PATCH 3/5] Made calendars property a string array rather than just string --- core/src/main/kotlin/core/FinanceTypes.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/core/FinanceTypes.kt b/core/src/main/kotlin/core/FinanceTypes.kt index c03c3b52c9..3885575176 100644 --- a/core/src/main/kotlin/core/FinanceTypes.kt +++ b/core/src/main/kotlin/core/FinanceTypes.kt @@ -225,7 +225,7 @@ fun LocalDate.isWorkingDay(accordingToCalendar: BusinessCalendar): Boolean = acc * typical feature of financial contracts, in which a business may not want a payment event to fall on a day when * no staff are around to handle problems. */ -open class BusinessCalendar private constructor(val calendars:String, val holidayDates: List) { +open class BusinessCalendar private constructor(val calendars: Array, val holidayDates: List) { class UnknownCalendar(name: String): Exception("$name not found") companion object { @@ -239,7 +239,7 @@ open class BusinessCalendar private constructor(val calendars:String, val holida fun parseDateFromString(it: String) = LocalDate.parse(it, DateTimeFormatter.ISO_LOCAL_DATE) /** Returns a business calendar that combines all the named holiday calendars into one list of holiday dates. */ - fun getInstance(vararg calname: String) = BusinessCalendar( calname.joinToString(","), + fun getInstance(vararg calname: String) = BusinessCalendar(calname, calname.flatMap { (TEST_CALENDAR_DATA[it] ?: throw UnknownCalendar(it)).split(",") }. toSet(). map{ parseDateFromString(it) }. From 5b7fb86b6b1b5e288df98285a01ef1e9f77aed62 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Tue, 22 Mar 2016 10:52:24 +0000 Subject: [PATCH 4/5] Use TypeSafe Config library instead of Java properties files for the config file format. The default formats are compatible so this should not be disruptive. --- build.gradle | 5 ++- .../kotlin/core/node/NodeConfiguration.kt | 34 +++++-------------- src/main/kotlin/demos/TraderDemo.kt | 12 +++---- src/main/resources/reference.conf | 2 ++ 4 files changed, 18 insertions(+), 35 deletions(-) create mode 100644 src/main/resources/reference.conf diff --git a/build.gradle b/build.gradle index 36e1441744..b783282a5e 100644 --- a/build.gradle +++ b/build.gradle @@ -108,7 +108,10 @@ dependencies { compile "io.dropwizard.metrics:metrics-core:3.1.2" // JimFS: in memory java.nio filesystem. Used for test and simulation utilities. - compile 'com.google.jimfs:jimfs:1.1' + compile "com.google.jimfs:jimfs:1.1" + + // TypeSafe Config: for simple and human friendly config files. + compile "com.typesafe:config:1.3.0" // Unit testing helpers. testCompile 'junit:junit:4.12' diff --git a/src/main/kotlin/core/node/NodeConfiguration.kt b/src/main/kotlin/core/node/NodeConfiguration.kt index d4233c17ef..a61a80cd38 100644 --- a/src/main/kotlin/core/node/NodeConfiguration.kt +++ b/src/main/kotlin/core/node/NodeConfiguration.kt @@ -8,37 +8,19 @@ package core.node -import java.util.* -import kotlin.reflect.declaredMemberProperties +import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory +import kotlin.reflect.KProperty interface NodeConfiguration { val myLegalName: String val exportJMXto: String } -object DefaultConfiguration : NodeConfiguration { - override val myLegalName: String = "Vast Global MegaCorp" - override val exportJMXto: String = "" // can be "http" or empty +// Allow the use of "String by config" syntax. TODO: Make it more flexible. +operator fun Config.getValue(receiver: NodeConfigurationFromConfig, metadata: KProperty<*>) = getString(metadata.name) - fun toProperties(): Properties { - val settings = DefaultConfiguration::class.declaredMemberProperties.map { it.name to it.get(this@DefaultConfiguration).toString() } - val p = Properties().apply { - for (setting in settings) { - setProperty(setting.first, setting.second) - } - } - return p - } -} - -/** - * A simple wrapper around a plain old Java .properties file. The keys have the same name as in the source code. - * - * TODO: Replace Java properties file with a better config file format (maybe yaml). - * We want to be able to configure via a GUI too, so an ability to round-trip whitespace, comments etc when machine - * editing the file is a must-have. - */ -class NodeConfigurationFromProperties(private val properties: Properties) : NodeConfiguration { - override val myLegalName: String get() = properties.getProperty("myLegalName") - override val exportJMXto: String get() = properties.getProperty("exportJMXto") +class NodeConfigurationFromConfig(val config: Config = ConfigFactory.load()) : NodeConfiguration { + override val myLegalName: String by config + override val exportJMXto: String by config } \ No newline at end of file diff --git a/src/main/kotlin/demos/TraderDemo.kt b/src/main/kotlin/demos/TraderDemo.kt index a99ac5e2d5..ea914c01fe 100644 --- a/src/main/kotlin/demos/TraderDemo.kt +++ b/src/main/kotlin/demos/TraderDemo.kt @@ -10,16 +10,16 @@ package demos import co.paralleluniverse.fibers.Suspendable import com.google.common.net.HostAndPort +import com.typesafe.config.ConfigFactory import contracts.CommercialPaper import core.* import core.crypto.SecureHash import core.crypto.generateKeyPair import core.messaging.LegallyIdentifiableNode import core.messaging.SingleMessageRecipient -import core.node.DefaultConfiguration import core.node.Node import core.node.NodeConfiguration -import core.node.NodeConfigurationFromProperties +import core.node.NodeConfigurationFromConfig import core.node.services.ArtemisMessagingService import core.node.services.NodeAttachmentService import core.node.services.NodeWalletService @@ -37,7 +37,6 @@ import java.nio.file.Path import java.nio.file.Paths import java.security.PublicKey import java.time.Instant -import java.util.* import kotlin.system.exitProcess import kotlin.test.assertEquals @@ -291,11 +290,8 @@ private fun loadConfigFile(configFile: Path): NodeConfiguration { askAdminToEditConfig(configFile) } - val config = configFile.toFile().reader().use { - NodeConfigurationFromProperties( - Properties(DefaultConfiguration.toProperties()).apply { load(it) } - ) - } + System.setProperty("config.file", configFile.toAbsolutePath().toString()) + val config = NodeConfigurationFromConfig(ConfigFactory.load()) // Make sure admin did actually edit at least the legal name. if (config.myLegalName == defaultLegalName) diff --git a/src/main/resources/reference.conf b/src/main/resources/reference.conf new file mode 100644 index 0000000000..00fff9a9d9 --- /dev/null +++ b/src/main/resources/reference.conf @@ -0,0 +1,2 @@ +myLegalName = "Vast Global MegaCorp, Ltd" +exportJMXto = "http" \ No newline at end of file From e21f61ff107cc65161d2eba1997c2d4c48904a09 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Wed, 23 Mar 2016 16:47:55 +0000 Subject: [PATCH 5/5] Minor: add toString methods to progress tracker objects --- .../main/kotlin/core/utilities/ProgressTracker.kt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/core/src/main/kotlin/core/utilities/ProgressTracker.kt b/core/src/main/kotlin/core/utilities/ProgressTracker.kt index db8dea4e9e..e578e40e80 100644 --- a/core/src/main/kotlin/core/utilities/ProgressTracker.kt +++ b/core/src/main/kotlin/core/utilities/ProgressTracker.kt @@ -42,9 +42,15 @@ import java.util.* */ class ProgressTracker(vararg steps: Step) { sealed class Change { - class Position(val newStep: Step) : Change() - class Rendering(val ofStep: Step) : Change() - class Structural(val parent: Step) : Change() + class Position(val newStep: Step) : Change() { + override fun toString() = newStep.label + } + class Rendering(val ofStep: Step) : Change() { + override fun toString() = ofStep.label + } + class Structural(val parent: Step) : Change() { + override fun toString() = "Structural step change in child of ${parent.label}" + } } /** The superclass of all step objects. */