Merged in rnicoll-nodeinfo-services (pull request #54)

Infrastructure work for network map service
This commit is contained in:
Ross Nicoll 2016-04-11 16:10:09 +01:00
commit ff5078fbbc
28 changed files with 209 additions and 109 deletions

View File

@ -9,6 +9,7 @@ import java.security.PublicKey
* service would provide.
*/
interface IdentityService {
object Type : ServiceType("corda.identity")
fun partyFromKey(key: PublicKey): Party?
fun partyFromName(name: String): Party?
}

View File

@ -0,0 +1,33 @@
/*
* Copyright 2016 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
* set forth therein.
*
* All other rights reserved.
*/
package core.node.services
/**
* Identifier for service types a node can expose.
*/
abstract class ServiceType(val id: String) {
init {
// Enforce:
//
// * IDs must start with a lower case letter
// * IDs can only contain alphanumeric, full stop and underscore ASCII characters
require(id.matches(Regex("[a-z][a-zA-Z0-9._]+")))
}
override operator fun equals(other: Any?): Boolean =
if (other is ServiceType) {
id == other.id
} else {
false
}
override fun hashCode(): Int = id.hashCode()
override fun toString(): String = id.toString()
}

View File

@ -15,6 +15,7 @@ import core.serialization.SerializedBytes
* themselves.
*/
interface TimestamperService {
object Type : ServiceType("corda.timestamper")
@Suspendable
fun timestamp(wtxBytes: SerializedBytes<WireTransaction>): DigitalSignature.LegallyIdentifiable

View File

@ -15,6 +15,7 @@ import core.messaging.StateMachineManager
import core.node.services.*
import core.serialization.deserialize
import core.serialization.serialize
import core.testing.MockNetworkMapCache
import org.slf4j.Logger
import java.nio.file.FileAlreadyExistsException
import java.nio.file.Files
@ -58,7 +59,7 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
}
val info: NodeInfo by lazy {
NodeInfo(net.myAddress, storage.myLegalIdentity, findMyLocation())
NodeInfo(net.myAddress, storage.myLegalIdentity, emptySet(), findMyLocation())
}
protected open fun findMyLocation(): PhysicalLocation? = CityDatabase[configuration.nearestCity]
@ -90,6 +91,8 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
lateinit var api: APIServer
open fun start(): AbstractNode {
require(timestamperAddress == null || timestamperAddress.advertisedServices.contains(TimestamperService.Type))
{"Timestamper address must indicate a node that provides timestamping services"}
log.info("Node starting up ...")
storage = initialiseStorageService(dir)
@ -117,7 +120,7 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
timestamperAddress
} else {
inNodeTimestampingService = NodeTimestamperService(net, storage.myLegalIdentity, storage.myLegalIdentityKey, platformClock)
NodeInfo(net.myAddress, storage.myLegalIdentity)
NodeInfo(net.myAddress, storage.myLegalIdentity, setOf(TimestamperService.Type))
}
(services.networkMapCache as MockNetworkMapCache).timestampingNodes.add(tsid)
}

View File

@ -7,7 +7,6 @@ import com.codahale.metrics.JmxReporter
import com.google.common.net.HostAndPort
import core.messaging.MessagingService
import core.node.services.ArtemisMessagingService
import core.node.services.NodeInfo
import core.node.servlets.AttachmentDownloadServlet
import core.node.servlets.DataUploadServlet
import core.utilities.loggerFor

View File

@ -0,0 +1,19 @@
/*
* Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
* set forth therein.
*
* All other rights reserved.
*/
package core.node
import core.Party
import core.messaging.SingleMessageRecipient
import core.node.services.ServiceType
/**
* Info about a network node that acts on behalf of some form of contract party.
*/
data class NodeInfo(val address: SingleMessageRecipient, val identity: Party,
var advertisedServices: Set<ServiceType> = emptySet(),
val physicalLocation: PhysicalLocation? = null)

View File

