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
This commit is contained in:
rick.parker
2016-03-18 14:40:39 +00:00
parent a0d474f270
commit 9f7ae4c61d
13 changed files with 502 additions and 33 deletions

View File

@ -47,6 +47,23 @@ interface OwnableState : ContractState {
fun withNewOwner(newOwner: PublicKey): Pair<CommandData, OwnableState> fun withNewOwner(newOwner: PublicKey): Pair<CommandData, OwnableState>
} }
/**
* 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<PublicKey>): Boolean
}
/** Returns the SHA-256 hash of the serialised contents of this state (not cached!) */ /** Returns the SHA-256 hash of the serialised contents of this state (not cached!) */
fun ContractState.hash(): SecureHash = SecureHash.sha256(serialize().bits) fun ContractState.hash(): SecureHash = SecureHash.sha256(serialize().bits)

View File

@ -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<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,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<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.linearHeadsInstanceOf(LinearState::class.java) {
it.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) {
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<StateRef, ProtocolRequiringAttention> {
throw UnsupportedOperationException()
}
override fun provideProtocolResponse(protocol: ProtocolInstanceRef, choice: SecureHash, args: Map<String, Any?>) {
throw UnsupportedOperationException()
}
}

View File

@ -1,11 +1,21 @@
package api package api
import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParseException
import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonToken
import com.fasterxml.jackson.databind.* 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.databind.module.SimpleModule
import com.fasterxml.jackson.module.kotlin.KotlinModule 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.LocalDate
import java.time.LocalDateTime
import javax.ws.rs.ext.ContextResolver import javax.ws.rs.ext.ContextResolver
import javax.ws.rs.ext.Provider 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 * and to organise serializers / deserializers for java.time.* classes as necessary
*/ */
@Provider @Provider
class Config: ContextResolver<ObjectMapper> { class Config(val services: ServiceHub): ContextResolver<ObjectMapper> {
val defaultObjectMapper = createDefaultMapper() val defaultObjectMapper = createDefaultMapper(services)
override fun getContext(type: java.lang.Class<*>): ObjectMapper { override fun getContext(type: java.lang.Class<*>): ObjectMapper {
return defaultObjectMapper return defaultObjectMapper
} }
class ServiceHubObjectMapper(var serviceHub: ServiceHub): ObjectMapper() {
}
companion object { companion object {
private fun createDefaultMapper(): ObjectMapper { private fun createDefaultMapper(services: ServiceHub): ObjectMapper {
val mapper = ObjectMapper() val mapper = ServiceHubObjectMapper(services)
mapper.enable(SerializationFeature.INDENT_OUTPUT); mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
mapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS) 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") val timeModule = SimpleModule("java.time")
timeModule.addSerializer(LocalDate::class.java, ToStringSerializer) timeModule.addSerializer(LocalDate::class.java, ToStringSerializer)
timeModule.addDeserializer(LocalDate::class.java, LocalDateDeserializer) 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(timeModule)
mapper.registerModule(cordaModule)
mapper.registerModule(KotlinModule()) mapper.registerModule(KotlinModule())
return mapper return mapper
} }
@ -45,7 +75,69 @@ class Config: ContextResolver<ObjectMapper> {
object LocalDateDeserializer: JsonDeserializer<LocalDate>() { object LocalDateDeserializer: JsonDeserializer<LocalDate>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): LocalDate { 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<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
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<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()
}
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<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,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
}
}

View File

@ -9,6 +9,7 @@
package core.messaging package core.messaging
import core.Party import core.Party
import core.crypto.DummyPublicKey
import java.util.* import java.util.*
/** Info about a network node that has is operated by some sort of verified identity. */ /** 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 { interface NetworkMapService {
val timestampingNodes: List<LegallyIdentifiableNode> val timestampingNodes: List<LegallyIdentifiableNode>
val partyNodes: List<LegallyIdentifiableNode>
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. // TODO: Move this to the test tree once a real network map is implemented and this scaffolding is no longer needed.
class MockNetworkMapService : NetworkMapService { class MockNetworkMapService : NetworkMapService {
data class MockAddress(val id: String): SingleMessageRecipient
override val timestampingNodes = Collections.synchronizedList(ArrayList<LegallyIdentifiableNode>()) override val timestampingNodes = Collections.synchronizedList(ArrayList<LegallyIdentifiableNode>())
override val partyNodes = Collections.synchronizedList(ArrayList<LegallyIdentifiableNode>())
init {
partyNodes.add(LegallyIdentifiableNode(MockAddress("excalibur:8080"), Party("Excalibur", DummyPublicKey("Excalibur"))))
partyNodes.add(LegallyIdentifiableNode(MockAddress("another:8080"), Party("ANOther", DummyPublicKey("ANOther"))))
}
} }

View File

@ -16,6 +16,8 @@
package core.node package core.node
import api.APIServer
import api.APIServerImpl
import com.codahale.metrics.MetricRegistry import com.codahale.metrics.MetricRegistry
import contracts.* import contracts.*
import core.* import core.*
@ -30,6 +32,7 @@ import java.nio.file.FileAlreadyExistsException
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.security.KeyPair import java.security.KeyPair
import java.time.Clock
import java.util.* import java.util.*
import java.util.concurrent.Executors 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 * 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. * 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 { companion object {
val PRIVATE_KEY_FILE_NAME = "identity-private-key" val PRIVATE_KEY_FILE_NAME = "identity-private-key"
val PUBLIC_IDENTITY_FILE_NAME = "identity-public" 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 keyManagementService: KeyManagementService get() = keyManagement
override val identityService: IdentityService get() = identity override val identityService: IdentityService get() = identity
override val monitoringService: MonitoringService = MonitoringService(MetricRegistry()) override val monitoringService: MonitoringService = MonitoringService(MetricRegistry())
override val clock: Clock get() = platformClock
} }
val legallyIdentifableAddress: LegallyIdentifiableNode get() = LegallyIdentifiableNode(net.myAddress, storage.myLegalIdentity) 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 var inNodeTimestampingService: NodeTimestamperService? = null
lateinit var identity: IdentityService lateinit var identity: IdentityService
lateinit var net: MessagingService lateinit var net: MessagingService
lateinit var api: APIServer
open fun start(): AbstractNode { open fun start(): AbstractNode {
log.info("Node starting up ...") log.info("Node starting up ...")
@ -99,6 +104,7 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
wallet = NodeWalletService(services) wallet = NodeWalletService(services)
keyManagement = E2ETestKeyManagementService() keyManagement = E2ETestKeyManagementService()
makeInterestRateOracleService() makeInterestRateOracleService()
api = APIServerImpl(this)
// Insert a network map entry for the timestamper: this is all temp scaffolding and will go away. If we are // 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. // 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 inNodeTimestampingService = null
timestamperAddress timestamperAddress
} else { } else {
inNodeTimestampingService = NodeTimestamperService(net, storage.myLegalIdentity, storage.myLegalIdentityKey) inNodeTimestampingService = NodeTimestamperService(net, storage.myLegalIdentity, storage.myLegalIdentityKey, platformClock)
LegallyIdentifiableNode(net.myAddress, storage.myLegalIdentity) LegallyIdentifiableNode(net.myAddress, storage.myLegalIdentity)
} }
(services.networkMapService as MockNetworkMapService).timestampingNodes.add(tsid) (services.networkMapService as MockNetworkMapService).timestampingNodes.add(tsid)

View File

@ -22,6 +22,7 @@ import org.eclipse.jetty.server.handler.HandlerCollection
import org.eclipse.jetty.servlet.ServletContextHandler import org.eclipse.jetty.servlet.ServletContextHandler
import org.eclipse.jetty.servlet.ServletHolder import org.eclipse.jetty.servlet.ServletHolder
import org.eclipse.jetty.webapp.WebAppContext import org.eclipse.jetty.webapp.WebAppContext
import org.glassfish.jersey.server.ResourceConfig
import org.glassfish.jersey.server.ServerProperties import org.glassfish.jersey.server.ServerProperties
import org.glassfish.jersey.servlet.ServletContainer import org.glassfish.jersey.servlet.ServletContainer
import java.io.RandomAccessFile import java.io.RandomAccessFile
@ -30,8 +31,8 @@ import java.nio.channels.FileLock
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.StandardOpenOption import java.nio.file.StandardOpenOption
import java.time.Clock
import javax.management.ObjectName import javax.management.ObjectName
import kotlin.reflect.KClass
class ConfigurationException(message: String) : Exception(message) class ConfigurationException(message: String) : Exception(message)
@ -46,9 +47,11 @@ class ConfigurationException(message: String) : Exception(message)
* have to specify that yourself. * have to specify that yourself.
* @param configuration This is typically loaded from a .properties file * @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 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, 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 { companion object {
/** The port that is used by default if none is specified. As you know, 31337 is the most elite number. */ /** The port that is used by default if none is specified. As you know, 31337 is the most elite number. */
val DEFAULT_PORT = 31337 val DEFAULT_PORT = 31337
@ -88,14 +91,18 @@ class Node(dir: Path, val p2pAddr: HostAndPort, configuration: NodeConfiguration
addServlet(DataUploadServlet::class.java, "/upload/*") addServlet(DataUploadServlet::class.java, "/upload/*")
addServlet(AttachmentDownloadServlet::class.java, "/attachments/*") addServlet(AttachmentDownloadServlet::class.java, "/attachments/*")
setAttribute("services", services) val resourceConfig = ResourceConfig()
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
// Add your API provider classes (annotated for JAX-RS) here // 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 server.handler = handlerCollection
@ -103,11 +110,6 @@ class Node(dir: Path, val p2pAddr: HostAndPort, configuration: NodeConfiguration
return server return server
} }
private fun setProviders(jerseyServlet: ServletHolder, vararg providerClasses: KClass<out Any>) {
val providerClassNames = providerClasses.map { it.java.canonicalName }.joinToString()
jerseyServlet.setInitParameter(ServerProperties.PROVIDER_CLASSNAMES, providerClassNames)
}
override fun start(): Node { override fun start(): Node {
alreadyRunningNodeCheck() alreadyRunningNodeCheck()
super.start() super.start()
@ -161,4 +163,4 @@ class Node(dir: Path, val p2pAddr: HostAndPort, configuration: NodeConfiguration
if (nodeFileLock == null) if (nodeFileLock == null)
nodeFileLock = RandomAccessFile(file, "rw").channel.lock() nodeFileLock = RandomAccessFile(file, "rw").channel.lock()
} }
} }

View File

@ -11,6 +11,7 @@ package core.node.services
import com.codahale.metrics.Gauge import com.codahale.metrics.Gauge
import contracts.Cash import contracts.Cash
import core.* import core.*
import core.crypto.SecureHash
import core.utilities.loggerFor import core.utilities.loggerFor
import core.utilities.trace import core.utilities.trace
import java.security.PublicKey import java.security.PublicKey
@ -42,6 +43,14 @@ class NodeWalletService(private val services: ServiceHub) : WalletService {
*/ */
override val cashBalances: Map<Currency, Amount> get() = mutex.locked { wallet }.cashBalances 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.filter { it.state is LinearState }.associateBy { (it.state as LinearState).thread }.mapValues { it.value as StateAndRef<LinearState> }
}
override fun notifyAll(txns: Iterable<WireTransaction>): Wallet { override fun notifyAll(txns: Iterable<WireTransaction>): Wallet {
val ourKeys = services.keyManagementService.keys.keys val ourKeys = services.keyManagementService.keys.keys
@ -68,11 +77,21 @@ class NodeWalletService(private val services: ServiceHub) : WalletService {
} }
} }
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 { private fun Wallet.update(tx: WireTransaction, ourKeys: Set<PublicKey>): Wallet {
val ourNewStates = tx.outputs. val ourNewStates = tx.outputs.
filterIsInstance<OwnableState>(). filter { isRelevant(it, ourKeys) }.
filter { it.owner in ourKeys }. map { tx.outRef<ContractState>(it) }
map { tx.outRef<OwnableState>(it) }
// Now calculate the states that are being spent by this transaction. // Now calculate the states that are being spent by this transaction.
val consumed: Set<StateRef> = states.map { it.ref }.intersect(tx.inputs) val consumed: Set<StateRef> = states.map { it.ref }.intersect(tx.inputs)

View File

@ -18,6 +18,7 @@ import java.io.InputStream
import java.security.KeyPair import java.security.KeyPair
import java.security.PrivateKey import java.security.PrivateKey
import java.security.PublicKey import java.security.PublicKey
import java.time.Clock
import java.util.* 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 * 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. * about new transactions from our peers and generate new transactions that consume states ourselves.
*/ */
data class Wallet(val states: List<StateAndRef<OwnableState>>) { data class Wallet(val states: List<StateAndRef<ContractState>>) {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
inline fun <reified T : OwnableState> statesOfType() = states.filter { it.state is T } as List<StateAndRef<T>> inline fun <reified T : OwnableState> statesOfType() = states.filter { it.state is T } as List<StateAndRef<T>>
@ -67,6 +68,20 @@ interface WalletService {
*/ */
val cashBalances: Map<Currency, Amount> val cashBalances: Map<Currency, Amount>
/**
* Returns a snapshot of the heads of LinearStates
*/
val linearHeads: Map<SecureHash, StateAndRef<LinearState>>
fun <T : LinearState> linearHeadsInstanceOf(clazz: Class<T>, predicate: (T) -> Boolean = { true } ): Map<SecureHash, StateAndRef<LinearState>> {
return linearHeads.filterValues { clazz.isInstance(it.state) }.filterValues { predicate(it.state as T) }
}
fun statesForRefs(refs: List<StateRef>): Map<StateRef, ContractState?> {
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 * 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! * 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 networkService: MessagingService
val networkMapService: NetworkMapService val networkMapService: NetworkMapService
val monitoringService: MonitoringService val monitoringService: MonitoringService
val clock: Clock
/** /**
* Given a [LedgerTransaction], looks up all its dependencies in the local database, uses the identity service to map * 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) } val ltxns = dependencies.map { it.verifyToLedgerTransaction(identityService, storageService.attachments) }
TransactionGroup(setOf(ltx), ltxns.toSet()).verify(storageService.contractPrograms) 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<SignedTransaction>) {
storageService.validatedTransactions.putAll(txs.groupBy { it.id }.mapValues { it.value.first() })
walletService.notifyAll(txs.map { it.tx })
}
} }

View File

@ -20,6 +20,7 @@ import core.utilities.loggerFor
import org.slf4j.Logger import org.slf4j.Logger
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.time.Clock
import java.util.* import java.util.*
import java.util.concurrent.ExecutorService import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors 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, 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<MockNode>() override val log: Logger = loggerFor<MockNode>()
override val serverThread: ExecutorService = override val serverThread: ExecutorService =
if (mockNet.threadPerNode) if (mockNet.threadPerNode)

View File

@ -75,7 +75,7 @@ class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
// It may seem tempting to write transactions to the database as we receive them, instead of all at once // 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 // 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. // dependencies aren't, or an unvalidated and possibly broken tx is there.
serviceHub.storageService.validatedTransactions.putAll(downloadedSignedTxns.associateBy { it.id }) serviceHub.recordTransactions(downloadedSignedTxns)
} }
@Suspendable @Suspendable

View File

@ -14,14 +14,11 @@ import core.messaging.MessagingService
import core.messaging.MockNetworkMapService import core.messaging.MockNetworkMapService
import core.messaging.NetworkMapService import core.messaging.NetworkMapService
import core.node.services.* import core.node.services.*
import core.node.AbstractNode
import core.node.services.StorageServiceImpl
import core.serialization.SerializedBytes import core.serialization.SerializedBytes
import core.serialization.deserialize import core.serialization.deserialize
import core.testutils.TEST_KEYS_TO_CORP_MAP import core.testutils.TEST_KEYS_TO_CORP_MAP
import core.testutils.TEST_PROGRAM_MAP import core.testutils.TEST_PROGRAM_MAP
import core.testutils.TEST_TX_TIME import core.testutils.TEST_TX_TIME
import org.slf4j.LoggerFactory
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
@ -76,6 +73,8 @@ class MockKeyManagementService(vararg initialKeys: KeyPair) : KeyManagementServi
} }
class MockWalletService(val states: List<StateAndRef<OwnableState>>) : WalletService { class MockWalletService(val states: List<StateAndRef<OwnableState>>) : WalletService {
override val linearHeads: Map<SecureHash, StateAndRef<LinearState>>
get() = TODO("Use NodeWalletService instead")
override val cashBalances: Map<Currency, Amount> override val cashBalances: Map<Currency, Amount>
get() = TODO("Use NodeWalletService instead") get() = TODO("Use NodeWalletService instead")
@ -132,7 +131,8 @@ class MockServices(
val net: MessagingService? = null, val net: MessagingService? = null,
val identity: IdentityService? = MockIdentityService, val identity: IdentityService? = MockIdentityService,
val storage: StorageService? = MockStorageService(), val storage: StorageService? = MockStorageService(),
val networkMap: NetworkMapService? = MockNetworkMapService() val networkMap: NetworkMapService? = MockNetworkMapService(),
val overrideClock: Clock? = Clock.systemUTC()
) : ServiceHub { ) : ServiceHub {
override val walletService: WalletService override val walletService: WalletService
get() = wallet ?: throw UnsupportedOperationException() get() = wallet ?: throw UnsupportedOperationException()
@ -146,6 +146,8 @@ class MockServices(
get() = networkMap ?: throw UnsupportedOperationException() get() = networkMap ?: throw UnsupportedOperationException()
override val storageService: StorageService override val storageService: StorageService
get() = storage ?: throw UnsupportedOperationException() get() = storage ?: throw UnsupportedOperationException()
override val clock: Clock
get() = overrideClock ?: throw UnsupportedOperationException()
override val monitoringService: MonitoringService = MonitoringService(MetricRegistry()) override val monitoringService: MonitoringService = MonitoringService(MetricRegistry())