Merge branch 'master' into dynamic-loading

This commit is contained in:
sofusmortensen 2016-03-24 13:10:36 +00:00
commit 58a509de63
19 changed files with 531 additions and 73 deletions

View File

@ -112,7 +112,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'

View File

@ -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<LocalDate>) {
open class BusinessCalendar private constructor(val calendars: Array<out String>, val holidayDates: List<LocalDate>) {
class UnknownCalendar(name: String): Exception("$name not found")
companion object {
@ -239,7 +239,7 @@ open class BusinessCalendar private constructor(val holidayDates: List<LocalDate
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(
fun getInstance(vararg calname: String) = BusinessCalendar(calname,
calname.flatMap { (TEST_CALENDAR_DATA[it] ?: throw UnknownCalendar(it)).split(",") }.
toSet().
map{ parseDateFromString(it) }.

View File

@ -45,6 +45,23 @@ interface OwnableState : ContractState {
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!) */
fun ContractState.hash(): SecureHash = SecureHash.sha256(serialize().bits)

View File

@ -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. */

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
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<ObjectMapper> {
class Config(val services: ServiceHub): ContextResolver<ObjectMapper> {
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<ObjectMapper> {
object LocalDateDeserializer: JsonDeserializer<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
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<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.
class MockNetworkMapService : NetworkMapService {
data class MockAddress(val id: String): SingleMessageRecipient
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
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)
@ -73,6 +77,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 ...")
@ -83,6 +88,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.
@ -90,7 +96,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)

View File

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

View File

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

View File

@ -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<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 {
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 {
val ourNewStates = tx.outputs.
filterIsInstance<OwnableState>().
filter { it.owner in ourKeys }.
map { tx.outRef<OwnableState>(it) }
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)

View File

@ -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<StateAndRef<OwnableState>>) {
data class Wallet(val states: List<StateAndRef<ContractState>>) {
@Suppress("UNCHECKED_CAST")
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>
/**
* 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
* new states that they create. You should only insert transactions that have been successfully verified here!
@ -143,6 +158,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
@ -156,4 +172,14 @@ interface ServiceHub {
val ltxns = dependencies.map { it.verifyToLedgerTransaction(identityService, storageService.attachments) }
TransactionGroup(setOf(ltx), ltxns.toSet()).verify()
}
/**
* 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 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<MockNode>()
override val serverThread: ExecutorService =
if (mockNet.threadPerNode)

View File

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

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

View File

@ -0,0 +1,2 @@
myLegalName = "Vast Global MegaCorp, Ltd"
exportJMXto = "http"

View File

@ -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<StateAndRef<OwnableState>>) : WalletService {
override val linearHeads: Map<SecureHash, StateAndRef<LinearState>>
get() = TODO("Use NodeWalletService instead")
override val cashBalances: Map<Currency, Amount>
get() = TODO("Use NodeWalletService instead")
@ -125,7 +124,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()
@ -139,6 +139,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())