@ -0,0 +1,68 @@
/*
* Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
* set forth therein.
*
* All other rights reserved.
*/
package core.node
import java.util.*
/** A latitude/longitude pair. */
data class WorldCoordinate(val latitude: Double, val longitude: Double) {
init {
require(latitude in -90..90)
require(longitude in -180..180)
}
/**
* Convert to screen coordinates using the Mercator projection. You should have a world map image that
* you know the precise extents of for this function to work.
*
* Note that no world map ever has latitude extents of -90 to 90 because at these extremes the mapping tends
* to infinity. Google Maps, for example, uses a square map image, and square maps yield latitude extents
* of 85.0511 to -85.0511 = arctan(sinh(π)).
*/
fun project(screenWidth: Double, screenHeight: Double, topLatitude: Double, bottomLatitude: Double,
leftLongitude: Double, rightLongitude: Double): Pair<Double, Double> {
require(latitude in bottomLatitude..topLatitude)
require(longitude in leftLongitude..rightLongitude)
fun deg2rad(deg: Double) = deg * Math.PI / 180.0
val leftLngRad = deg2rad(leftLongitude)
val rightLngRad = deg2rad(rightLongitude)
fun longitudeToScreenX(lng: Double) = screenWidth * (deg2rad(lng) - leftLngRad) / (rightLngRad - leftLngRad)
fun screenYRelative(latDeg: Double) = Math.log(Math.tan(latDeg / 360.0 * Math.PI + Math.PI / 4))
val topLatRel = screenYRelative(topLatitude)
val bottomLatRel = screenYRelative(bottomLatitude)
fun latitudeToScreenY(lat: Double) = screenHeight * (screenYRelative(lat) - topLatRel) / (bottomLatRel - topLatRel)
return Pair(longitudeToScreenX(longitude), latitudeToScreenY(latitude))
}
}
/**
* A labelled [WorldCoordinate], where the label is human meaningful. For example, the name of the nearest city.
* Labels should not refer to non-landmarks, for example, they should not contain the names of organisations.
*/
data class PhysicalLocation(val coordinate: WorldCoordinate, val description: String)
/**
* A simple lookup table of city names to their coordinates. Lookups are case insensitive.
*/
object CityDatabase {
private val cityMap = HashMap<String, PhysicalLocation>()
init {
javaClass.getResourceAsStream("cities.txt").bufferedReader().useLines { lines ->
for (line in lines) {
if (line.startsWith("#")) continue
val (name, lng, lat) = line.split('\t')
cityMap[name.toLowerCase()] = PhysicalLocation(WorldCoordinate(lat.toDouble(), lng.toDouble()), name)
}
}
}
operator fun get(name: String) = cityMap[name.toLowerCase()]
}

View File

@ -3,14 +3,10 @@ package core.node.services
import core.Party
import core.crypto.DummyPublicKey
import core.messaging.SingleMessageRecipient
import core.node.NodeInfo
import core.node.PhysicalLocation
import java.util.*
/**
* Info about a network node that acts on behalf of some sort of verified identity.
*/
data class NodeInfo(val address: SingleMessageRecipient, val identity: Party,
val physicalLocation: PhysicalLocation? = null)
/**
* A network map contains lists of nodes on the network along with information about their identity keys, services
* they provide and host names or IP addresses where they can be connected to. A reasonable architecture for the
@ -21,6 +17,7 @@ data class NodeInfo(val address: SingleMessageRecipient, val identity: Party,
* This interface assumes fast, synchronous access to an in-memory map.
*/
interface NetworkMapCache {
object Type : ServiceType("corda.network_map")
val timestampingNodes: List<NodeInfo>
val ratesOracleNodes: List<NodeInfo>
val partyNodes: List<NodeInfo>
@ -29,74 +26,3 @@ interface NetworkMapCache {
fun nodeForPartyName(name: String): NodeInfo? = 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 MockNetworkMapCache : NetworkMapCache {
data class MockAddress(val id: String) : SingleMessageRecipient
override val timestampingNodes = Collections.synchronizedList(ArrayList<NodeInfo>())
override val ratesOracleNodes = Collections.synchronizedList(ArrayList<NodeInfo>())
override val partyNodes = Collections.synchronizedList(ArrayList<NodeInfo>())
override val regulators = Collections.synchronizedList(ArrayList<NodeInfo>())
init {
partyNodes.add(NodeInfo(MockAddress("bankC:8080"), Party("Bank C", DummyPublicKey("Bank C"))))
partyNodes.add(NodeInfo(MockAddress("bankD:8080"), Party("Bank D", DummyPublicKey("Bank D"))))
}
}
/** A latitude/longitude pair. */
data class WorldCoordinate(val latitude: Double, val longitude: Double) {
init {
require(latitude in -90..90)
require(longitude in -180..180)
}
/**
* Convert to screen coordinates using the Mercator projection. You should have a world map image that
* you know the precise extents of for this function to work.
*
* Note that no world map ever has latitude extents of -90 to 90 because at these extremes the mapping tends
* to infinity. Google Maps, for example, uses a square map image, and square maps yield latitude extents
* of 85.0511 to -85.0511 = arctan(sinh(π)).
*/
fun project(screenWidth: Double, screenHeight: Double, topLatitude: Double, bottomLatitude: Double,
leftLongitude: Double, rightLongitude: Double): Pair<Double, Double> {
require(latitude in bottomLatitude..topLatitude)
require(longitude in leftLongitude..rightLongitude)
fun deg2rad(deg: Double) = deg * Math.PI / 180.0
val leftLngRad = deg2rad(leftLongitude)
val rightLngRad = deg2rad(rightLongitude)
fun longitudeToScreenX(lng: Double) = screenWidth * (deg2rad(lng) - leftLngRad) / (rightLngRad - leftLngRad)
fun screenYRelative(latDeg: Double) = Math.log(Math.tan(latDeg / 360.0 * Math.PI + Math.PI / 4))
val topLatRel = screenYRelative(topLatitude)
val bottomLatRel = screenYRelative(bottomLatitude)
fun latitudeToScreenY(lat: Double) = screenHeight * (screenYRelative(lat) - topLatRel) / (bottomLatRel - topLatRel)
return Pair(longitudeToScreenX(longitude), latitudeToScreenY(latitude))
}
}
/**
* A labelled [WorldCoordinate], where the label is human meaningful. For example, the name of the nearest city.
* Labels should not refer to non-landmarks, for example, they should not contain the names of organisations.
*/
data class PhysicalLocation(val coordinate: WorldCoordinate, val description: String)
/**
* A simple lookup table of city names to their coordinates. Lookups are case insensitive.
*/
object CityDatabase {
private val cityMap = HashMap<String, PhysicalLocation>()
init {
javaClass.getResourceAsStream("cities.txt").bufferedReader().useLines { lines ->
for (line in lines) {
if (line.startsWith("#")) continue
val (name, lng, lat) = line.split('\t')
cityMap[name.toLowerCase()] = PhysicalLocation(WorldCoordinate(lat.toDouble(), lng.toDouble()), name)
}
}
}
operator fun get(name: String) = cityMap[name.toLowerCase()]
}

View File

@ -25,6 +25,7 @@ import javax.annotation.concurrent.ThreadSafe
* for signing.
*/
object NodeInterestRates {
object Type : ServiceType("corda.interest_rates")
/** Parses a string of the form "LIBOR 16-March-2016 1M = 0.678" into a [FixOf] and [Fix] */
fun parseOneRate(s: String): Pair<FixOf, Fix> {
val (key, value) = s.split('=').map { it.trim() }

View File

@ -47,6 +47,7 @@ data class Wallet(val states: List<StateAndRef<ContractState>>) {
* consumed by someone else first!
*/
interface WalletService {
object Type : ServiceType("corda.wallet")
/**
* Returns a read-only snapshot of the wallet at the time the call is made. Note that if you consume states or
* keys in this wallet, you must inform the wallet service so it can update its internal state.
@ -101,6 +102,7 @@ inline fun <reified T : LinearState> WalletService.linearHeadsOfType() = linearH
* interface if/when one is developed.
*/
interface KeyManagementService {
object Type : ServiceType("corda.key_management")
/** Returns a snapshot of the current pubkey->privkey mapping. */
val keys: Map<PublicKey, PrivateKey>
@ -116,6 +118,7 @@ interface KeyManagementService {
* anything like that, this interface is only big enough to support the prototyping work.
*/
interface StorageService {
object Type : ServiceType("corda.storage")
/**
* A map of hash->tx where tx has been signature/contract validated and the states are known to be correct.
* The signatures aren't technically needed after that point, but we keep them around so that we can relay
@ -172,7 +175,9 @@ interface AttachmentStorage {
* Provides access to various metrics and ways to notify monitoring services of things, for sysadmin purposes.
* This is not an interface because it is too lightweight to bother mocking out.
*/
class MonitoringService(val metrics: MetricRegistry)
class MonitoringService(val metrics: MetricRegistry) {
object Type : ServiceType("corda.monitoring")
}
/**
* A service hub simply vends references to the other services a node has. Some of those services may be missing or

View File

@ -6,9 +6,11 @@ import com.google.common.util.concurrent.MoreExecutors
import core.ThreadBox
import core.crypto.sha256
import core.messaging.*
import core.node.NodeInfo
import core.node.services.DummyTimestampingAuthority
import core.node.services.NodeInfo
import core.node.services.NodeTimestamperService
import core.node.services.ServiceType
import core.node.services.TimestamperService
import core.utilities.loggerFor
import rx.Observable
import rx.subjects.PublishSubject
@ -150,7 +152,7 @@ class InMemoryMessagingNetwork {
val (handle, builder) = createNode(manuallyPumped)
val node = builder.start().get()
NodeTimestamperService(node, DummyTimestampingAuthority.identity, DummyTimestampingAuthority.key)
timestampingAdvert = NodeInfo(handle, DummyTimestampingAuthority.identity)
timestampingAdvert = NodeInfo(handle, DummyTimestampingAuthority.identity, setOf(TimestamperService.Type))
return Pair(timestampingAdvert!!, node)
}

View File

@ -0,0 +1,29 @@
/*
* Copyright 2016 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
* set forth therein.
*
* All other rights reserved.
*/
package core.testing
import core.Party
import core.crypto.DummyPublicKey
import core.messaging.SingleMessageRecipient
import core.node.services.NetworkMapCache
import core.node.NodeInfo
import java.util.*
class MockNetworkMapCache : NetworkMapCache {
data class MockAddress(val id: String) : SingleMessageRecipient
override val timestampingNodes = Collections.synchronizedList(ArrayList<NodeInfo>())
override val ratesOracleNodes = Collections.synchronizedList(ArrayList<NodeInfo>())
override val partyNodes = Collections.synchronizedList(ArrayList<NodeInfo>())
override val regulators = Collections.synchronizedList(ArrayList<NodeInfo>())
init {
partyNodes.add(NodeInfo(MockAddress("bankC:8080"), Party("Bank C", DummyPublicKey("Bank C"))))
partyNodes.add(NodeInfo(MockAddress("bankD:8080"), Party("Bank D", DummyPublicKey("Bank D"))))
}
}

View File

@ -7,9 +7,11 @@ import core.messaging.MessagingService
import core.messaging.SingleMessageRecipient
import core.node.AbstractNode
import core.node.NodeConfiguration
import core.node.NodeInfo
import core.node.PhysicalLocation
import core.node.services.FixedIdentityService
import core.node.services.NodeInfo
import core.node.services.PhysicalLocation
import core.node.services.ServiceType
import core.node.services.TimestamperService
import core.utilities.loggerFor
import org.slf4j.Logger
import java.nio.file.Files
@ -90,7 +92,8 @@ class MockNetwork(private val threadPerNode: Boolean = false,
}
/** Returns a started node, optionally created by the passed factory method */
fun createNode(withTimestamper: NodeInfo?, forcedID: Int = -1, nodeFactory: Factory = defaultFactory): MockNode {
fun createNode(withTimestamper: NodeInfo?, forcedID: Int = -1, nodeFactory: Factory = defaultFactory,
advertisedServices: Set<ServiceType> = emptySet()): MockNode {
val newNode = forcedID == -1
val id = if (newNode) counter++ else forcedID
@ -103,6 +106,7 @@ class MockNetwork(private val threadPerNode: Boolean = false,
override val nearestCity: String = "Atlantis"
}
val node = nodeFactory.create(path, config, this, withTimestamper).start()
node.info.advertisedServices = advertisedServices
_nodes.add(node)
return node
}
@ -127,7 +131,10 @@ class MockNetwork(private val threadPerNode: Boolean = false,
*/
fun createTwoNodes(nodeFactory: Factory = defaultFactory): Pair<MockNode, MockNode> {
require(nodes.isEmpty())
return Pair(createNode(null, -1, nodeFactory), createNode(nodes[0].info, -1, nodeFactory))
return Pair(
createNode(null, -1, nodeFactory, setOf(TimestamperService.Type)),
createNode(nodes[0].info, -1, nodeFactory)
)
}
fun addressToNode(address: SingleMessageRecipient): MockNode = nodes.single { it.net.myAddress == address }

View File

@ -1,11 +1,10 @@
package core.testing
import com.google.common.util.concurrent.ListenableFuture
import core.node.CityDatabase
import core.node.NodeConfiguration
import core.node.services.CityDatabase
import core.node.services.MockNetworkMapCache
import core.node.services.NodeInfo
import core.node.services.PhysicalLocation
import core.node.NodeInfo
import core.node.PhysicalLocation
import core.protocols.ProtocolLogic
import core.then
import core.utilities.ProgressTracker

View File

@ -7,9 +7,12 @@ import core.logElapsedTime
import core.node.Node
import core.node.NodeConfiguration
import core.node.NodeConfigurationFromConfig
import core.node.NodeInfo
import core.node.services.ArtemisMessagingService
import core.node.services.MockNetworkMapCache
import core.node.services.NodeInfo
import core.node.services.NodeInterestRates
import core.node.services.ServiceType
import core.node.services.TimestamperService
import core.testing.MockNetworkMapCache
import core.serialization.deserialize
import core.utilities.BriefLogFormatter
import demos.protocols.AutoOfferProtocol
@ -71,7 +74,7 @@ fun main(args: Array<String>) {
null
} else {
try {
nodeInfo(options.valueOf(timestamperNetAddr), options.valueOf(timestamperIdentityFile))
nodeInfo(options.valueOf(timestamperNetAddr), options.valueOf(timestamperIdentityFile), setOf(TimestamperService.Type))
} catch (e: Exception) {
null
}
@ -82,7 +85,7 @@ fun main(args: Array<String>) {
null
} else {
try {
nodeInfo(options.valueOf(rateOracleNetAddr), options.valueOf(rateOracleIdentityFile))
nodeInfo(options.valueOf(rateOracleNetAddr), options.valueOf(rateOracleIdentityFile), setOf(NodeInterestRates.Type))
} catch (e: Exception) {
null
}
@ -122,12 +125,12 @@ fun main(args: Array<String>) {
exitProcess(0)
}
fun nodeInfo(hostAndPortString: String, identityFile: String): NodeInfo {
fun nodeInfo(hostAndPortString: String, identityFile: String, advertisedServices: Set<ServiceType> = emptySet()): NodeInfo {
try {
val addr = HostAndPort.fromString(hostAndPortString).withDefaultPort(Node.DEFAULT_PORT)
val path = Paths.get(identityFile)
val party = Files.readAllBytes(path).deserialize<Party>(includeClassName = true)
return NodeInfo(ArtemisMessagingService.makeRecipient(addr), party)
return NodeInfo(ArtemisMessagingService.makeRecipient(addr), party, advertisedServices)
} catch (e: Exception) {
println("Could not find identify file $identityFile. If the file has just been created as part of starting the demo, please restart this node")
throw e

View File

@ -5,7 +5,7 @@ import core.*
import core.node.Node
import core.node.NodeConfiguration
import core.node.services.ArtemisMessagingService
import core.node.services.NodeInfo
import core.node.NodeInfo
import core.node.services.NodeInterestRates
import core.serialization.deserialize
import core.utilities.ANSIProgressRenderer

View File

@ -12,8 +12,8 @@ import core.node.Node
import core.node.NodeConfiguration
import core.node.NodeConfigurationFromConfig
import core.node.services.ArtemisMessagingService
import core.node.NodeInfo
import core.node.services.NodeAttachmentService
import core.node.services.NodeInfo
import core.node.services.NodeWalletService
import core.protocols.ProtocolLogic
import core.serialization.deserialize

View File

@ -3,10 +3,10 @@ package demos.protocols
import co.paralleluniverse.fibers.Suspendable
import co.paralleluniverse.strands.Strand
import core.node.Node
import core.node.services.MockNetworkMapCache
import core.node.services.NodeInfo
import core.node.NodeInfo
import core.protocols.ProtocolLogic
import core.serialization.deserialize
import core.testing.MockNetworkMapCache
import java.util.concurrent.TimeUnit

View File

@ -6,12 +6,12 @@ import contracts.DealState
import contracts.InterestRateSwap
import core.StateAndRef
import core.node.Node
import core.node.services.MockNetworkMapCache
import core.node.services.NodeInfo
import core.node.NodeInfo
import core.node.services.linearHeadsOfType
import core.protocols.ProtocolLogic
import core.random63BitValue
import core.serialization.deserialize
import core.testing.MockNetworkMapCache
import core.utilities.ANSIProgressRenderer
import core.utilities.ProgressTracker
import demos.DemoClock

View File

@ -4,7 +4,7 @@ import co.paralleluniverse.fibers.Suspendable
import core.*
import core.crypto.DigitalSignature
import core.messaging.SingleMessageRecipient
import core.node.services.NodeInfo
import core.node.NodeInfo
import core.protocols.ProtocolLogic
import core.utilities.ProgressTracker
import java.math.BigDecimal

View File

@ -6,7 +6,7 @@ import core.WireTransaction
import core.crypto.DigitalSignature
import core.messaging.MessageRecipients
import core.messaging.StateMachineManager
import core.node.services.NodeInfo
import core.node.NodeInfo
import core.node.services.NodeTimestamperService
import core.node.services.TimestamperService
import core.protocols.ProtocolLogic

View File

@ -7,7 +7,7 @@ import core.*
import core.crypto.DigitalSignature
import core.crypto.signWithECDSA
import core.messaging.SingleMessageRecipient
import core.node.services.NodeInfo
import core.node.NodeInfo
import core.protocols.ProtocolLogic
import core.utilities.ProgressTracker
import core.utilities.UntrustworthyData

View File

@ -9,7 +9,7 @@ import core.crypto.DigitalSignature
import core.crypto.signWithECDSA
import core.messaging.SingleMessageRecipient
import core.messaging.StateMachineManager
import core.node.services.NodeInfo
import core.node.NodeInfo
import core.protocols.ProtocolLogic
import core.utilities.ProgressTracker
import core.utilities.trace

View File

@ -6,6 +6,7 @@ import core.messaging.MessagingService
import core.node.services.*
import core.serialization.SerializedBytes
import core.serialization.deserialize
import core.testing.MockNetworkMapCache
import core.testutils.MockIdentityService
import core.testutils.TEST_PROGRAM_MAP
import core.testutils.TEST_TX_TIME

View File

@ -4,8 +4,9 @@ import core.Attachment
import core.crypto.SecureHash
import core.crypto.sha256
import core.node.NodeConfiguration
import core.node.NodeInfo
import core.node.services.NodeAttachmentService
import core.node.services.NodeInfo
import core.node.services.TimestamperService
import core.serialization.OpaqueBytes
import core.testing.MockNetwork
import core.testutils.rootCauseExceptions
@ -95,7 +96,7 @@ class AttachmentTests {
}
}
}
})
}, advertisedServices = setOf(TimestamperService.Type))
val n1 = network.createNode(n0.info)
// Insert an attachment into node zero's store directly.

View File

@ -5,6 +5,7 @@ import contracts.CommercialPaper
import core.*
import core.crypto.SecureHash
import core.node.NodeConfiguration
import core.node.NodeInfo
import core.node.services.*
import core.testing.InMemoryMessagingNetwork
import core.testing.MockNetwork

View File

@ -9,6 +9,7 @@ import core.node.services.*
import core.protocols.ProtocolLogic
import core.serialization.serialize
import core.testing.InMemoryMessagingNetwork
import core.testing.MockNetworkMapCache
import core.testutils.ALICE
import core.testutils.ALICE_KEY
import core.testutils.CASH