Merge branch 'master' into wn-redo-node-conf-docs

This commit is contained in:
Wawrzyniec 'Wawrzek' Niewodniczanski
2019-01-14 15:31:32 +00:00
committed by GitHub
329 changed files with 5107 additions and 3365 deletions

View File

@ -130,7 +130,6 @@ dependencies {
testCompile project(':client:jfx')
testCompile project(':finance:contracts')
testCompile project(':finance:workflows')
testCompile project(':finance:isolated')
// sample test schemas
testCompile project(path: ':finance:contracts', configuration: 'testArtifacts')
@ -169,6 +168,15 @@ dependencies {
// AgentLoader: dynamic loading of JVM agents
compile group: 'com.ea.agentloader', name: 'ea-agent-loader', version: "${eaagentloader_version}"
// BFT-Smart dependencies
compile 'commons-codec:commons-codec:1.10'
compile 'com.github.bft-smart:library:master-v1.1-beta-g6215ec8-87'
// Java Atomix: RAFT library
compile 'io.atomix.copycat:copycat-client:1.2.3'
compile 'io.atomix.copycat:copycat-server:1.2.3'
compile 'io.atomix.catalyst:catalyst-netty:1.1.2'
// Jetty dependencies for NetworkMapClient test.
// Web stuff: for HTTP[S] servlets
testCompile "org.eclipse.jetty:jetty-servlet:${jetty_version}"
@ -186,6 +194,7 @@ dependencies {
compile "com.palominolabs.metrics:metrics-new-relic:${metrics_new_relic_version}"
testCompile(project(':test-cli'))
testCompile(project(':test-utils'))
}
tasks.withType(JavaCompile) {

View File

@ -55,7 +55,7 @@ class AddressBindingFailureTests {
ServerSocket(0).use { socket ->
val address = InetSocketAddress(socket.localPort).toNetworkHostAndPort()
val address = InetSocketAddress("localhost", socket.localPort).toNetworkHostAndPort()
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), inMemoryDB = false, portAllocation = portAllocation)) {
assertThatThrownBy { startNode(customOverrides = overrides(address)).getOrThrow() }.isInstanceOfSatisfying(AddressBindingException::class.java) { exception ->

View File

@ -28,6 +28,7 @@ import net.corda.testing.node.NotarySpec
import net.corda.testing.node.User
import net.corda.testing.node.internal.cordappWithPackages
import org.assertj.core.api.Assertions.assertThat
import org.junit.Ignore
import org.junit.Test
class CordappConstraintsTests {
@ -37,7 +38,7 @@ class CordappConstraintsTests {
invokeRpc(CordaRPCOps::wellKnownPartyFromX500Name),
invokeRpc(CordaRPCOps::notaryIdentities),
invokeRpc("vaultTrackByCriteria")))
val UNSIGNED_FINANCE_CORDAPP = cordappWithPackages("net.corda.finance")
val UNSIGNED_FINANCE_CORDAPP = cordappWithPackages("net.corda.finance", "migration", "META-INF.services")
val SIGNED_FINANCE_CORDAPP = UNSIGNED_FINANCE_CORDAPP.signed()
}
@ -45,6 +46,7 @@ class CordappConstraintsTests {
fun `issue cash using signature constraints`() {
driver(DriverParameters(
networkParameters = testNetworkParameters(minimumPlatformVersion = 4),
cordappsForAllNodes = emptyList(),
inMemoryDB = false
)) {
val alice = startNode(NodeParameters(
@ -71,6 +73,7 @@ class CordappConstraintsTests {
fun `issue cash using hash and signature constraints`() {
driver(DriverParameters(
networkParameters = testNetworkParameters(minimumPlatformVersion = 4),
cordappsForAllNodes = emptyList(),
inMemoryDB = false
)) {
println("Starting the node using unsigned contract jar ...")

View File

@ -50,11 +50,11 @@ class AsymmetricCorDappsTests {
driver(DriverParameters(startNodesInProcess = false, cordappsForAllNodes = emptySet())) {
val nodeA = startNode(NodeParameters(
providedName = ALICE_NAME,
additionalCordapps = setOf(cordappForClasses(Ping::class.java))
additionalCordapps = setOf(cordappForClasses(Ping::class.java, AsymmetricCorDappsTests::class.java))
)).getOrThrow()
val nodeB = startNode(NodeParameters(
providedName = BOB_NAME,
additionalCordapps = setOf(cordappForClasses(Ping::class.java, Pong::class.java))
additionalCordapps = setOf(cordappForClasses(Ping::class.java, Pong::class.java, AsymmetricCorDappsTests::class.java))
)).getOrThrow()
nodeA.rpc.startFlow(::Ping, nodeB.nodeInfo.singleIdentity(), 1).returnValue.getOrThrow()
}
@ -62,8 +62,8 @@ class AsymmetricCorDappsTests {
@Test
fun `shared cordapps with asymmetric specific classes`() {
val sharedCordapp = cordappForClasses(Ping::class.java)
val cordappForNodeB = cordappForClasses(Pong::class.java)
val sharedCordapp = cordappForClasses(Ping::class.java, AsymmetricCorDappsTests::class.java)
val cordappForNodeB = cordappForClasses(Pong::class.java, AsymmetricCorDappsTests::class.java)
driver(DriverParameters(startNodesInProcess = false, cordappsForAllNodes = setOf(sharedCordapp))) {
val (nodeA, nodeB) = listOf(
startNode(NodeParameters(providedName = ALICE_NAME)),
@ -75,8 +75,8 @@ class AsymmetricCorDappsTests {
@Test
fun `shared cordapps with asymmetric specific classes in process`() {
val sharedCordapp = cordappForClasses(Ping::class.java)
val cordappForNodeB = cordappForClasses(Pong::class.java)
val sharedCordapp = cordappForClasses(Ping::class.java, AsymmetricCorDappsTests::class.java)
val cordappForNodeB = cordappForClasses(Pong::class.java, AsymmetricCorDappsTests::class.java)
driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = setOf(sharedCordapp))) {
val (nodeA, nodeB) = listOf(
startNode(NodeParameters(providedName = ALICE_NAME)),

View File

@ -1,10 +1,7 @@
package net.corda.node.flows
import net.corda.core.internal.*
import net.corda.core.internal.concurrent.transpose
import net.corda.core.internal.div
import net.corda.core.internal.list
import net.corda.core.internal.moveTo
import net.corda.core.internal.readLines
import net.corda.core.messaging.startTrackedFlow
import net.corda.core.utilities.getOrThrow
import net.corda.node.internal.CheckpointIncompatibleException
@ -18,9 +15,7 @@ import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.NodeParameters
import net.corda.testing.driver.driver
import net.corda.testing.node.TestCordapp
import net.corda.testing.node.internal.CustomCordapp
import net.corda.testing.node.internal.ListenProcessDeathException
import net.corda.testing.node.internal.cordappForClasses
import net.corda.testing.node.internal.*
import net.test.cordapp.v1.Record
import net.test.cordapp.v1.SendMessageFlow
import org.assertj.core.api.Assertions.assertThat
@ -36,13 +31,8 @@ import kotlin.test.assertNotNull
class FlowCheckpointVersionNodeStartupCheckTest {
companion object {
val message = Message("Hello world!")
val defaultCordapp = cordappForClasses(
MessageState::class.java,
MessageContract::class.java,
SendMessageFlow::class.java,
MessageSchema::class.java,
MessageSchemaV1::class.java,
Record::class.java
val defaultCordapp = cordappWithPackages(
MessageState::class.packageName, SendMessageFlow::class.packageName
)
}
@ -57,7 +47,7 @@ class FlowCheckpointVersionNodeStartupCheckTest {
val result = if (page.snapshot.states.isNotEmpty()) {
page.snapshot.states.first()
} else {
val r = page.updates.timeout(5, TimeUnit.SECONDS).take(1).toBlocking().single()
val r = page.updates.timeout(10, TimeUnit.SECONDS).take(1).toBlocking().single()
if (r.consumed.isNotEmpty()) r.consumed.first() else r.produced.first()
}
assertNotNull(result)
@ -90,18 +80,24 @@ class FlowCheckpointVersionNodeStartupCheckTest {
@Test
fun `restart node with incompatible version of suspended flow due to different jar hash`() {
driver(parametersForRestartingNodes()) {
val uniqueName = "different-jar-hash-test-${UUID.randomUUID()}"
val cordapp = defaultCordapp.copy(name = uniqueName)
val uniqueWorkflowJarName = "different-jar-hash-test-${UUID.randomUUID()}"
val uniqueContractJarName = "contract-$uniqueWorkflowJarName"
val defaultWorkflowJar = cordappWithPackages(SendMessageFlow::class.packageName)
val defaultContractJar = cordappWithPackages(MessageState::class.packageName)
val contractJar = defaultContractJar.copy(name = uniqueContractJarName)
val workflowJar = defaultWorkflowJar.copy(name = uniqueWorkflowJarName)
val bobBaseDir = createSuspendedFlowInBob(setOf(cordapp))
val bobBaseDir = createSuspendedFlowInBob(setOf(workflowJar, contractJar))
val cordappsDir = bobBaseDir / "cordapps"
val cordappJar = cordappsDir.list().single { it.toString().endsWith(".jar") }
val cordappJar = cordappsDir.list().single {
! it.toString().contains(uniqueContractJarName) && it.toString().endsWith(".jar")
}
// Make sure we're dealing with right jar
assertThat(cordappJar.fileName.toString()).contains(uniqueName)
assertThat(cordappJar.fileName.toString()).contains(uniqueWorkflowJarName)
// The name is part of the MANIFEST so changing it is sufficient to change the jar hash
val modifiedCordapp = cordapp.copy(name = "${cordapp.name}-modified")
val modifiedCordapp = workflowJar.copy(name = "${workflowJar.name}-modified")
val modifiedCordappJar = CustomCordapp.getJarFile(modifiedCordapp)
modifiedCordappJar.moveTo(cordappJar, REPLACE_EXISTING)
@ -135,14 +131,15 @@ class FlowCheckpointVersionNodeStartupCheckTest {
)).getOrThrow()
}
val logDir = baseDirectory(BOB_NAME) / NodeStartup.LOGS_DIRECTORY_NAME
val logFile = logDir.list { it.filter { it.fileName.toString().endsWith(".log") }.findAny().get() }
val logDir = baseDirectory(BOB_NAME)
val logFile = logDir.list { it.filter { it.fileName.toString().endsWith("out.log") }.findAny().get() }
val matchingLineCount = logFile.readLines { it.filter { line -> logMessage in line }.count() }
assertEquals(1, matchingLineCount)
}
private fun parametersForRestartingNodes(): DriverParameters {
return DriverParameters(
isDebug = true,
startNodesInProcess = false, // Start nodes in separate processes to ensure CordappLoader is not shared between restarts
inMemoryDB = false, // Ensure database is persisted between node restarts so we can keep suspended flows
cordappsForAllNodes = emptyList()

View File

@ -16,6 +16,7 @@ import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.unwrap
import net.corda.node.services.Permissions
import net.corda.node.services.statemachine.FlowTimeoutException
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.singleIdentity
@ -117,8 +118,6 @@ fun isQuasarAgentSpecified(): Boolean {
return jvmArgs.any { it.startsWith("-javaagent:") && it.contains("quasar") }
}
class ExceptionToCauseRetry : SQLException("deadlock")
class ExceptionToCauseFiniteRetry : ConstraintViolationException("Faked violation", SQLException("Fake"), "Fake name")
@StartableByRPC
@ -135,7 +134,7 @@ class InitiatorFlow(private val sessionsCount: Int, private val iterationsCount:
val visited = Visited(sessionNum, iterationNum, step)
if (visited !in seen) {
seen += visited
throw ExceptionToCauseRetry()
throw FlowTimeoutException()
}
}
}
@ -186,7 +185,7 @@ class InitiatedFlow(val session: FlowSession) : FlowLogic<Any>() {
val visited = Visited(sessionNum, iterationNum, step)
if (visited !in seen) {
seen += visited
throw ExceptionToCauseRetry()
throw FlowTimeoutException()
}
}
}

View File

@ -43,6 +43,4 @@ private fun FlowHandle<*>.waitForCompletion() {
} catch (e: Exception) {
// This is expected to throw an exception, using getOrThrow() just to wait until done.
}
}
private fun NodeHandle.logFile(): File = (baseDirectory / "logs").toFile().walk().filter { it.name.startsWith("node-") && it.extension == "log" }.single()
}

View File

@ -0,0 +1,40 @@
package net.corda.node.logging
import net.corda.core.messaging.startFlow
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.finance.DOLLARS
import net.corda.finance.flows.CashIssueAndPaymentFlow
import net.corda.node.services.Permissions.Companion.all
import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.driver
import net.corda.testing.node.User
import net.corda.testing.node.internal.FINANCE_CORDAPPS
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
class IssueCashLoggingTests {
@Test
fun `issuing and sending cash as payment do not result in duplicate insertion warnings`() {
val user = User("mark", "dadada", setOf(all()))
driver(DriverParameters(cordappsForAllNodes = FINANCE_CORDAPPS)) {
val nodeA = startNode(rpcUsers = listOf(user)).getOrThrow()
val nodeB = startNode().getOrThrow()
val amount = 1.DOLLARS
val ref = OpaqueBytes.of(0)
val recipient = nodeB.nodeInfo.legalIdentities[0]
nodeA.rpc.startFlow(::CashIssueAndPaymentFlow, amount, ref, recipient, false, defaultNotaryIdentity).returnValue.getOrThrow()
val linesWithDuplicateInsertionWarningsInA = nodeA.logFile().useLines { lines -> lines.filter(String::containsDuplicateInsertWarning).toList() }
val linesWithDuplicateInsertionWarningsInB = nodeB.logFile().useLines { lines -> lines.filter(String::containsDuplicateInsertWarning).toList() }
assertThat(linesWithDuplicateInsertionWarningsInA).isEmpty()
assertThat(linesWithDuplicateInsertionWarningsInB).isEmpty()
}
}
}
private fun String.containsDuplicateInsertWarning(): Boolean = contains("Double insert") && contains("not inserting the second time")

View File

@ -0,0 +1,7 @@
package net.corda.node.logging
import net.corda.core.internal.div
import net.corda.testing.driver.NodeHandle
import java.io.File
fun NodeHandle.logFile(): File = (baseDirectory / "logs").toFile().walk().filter { it.name.startsWith("node-") && it.extension == "log" }.single()

View File

@ -1,117 +1,143 @@
package net.corda.node.services
import com.nhaarman.mockito_kotlin.any
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.CordaRuntimeException
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.*
import net.corda.core.cordapp.CordappProvider
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.*
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.internal.toLedgerTransaction
import net.corda.core.node.NetworkParameters
import net.corda.core.node.ServicesForResolution
import net.corda.core.node.services.AttachmentStorage
import net.corda.core.node.services.IdentityService
import net.corda.core.node.services.NetworkParametersStorage
import net.corda.core.serialization.SerializationFactory
import net.corda.core.serialization.serialize
import net.corda.core.internal.*
import net.corda.core.internal.concurrent.transpose
import net.corda.core.messaging.startFlow
import net.corda.core.serialization.internal.UntrustedAttachmentsException
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.getOrThrow
import net.corda.node.VersionInfo
import net.corda.node.internal.cordapp.CordappProviderImpl
import net.corda.node.internal.cordapp.JarScanningCordappLoader
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.common.internal.addNotary
import net.corda.testing.core.DUMMY_BANK_A_NAME
import net.corda.core.utilities.unwrap
import net.corda.testing.common.internal.checkNotOnClasspath
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.core.TestIdentity
import net.corda.testing.core.singleIdentity
import net.corda.testing.driver.DriverDSL
import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.NodeParameters
import net.corda.testing.driver.driver
import net.corda.testing.internal.MockCordappConfigProvider
import net.corda.testing.internal.rigorousMock
import net.corda.testing.internal.withoutTestSerialization
import net.corda.testing.node.NotarySpec
import net.corda.testing.node.internal.cordappsForPackages
import net.corda.testing.services.MockAttachmentStorage
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Test
import java.net.URL
import java.net.URLClassLoader
import kotlin.test.assertFailsWith
class AttachmentLoadingTests {
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
private val attachments = MockAttachmentStorage()
private val provider = CordappProviderImpl(JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR), VersionInfo.UNKNOWN), MockCordappConfigProvider(), attachments).apply {
start(testNetworkParameters().whitelistedContractImplementations)
}
private val cordapp get() = provider.cordapps.first()
private val attachmentId get() = provider.getCordappAttachmentId(cordapp)!!
private val appContext get() = provider.getAppContext(cordapp)
private companion object {
val isolatedJAR = AttachmentLoadingTests::class.java.getResource("isolated.jar")!!
const val ISOLATED_CONTRACT_ID = "net.corda.finance.contracts.isolated.AnotherDummyContract"
val isolatedJar: URL = AttachmentLoadingTests::class.java.getResource("/isolated.jar")
val isolatedClassLoader = URLClassLoader(arrayOf(isolatedJar))
val issuanceFlowClass: Class<FlowLogic<StateRef>> = uncheckedCast(loadFromIsolated("net.corda.isolated.workflows.IsolatedIssuanceFlow"))
val bankAName = CordaX500Name("BankA", "Zurich", "CH")
val bankBName = CordaX500Name("BankB", "Zurich", "CH")
val flowInitiatorClass: Class<out FlowLogic<*>> =
Class.forName("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator", true, URLClassLoader(arrayOf(isolatedJAR)))
.asSubclass(FlowLogic::class.java)
val DUMMY_BANK_A = TestIdentity(DUMMY_BANK_A_NAME, 40).party
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
}
private val services = object : ServicesForResolution {
private val testNetworkParameters = testNetworkParameters().addNotary(DUMMY_NOTARY)
override fun loadState(stateRef: StateRef): TransactionState<*> = throw NotImplementedError()
override fun loadStates(stateRefs: Set<StateRef>): Set<StateAndRef<ContractState>> = throw NotImplementedError()
override fun loadContractAttachment(stateRef: StateRef, interestedContractClassName : ContractClassName?): Attachment = throw NotImplementedError()
override val identityService = rigorousMock<IdentityService>().apply {
doReturn(null).whenever(this).partyFromKey(DUMMY_BANK_A.owningKey)
}
override val attachments: AttachmentStorage get() = this@AttachmentLoadingTests.attachments
override val cordappProvider: CordappProvider get() = this@AttachmentLoadingTests.provider
override val networkParameters: NetworkParameters = testNetworkParameters
override val networkParametersStorage: NetworkParametersStorage get() = rigorousMock<NetworkParametersStorage>().apply {
doReturn(testNetworkParameters.serialize().hash).whenever(this).currentHash
doReturn(testNetworkParameters).whenever(this).lookup(any())
}
}
@Test
fun `test a wire transaction has loaded the correct attachment`() {
val appClassLoader = appContext.classLoader
val contractClass = appClassLoader.loadClass(ISOLATED_CONTRACT_ID).asSubclass(Contract::class.java)
val generateInitialMethod = contractClass.getDeclaredMethod("generateInitial", PartyAndReference::class.java, Integer.TYPE, Party::class.java)
val contract = contractClass.newInstance()
val txBuilder = generateInitialMethod.invoke(contract, DUMMY_BANK_A.ref(1), 1, DUMMY_NOTARY) as TransactionBuilder
val context = SerializationFactory.defaultFactory.defaultContext.withClassLoader(appClassLoader)
val ledgerTx = txBuilder.toLedgerTransaction(services, context)
contract.verify(ledgerTx)
val actual = ledgerTx.attachments.first()
val expected = attachments.openAttachment(attachmentId)!!
assertEquals(expected, actual)
}
@Test
fun `test that attachments retrieved over the network are not used for code`() {
withoutTestSerialization {
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = emptySet())) {
val additionalCordapps = cordappsForPackages("net.corda.finance.contracts.isolated")
val bankA = startNode(NodeParameters(providedName = bankAName, additionalCordapps = additionalCordapps)).getOrThrow()
val bankB = startNode(NodeParameters(providedName = bankBName, additionalCordapps = additionalCordapps)).getOrThrow()
assertFailsWith<CordaRuntimeException>("Party C=CH,L=Zurich,O=BankB rejected session request: Don't know net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator") {
bankA.rpc.startFlowDynamic(flowInitiatorClass, bankB.nodeInfo.legalIdentities.first()).returnValue.getOrThrow()
}
init {
checkNotOnClasspath("net.corda.isolated.contracts.AnotherDummyContract") {
"isolated module cannot be on the classpath as otherwise it will be pulled into the nodes the driver creates and " +
"contaminate the tests. This is a known issue with the driver and we must work around it until it's fixed."
}
Unit
}
fun loadFromIsolated(className: String): Class<*> = Class.forName(className, false, isolatedClassLoader)
}
@Test
fun `contracts downloaded from the network are not executed without the DJVM`() {
driver(DriverParameters(
startNodesInProcess = false,
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = false)),
cordappsForAllNodes = cordappsForPackages(javaClass.packageName)
)) {
installIsolatedCordapp(ALICE_NAME)
val (alice, bob) = listOf(
startNode(providedName = ALICE_NAME),
startNode(providedName = BOB_NAME)
).transpose().getOrThrow()
val stateRef = alice.rpc.startFlowDynamic(issuanceFlowClass, 1234).returnValue.getOrThrow()
assertThatThrownBy { alice.rpc.startFlow(::ConsumeAndBroadcastFlow, stateRef, bob.nodeInfo.singleIdentity()).returnValue.getOrThrow() }
// ConsumeAndBroadcastResponderFlow re-throws any non-FlowExceptions with just their class name in the message so that
// we can verify here Bob threw the correct exception
.hasMessage(UntrustedAttachmentsException::class.java.name)
}
}
}
@Test
fun `contract is executed if installed locally`() {
driver(DriverParameters(
startNodesInProcess = false,
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = false)),
cordappsForAllNodes = cordappsForPackages(javaClass.packageName)
)) {
installIsolatedCordapp(ALICE_NAME)
installIsolatedCordapp(BOB_NAME)
val (alice, bob) = listOf(
startNode(providedName = ALICE_NAME),
startNode(providedName = BOB_NAME)
).transpose().getOrThrow()
val stateRef = alice.rpc.startFlowDynamic(issuanceFlowClass, 1234).returnValue.getOrThrow()
alice.rpc.startFlow(::ConsumeAndBroadcastFlow, stateRef, bob.nodeInfo.singleIdentity()).returnValue.getOrThrow()
}
}
private fun DriverDSL.installIsolatedCordapp(name: CordaX500Name) {
val cordappsDir = (baseDirectory(name) / "cordapps").createDirectories()
isolatedJar.toPath().copyToDirectory(cordappsDir)
}
@InitiatingFlow
@StartableByRPC
class ConsumeAndBroadcastFlow(private val stateRef: StateRef, private val otherSide: Party) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
val notary = serviceHub.networkMapCache.notaryIdentities[0]
val stateAndRef = serviceHub.toStateAndRef<ContractState>(stateRef)
val stx = serviceHub.signInitialTransaction(
TransactionBuilder(notary)
.addInputState(stateAndRef)
.addOutputState(ConsumeContract.State())
.addCommand(Command(ConsumeContract.Cmd, ourIdentity.owningKey))
)
stx.verify(serviceHub, checkSufficientSignatures = false)
val session = initiateFlow(otherSide)
subFlow(FinalityFlow(stx, session))
// It's important we wait on this dummy receive, as otherwise it's possible we miss any errors the other side throws
session.receive<String>().unwrap { require(it == "OK") { "Not OK: $it"} }
}
}
@InitiatedBy(ConsumeAndBroadcastFlow::class)
class ConsumeAndBroadcastResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
try {
subFlow(ReceiveFinalityFlow(otherSide))
} catch (e: FlowException) {
throw e
} catch (e: Exception) {
throw FlowException(e.javaClass.name)
}
otherSide.send("OK")
}
}
class ConsumeContract : Contract {
override fun verify(tx: LedgerTransaction) {
// Accept everything
}
class State : ContractState {
override val participants: List<AbstractParty> get() = emptyList()
}
object Cmd : TypeOnlyCommandData()
}
}

View File

@ -22,6 +22,7 @@ import net.corda.testing.driver.driver
import net.corda.testing.node.NotarySpec
import net.corda.testing.node.User
import net.corda.testing.node.internal.DummyClusterSpec
import net.corda.testing.node.internal.FINANCE_CORDAPPS
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import rx.Observable
@ -42,7 +43,8 @@ class DistributedServiceTests {
invokeRpc(CordaRPCOps::stateMachinesFeed))
)
driver(DriverParameters(
extraCordappPackagesToScan = listOf("net.corda.finance", "net.corda.notary.raft"),
extraCordappPackagesToScan = listOf("net.corda.notary.raft"),
cordappsForAllNodes = FINANCE_CORDAPPS,
notarySpecs = listOf(NotarySpec(
DUMMY_NOTARY_NAME,
rpcUsers = listOf(testUser),

View File

@ -100,17 +100,6 @@ class NodeRegistrationTest {
aliceName.organisation,
genevieveName.organisation,
notaryName.organisation)
// Check the nodes can communicate among themselves (and the notary).
val anonymous = false
genevieve.rpc.startFlow(
::CashIssueAndPaymentFlow,
1000.DOLLARS,
OpaqueBytes.of(12),
alice.nodeInfo.singleIdentity(),
anonymous,
defaultNotaryIdentity
).returnValue.getOrThrow()
}
}
}

View File

@ -20,6 +20,7 @@ import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.driver
import net.corda.testing.driver.internal.incrementalPortAllocation
import net.corda.testing.node.User
import net.corda.testing.node.internal.FINANCE_CORDAPPS
import org.junit.Test
import java.util.*
@ -28,7 +29,7 @@ class AdditionP2PAddressModeTest {
@Test
fun `runs nodes with one configured to use additionalP2PAddresses`() {
val testUser = User("test", "test", setOf(all()))
driver(DriverParameters(startNodesInProcess = true, inMemoryDB = true, extraCordappPackagesToScan = listOf("net.corda.finance"))) {
driver(DriverParameters(startNodesInProcess = true, inMemoryDB = true, cordappsForAllNodes = FINANCE_CORDAPPS)) {
val mainAddress = portAllocation.nextHostAndPort().toString()
val altAddress = portAllocation.nextHostAndPort().toString()
val haConfig = mutableMapOf<String, Any?>()

View File

@ -20,8 +20,8 @@ class VaultRestartTest {
@Test
fun `restart and query vault after adding some cash states`() {
driver(DriverParameters(inMemoryDB = false, startNodesInProcess = false,
extraCordappPackagesToScan = listOf("net.corda.finance.contracts", "net.corda.finance.schemas"))) {
driver(DriverParameters(inMemoryDB = false, startNodesInProcess = false, isDebug = true,
extraCordappPackagesToScan = listOf("net.corda.finance", "migration"))) {
val node = startNode(providedName = DUMMY_BANK_A_NAME, customOverrides = mapOf("p2pAddress" to "localhost:30000")).getOrThrow()
val expected = 500.DOLLARS

View File

@ -15,6 +15,7 @@ import javax.persistence.Table
@CordaSerializable
data class Message(val value: String)
@BelongsToContract(MessageContract::class)
data class MessageState(val message: Message, val by: Party, override val linearId: UniqueIdentifier = UniqueIdentifier()) : LinearState, QueryableState {
override val participants: List<AbstractParty> = listOf(by)

Binary file not shown.

View File

@ -8,7 +8,7 @@ import net.corda.core.schemas.MappedSchema
/**
* Handles loading [Cordapp]s.
*/
interface CordappLoader {
interface CordappLoader : AutoCloseable {
/**
* Returns all [Cordapp]s found.

View File

@ -33,7 +33,6 @@ import net.corda.core.utilities.days
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.minutes
import net.corda.node.CordaClock
import net.corda.node.SerialFilter
import net.corda.node.VersionInfo
import net.corda.node.cordapp.CordappLoader
import net.corda.node.internal.classloading.requireAnnotation
@ -93,7 +92,6 @@ import java.lang.reflect.InvocationTargetException
import java.nio.file.Paths
import java.security.KeyPair
import java.security.KeyStoreException
import java.security.PublicKey
import java.security.cert.X509Certificate
import java.sql.Connection
import java.time.Clock
@ -145,7 +143,10 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
}
}
val cordappLoader: CordappLoader = makeCordappLoader(configuration, versionInfo)
private val notaryLoader = configuration.notary?.let {
NotaryLoader(it, versionInfo)
}
val cordappLoader: CordappLoader = makeCordappLoader(configuration, versionInfo).closeOnStop()
val schemaService = NodeSchemaService(cordappLoader.cordappSchemas).tokenize()
val identityService = PersistentIdentityService(cacheFactory).tokenize()
val database: CordaPersistence = createCordaPersistence(
@ -170,7 +171,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
val attachments = NodeAttachmentService(metricRegistry, cacheFactory, database).tokenize()
val cryptoService = configuration.makeCryptoService()
@Suppress("LeakingThis")
val networkParametersStorage = makeParametersStorage()
val networkParametersStorage = makeNetworkParametersStorage()
val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(configuration.cordappDirectories), attachments).tokenize()
@Suppress("LeakingThis")
val keyManagementService = makeKeyManagementService(identityService).tokenize()
@ -178,7 +179,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
attachments.servicesForResolution = it
}
@Suppress("LeakingThis")
val vaultService = makeVaultService(keyManagementService, servicesForResolution, database).tokenize()
val vaultService = makeVaultService(keyManagementService, servicesForResolution, database, cordappLoader).tokenize()
val nodeProperties = NodePropertiesPersistentStore(StubbedNodeUniqueIdProvider::value, database, cacheFactory)
val flowLogicRefFactory = FlowLogicRefFactoryImpl(cordappLoader.appClassLoader)
// TODO Cancelling parameters updates - if we do that, how we ensure that no one uses cancelled parameters in the transactions?
@ -373,8 +374,8 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
// the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with
// the identity key. But the infrastructure to make that easy isn't here yet.
keyManagementService.start(keyPairs)
val notaryService = makeNotaryService(myNotaryIdentity)
installCordaServices(myNotaryIdentity)
val notaryService = maybeStartNotaryService(myNotaryIdentity)
installCordaServices()
contractUpgradeService.start()
vaultService.start()
ScheduledActivityObserver.install(vaultService, schedulerService, flowLogicRefFactory)
@ -537,14 +538,13 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
}
private fun makeCordappLoader(configuration: NodeConfiguration, versionInfo: VersionInfo): CordappLoader {
val generatedCordapps = mutableListOf(VirtualCordapp.generateCoreCordapp(versionInfo))
if (isRunningSimpleNotaryService(configuration)) {
// For backwards compatibility purposes the single node notary implementation is built-in: a virtual
// CorDapp will be generated.
generatedCordapps += VirtualCordapp.generateSimpleNotaryCordapp(versionInfo)
val generatedCordapps = mutableListOf(VirtualCordapp.generateCore(versionInfo))
notaryLoader?.builtInNotary?.let { notaryImpl ->
generatedCordapps += notaryImpl
}
val blacklistedKeys = if (configuration.devMode) emptyList()
else configuration.cordappSignerKeyFingerprintBlacklist.mapNotNull {
else configuration.cordappSignerKeyFingerprintBlacklist.map {
try {
SecureHash.parse(it)
} catch (e: IllegalArgumentException) {
@ -566,7 +566,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
private class ServiceInstantiationException(cause: Throwable?) : CordaException("Service Instantiation Error", cause)
private fun installCordaServices(myNotaryIdentity: PartyAndCertificate?) {
private fun installCordaServices() {
val loadedServices = cordappLoader.cordapps.flatMap { it.services }
loadedServices.forEach {
try {
@ -711,7 +711,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
return DBTransactionStorage(database, cacheFactory)
}
protected open fun makeParametersStorage(): NetworkParametersStorageInternal {
protected open fun makeNetworkParametersStorage(): NetworkParametersStorage {
return DBNetworkParametersStorage(cacheFactory, database, networkMapClient).tokenize()
}
@ -781,20 +781,10 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
logVendorString(database, log)
}
private fun makeNotaryService(myNotaryIdentity: PartyAndCertificate?): NotaryService? {
return configuration.notary?.let { notaryConfig ->
val serviceClass = getNotaryServiceClass(notaryConfig.className)
log.info("Starting notary service: $serviceClass")
val notaryKey = myNotaryIdentity?.owningKey
?: throw IllegalArgumentException("Unable to start notary service $serviceClass: notary identity not found")
/** Some notary implementations only work with Java serialization. */
maybeInstallSerializationFilter(serviceClass)
val constructor = serviceClass.getDeclaredConstructor(ServiceHubInternal::class.java, PublicKey::class.java)
.apply { isAccessible = true }
val service = constructor.newInstance(services, notaryKey) as NotaryService
/** Loads and starts a notary service if it is configured. */
private fun maybeStartNotaryService(myNotaryIdentity: PartyAndCertificate?): NotaryService? {
return notaryLoader?.let { loader ->
val service = loader.loadService(myNotaryIdentity, services, cordappLoader)
service.run {
tokenize()
@ -806,30 +796,6 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
}
}
/** Installs a custom serialization filter defined by a notary service implementation. Only supported in dev mode. */
private fun maybeInstallSerializationFilter(serviceClass: Class<out NotaryService>) {
try {
@Suppress("UNCHECKED_CAST")
val filter = serviceClass.getDeclaredMethod("getSerializationFilter").invoke(null) as ((Class<*>) -> Boolean)
if (configuration.devMode) {
log.warn("Installing a custom Java serialization filter, required by ${serviceClass.name}. " +
"Note this is only supported in dev mode a production node will fail to start if serialization filters are used.")
SerialFilter.install(filter)
} else {
throw UnsupportedOperationException("Unable to install a custom Java serialization filter, not in dev mode.")
}
} catch (e: NoSuchMethodException) {
// No custom serialization filter declared
}
}
private fun getNotaryServiceClass(className: String): Class<out NotaryService> {
val loadedImplementations = cordappLoader.cordapps.mapNotNull { it.notaryService }
log.debug("Notary service implementations found: ${loadedImplementations.joinToString(", ")}")
return loadedImplementations.firstOrNull { it.name == className }
?: throw IllegalArgumentException("The notary service implementation specified in the configuration: $className is not found. Available implementations: ${loadedImplementations.joinToString(", ")}}")
}
protected open fun makeKeyManagementService(identityService: PersistentIdentityService): KeyManagementServiceInternal {
// Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because
// the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with
@ -969,8 +935,9 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
protected open fun makeVaultService(keyManagementService: KeyManagementService,
services: ServicesForResolution,
database: CordaPersistence): VaultServiceInternal {
return NodeVaultService(platformClock, keyManagementService, services, database, schemaService)
database: CordaPersistence,
cordappLoader: CordappLoader): VaultServiceInternal {
return NodeVaultService(platformClock, keyManagementService, services, database, schemaService, cordappLoader.appClassLoader)
}
/** Load configured JVM agents */
@ -1011,7 +978,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
override val configuration: NodeConfiguration get() = this@AbstractNode.configuration
override val networkMapUpdater: NetworkMapUpdater get() = this@AbstractNode.networkMapUpdater
override val cacheFactory: NamedCacheFactory get() = this@AbstractNode.cacheFactory
override val networkParametersStorage: NetworkParametersStorage get() = this@AbstractNode.networkParametersStorage
override val networkParametersService: NetworkParametersStorage get() = this@AbstractNode.networkParametersStorage
private lateinit var _myInfo: NodeInfo
override val myInfo: NodeInfo get() = _myInfo

View File

@ -5,10 +5,9 @@ import net.corda.core.identity.Party
import net.corda.core.internal.DigitalSignatureWithCert
import net.corda.core.internal.NamedCacheFactory
import net.corda.core.internal.SignedDataWithCert
import net.corda.core.internal.notary.HistoricNetworkParameterStorage
import net.corda.core.internal.NetworkParametersServiceInternal
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NotaryInfo
import net.corda.core.node.services.NetworkParametersStorage
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.serialization.deserialize
@ -27,7 +26,7 @@ import org.apache.commons.lang.ArrayUtils
import java.security.cert.X509Certificate
import javax.persistence.*
interface NetworkParametersStorageInternal : NetworkParametersStorage {
interface NetworkParametersStorage : NetworkParametersServiceInternal {
/**
* Return parameters epoch for the given parameters hash. Null if there are no parameters for this hash in the storage and we are unable to
* get them from network map.
@ -50,7 +49,7 @@ class DBNetworkParametersStorage(
// TODO It's very inefficient solution (at least at the beginning when node joins without historical data)
// We could have historic parameters endpoint or always add parameters as an attachment to the transaction.
private val networkMapClient: NetworkMapClient?
) : NetworkParametersStorageInternal, HistoricNetworkParameterStorage, SingletonSerializeAsToken() {
) : NetworkParametersStorage, SingletonSerializeAsToken() {
private lateinit var trustRoot: X509Certificate
companion object {

View File

@ -43,7 +43,9 @@ import net.corda.node.services.api.FlowStarter
import net.corda.node.services.api.ServiceHubInternal
import net.corda.node.services.api.StartedNodeServices
import net.corda.node.services.config.*
import net.corda.node.services.messaging.*
import net.corda.node.services.messaging.ArtemisMessagingServer
import net.corda.node.services.messaging.MessagingService
import net.corda.node.services.messaging.P2PMessagingClient
import net.corda.node.services.rpc.ArtemisRpcBroker
import net.corda.node.services.rpc.InternalRPCMessagingClient
import net.corda.node.services.rpc.RPCServerConfiguration
@ -311,6 +313,9 @@ open class Node(configuration: NodeConfiguration,
private fun getAdvertisedAddress(): NetworkHostAndPort {
return with(configuration) {
require(p2pAddress.host != "0.0.0.0") {
"Invalid p2pAddress: $p2pAddress contains 0.0.0.0 which is not suitable as an advertised node address"
}
val host = if (detectPublicIp) {
tryDetectIfNotPublicHost(p2pAddress.host) ?: p2pAddress.host
} else {
@ -323,10 +328,11 @@ open class Node(configuration: NodeConfiguration,
/**
* Checks whether the specified [host] is a public IP address or hostname. If not, tries to discover the current
* machine's public IP address to be used instead by looking through the network interfaces.
* TODO this code used to rely on the networkmap node, we might want to look at a different solution.
*/
private fun tryDetectIfNotPublicHost(host: String): String? {
return if (!AddressUtils.isPublic(host)) {
return if (host.toLowerCase() == "localhost") {
log.warn("p2pAddress specified as localhost. Trying to autodetect a suitable public address to advertise in network map." +
"To disable autodetect set detectPublicIp = false in the node.conf, or consider using messagingServerAddress and messagingServerExternal")
val foundPublicIP = AddressUtils.tryDetectPublicIP()
if (foundPublicIP == null) {
try {

View File

@ -1,7 +1,10 @@
package net.corda.node.internal
import io.netty.channel.unix.Errors
import net.corda.cliutils.*
import net.corda.cliutils.CliWrapperBase
import net.corda.cliutils.CordaCliWrapper
import net.corda.cliutils.CordaVersionProvider
import net.corda.cliutils.ExitCodes
import net.corda.core.crypto.Crypto
import net.corda.core.internal.*
import net.corda.core.internal.concurrent.thenMatch
@ -26,7 +29,7 @@ import net.corda.nodeapi.internal.persistence.DatabaseIncompatibleException
import net.corda.tools.shell.InteractiveShell
import org.fusesource.jansi.Ansi
import org.slf4j.bridge.SLF4JBridgeHandler
import picocli.CommandLine.*
import picocli.CommandLine.Mixin
import sun.misc.VMSupport
import java.io.IOException
import java.io.RandomAccessFile
@ -35,6 +38,7 @@ import java.net.InetAddress
import java.nio.file.Path
import java.time.DayOfWeek
import java.time.ZonedDateTime
import java.util.function.Consumer
/** An interface that can be implemented to tell the node what to do once it's intitiated. */
interface RunAfterNodeInitialisation {
@ -143,10 +147,10 @@ open class NodeStartup : NodeStartupLogging {
val configuration = cmdLineOptions.parseConfiguration(rawConfig).doIfValid { logRawConfig(rawConfig) }.doOnErrors(::logConfigurationErrors).optional ?: return ExitCodes.FAILURE
// Step 6. Configuring special serialisation requirements, i.e., bft-smart relies on Java serialization.
if (attempt { banJavaSerialisation(configuration) }.doOnException { error -> error.logAsUnexpected("Exception while configuring serialisation") } !is Try.Success) return ExitCodes.FAILURE
if (attempt { banJavaSerialisation(configuration) }.doOnFailure(Consumer { error -> error.logAsUnexpected("Exception while configuring serialisation") }) !is Try.Success) return ExitCodes.FAILURE
// Step 7. Any actions required before starting up the Corda network layer.
if (attempt { preNetworkRegistration(configuration) }.doOnException(::handleRegistrationError) !is Try.Success) return ExitCodes.FAILURE
if (attempt { preNetworkRegistration(configuration) }.doOnFailure(Consumer(::handleRegistrationError)) !is Try.Success) return ExitCodes.FAILURE
// Step 8. Log startup info.
logStartupInfo(versionInfo, configuration)
@ -155,7 +159,7 @@ open class NodeStartup : NodeStartupLogging {
if (attempt {
cmdLineOptions.baseDirectory.createDirectories()
afterNodeInitialisation.run(createNode(configuration, versionInfo))
}.doOnException(::handleStartError) !is Try.Success) return ExitCodes.FAILURE
}.doOnFailure(Consumer(::handleStartError)) !is Try.Success) return ExitCodes.FAILURE
return ExitCodes.SUCCESS
}
@ -395,24 +399,26 @@ interface NodeStartupLogging {
val startupErrors = setOf(MultipleCordappsForFlowException::class, CheckpointIncompatibleException::class, AddressBindingException::class, NetworkParametersReader::class, DatabaseIncompatibleException::class)
}
fun <RESULT> attempt(action: () -> RESULT): Try<RESULT> = Try.on(action)
fun <RESULT> attempt(action: () -> RESULT): Try<RESULT> = Try.on(action).throwError()
fun Exception.logAsExpected(message: String? = this.message, print: (String?) -> Unit = logger::error) = print(message)
fun Throwable.logAsExpected(message: String? = this.message, print: (String?) -> Unit = logger::error) = print(message)
fun Exception.logAsUnexpected(message: String? = this.message, error: Exception = this, print: (String?, Throwable) -> Unit = logger::error) = print("$message${this.message?.let { ": $it" } ?: ""}", error)
fun Throwable.logAsUnexpected(message: String? = this.message, error: Throwable = this, print: (String?, Throwable) -> Unit = logger::error) {
print("$message${this.message?.let { ": $it" } ?: ""}", error)
}
fun handleRegistrationError(error: Exception) {
fun handleRegistrationError(error: Throwable) {
when (error) {
is NodeRegistrationException -> error.logAsExpected("Issue with Node registration: ${error.message}")
else -> error.logAsUnexpected("Exception during node registration")
}
}
fun Exception.isOpenJdkKnownIssue() = message?.startsWith("Unknown named curve:") == true
fun Throwable.isOpenJdkKnownIssue() = message?.startsWith("Unknown named curve:") == true
fun Exception.isExpectedWhenStartingNode() = startupErrors.any { error -> error.isInstance(this) }
fun Throwable.isExpectedWhenStartingNode() = startupErrors.any { error -> error.isInstance(this) }
fun handleStartError(error: Exception) {
fun handleStartError(error: Throwable) {
when {
error.isExpectedWhenStartingNode() -> error.logAsExpected()
error is CouldNotCreateDataSourceException -> error.logAsUnexpected()
@ -426,6 +432,7 @@ interface NodeStartupLogging {
fun CliWrapperBase.initLogging(baseDirectory: Path) {
System.setProperty("defaultLogLevel", specifiedLogLevel) // These properties are referenced from the XML config file.
if (verbose) {
System.setProperty("consoleLoggingEnabled", "true")
System.setProperty("consoleLogLevel", specifiedLogLevel)
Node.renderBasicInfoToConsole = false
}

View File

@ -6,8 +6,8 @@ import net.corda.core.internal.SerializedStateAndRef
import net.corda.core.node.NetworkParameters
import net.corda.core.node.ServicesForResolution
import net.corda.core.node.services.AttachmentStorage
import net.corda.core.node.services.NetworkParametersStorage
import net.corda.core.node.services.IdentityService
import net.corda.core.node.services.NetworkParametersService
import net.corda.core.node.services.TransactionStorage
import net.corda.core.transactions.ContractUpgradeWireTransaction
import net.corda.core.transactions.NotaryChangeWireTransaction
@ -18,10 +18,10 @@ data class ServicesForResolutionImpl(
override val identityService: IdentityService,
override val attachments: AttachmentStorage,
override val cordappProvider: CordappProvider,
override val networkParametersStorage: NetworkParametersStorage,
override val networkParametersService: NetworkParametersService,
private val validatedTransactions: TransactionStorage
) : ServicesForResolution {
override val networkParameters: NetworkParameters get() = networkParametersStorage.lookup(networkParametersStorage.currentHash) ?:
override val networkParameters: NetworkParameters get() = networkParametersService.lookup(networkParametersService.currentHash) ?:
throw IllegalArgumentException("No current parameters in network parameters storage")
@Throws(TransactionResolutionException::class)

View File

@ -39,28 +39,12 @@ open class CordappProviderImpl(val cordappLoader: CordappLoader,
fun start(whitelistedContractImplementations: Map<String, List<AttachmentId>>) {
cordappAttachments.putAll(loadContractsIntoAttachmentStore())
verifyInstalledCordapps(whitelistedContractImplementations)
verifyInstalledCordapps()
}
private fun verifyInstalledCordapps(whitelistedContractImplementations: Map<String, List<AttachmentId>>) {
private fun verifyInstalledCordapps() {
// This will invoke the lazy flowCordappMap property, thus triggering the MultipleCordappsForFlow check.
cordappLoader.flowCordappMap
if (whitelistedContractImplementations.isEmpty()) {
log.warn("The network parameters don't specify any whitelisted contract implementations. Please contact your zone operator. See https://docs.corda.net/network-map.html")
return
}
// Verify that the installed contract classes correspond with the whitelist hash
// And warn if node is not using latest CorDapp
cordappAttachments.keys.map(attachmentStorage::openAttachment).mapNotNull { it as? ContractAttachment }.forEach { attch ->
(attch.allContracts intersect whitelistedContractImplementations.keys).forEach { contractClassName ->
when {
attch.id !in whitelistedContractImplementations[contractClassName]!! -> log.error("Contract $contractClassName found in attachment ${attch.id} is not whitelisted in the network parameters. If this is a production node contact your zone operator. See https://docs.corda.net/network-map.html")
attch.id != whitelistedContractImplementations[contractClassName]!!.last() -> log.warn("You are not using the latest CorDapp version for contract: $contractClassName. Please contact your zone operator.")
}
}
}
}
override fun getAppContext(): CordappContext {

View File

@ -43,13 +43,6 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
private val versionInfo: VersionInfo = VersionInfo.UNKNOWN,
extraCordapps: List<CordappImpl>,
private val signerKeyFingerprintBlacklist: List<SecureHash.SHA256> = emptyList()) : CordappLoaderTemplate() {
override val cordapps: List<CordappImpl> by lazy {
loadCordapps() + extraCordapps
}
override val appClassLoader: ClassLoader = URLClassLoader(cordappJarPaths.stream().map { it.url }.toTypedArray(), javaClass.classLoader)
init {
if (cordappJarPaths.isEmpty()) {
logger.info("No CorDapp paths provided")
@ -58,6 +51,12 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
}
}
override val cordapps: List<CordappImpl> by lazy { loadCordapps() + extraCordapps }
override val appClassLoader: URLClassLoader = URLClassLoader(cordappJarPaths.stream().map { it.url }.toTypedArray(), javaClass.classLoader)
override fun close() = appClassLoader.close()
companion object {
private val logger = contextLogger()
@ -260,7 +259,10 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
}
private fun findPlugins(cordappJarPath: RestrictedURL): List<SerializationWhitelist> {
return ServiceLoader.load(SerializationWhitelist::class.java, URLClassLoader(arrayOf(cordappJarPath.url), appClassLoader)).toList().filter {
val whitelists = URLClassLoader(arrayOf(cordappJarPath.url)).use {
ServiceLoader.load(SerializationWhitelist::class.java, it).toList()
}
return whitelists.filter {
it.javaClass.location == cordappJarPath.url && it.javaClass.name.startsWith(cordappJarPath.qualifiedNamePrefix)
} + DefaultWhitelist // Always add the DefaultWhitelist to the whitelist for an app.
}
@ -378,8 +380,4 @@ abstract class CordappLoaderTemplate : CordappLoader {
override val cordappSchemas: Set<MappedSchema> by lazy {
cordapps.flatMap { it.customSchemas }.toSet()
}
override val appClassLoader: ClassLoader by lazy {
URLClassLoader(cordapps.stream().map { it.jarPath }.toTypedArray(), javaClass.classLoader)
}
}

View File

@ -8,6 +8,10 @@ import net.corda.core.internal.location
import net.corda.node.VersionInfo
import net.corda.node.services.transactions.NodeNotarySchemaV1
import net.corda.node.services.transactions.SimpleNotaryService
import net.corda.notary.experimental.bftsmart.BFTSmartNotarySchemaV1
import net.corda.notary.experimental.bftsmart.BFTSmartNotaryService
import net.corda.notary.experimental.raft.RaftNotarySchemaV1
import net.corda.notary.experimental.raft.RaftNotaryService
internal object VirtualCordapp {
/** A list of the core RPC flows present in Corda */
@ -18,7 +22,7 @@ internal object VirtualCordapp {
)
/** A Cordapp representing the core package which is not scanned automatically. */
fun generateCoreCordapp(versionInfo: VersionInfo): CordappImpl {
fun generateCore(versionInfo: VersionInfo): CordappImpl {
return CordappImpl(
contractClassNames = listOf(),
initiatedFlows = listOf(),
@ -33,7 +37,7 @@ internal object VirtualCordapp {
allFlows = listOf(),
jarPath = ContractUpgradeFlow.javaClass.location, // Core JAR location
jarHash = SecureHash.allOnesHash,
minimumPlatformVersion = 1,
minimumPlatformVersion = versionInfo.platformVersion,
targetPlatformVersion = versionInfo.platformVersion,
notaryService = null,
isLoaded = false
@ -41,7 +45,7 @@ internal object VirtualCordapp {
}
/** A Cordapp for the built-in notary service implementation. */
fun generateSimpleNotaryCordapp(versionInfo: VersionInfo): CordappImpl {
fun generateSimpleNotary(versionInfo: VersionInfo): CordappImpl {
return CordappImpl(
contractClassNames = listOf(),
initiatedFlows = listOf(),
@ -56,10 +60,56 @@ internal object VirtualCordapp {
allFlows = listOf(),
jarPath = SimpleNotaryService::class.java.location,
jarHash = SecureHash.allOnesHash,
minimumPlatformVersion = 1,
minimumPlatformVersion = versionInfo.platformVersion,
targetPlatformVersion = versionInfo.platformVersion,
notaryService = SimpleNotaryService::class.java,
isLoaded = false
)
}
/** A Cordapp for the built-in Raft notary service implementation. */
fun generateRaftNotary(versionInfo: VersionInfo): CordappImpl {
return CordappImpl(
contractClassNames = listOf(),
initiatedFlows = listOf(),
rpcFlows = listOf(),
serviceFlows = listOf(),
schedulableFlows = listOf(),
services = listOf(),
serializationWhitelists = listOf(),
serializationCustomSerializers = listOf(),
customSchemas = setOf(RaftNotarySchemaV1),
info = Cordapp.Info.Default("corda-notary-raft", versionInfo.vendor, versionInfo.releaseVersion, "Open Source (Apache 2)"),
allFlows = listOf(),
jarPath = RaftNotaryService::class.java.location,
jarHash = SecureHash.allOnesHash,
minimumPlatformVersion = versionInfo.platformVersion,
targetPlatformVersion = versionInfo.platformVersion,
notaryService = RaftNotaryService::class.java,
isLoaded = false
)
}
/** A Cordapp for the built-in BFT-Smart notary service implementation. */
fun generateBFTSmartNotary(versionInfo: VersionInfo): CordappImpl {
return CordappImpl(
contractClassNames = listOf(),
initiatedFlows = listOf(),
rpcFlows = listOf(),
serviceFlows = listOf(),
schedulableFlows = listOf(),
services = listOf(),
serializationWhitelists = listOf(),
serializationCustomSerializers = listOf(),
customSchemas = setOf(BFTSmartNotarySchemaV1),
info = Cordapp.Info.Default("corda-notary-bft-smart", versionInfo.vendor, versionInfo.releaseVersion, "Open Source (Apache 2)"),
allFlows = listOf(),
jarPath = BFTSmartNotaryService::class.java.location,
jarHash = SecureHash.allOnesHash,
minimumPlatformVersion = versionInfo.platformVersion,
targetPlatformVersion = versionInfo.platformVersion,
notaryService = BFTSmartNotaryService::class.java,
isLoaded = false
)
}
}

View File

@ -16,6 +16,7 @@ import picocli.CommandLine.Mixin
import picocli.CommandLine.Option
import java.io.File
import java.nio.file.Path
import java.util.function.Consumer
class InitialRegistrationCli(val startup: NodeStartup): CliWrapperBase("initial-registration", "Start initial node registration with Corda network to obtain certificate from the permissioning server.") {
@Option(names = ["-t", "--network-root-truststore"], description = ["Network root trust store obtained from network operator."])
@ -83,7 +84,7 @@ class InitialRegistration(val baseDirectory: Path, private val networkRootTrustS
private fun initialRegistration(config: NodeConfiguration) {
// Null checks for [compatibilityZoneURL], [rootTruststorePath] and [rootTruststorePassword] has been done in [CmdLineOptions.loadConfig]
attempt { registerWithNetwork(config) }.doOnException(this::handleRegistrationError) as Try.Success
attempt { registerWithNetwork(config) }.doOnFailure(Consumer(this::handleRegistrationError)) as Try.Success
// At this point the node registration was successful. We can delete the marker file.
deleteNodeRegistrationMarker(baseDirectory)
}

View File

@ -4,6 +4,7 @@ import net.corda.core.cordapp.Cordapp
import net.corda.core.internal.toSynchronised
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.SerializationCustomSerializer
import net.corda.core.serialization.SerializationWhitelist
import net.corda.serialization.internal.CordaSerializationMagic
import net.corda.serialization.internal.amqp.*
import net.corda.serialization.internal.amqp.custom.RxNotificationSerializer
@ -14,12 +15,13 @@ import net.corda.serialization.internal.amqp.custom.RxNotificationSerializer
*/
class AMQPServerSerializationScheme(
cordappCustomSerializers: Set<SerializationCustomSerializer<*, *>>,
cordappSerializationWhitelists: Set<SerializationWhitelist>,
serializerFactoriesForContexts: MutableMap<SerializationFactoryCacheKey, SerializerFactory>
) : AbstractAMQPSerializationScheme(cordappCustomSerializers, serializerFactoriesForContexts) {
constructor(cordapps: List<Cordapp>) : this(cordapps.customSerializers, AccessOrderLinkedHashMap<SerializationFactoryCacheKey, SerializerFactory>(128).toSynchronised())
constructor(cordapps: List<Cordapp>, serializerFactoriesForContexts: MutableMap<SerializationFactoryCacheKey, SerializerFactory>) : this(cordapps.customSerializers, serializerFactoriesForContexts)
) : AbstractAMQPSerializationScheme(cordappCustomSerializers, cordappSerializationWhitelists, serializerFactoriesForContexts) {
constructor(cordapps: List<Cordapp>) : this(cordapps.customSerializers, cordapps.serializationWhitelists, AccessOrderLinkedHashMap<SerializationFactoryCacheKey, SerializerFactory>(128).toSynchronised())
constructor(cordapps: List<Cordapp>, serializerFactoriesForContexts: MutableMap<SerializationFactoryCacheKey, SerializerFactory>) : this(cordapps.customSerializers, cordapps.serializationWhitelists, serializerFactoriesForContexts)
constructor() : this(emptySet(), AccessOrderLinkedHashMap<SerializationFactoryCacheKey, SerializerFactory>(128).toSynchronised() )
constructor() : this(emptySet(), emptySet(), AccessOrderLinkedHashMap<SerializationFactoryCacheKey, SerializerFactory>(128).toSynchronised() )
override fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory {
throw UnsupportedOperationException()

View File

@ -7,6 +7,7 @@ import net.corda.core.contracts.requireThat
import net.corda.core.flows.*
import net.corda.core.identity.Party
import net.corda.core.internal.ContractUpgradeUtils
import net.corda.core.internal.warnOnce
import net.corda.core.node.StatesToRecord
import net.corda.core.transactions.ContractUpgradeWireTransaction
import net.corda.core.transactions.SignedTransaction
@ -15,6 +16,7 @@ class FinalityHandler(val sender: FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
subFlow(ReceiveTransactionFlow(sender, true, StatesToRecord.ONLY_RELEVANT))
logger.warnOnce("Insecure API to record finalised transaction was used by ${sender.counterparty} (${sender.getCounterpartyFlowInfo()})")
}
}

View File

@ -72,7 +72,7 @@ interface IdentityServiceInternal : IdentityService {
}
// Ensure we record the first identity of the same name, first
val wellKnownCert = identityCertChain.single { CertRole.extract(it)?.isWellKnown ?: false }
if (wellKnownCert != identity.certificate) {
if (wellKnownCert != identity.certificate && !isNewRandomIdentity) {
val idx = identityCertChain.lastIndexOf(wellKnownCert)
val firstPath = X509Utilities.buildCertPath(identityCertChain.slice(idx until identityCertChain.size))
verifyAndRegisterIdentity(trustAnchor, PartyAndCertificate(firstPath))

View File

@ -16,6 +16,8 @@ import net.corda.nodeapi.internal.config.MutualSslConfiguration
import net.corda.nodeapi.internal.config.User
import net.corda.nodeapi.internal.cryptoservice.CryptoService
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.notary.experimental.bftsmart.BFTSmartConfig
import net.corda.notary.experimental.raft.RaftConfig
import net.corda.tools.shell.SSHDConfiguration
import java.net.URL
import java.nio.file.Path
@ -34,6 +36,7 @@ interface NodeConfiguration {
val security: SecurityConfiguration?
val devMode: Boolean
val devModeOptions: DevModeOptions?
@Deprecated(message = "Use of single compatibility zone URL is deprecated", replaceWith = ReplaceWith("networkServices.networkMapURL"))
val compatibilityZoneURL: URL?
val networkServices: NetworkServicesConfig?
@Suppress("DEPRECATION")
@ -140,7 +143,7 @@ data class NotaryConfig(
/** The legal name of cluster in case of a distributed notary service. */
val serviceLegalName: CordaX500Name? = null,
/** The name of the notary service class to load. */
val className: String = "net.corda.node.services.transactions.SimpleNotaryService",
val className: String? = null,
/**
* If the wait time estimate on the internal queue exceeds this value, the notary may send
* a wait time update to the client (implementation specific and dependent on the counter
@ -148,7 +151,9 @@ data class NotaryConfig(
*/
val etaMessageThresholdSeconds: Int = NotaryServiceFlow.defaultEstimatedWaitTime.seconds.toInt(),
/** Notary implementation-specific configuration parameters. */
val extraConfig: Config? = null
val extraConfig: Config? = null,
val raft: RaftConfig? = null,
val bftSMaRt: BFTSmartConfig? = null
)
/**

View File

@ -43,6 +43,8 @@ import net.corda.nodeapi.internal.config.User
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel
import net.corda.nodeapi.internal.persistence.SchemaInitializationType
import net.corda.notary.experimental.bftsmart.BFTSmartConfig
import net.corda.notary.experimental.raft.RaftConfig
import net.corda.tools.shell.SSHDConfiguration
internal object UserSpec : Configuration.Specification<User>("User") {
@ -165,15 +167,38 @@ internal object FlowTimeoutConfigurationSpec : Configuration.Specification<FlowT
internal object NotaryConfigSpec : Configuration.Specification<NotaryConfig>("NotaryConfig") {
private val validating by boolean()
private val serviceLegalName by string().mapValid(::toCordaX500Name).optional()
private val className by string().optional().withDefaultValue("net.corda.node.services.transactions.SimpleNotaryService")
private val className by string().optional()
private val etaMessageThresholdSeconds by int().optional().withDefaultValue(NotaryServiceFlow.defaultEstimatedWaitTime.seconds.toInt())
private val extraConfig by nestedObject().map(ConfigObject::toConfig).optional()
private val raft by nested(RaftConfigSpec).optional()
private val bftSMaRt by nested(BFTSmartConfigSpec).optional()
override fun parseValid(configuration: Config): Valid<NotaryConfig> {
return valid(NotaryConfig(configuration[validating], configuration[serviceLegalName], configuration[className], configuration[etaMessageThresholdSeconds], configuration[extraConfig]))
return valid(NotaryConfig(configuration[validating], configuration[serviceLegalName], configuration[className], configuration[etaMessageThresholdSeconds], configuration[extraConfig], configuration[raft], configuration[bftSMaRt]))
}
}
internal object RaftConfigSpec : Configuration.Specification<RaftConfig>("RaftConfig") {
private val nodeAddress by string().mapValid(::toNetworkHostAndPort)
private val clusterAddresses by string().mapValid(::toNetworkHostAndPort).listOrEmpty()
override fun parseValid(configuration: Config): Valid<RaftConfig> {
return valid(RaftConfig(configuration[nodeAddress], configuration[clusterAddresses]))
}
}
internal object BFTSmartConfigSpec : Configuration.Specification<BFTSmartConfig>("BFTSmartConfig") {
private val replicaId by int()
private val clusterAddresses by string().mapValid(::toNetworkHostAndPort).listOrEmpty()
private val debug by boolean().optional().withDefaultValue(false)
private val exposeRaces by boolean().optional().withDefaultValue(false)
override fun parseValid(configuration: Config): Valid<BFTSmartConfig> {
return valid(BFTSmartConfig(configuration[replicaId], configuration[clusterAddresses], configuration[debug], configuration[exposeRaces]))
}
}
internal object NodeRpcSettingsSpec : Configuration.Specification<NodeRpcSettings>("NodeRpcSettings") {
internal object BrokerRpcSslOptionsSpec : Configuration.Specification<BrokerRpcSslOptions>("BrokerRpcSslOptions") {
private val keyStorePath by string().mapValid(::toPath)

View File

@ -188,12 +188,10 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri
lateinit var ourNames: Set<CordaX500Name>
// Allows us to cheaply eliminate keys we know belong to others by using the cache contents without triggering loading.
fun stripCachedPeerKeys(keys: Iterable<PublicKey>): Iterable<PublicKey> {
return keys.filter {
val party = keyToParties.getIfCached(mapToKey(it))?.party?.name
party == null || party in ourNames
}
// Allows us to eliminate keys we know belong to others by using the cache contents that might have been seen during other identity activity.
// Concentrating activity on the identity cache works better than spreading checking across identity and key management, because we cache misses too.
fun stripNotOurKeys(keys: Iterable<PublicKey>): Iterable<PublicKey> {
return keys.filter { certificateFromKey(it)?.name in ourNames }
}
}

View File

@ -1,13 +1,13 @@
package net.corda.node.services.keys
import net.corda.core.crypto.*
import net.corda.core.crypto.internal.AliasPrivateKey
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.internal.NamedCacheFactory
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.serialization.serialize
import net.corda.core.utilities.MAX_HASH_HEX_SIZE
import net.corda.node.services.identity.PersistentIdentityService
import net.corda.core.crypto.internal.AliasPrivateKey
import net.corda.node.utilities.AppendOnlyPersistentMap
import net.corda.nodeapi.internal.cryptoservice.CryptoService
import net.corda.nodeapi.internal.persistence.CordaPersistence
@ -103,7 +103,7 @@ class BasicHSMKeyManagementService(cacheFactory: NamedCacheFactory, val identity
}
override fun filterMyKeys(candidateKeys: Iterable<PublicKey>): Iterable<PublicKey> = database.transaction {
identityService.stripCachedPeerKeys(candidateKeys).filter { containsPublicKey(it) } // TODO: bulk cache access.
identityService.stripNotOurKeys(candidateKeys)
}
// Unlike initial keys, freshkey() is related confidential keys and it utilises platform's software key generation

View File

@ -71,7 +71,7 @@ class PersistentKeyManagementService(cacheFactory: NamedCacheFactory, val identi
override val keys: Set<PublicKey> get() = database.transaction { keysMap.allPersisted().map { it.first }.toSet() }
override fun filterMyKeys(candidateKeys: Iterable<PublicKey>): Iterable<PublicKey> = database.transaction {
identityService.stripCachedPeerKeys(candidateKeys).filter { keysMap[it] != null } // TODO: bulk cache access.
identityService.stripNotOurKeys(candidateKeys)
}
override fun freshKey(): PublicKey {

View File

@ -13,7 +13,7 @@ import net.corda.core.node.services.KeyManagementService
import net.corda.core.serialization.serialize
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.minutes
import net.corda.node.internal.NetworkParametersStorageInternal
import net.corda.node.internal.NetworkParametersStorage
import net.corda.node.services.api.NetworkMapCacheInternal
import net.corda.node.services.config.NetworkParameterAcceptanceSettings
import net.corda.node.utilities.NamedThreadFactory
@ -41,7 +41,7 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
private val networkMapClient: NetworkMapClient?,
private val baseDirectory: Path,
private val extraNetworkMapKeys: List<UUID>,
private val networkParametersStorage: NetworkParametersStorageInternal
private val networkParametersStorage: NetworkParametersStorage
) : AutoCloseable {
companion object {
private val logger = contextLogger()

View File

@ -29,6 +29,7 @@ import net.corda.node.utilities.InfrequentlyMutatedCache
import net.corda.node.utilities.NonInvalidatingCache
import net.corda.node.utilities.NonInvalidatingWeightBasedCache
import net.corda.nodeapi.exceptions.DuplicateAttachmentException
import net.corda.nodeapi.exceptions.DuplicateContractClassException
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
import net.corda.nodeapi.internal.persistence.currentDBSession
@ -305,11 +306,42 @@ class NodeAttachmentService(
currentDBSession().find(NodeAttachmentService.DBAttachment::class.java, attachmentId.toString()) != null
}
private fun verifyVersionUniquenessForSignedAttachments(contractClassNames: List<ContractClassName>, contractVersion: Int, signers: List<PublicKey>?){
if (signers != null && signers.isNotEmpty()) {
contractClassNames.forEach {
val existingContractsImplementations = queryAttachments(AttachmentQueryCriteria.AttachmentsQueryCriteria(
contractClassNamesCondition = Builder.equal(listOf(it)),
versionCondition = Builder.equal(contractVersion),
uploaderCondition = Builder.`in`(TRUSTED_UPLOADERS),
isSignedCondition = Builder.equal(true))
)
if (existingContractsImplementations.isNotEmpty()) {
throw DuplicateContractClassException(it, contractVersion, existingContractsImplementations.map { it.toString() })
}
}
}
}
private fun increaseDefaultVersionIfWhitelistedAttachment(contractClassNames: List<ContractClassName>, contractVersionFromFile: Int, attachmentId : AttachmentId) =
if (contractVersionFromFile == DEFAULT_CORDAPP_VERSION) {
val versions = contractClassNames.mapNotNull { servicesForResolution.networkParameters.whitelistedContractImplementations[it]?.indexOf(attachmentId) }.filter { it >= 0 }.map { it + 1 } // +1 as versions starts from 1 not 0
val max = versions.max()
if (max != null && max > contractVersionFromFile) {
val msg = "Updating version of attachment $attachmentId from '$contractVersionFromFile' to '$max'"
if (versions.toSet().size > 1)
log.warn("Several versions based on whitelistedContractImplementations position are available: ${versions.toSet()}. $msg")
else
log.debug(msg)
max
} else contractVersionFromFile
}
else contractVersionFromFile
// TODO: PLT-147: The attachment should be randomised to prevent brute force guessing and thus privacy leaks.
private fun import(jar: InputStream, uploader: String?, filename: String?): AttachmentId {
return database.transaction {
withContractsInJar(jar) { contractClassNames, inputStream ->
require(inputStream !is JarInputStream){"Input stream must not be a JarInputStream"}
require(inputStream !is JarInputStream) { "Input stream must not be a JarInputStream" }
// Read the file into RAM and then calculate its hash. The attachment must fit into memory.
// TODO: Switch to a two-phase insert so we can handle attachments larger than RAM.
@ -322,8 +354,11 @@ class NodeAttachmentService(
if (!hasAttachment(id)) {
checkIsAValidJAR(bytes.inputStream())
val jarSigners = getSigners(bytes)
val contractVersion = getVersion(bytes)
val contractVersion = increaseDefaultVersionIfWhitelistedAttachment(contractClassNames, getVersion(bytes), id)
val session = currentDBSession()
verifyVersionUniquenessForSignedAttachments(contractClassNames, contractVersion, jarSigners)
val attachment = NodeAttachmentService.DBAttachment(
attId = id.toString(),
content = bytes,
@ -335,23 +370,28 @@ class NodeAttachmentService(
)
session.save(attachment)
attachmentCount.inc()
log.info("Stored new attachment $id")
log.info("Stored new attachment: id=$id uploader=$uploader filename=$filename")
contractClassNames.forEach { contractsCache.invalidate(it) }
return@withContractsInJar id
}
if (isUploaderTrusted(uploader)) {
val session = currentDBSession()
val attachment = session.get(NodeAttachmentService.DBAttachment::class.java, id.toString())
// update the `upLoader` field (as the existing attachment may have been resolved from a peer)
// update the `uploader` field (as the existing attachment may have been resolved from a peer)
if (attachment.uploader != uploader) {
verifyVersionUniquenessForSignedAttachments(contractClassNames, attachment.version, attachment.signers)
attachment.uploader = uploader
session.saveOrUpdate(attachment)
log.info("Updated attachment $id with uploader $uploader")
contractClassNames.forEach { contractsCache.invalidate(it) }
// TODO: this is racey. ENT-2870
attachmentCache.invalidate(id)
attachmentContentCache.invalidate(id)
loadAttachmentContent(id)?.let { attachmentAndContent ->
// TODO: this is racey. ENT-2870
attachmentContentCache.put(id, Optional.of(attachmentAndContent))
attachmentCache.put(id, Optional.of(attachmentAndContent.first))
}
return@withContractsInJar id
}
// If the uploader is the same, throw the exception because the attachment cannot be overridden by the same uploader.
}
throw DuplicateAttachmentException(id.toString())
}
@ -359,16 +399,16 @@ class NodeAttachmentService(
}
private fun getSigners(attachmentBytes: ByteArray) =
JarSignatureCollector.collectSigners(JarInputStream(attachmentBytes.inputStream()))
JarSignatureCollector.collectSigners(JarInputStream(attachmentBytes.inputStream()))
private fun getVersion(attachmentBytes: ByteArray) =
JarInputStream(attachmentBytes.inputStream()).use {
try {
it.manifest?.mainAttributes?.getValue(CORDAPP_CONTRACT_VERSION)?.toInt() ?: DEFAULT_CORDAPP_VERSION
} catch (e: NumberFormatException) {
DEFAULT_CORDAPP_VERSION
JarInputStream(attachmentBytes.inputStream()).use {
try {
it.manifest?.mainAttributes?.getValue(CORDAPP_CONTRACT_VERSION)?.toInt() ?: DEFAULT_CORDAPP_VERSION
} catch (e: NumberFormatException) {
DEFAULT_CORDAPP_VERSION
}
}
}
@Suppress("OverridingDeprecatedMember")
override fun importOrGetAttachment(jar: InputStream): AttachmentId {
@ -409,8 +449,8 @@ class NodeAttachmentService(
}
fun toList(): List<AttachmentId> =
if(signed != null) {
if(unsigned != null) {
if (signed != null) {
if (unsigned != null) {
listOf(signed, unsigned)
} else listOf(signed)
} else listOf(unsigned!!)
@ -426,7 +466,8 @@ class NodeAttachmentService(
private fun getContractAttachmentVersions(contractClassName: String): NavigableMap<Version, AttachmentIds> = contractsCache.get(contractClassName) { name ->
val attachmentQueryCriteria = AttachmentQueryCriteria.AttachmentsQueryCriteria(contractClassNamesCondition = Builder.equal(listOf(name)),
versionCondition = Builder.greaterThanOrEqual(0), uploaderCondition = Builder.`in`(TRUSTED_UPLOADERS))
val attachmentSort = AttachmentSort(listOf(AttachmentSort.AttachmentSortColumn(AttachmentSort.AttachmentSortAttribute.VERSION, Sort.Direction.DESC)))
val attachmentSort = AttachmentSort(listOf(AttachmentSort.AttachmentSortColumn(AttachmentSort.AttachmentSortAttribute.VERSION, Sort.Direction.DESC),
AttachmentSort.AttachmentSortColumn(AttachmentSort.AttachmentSortAttribute.INSERTION_DATE, Sort.Direction.DESC)))
database.transaction {
val session = currentDBSession()
val criteriaBuilder = session.criteriaBuilder
@ -443,15 +484,17 @@ class NodeAttachmentService(
val query = session.createQuery(criteriaQuery)
// execution
TreeMap(query.resultList.groupBy { it.version }.map { makeAttachmentIds(it) }.toMap())
TreeMap(query.resultList.groupBy { it.version }.map { makeAttachmentIds(it, name) }.toMap())
}
}
private fun makeAttachmentIds(it: Map.Entry<Int, List<DBAttachment>>): Pair<Version, AttachmentIds> {
check(it.value.size <= 2)
val signed = it.value.filter { it.signers?.isNotEmpty() ?: false }.map { AttachmentId.parse(it.attId) }.singleOrNull()
val unsigned = it.value.filter { it.signers?.isEmpty() ?: true }.map { AttachmentId.parse(it.attId) }.singleOrNull()
return it.key to AttachmentIds(signed, unsigned)
private fun makeAttachmentIds(it: Map.Entry<Int, List<DBAttachment>>, contractClassName: String): Pair<Version, AttachmentIds> {
val signed = it.value.filter { it.signers?.isNotEmpty() ?: false }.map { AttachmentId.parse(it.attId) }
check (signed.size <= 1) //sanity check
val unsigned = it.value.filter { it.signers?.isEmpty() ?: true }.map { AttachmentId.parse(it.attId) }
if (unsigned.size > 1)
log.warn("Selecting attachment ${unsigned.first()} from duplicated, unsigned attachments ${unsigned.map { it.toString() }} for contract $contractClassName version '${it.key}'.")
return it.key to AttachmentIds(signed.singleOrNull(), unsigned.firstOrNull())
}
override fun getContractAttachmentWithHighestContractVersion(contractClassName: String, minContractVersion: Int): AttachmentId? {
@ -464,5 +507,4 @@ class NodeAttachmentService(
val versions: NavigableMap<Version, AttachmentIds> = getContractAttachmentVersions(contractClassName)
return versions.values.flatMap { it.toList() }.toSet()
}
}

View File

@ -60,7 +60,8 @@ class NodeSchemaService(private val extraSchemas: Set<MappedSchema> = emptySet()
fun internalSchemas() = requiredSchemas.keys + extraSchemas.filter { schema -> // when mapped schemas from the finance module are present, they are considered as internal ones
schema::class.qualifiedName == "net.corda.finance.schemas.CashSchemaV1" ||
schema::class.qualifiedName == "net.corda.finance.schemas.CommercialPaperSchemaV1" ||
schema::class.qualifiedName == "net.corda.node.services.transactions.NodeNotarySchemaV1"
schema::class.qualifiedName == "net.corda.node.services.transactions.NodeNotarySchemaV1" ||
schema::class.qualifiedName?.startsWith("net.corda.notary.") ?: false
}
override val schemaOptions: Map<MappedSchema, SchemaService.SchemaOptions> = requiredSchemas + extraSchemas.associateBy({ it }, { SchemaOptions() })

View File

@ -1,13 +1,28 @@
package net.corda.node.services.transactions
import net.corda.core.internal.concurrent.fork
import net.corda.core.concurrent.CordaFuture
import net.corda.core.contracts.Attachment
import net.corda.core.internal.TransactionVerifierServiceInternal
import net.corda.core.internal.concurrent.openFuture
import net.corda.core.internal.prepareVerify
import net.corda.core.node.services.TransactionVerifierService
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.transactions.LedgerTransaction
import java.util.concurrent.Executors
import net.corda.nodeapi.internal.persistence.withoutDatabaseAccess
class InMemoryTransactionVerifierService(numberOfWorkers: Int) : SingletonSerializeAsToken(), TransactionVerifierService, AutoCloseable {
private val workerPool = Executors.newFixedThreadPool(numberOfWorkers)
override fun verify(transaction: LedgerTransaction) = workerPool.fork(transaction::verify)
override fun close() = workerPool.shutdown()
class InMemoryTransactionVerifierService(numberOfWorkers: Int) : SingletonSerializeAsToken(), TransactionVerifierService, TransactionVerifierServiceInternal, AutoCloseable {
override fun verify(transaction: LedgerTransaction): CordaFuture<Unit> = this.verify(transaction, emptyList())
override fun verify(transaction: LedgerTransaction, extraAttachments: List<Attachment>): CordaFuture<Unit> {
return openFuture<Unit>().apply {
capture {
val verifier = transaction.prepareVerify(extraAttachments)
withoutDatabaseAccess {
verifier.verify()
}
}
}
}
override fun close() {}
}

View File

@ -72,13 +72,13 @@ class NonValidatingNotaryFlow(otherSideSession: FlowSession, service: SinglePart
if (attachedParameterHash == null) {
throw IllegalArgumentException("Transaction must contain network parameters.")
}
val attachedParameters = serviceHub.networkParametersStorage.lookup(attachedParameterHash)
val attachedParameters = serviceHub.networkParametersService.lookup(attachedParameterHash)
?: throw IllegalStateException("Unable to resolve network parameters from hash: $attachedParameterHash")
checkInWhitelist(attachedParameters, notary)
} else {
// Using current network parameters for platform versions 3 or earlier.
val defaultParams = with(serviceHub.networkParametersStorage) {
val defaultParams = with(serviceHub.networkParametersService) {
lookup(currentHash)
?: throw IllegalStateException("Unable to verify whether the notary $notary is whitelisted: current network parameters not set.")
}

View File

@ -231,11 +231,12 @@ class HibernateAttachmentQueryCriteriaParser(override val criteriaBuilder: Crite
}
criteria.isSignedCondition?.let { isSigned ->
val joinDBAttachmentToSigners = root.joinList<NodeAttachmentService.DBAttachment, PublicKey>("signers")
if (isSigned == Builder.equal(true))
if (isSigned == Builder.equal(true)) {
val joinDBAttachmentToSigners = root.joinList<NodeAttachmentService.DBAttachment, PublicKey>("signers")
predicateSet.add(criteriaBuilder.and(joinDBAttachmentToSigners.isNotNull))
else
predicateSet.add(criteriaBuilder.and(joinDBAttachmentToSigners.isNull))
} else {
predicateSet.add(criteriaBuilder.equal(criteriaBuilder.size(root.get<List<PublicKey>?>("signers")),0))
}
}
criteria.versionCondition?.let {

View File

@ -16,6 +16,7 @@ import net.corda.core.schemas.PersistentStateRef
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.transactions.*
import net.corda.core.utilities.*
import net.corda.node.cordapp.CordappLoader
import net.corda.node.services.api.SchemaService
import net.corda.node.services.api.VaultServiceInternal
import net.corda.node.services.schema.PersistentStateService
@ -57,7 +58,8 @@ class NodeVaultService(
private val keyManagementService: KeyManagementService,
private val servicesForResolution: ServicesForResolution,
private val database: CordaPersistence,
private val schemaService: SchemaService
private val schemaService: SchemaService,
private val appClassloader: ClassLoader
) : SingletonSerializeAsToken(), VaultServiceInternal {
private companion object {
private val log = contextLogger()
@ -637,7 +639,7 @@ class NodeVaultService(
val unknownTypes = mutableSetOf<String>()
distinctTypes.forEach { type ->
val concreteType: Class<ContractState>? = try {
uncheckedCast(Class.forName(type))
uncheckedCast(Class.forName(type, true, appClassloader))
} catch (e: ClassNotFoundException) {
unknownTypes += type
null

View File

@ -8,8 +8,7 @@ import net.corda.nodeapi.internal.persistence.DatabaseTransaction
import net.corda.nodeapi.internal.persistence.contextTransaction
import net.corda.nodeapi.internal.persistence.currentDBSession
import java.lang.ref.WeakReference
import java.util.HashSet
import java.util.NoSuchElementException
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
@ -137,19 +136,6 @@ abstract class AppendOnlyPersistentMapBase<K, V, E, out EK>(
operator fun contains(key: K) = get(key) != null
/**
* Allow checking the cache content without falling back to database if there's a miss.
*
* @param key The cache key
* @return The value in the cache, or null if not present.
*/
fun getIfCached(key: K): V? {
val transactional = cache.getIfPresent(key!!)
return if (transactional?.isPresent ?: false) {
transactional?.value
} else null
}
/**
* Removes all of the mappings from this map and underlying storage. The map will be empty after this call returns.
* WARNING!! The method is not thread safe.

View File

@ -0,0 +1,93 @@
package net.corda.node.utilities
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.internal.cordapp.CordappImpl
import net.corda.core.internal.notary.NotaryService
import net.corda.core.utilities.contextLogger
import net.corda.node.SerialFilter
import net.corda.node.VersionInfo
import net.corda.node.cordapp.CordappLoader
import net.corda.node.internal.cordapp.VirtualCordapp
import net.corda.node.services.api.ServiceHubInternal
import net.corda.node.services.config.NotaryConfig
import net.corda.node.services.transactions.SimpleNotaryService
import net.corda.notary.experimental.bftsmart.BFTSmartNotaryService
import net.corda.notary.experimental.raft.RaftNotaryService
import java.security.PublicKey
class NotaryLoader(
private val config: NotaryConfig,
versionInfo: VersionInfo
) {
companion object {
private val log = contextLogger()
}
/**
* A virtual CorDapp containing the notary implementation if one of the built-in notaries is used.
* [Null] if a notary implementation is expected to be loaded from an external CorDapp.
*/
val builtInNotary: CordappImpl?
private val builtInServiceClass: Class<out NotaryService>?
init {
builtInServiceClass = if (config.className.isNullOrBlank()) {
// Using a built-in notary
when {
config.bftSMaRt != null -> {
builtInNotary = VirtualCordapp.generateBFTSmartNotary(versionInfo)
BFTSmartNotaryService::class.java
}
config.raft != null -> {
builtInNotary = VirtualCordapp.generateRaftNotary(versionInfo)
RaftNotaryService::class.java
}
else -> {
builtInNotary = VirtualCordapp.generateSimpleNotary(versionInfo)
SimpleNotaryService::class.java
}
}
} else {
// Using a notary from an external CorDapp
builtInNotary = null
null
}
}
fun loadService(myNotaryIdentity: PartyAndCertificate?, services: ServiceHubInternal, cordappLoader: CordappLoader): NotaryService {
val serviceClass = builtInServiceClass ?: scanCorDapps(cordappLoader)
log.info("Starting notary service: $serviceClass")
val notaryKey = myNotaryIdentity?.owningKey
?: throw IllegalArgumentException("Unable to start notary service $serviceClass: notary identity not found")
/** Some notary implementations only work with Java serialization. */
maybeInstallSerializationFilter(serviceClass)
val constructor = serviceClass
.getDeclaredConstructor(ServiceHubInternal::class.java, PublicKey::class.java)
.apply { isAccessible = true }
return constructor.newInstance(services, notaryKey)
}
/** Looks for the config specified notary service implementation in loaded CorDapps. This mechanism is for internal use only. */
private fun scanCorDapps(cordappLoader: CordappLoader): Class<out NotaryService> {
val loadedImplementations = cordappLoader.cordapps.mapNotNull { it.notaryService }
log.debug("Notary service implementations found: ${loadedImplementations.joinToString(", ")}")
return loadedImplementations.firstOrNull { it.name == config.className }
?: throw IllegalArgumentException("The notary service implementation specified in the configuration: ${config.className} is not found. Available implementations: ${loadedImplementations.joinToString(", ")}}")
}
/** Installs a custom serialization filter defined by a notary service implementation. Only supported in dev mode. */
private fun maybeInstallSerializationFilter(serviceClass: Class<out NotaryService>) {
try {
@Suppress("UNCHECKED_CAST")
val filter = serviceClass
.getDeclaredMethod("getSerializationFilter")
.invoke(null) as ((Class<*>) -> Boolean)
SerialFilter.install(filter)
} catch (e: NoSuchMethodException) {
// No custom serialization filter declared
}
}
}

View File

@ -43,7 +43,9 @@ open class NetworkRegistrationHelper(
networkRootTrustStorePassword: String,
private val nodeCaKeyAlias: String,
private val certRole: CertRole,
private val nextIdleDuration: (Duration?) -> Duration? = FixedPeriodLimitedRetrialStrategy(10, Duration.ofMinutes(1))
private val nextIdleDuration: (Duration?) -> Duration? = FixedPeriodLimitedRetrialStrategy(10, Duration.ofMinutes(1)),
protected val logProgress: (String) -> Unit = ::println,
protected val logError: (String) -> Unit = System.err::println
) {
companion object {
@ -86,7 +88,7 @@ open class NetworkRegistrationHelper(
// SELF_SIGNED_PRIVATE_KEY is used as progress indicator.
if (certStore.contains(nodeCaKeyAlias) && !certStore.contains(SELF_SIGNED_PRIVATE_KEY)) {
println("Certificate already exists, Corda node will now terminate...")
logProgress("Certificate already exists, Corda node will now terminate...")
return
}
@ -106,7 +108,7 @@ open class NetworkRegistrationHelper(
certStore.setCertPathOnly(nodeCaKeyAlias, nodeCaCertificates)
certStore.value.internal.deleteEntry(SELF_SIGNED_PRIVATE_KEY)
certStore.value.save()
println("Private key '$nodeCaKeyAlias' and its certificate-chain stored successfully.")
logProgress("Private key '$nodeCaKeyAlias' and its certificate-chain stored successfully.")
onSuccess(nodeCaPublicKey, cryptoService.getSigner(nodeCaKeyAlias), nodeCaCertificates, tlsCrlIssuerCert?.subjectX500Principal?.toX500Name())
// All done, clean up temp files.
@ -124,9 +126,9 @@ open class NetworkRegistrationHelper(
private fun getTlsCrlIssuerCert(): X509Certificate? {
val tlsCrlIssuerCert = validateAndGetTlsCrlIssuerCert()
if (tlsCrlIssuerCert == null && isTlsCrlIssuerCertRequired()) {
System.err.println("""tlsCrlIssuerCert config does not match the root certificate issuer and nor is there any other certificate in the trust store with a matching issuer.
logError("""tlsCrlIssuerCert config does not match the root certificate issuer and nor is there any other certificate in the trust store with a matching issuer.
| Please make sure the config is correct or that the correct certificate for the CRL issuer is added to the node's trust store.
| The node will now terminate.""".trimMargin())
| The node registration will now terminate.""".trimMargin())
throw IllegalArgumentException("TLS CRL issuer certificate not found in the trust store.")
}
return tlsCrlIssuerCert
@ -161,7 +163,7 @@ open class NetworkRegistrationHelper(
// Validate certificate chain returned from the doorman with the root cert obtained via out-of-band process, to prevent MITM attack on doorman server.
X509Utilities.validateCertificateChain(rootCert, certificates)
println("Certificate signing request approved, storing private key with the certificate chain.")
logProgress("Certificate signing request approved, storing private key with the certificate chain.")
}
private fun CertificateStore.loadOrCreateKeyPair(alias: String, entryPassword: String = password): KeyPair {
@ -188,7 +190,7 @@ open class NetworkRegistrationHelper(
*/
private fun pollServerForCertificates(requestId: String): List<X509Certificate> {
try {
println("Start polling server for certificate signing approval.")
logProgress("Start polling server for certificate signing approval.")
// Poll server to download the signed certificate once request has been approved.
var idlePeriodDuration: Duration? = null
while (true) {
@ -209,9 +211,9 @@ open class NetworkRegistrationHelper(
}
}
} catch (certificateRequestException: CertificateRequestException) {
System.err.println(certificateRequestException.message)
System.err.println("Please make sure the details in configuration file are correct and try again.")
System.err.println("Corda node will now terminate.")
certificateRequestException.message?.let { logError(it) }
logError("Please make sure the details in configuration file are correct and try again.")
logError("Corda node registration will now terminate.")
requestIdStore.deleteIfExists()
throw certificateRequestException
}
@ -233,24 +235,21 @@ open class NetworkRegistrationHelper(
JcaPEMWriter(writer).use {
it.writeObject(PemObject("CERTIFICATE REQUEST", request.encoded))
}
println("Certificate signing request with the following information will be submitted to the Corda certificate signing server.")
println()
println("Legal Name: $myLegalName")
println("Email: $emailAddress")
println()
println("Public Key: $publicKey")
println()
println("$writer")
logProgress("Certificate signing request with the following information will be submitted to the Corda certificate signing server.")
logProgress("Legal Name: $myLegalName")
logProgress("Email: $emailAddress")
logProgress("Public Key: $publicKey")
logProgress("$writer")
// Post request to signing server via http.
println("Submitting certificate signing request to Corda certificate signing server.")
logProgress("Submitting certificate signing request to Corda certificate signing server.")
val requestId = certService.submitRequest(request)
// Persists request ID to file in case of node shutdown.
requestIdStore.writeLines(listOf(requestId))
println("Successfully submitted request to Corda certificate signing server, request ID: $requestId.")
logProgress("Successfully submitted request to Corda certificate signing server, request ID: $requestId.")
requestId
} else {
val requestId = requestIdStore.readLines { it.findFirst().get() }
println("Resuming from previous certificate signing request, request ID: $requestId.")
logProgress("Resuming from previous certificate signing request, request ID: $requestId.")
requestId
}
} catch (e: Exception) {
@ -274,15 +273,19 @@ class NodeRegistrationException(
class NodeRegistrationHelper(
private val config: NodeConfiguration,
certService: NetworkRegistrationService, regConfig: NodeRegistrationOption, computeNextIdleDoormanConnectionPollInterval: (Duration?) -> Duration? = FixedPeriodLimitedRetrialStrategy(10, Duration.ofMinutes(1))) :
NetworkRegistrationHelper(
config,
certService,
regConfig.networkRootTrustStorePath,
regConfig.networkRootTrustStorePassword,
CORDA_CLIENT_CA,
CertRole.NODE_CA,
computeNextIdleDoormanConnectionPollInterval) {
certService: NetworkRegistrationService,
regConfig: NodeRegistrationOption,
computeNextIdleDoormanConnectionPollInterval: (Duration?) -> Duration? = FixedPeriodLimitedRetrialStrategy(10, Duration.ofMinutes(1)),
logProgress: (String) -> Unit = ::println,
logError: (String) -> Unit = System.err::println) :
NetworkRegistrationHelper(
config,
certService,
regConfig.networkRootTrustStorePath,
regConfig.networkRootTrustStorePassword,
CORDA_CLIENT_CA,
CertRole.NODE_CA,
computeNextIdleDoormanConnectionPollInterval, logProgress, logError) {
companion object {
val logger = contextLogger()
@ -297,7 +300,7 @@ class NodeRegistrationHelper(
val keyStore = config.p2pSslOptions.keyStore
val certificateStore = keyStore.get(createNew = true)
certificateStore.update {
println("Generating SSL certificate for node messaging service.")
logProgress("Generating SSL certificate for node messaging service.")
val sslKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val issuerCertificate = nodeCaCertificateChain.first()
val validityWindow = X509Utilities.getCertificateValidityWindow(DEFAULT_VALIDITY_WINDOW.first, DEFAULT_VALIDITY_WINDOW.second, issuerCertificate)
@ -319,7 +322,7 @@ class NodeRegistrationHelper(
X509Utilities.validateCertificateChain(rootCert, sslCertificateChain)
setPrivateKey(CORDA_CLIENT_TLS, sslKeyPair.private, sslCertificateChain, keyStore.entryPassword)
}
println("SSL private key and certificate chain stored in ${keyStore.path}.")
logProgress("SSL private key and certificate chain stored in ${keyStore.path}.")
}
private fun createTruststore(rootCertificate: X509Certificate) {
@ -328,7 +331,7 @@ class NodeRegistrationHelper(
if (this.aliases().hasNext()) {
logger.warn("The node's trust store already exists. The following certificates will be overridden: ${this.aliases().asSequence()}")
}
println("Generating trust store for corda node.")
logProgress("Generating trust store for corda node.")
// Assumes certificate chain always starts with client certificate and end with root certificate.
setCertificate(CORDA_ROOT_CA, rootCertificate)
// Copy remaining certificates from the network-trust-store
@ -338,7 +341,7 @@ class NodeRegistrationHelper(
setCertificate(it, certificate)
}
}
println("Node trust store stored in ${config.p2pSslOptions.trustStore.path}.")
logProgress("Node trust store stored in ${config.p2pSslOptions.trustStore.path}.")
}
override fun validateAndGetTlsCrlIssuerCert(): X509Certificate? {

View File

@ -0,0 +1,358 @@
package net.corda.notary.experimental.bftsmart
import bftsmart.communication.ServerCommunicationSystem
import bftsmart.communication.client.netty.NettyClientServerCommunicationSystemClientSide
import bftsmart.communication.client.netty.NettyClientServerSession
import bftsmart.statemanagement.strategy.StandardStateManager
import bftsmart.tom.MessageContext
import bftsmart.tom.ServiceProxy
import bftsmart.tom.ServiceReplica
import bftsmart.tom.core.TOMLayer
import bftsmart.tom.core.messages.TOMMessage
import bftsmart.tom.server.defaultservices.DefaultRecoverable
import bftsmart.tom.server.defaultservices.DefaultReplier
import bftsmart.tom.util.Extractor
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.TimeWindow
import net.corda.core.crypto.*
import net.corda.core.flows.NotarisationPayload
import net.corda.core.flows.NotarisationRequestSignature
import net.corda.core.flows.NotaryError
import net.corda.core.flows.StateConsumptionDetails
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.internal.declaredField
import net.corda.core.internal.notary.NotaryInternalException
import net.corda.core.internal.notary.isConsumedByTheSameTx
import net.corda.core.internal.notary.validateTimeWindow
import net.corda.core.internal.toTypedArray
import net.corda.core.schemas.PersistentStateRef
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import net.corda.node.services.api.ServiceHubInternal
import net.corda.node.services.transactions.PersistentUniquenessProvider
import net.corda.node.utilities.AppendOnlyPersistentMap
import net.corda.nodeapi.internal.persistence.currentDBSession
import net.corda.notary.experimental.bftsmart.BFTSmart.Client
import net.corda.notary.experimental.bftsmart.BFTSmart.Replica
import java.nio.file.Path
import java.security.PublicKey
import java.util.*
/**
* Implements a replicated transaction commit log based on the [BFT-SMaRt](https://github.com/bft-smart/library)
* consensus algorithm. Every replica in the cluster is running a [Replica] maintaining the state, and a [Client] is used
* to relay state modification requests to all [Replica]s.
*/
// TODO: Define and document the configuration of the bft-smart cluster.
// TODO: Potentially update the bft-smart API for our use case or rebuild client and server from lower level building
// blocks bft-smart provides.
// TODO: Support cluster membership changes. This requires reading about reconfiguration of bft-smart clusters and
// perhaps a design doc. In general, it seems possible to use the state machine to reconfigure the cluster (reaching
// consensus about membership changes). Nodes that join the cluster for the first time or re-join can go through
// a "recovering" state and request missing data from their peers.
object BFTSmart {
/** Sent from [Client] to [Replica]. */
@CordaSerializable
data class CommitRequest(val payload: NotarisationPayload, val callerIdentity: Party)
/** Sent from [Replica] to [Client]. */
@CordaSerializable
sealed class ReplicaResponse {
data class Error(val error: SignedData<NotaryError>) : ReplicaResponse()
data class Signature(val txSignature: TransactionSignature) : ReplicaResponse()
}
/** An aggregate response from all replica ([Replica]) replies sent from [Client] back to the calling application. */
@CordaSerializable
sealed class ClusterResponse {
data class Error(val errors: List<SignedData<NotaryError>>) : ClusterResponse()
data class Signatures(val txSignatures: List<TransactionSignature>) : ClusterResponse()
}
interface Cluster {
/** Avoid bug where a replica fails to start due to a consensus change during the BFT startup sequence. */
fun waitUntilAllReplicasHaveInitialized()
}
class Client(config: BFTSmartConfigInternal, private val clientId: Int, private val cluster: Cluster, private val notaryService: BFTSmartNotaryService) : SingletonSerializeAsToken() {
companion object {
private val log = contextLogger()
}
/** A proxy for communicating with the BFT cluster */
private val proxy = ServiceProxy(clientId, config.path.toString(), buildResponseComparator(), buildExtractor())
private val sessionTable = (proxy.communicationSystem as NettyClientServerCommunicationSystemClientSide).declaredField<Map<Int, NettyClientServerSession>>("sessionTable").value
fun dispose() {
proxy.close() // XXX: Does this do enough?
}
private fun awaitClientConnectionToCluster() {
// TODO: Hopefully we only need to wait for the client's initial connection to the cluster, and this method can be moved to some startup code.
// TODO: Investigate ConcurrentModificationException in this method.
while (true) {
val inactive = sessionTable.entries.mapNotNull { if (it.value.channel.isActive) null else it.key }
if (inactive.isEmpty()) break
log.info("Client-replica channels not yet active: $clientId to $inactive")
Thread.sleep((inactive.size * 100).toLong())
}
}
/**
* Sends a transaction commit request to the BFT cluster. The [proxy] will deliver the request to every
* replica, and block until a sufficient number of replies are received.
*/
fun commitTransaction(payload: NotarisationPayload, otherSide: Party): ClusterResponse {
awaitClientConnectionToCluster()
cluster.waitUntilAllReplicasHaveInitialized()
val requestBytes = CommitRequest(payload, otherSide).serialize().bytes
val responseBytes = proxy.invokeOrdered(requestBytes)
return responseBytes.deserialize()
}
/** A comparator to check if replies from two replicas are the same. */
private fun buildResponseComparator(): Comparator<ByteArray> {
return Comparator { o1, o2 ->
val reply1 = o1.deserialize<ReplicaResponse>()
val reply2 = o2.deserialize<ReplicaResponse>()
if (reply1 is ReplicaResponse.Error && reply2 is ReplicaResponse.Error) {
// TODO: for now we treat all errors as equal, compare by error type as well
0
} else if (reply1 is ReplicaResponse.Signature && reply2 is ReplicaResponse.Signature) 0 else -1
}
}
/** An extractor to build the final response message for the client application from all received replica replies. */
private fun buildExtractor(): Extractor {
return Extractor { replies, _, lastReceived ->
val responses = replies.mapNotNull { it?.content?.deserialize<ReplicaResponse>() }
val accepted = responses.filterIsInstance<ReplicaResponse.Signature>()
val rejected = responses.filterIsInstance<ReplicaResponse.Error>()
log.debug { "BFT Client $clientId: number of replicas accepted the commit: ${accepted.size}, rejected: ${rejected.size}" }
// TODO: only return an aggregate if the majority of signatures are replies
// TODO: return an error reported by the majority and not just the first one
val aggregateResponse = if (accepted.isNotEmpty()) {
log.debug { "Cluster response - signatures: ${accepted.map { it.txSignature }}" }
ClusterResponse.Signatures(accepted.map { it.txSignature })
} else {
log.debug { "Cluster response - error: ${rejected.first().error}" }
ClusterResponse.Error(rejected.map { it.error })
}
val messageContent = aggregateResponse.serialize().bytes
// TODO: is it safe use the last message for sender/session/sequence info
val reply = replies[lastReceived]
TOMMessage(reply.sender, reply.session, reply.sequence, messageContent, reply.viewID)
}
}
}
/** ServiceReplica doesn't have any kind of shutdown method, so we add one in this subclass. */
private class CordaServiceReplica(replicaId: Int, configHome: Path, owner: DefaultRecoverable) : ServiceReplica(replicaId, configHome.toString(), owner, owner, null, DefaultReplier()) {
private val tomLayerField = declaredField<TOMLayer>(ServiceReplica::class, "tomLayer")
private val csField = declaredField<ServerCommunicationSystem>(ServiceReplica::class, "cs")
fun dispose() {
// Half of what restart does:
val tomLayer = tomLayerField.value
tomLayer.shutdown() // Non-blocking.
val cs = csField.value
cs.join()
cs.serversConn.join()
tomLayer.join()
tomLayer.deliveryThread.join()
// TODO: At the cluster level, join all Sender/Receiver threads.
}
}
/**
* Maintains the commit log and executes commit commands received from the [Client].
*
* The validation logic can be specified by implementing the [executeCommand] method.
*/
abstract class Replica(config: BFTSmartConfigInternal,
replicaId: Int,
createMap: () -> AppendOnlyPersistentMap<StateRef, SecureHash,
BFTSmartNotaryService.CommittedState, PersistentStateRef>,
protected val services: ServiceHubInternal,
protected val notaryIdentityKey: PublicKey) : DefaultRecoverable() {
companion object {
private val log = contextLogger()
}
private val stateManagerOverride = run {
// Mock framework shutdown is not in reverse order, and we need to stop the faulty replicas first:
val exposeStartupRace = config.exposeRaces && replicaId < maxFaultyReplicas(config.clusterSize)
object : StandardStateManager() {
override fun askCurrentConsensusId() {
if (exposeStartupRace) Thread.sleep(20000) // Must be long enough for the non-redundant replicas to reach a non-initial consensus.
super.askCurrentConsensusId()
}
}
}
override fun getStateManager() = stateManagerOverride
// Must be initialised before ServiceReplica is started
private val commitLog = services.database.transaction { createMap() }
private val replica = run {
config.waitUntilReplicaWillNotPrintStackTrace(replicaId)
@Suppress("LeakingThis")
(CordaServiceReplica(replicaId, config.path, this))
}
fun dispose() {
replica.dispose()
}
override fun appExecuteUnordered(command: ByteArray, msgCtx: MessageContext): ByteArray? {
throw NotImplementedError("No unordered operations supported")
}
override fun appExecuteBatch(command: Array<ByteArray>, mcs: Array<MessageContext>): Array<ByteArray?> {
return Arrays.stream(command).map(this::executeCommand).toTypedArray()
}
/**
* Implement logic to execute the command and commit the transaction to the log.
* Helper methods are provided for transaction processing: [commitInputStates], and [sign].
*/
abstract fun executeCommand(command: ByteArray): ByteArray?
private fun checkConflict(
conflictingStates: LinkedHashMap<StateRef, StateConsumptionDetails>,
states: List<StateRef>,
type: StateConsumptionDetails.ConsumedStateType
) {
states.forEach { stateRef ->
commitLog[stateRef]?.let { conflictingStates[stateRef] = StateConsumptionDetails(it.sha256(), type) }
}
}
protected fun commitInputStates(
states: List<StateRef>,
txId: SecureHash,
callerName: CordaX500Name,
requestSignature: NotarisationRequestSignature,
timeWindow: TimeWindow?,
references: List<StateRef> = emptyList()
) {
log.debug { "Attempting to commit inputs for transaction: $txId" }
services.database.transaction {
logRequest(txId, callerName, requestSignature)
val conflictingStates = LinkedHashMap<StateRef, StateConsumptionDetails>()
checkConflict(conflictingStates, states, StateConsumptionDetails.ConsumedStateType.INPUT_STATE)
checkConflict(conflictingStates, references, StateConsumptionDetails.ConsumedStateType.REFERENCE_INPUT_STATE)
if (conflictingStates.isNotEmpty()) {
if (states.isEmpty()) {
handleReferenceConflicts(txId, conflictingStates)
} else {
handleConflicts(txId, conflictingStates)
}
} else {
handleNoConflicts(timeWindow, states, txId)
}
}
}
private fun previouslyCommitted(txId: SecureHash): Boolean {
val session = currentDBSession()
return session.find(BFTSmartNotaryService.CommittedTransaction::class.java, txId.toString()) != null
}
private fun handleReferenceConflicts(txId: SecureHash, conflictingStates: LinkedHashMap<StateRef, StateConsumptionDetails>) {
if (!previouslyCommitted(txId)) {
val conflictError = NotaryError.Conflict(txId, conflictingStates)
log.debug { "Failure, input states already committed: ${conflictingStates.keys}" }
throw NotaryInternalException(conflictError)
}
log.debug { "Transaction $txId already notarised" }
}
private fun handleConflicts(txId: SecureHash, conflictingStates: LinkedHashMap<StateRef, StateConsumptionDetails>) {
if (isConsumedByTheSameTx(txId.sha256(), conflictingStates)) {
log.debug { "Transaction $txId already notarised" }
return
} else {
log.debug { "Failure, input states already committed: ${conflictingStates.keys}" }
val conflictError = NotaryError.Conflict(txId, conflictingStates)
throw NotaryInternalException(conflictError)
}
}
private fun handleNoConflicts(timeWindow: TimeWindow?, states: List<StateRef>, txId: SecureHash) {
// Skip if this is a re-notarisation of a reference-only transaction
if (states.isEmpty() && previouslyCommitted(txId)) {
return
}
val outsideTimeWindowError = validateTimeWindow(services.clock.instant(), timeWindow)
if (outsideTimeWindowError == null) {
states.forEach { stateRef ->
commitLog[stateRef] = txId
}
val session = currentDBSession()
session.persist(BFTSmartNotaryService.CommittedTransaction(txId.toString()))
log.debug { "Successfully committed all input states: $states" }
} else {
throw NotaryInternalException(outsideTimeWindowError)
}
}
private fun logRequest(txId: SecureHash, callerName: CordaX500Name, requestSignature: NotarisationRequestSignature) {
val request = PersistentUniquenessProvider.Request(
consumingTxHash = txId.toString(),
partyName = callerName.toString(),
requestSignature = requestSignature.serialize().bytes,
requestDate = services.clock.instant()
)
val session = currentDBSession()
session.persist(request)
}
/** Generates a signature over an arbitrary array of bytes. */
protected fun sign(bytes: ByteArray): DigitalSignature.WithKey {
return services.database.transaction { services.keyManagementService.sign(bytes, notaryIdentityKey) }
}
/** Generates a transaction signature over the specified transaction [txId]. */
protected fun sign(txId: SecureHash): TransactionSignature {
val signableData = SignableData(txId, SignatureMetadata(services.myInfo.platformVersion, Crypto.findSignatureScheme(notaryIdentityKey).schemeNumberID))
return services.database.transaction { services.keyManagementService.sign(signableData, notaryIdentityKey) }
}
// TODO:
// - Test snapshot functionality with different bft-smart cluster configurations.
// - Add streaming to support large data sets.
override fun getSnapshot(): ByteArray {
// LinkedHashMap for deterministic serialisation
val committedStates = LinkedHashMap<StateRef, SecureHash>()
val requests = services.database.transaction {
commitLog.allPersisted().forEach { committedStates[it.first] = it.second }
val criteriaQuery = session.criteriaBuilder.createQuery(PersistentUniquenessProvider.Request::class.java)
criteriaQuery.select(criteriaQuery.from(PersistentUniquenessProvider.Request::class.java))
session.createQuery(criteriaQuery).resultList
}
return (committedStates to requests).serialize().bytes
}
override fun installSnapshot(bytes: ByteArray) {
val (committedStates, requests) = bytes.deserialize<Pair<LinkedHashMap<StateRef, SecureHash>, List<PersistentUniquenessProvider.Request>>>()
services.database.transaction {
commitLog.clear()
commitLog.putAll(committedStates)
val deleteQuery = session.criteriaBuilder.createCriteriaDelete(PersistentUniquenessProvider.Request::class.java)
deleteQuery.from(PersistentUniquenessProvider.Request::class.java)
session.createQuery(deleteQuery).executeUpdate()
requests.forEach { session.persist(it) }
}
}
}
}

View File

@ -0,0 +1,112 @@
package net.corda.notary.experimental.bftsmart
import net.corda.core.internal.div
import net.corda.core.internal.writer
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import net.corda.node.services.transactions.PathManager
import java.io.PrintWriter
import java.net.InetAddress
import java.net.Socket
import java.net.SocketException
import java.nio.file.Files
import java.util.concurrent.TimeUnit.MILLISECONDS
data class BFTSmartConfig(
/** The zero-based index of the current replica. All replicas must specify a unique replica id. */
val replicaId: Int,
/**
* Must list the addresses of all the members in the cluster. At least one of the members must be active and
* be able to communicate with the cluster leader for the node to join the cluster. If empty,
* a new cluster will be bootstrapped.
*/
val clusterAddresses: List<NetworkHostAndPort>,
val debug: Boolean = false,
/** Used for testing purposes only. */
val exposeRaces: Boolean = false
) {
init {
require(replicaId >= 0) { "replicaId cannot be negative" }
}
}
/**
* BFT Smart can only be configured via files in a configHome directory.
* Each instance of this class creates such a configHome, accessible via [path].
* The files are deleted on [close] typically via [use], see [PathManager] for details.
*/
class BFTSmartConfigInternal(private val replicaAddresses: List<NetworkHostAndPort>, debug: Boolean, val exposeRaces: Boolean) : PathManager<BFTSmartConfigInternal>(Files.createTempDirectory("bft-smart-config")) {
companion object {
private val log = contextLogger()
internal const val portIsClaimedFormat = "Port %s is claimed by another replica: %s"
}
val clusterSize get() = replicaAddresses.size
init {
val claimedPorts = mutableSetOf<NetworkHostAndPort>()
val n = clusterSize
(0 until n).forEach { replicaId ->
// Each replica claims the configured port and the next one:
replicaPorts(replicaId).forEach { port ->
claimedPorts.add(port) || throw IllegalArgumentException(portIsClaimedFormat.format(port, claimedPorts))
}
}
configWriter("hosts.config") {
replicaAddresses.forEachIndexed { index, (host, port) ->
// The documentation strongly recommends IP addresses:
println("$index ${InetAddress.getByName(host).hostAddress} $port")
}
}
val systemConfig = String.format(javaClass.getResource("system.config.printf").readText(), n, maxFaultyReplicas(n), if (debug) 1 else 0, (0 until n).joinToString(","))
configWriter("system.config") {
print(systemConfig)
}
}
private fun configWriter(name: String, block: PrintWriter.() -> Unit) {
// Default charset, consistent with loaders:
(path / name).writer().use {
PrintWriter(it).use {
it.run(block)
}
}
}
fun waitUntilReplicaWillNotPrintStackTrace(contextReplicaId: Int) {
// A replica will printStackTrace until all lower-numbered replicas are listening.
// But we can't probe a replica without it logging EOFException when our probe succeeds.
// So to keep logging to a minimum we only check the previous replica:
val peerId = contextReplicaId - 1
if (peerId < 0) return
// The printStackTrace we want to avoid is in replica-replica communication code:
val address = BFTSmartPort.FOR_REPLICAS.ofReplica(replicaAddresses[peerId])
log.debug { "Waiting for replica $peerId to start listening on: $address" }
while (!address.isListening()) MILLISECONDS.sleep(200)
log.debug { "Replica $peerId is ready for P2P." }
}
private fun replicaPorts(replicaId: Int): List<NetworkHostAndPort> {
val base = replicaAddresses[replicaId]
return BFTSmartPort.values().map { it.ofReplica(base) }
}
}
private enum class BFTSmartPort(private val off: Int) {
FOR_CLIENTS(0),
FOR_REPLICAS(1);
fun ofReplica(base: NetworkHostAndPort) = NetworkHostAndPort(base.host, base.port + off)
}
private fun NetworkHostAndPort.isListening() = try {
Socket(host, port).use { true } // Will cause one error to be logged in the replica on success.
} catch (e: SocketException) {
false
}
fun maxFaultyReplicas(clusterSize: Int) = (clusterSize - 1) / 3
fun minCorrectReplicas(clusterSize: Int) = (2 * clusterSize + 3) / 3
fun minClusterSize(maxFaultyReplicas: Int) = maxFaultyReplicas * 3 + 1

View File

@ -0,0 +1,210 @@
package net.corda.notary.experimental.bftsmart
import co.paralleluniverse.fibers.Suspendable
import com.google.common.util.concurrent.SettableFuture
import net.corda.core.contracts.StateRef
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SignedData
import net.corda.core.flows.*
import net.corda.core.identity.Party
import net.corda.core.internal.notary.NotaryInternalException
import net.corda.core.internal.notary.NotaryService
import net.corda.core.internal.notary.verifySignature
import net.corda.core.schemas.PersistentStateRef
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.transactions.CoreTransaction
import net.corda.core.transactions.FilteredTransaction
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.unwrap
import net.corda.node.services.api.ServiceHubInternal
import net.corda.node.services.transactions.PersistentUniquenessProvider
import net.corda.node.utilities.AppendOnlyPersistentMap
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
import java.security.PublicKey
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.Id
import javax.persistence.Table
import kotlin.concurrent.thread
/**
* A non-validating notary service operated by a group of parties that don't necessarily trust each other.
*
* A transaction is notarised when the consensus is reached by the cluster on its uniqueness, and time-window validity.
*/
class BFTSmartNotaryService(
override val services: ServiceHubInternal,
override val notaryIdentityKey: PublicKey
) : NotaryService() {
companion object {
private val log = contextLogger()
@JvmStatic
val serializationFilter
get() = { clazz: Class<*> ->
clazz.name.let {
it.startsWith("bftsmart.")
|| it.startsWith("java.security.")
|| it.startsWith("java.util.")
|| it.startsWith("java.lang.")
|| it.startsWith("java.net.")
}
}
}
private val notaryConfig = services.configuration.notary
?: throw IllegalArgumentException("Failed to register ${BFTSmartNotaryService::class.java}: notary configuration not present")
private val bftSMaRtConfig = notaryConfig.bftSMaRt
?: throw IllegalArgumentException("Failed to register ${BFTSmartNotaryService::class.java}: BFT-Smart configuration not present")
private val cluster: BFTSmart.Cluster = makeBFTCluster(notaryIdentityKey, bftSMaRtConfig)
protected open fun makeBFTCluster(notaryKey: PublicKey, bftSMaRtConfig: BFTSmartConfig): BFTSmart.Cluster {
return object : BFTSmart.Cluster {
override fun waitUntilAllReplicasHaveInitialized() {
log.warn("A BFT replica may still be initializing, in which case the upcoming consensus change may cause it to spin.")
}
}
}
private val client: BFTSmart.Client
private val replicaHolder = SettableFuture.create<Replica>()
init {
client = BFTSmartConfigInternal(bftSMaRtConfig.clusterAddresses, bftSMaRtConfig.debug, bftSMaRtConfig.exposeRaces)
.use {
val replicaId = bftSMaRtConfig.replicaId
val configHandle = it.handle()
// Replica startup must be in parallel with other replicas, otherwise the constructor may not return:
thread(name = "BFT SMaRt replica $replicaId init", isDaemon = true) {
configHandle.use {
val replica = Replica(it, replicaId, { createMap() }, services, notaryIdentityKey)
replicaHolder.set(replica)
log.info("BFT SMaRt replica $replicaId is running.")
}
}
BFTSmart.Client(it, replicaId, cluster, this)
}
}
fun waitUntilReplicaHasInitialized() {
log.debug { "Waiting for replica ${bftSMaRtConfig.replicaId} to initialize." }
replicaHolder.getOrThrow() // It's enough to wait for the ServiceReplica constructor to return.
}
fun commitTransaction(payload: NotarisationPayload, otherSide: Party) = client.commitTransaction(payload, otherSide)
override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic<Void?> = ServiceFlow(otherPartySession, this)
private class ServiceFlow(val otherSideSession: FlowSession, val service: BFTSmartNotaryService) : FlowLogic<Void?>() {
@Suspendable
override fun call(): Void? {
val payload = otherSideSession.receive<NotarisationPayload>().unwrap { it }
val response = commit(payload)
otherSideSession.send(response)
return null
}
private fun commit(payload: NotarisationPayload): NotarisationResponse {
val response = service.commitTransaction(payload, otherSideSession.counterparty)
when (response) {
is BFTSmart.ClusterResponse.Error -> {
// TODO: here we assume that all error will be the same, but there might be invalid onces from mailicious nodes
val responseError = response.errors.first().verified()
throw NotaryException(responseError, payload.coreTransaction.id)
}
is BFTSmart.ClusterResponse.Signatures -> {
log.debug("All input states of transaction ${payload.coreTransaction.id} have been committed")
return NotarisationResponse(response.txSignatures)
}
}
}
}
@Entity
@Table(name = "${NODE_DATABASE_PREFIX}bft_committed_txs")
class CommittedTransaction(
@Id
@Column(name = "transaction_id", nullable = false, length = 64)
val transactionId: String
)
@Entity
@Table(name = "${NODE_DATABASE_PREFIX}bft_committed_states")
class CommittedState(id: PersistentStateRef, consumingTxHash: String) : PersistentUniquenessProvider.BaseComittedState(id, consumingTxHash)
private fun createMap(): AppendOnlyPersistentMap<StateRef, SecureHash, CommittedState, PersistentStateRef> {
return AppendOnlyPersistentMap(
cacheFactory = services.cacheFactory,
name = "BFTNonValidatingNotaryService_transactions",
toPersistentEntityKey = { PersistentStateRef(it.txhash.toString(), it.index) },
fromPersistentEntity = {
//TODO null check will become obsolete after making DB/JPA columns not nullable
val txId = it.id.txId
val index = it.id.index
Pair(
StateRef(txhash = SecureHash.parse(txId), index = index),
SecureHash.parse(it.consumingTxHash)
)
},
toPersistentEntity = { (txHash, index): StateRef, id: SecureHash ->
CommittedState(
id = PersistentStateRef(txHash.toString(), index),
consumingTxHash = id.toString()
)
},
persistentEntityClass = CommittedState::class.java
)
}
private class Replica(config: BFTSmartConfigInternal,
replicaId: Int,
createMap: () -> AppendOnlyPersistentMap<StateRef, SecureHash, CommittedState, PersistentStateRef>,
services: ServiceHubInternal,
notaryIdentityKey: PublicKey) : BFTSmart.Replica(config, replicaId, createMap, services, notaryIdentityKey) {
override fun executeCommand(command: ByteArray): ByteArray {
val commitRequest = command.deserialize<BFTSmart.CommitRequest>()
verifyRequest(commitRequest)
val response = verifyAndCommitTx(commitRequest.payload.coreTransaction, commitRequest.callerIdentity, commitRequest.payload.requestSignature)
return response.serialize().bytes
}
private fun verifyAndCommitTx(transaction: CoreTransaction, callerIdentity: Party, requestSignature: NotarisationRequestSignature): BFTSmart.ReplicaResponse {
return try {
val id = transaction.id
val inputs = transaction.inputs
val references = transaction.references
val notary = transaction.notary
val timeWindow = (transaction as? FilteredTransaction)?.timeWindow
if (notary !in services.myInfo.legalIdentities) throw NotaryInternalException(NotaryError.WrongNotary)
commitInputStates(inputs, id, callerIdentity.name, requestSignature, timeWindow, references)
log.debug { "Inputs committed successfully, signing $id" }
BFTSmart.ReplicaResponse.Signature(sign(id))
} catch (e: NotaryInternalException) {
log.debug { "Error processing transaction: ${e.error}" }
val serializedError = e.error.serialize()
val errorSignature = sign(serializedError.bytes)
val signedError = SignedData(serializedError, errorSignature)
BFTSmart.ReplicaResponse.Error(signedError)
}
}
private fun verifyRequest(commitRequest: BFTSmart.CommitRequest) {
val transaction = commitRequest.payload.coreTransaction
val notarisationRequest = NotarisationRequest(transaction.inputs, transaction.id)
notarisationRequest.verifySignature(commitRequest.payload.requestSignature, commitRequest.callerIdentity)
}
}
override fun start() {
}
override fun stop() {
replicaHolder.getOrThrow().dispose()
client.dispose()
}
}

View File

@ -0,0 +1,20 @@
package net.corda.notary.experimental.bftsmart
import net.corda.core.schemas.MappedSchema
import net.corda.node.services.transactions.PersistentUniquenessProvider
object BFTSmartNotarySchema
object BFTSmartNotarySchemaV1 : MappedSchema(
schemaFamily = BFTSmartNotarySchema.javaClass,
version = 1,
mappedTypes = listOf(
PersistentUniquenessProvider.BaseComittedState::class.java,
PersistentUniquenessProvider.Request::class.java,
BFTSmartNotaryService.CommittedState::class.java,
BFTSmartNotaryService.CommittedTransaction::class.java
)
) {
override val migrationResource: String?
get() = "notary-bft-smart.changelog-master"
}

View File

@ -0,0 +1,18 @@
package net.corda.notary.experimental.raft
import net.corda.core.utilities.NetworkHostAndPort
/** Configuration properties specific to the RaftNotaryService. */
data class RaftConfig(
/**
* The host and port to which to bind the embedded Raft server. Note that the Raft cluster uses a
* separate transport layer for communication that does not integrate with ArtemisMQ messaging services.
*/
val nodeAddress: NetworkHostAndPort,
/**
* Must list the addresses of all the members in the cluster. At least one of the members mustbe active and
* be able to communicate with the cluster leader for the node to join the cluster. If empty, a new cluster
* will be bootstrapped.
*/
val clusterAddresses: List<NetworkHostAndPort>
)

View File

@ -0,0 +1,48 @@
package net.corda.notary.experimental.raft
import net.corda.core.flows.FlowSession
import net.corda.core.internal.notary.NotaryServiceFlow
import net.corda.core.internal.notary.SinglePartyNotaryService
import net.corda.core.utilities.seconds
import net.corda.node.services.api.ServiceHubInternal
import net.corda.node.services.transactions.NonValidatingNotaryFlow
import net.corda.node.services.transactions.ValidatingNotaryFlow
import java.security.PublicKey
/** A highly available notary service using the Raft algorithm to achieve consensus. */
class RaftNotaryService(
override val services: ServiceHubInternal,
override val notaryIdentityKey: PublicKey
) : SinglePartyNotaryService() {
private val notaryConfig = services.configuration.notary
?: throw IllegalArgumentException("Failed to register ${RaftNotaryService::class.java}: notary configuration not present")
override val uniquenessProvider = with(services) {
val raftConfig = notaryConfig.raft
?: throw IllegalArgumentException("Failed to register ${RaftNotaryService::class.java}: raft configuration not present")
RaftUniquenessProvider(
configuration.baseDirectory,
configuration.p2pSslOptions,
database,
clock,
monitoringService.metrics,
services.cacheFactory,
raftConfig
)
}
override fun createServiceFlow(otherPartySession: FlowSession): NotaryServiceFlow {
return if (notaryConfig.validating) {
ValidatingNotaryFlow(otherPartySession, this, notaryConfig.etaMessageThresholdSeconds.seconds)
} else NonValidatingNotaryFlow(otherPartySession, this, notaryConfig.etaMessageThresholdSeconds.seconds)
}
override fun start() {
uniquenessProvider.start()
}
override fun stop() {
uniquenessProvider.stop()
}
}

View File

@ -0,0 +1,258 @@
package net.corda.notary.experimental.raft
import io.atomix.catalyst.buffer.BufferInput
import io.atomix.catalyst.buffer.BufferOutput
import io.atomix.catalyst.serializer.Serializer
import io.atomix.catalyst.serializer.TypeSerializer
import io.atomix.copycat.Command
import io.atomix.copycat.Query
import io.atomix.copycat.server.Commit
import io.atomix.copycat.server.Snapshottable
import io.atomix.copycat.server.StateMachine
import io.atomix.copycat.server.storage.snapshot.SnapshotReader
import io.atomix.copycat.server.storage.snapshot.SnapshotWriter
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.TimeWindow
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256
import net.corda.core.flows.NotaryError
import net.corda.core.flows.StateConsumptionDetails
import net.corda.core.internal.VisibleForTesting
import net.corda.core.internal.notary.isConsumedByTheSameTx
import net.corda.core.internal.notary.validateTimeWindow
import net.corda.core.serialization.SerializationDefaults
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.internal.CheckpointSerializationDefaults
import net.corda.core.serialization.internal.checkpointSerialize
import net.corda.core.serialization.serialize
import net.corda.core.utilities.ByteSequence
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import net.corda.node.services.transactions.PersistentUniquenessProvider
import net.corda.node.utilities.AppendOnlyPersistentMap
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.currentDBSession
import net.corda.serialization.internal.CordaSerializationEncoding
import java.time.Clock
/**
* Notarised contract state commit log, replicated across a Copycat Raft cluster.
*
* Copycat ony supports in-memory state machines, so we back the state with JDBC tables.
* State re-synchronisation is achieved by replaying the command log to the new (or re-joining) cluster member.
*/
class RaftTransactionCommitLog<E, EK>(
private val db: CordaPersistence,
private val nodeClock: Clock,
createMap: () -> AppendOnlyPersistentMap<StateRef, Pair<Long, SecureHash>, E, EK>
) : StateMachine(), Snapshottable {
object Commands {
class CommitTransaction @JvmOverloads constructor(
val states: List<StateRef>,
val txId: SecureHash,
val requestingParty: String,
val requestSignature: ByteArray,
val timeWindow: TimeWindow? = null,
val references: List<StateRef> = emptyList()
) : Command<NotaryError?> {
override fun compaction(): Command.CompactionMode {
// The FULL compaction mode retains the command in the log until it has been stored and applied on all
// servers in the cluster. Once the commit has been applied to a state machine and closed it may be
// removed from the log during minor or major compaction.
//
// Note that we are not closing the commits, thus our log grows without bounds. We let the log grow on
// purpose to be able to increase the size of a running cluster, e.g. to add and decommission nodes.
// TODO: Cluster membership changes need testing.
// TODO: I'm wondering if we should support resizing notary clusters, or if we could require users to
// setup a new cluster of the desired size and transfer the data.
return Command.CompactionMode.FULL
}
}
class Get(val key: StateRef) : Query<SecureHash?>
}
private val map = db.transaction { createMap() }
/** Commits the input states for the transaction as specified in the given [Commands.CommitTransaction]. */
fun commitTransaction(raftCommit: Commit<Commands.CommitTransaction>): NotaryError? {
val conflictingStates = LinkedHashMap<StateRef, StateConsumptionDetails>()
fun checkConflict(states: List<StateRef>, type: StateConsumptionDetails.ConsumedStateType) = states.forEach { stateRef ->
map[stateRef]?.let { conflictingStates[stateRef] = StateConsumptionDetails(it.second.sha256(), type) }
}
raftCommit.use {
val index = it.index()
return db.transaction {
val commitCommand = raftCommit.command()
logRequest(commitCommand)
val txId = commitCommand.txId
log.debug("State machine commit: attempting to store entries with keys (${commitCommand.states.joinToString()})")
checkConflict(commitCommand.states, StateConsumptionDetails.ConsumedStateType.INPUT_STATE)
checkConflict(commitCommand.references, StateConsumptionDetails.ConsumedStateType.REFERENCE_INPUT_STATE)
if (conflictingStates.isNotEmpty()) {
if (commitCommand.states.isEmpty()) {
handleReferenceConflicts(txId, conflictingStates)
} else {
handleConflicts(txId, conflictingStates)
}
} else {
handleNoConflicts(commitCommand.timeWindow, commitCommand.states, txId, index)
}
}
}
}
private fun handleReferenceConflicts(txId: SecureHash, conflictingStates: LinkedHashMap<StateRef, StateConsumptionDetails>): NotaryError? {
if (!previouslyCommitted(txId)) {
val conflictError = NotaryError.Conflict(txId, conflictingStates)
log.debug { "Failure, input states already committed: ${conflictingStates.keys}" }
return conflictError
}
log.debug { "Transaction $txId already notarised" }
return null
}
private fun handleConflicts(txId: SecureHash, conflictingStates: java.util.LinkedHashMap<StateRef, StateConsumptionDetails>): NotaryError? {
return if (isConsumedByTheSameTx(txId.sha256(), conflictingStates)) {
log.debug { "Transaction $txId already notarised" }
null
} else {
log.debug { "Failure, input states already committed: ${conflictingStates.keys}" }
NotaryError.Conflict(txId, conflictingStates)
}
}
private fun handleNoConflicts(timeWindow: TimeWindow?, states: List<StateRef>, txId: SecureHash, index: Long): NotaryError? {
// Skip if this is a re-notarisation of a reference-only transaction
if (states.isEmpty() && previouslyCommitted(txId)) {
return null
}
val outsideTimeWindowError = validateTimeWindow(clock.instant(), timeWindow)
return if (outsideTimeWindowError == null) {
val entries = states.map { it to Pair(index, txId) }.toMap()
map.putAll(entries)
val session = currentDBSession()
session.persist(RaftUniquenessProvider.CommittedTransaction(txId.toString()))
log.debug { "Successfully committed all input states: $states" }
null
} else {
outsideTimeWindowError
}
}
private fun previouslyCommitted(txId: SecureHash): Boolean {
val session = currentDBSession()
return session.find(RaftUniquenessProvider.CommittedTransaction::class.java, txId.toString()) != null
}
private fun logRequest(commitCommand: Commands.CommitTransaction) {
val request = PersistentUniquenessProvider.Request(
consumingTxHash = commitCommand.txId.toString(),
partyName = commitCommand.requestingParty,
requestSignature = commitCommand.requestSignature,
requestDate = nodeClock.instant()
)
val session = currentDBSession()
session.persist(request)
}
/** Gets the consuming transaction id for a given state reference. */
fun get(commit: Commit<Commands.Get>): SecureHash? {
commit.use {
val key = it.operation().key
return db.transaction { map[key]?.second }
}
}
/**
* Writes out all committed state and notarisation request entries to disk. Note that this operation does not
* load all entries into memory, as the [SnapshotWriter] is using a disk-backed buffer internally, and iterating
* map entries results in only a fixed number of recently accessed entries to ever be kept in memory.
*/
override fun snapshot(writer: SnapshotWriter) {
db.transaction {
writer.writeInt(map.size)
map.allPersisted().forEach {
val bytes = it.serialize(context = SerializationDefaults.STORAGE_CONTEXT).bytes
writer.writeUnsignedShort(bytes.size)
writer.writeObject(bytes)
}
val criteriaQuery = session.criteriaBuilder.createQuery(PersistentUniquenessProvider.Request::class.java)
criteriaQuery.select(criteriaQuery.from(PersistentUniquenessProvider.Request::class.java))
val results = session.createQuery(criteriaQuery).resultList
writer.writeInt(results.size)
results.forEach {
val bytes = it.serialize(context = SerializationDefaults.STORAGE_CONTEXT).bytes
writer.writeUnsignedShort(bytes.size)
writer.writeObject(bytes)
}
}
}
/** Reads entries from disk and populates the committed state and notarisation request tables. */
override fun install(reader: SnapshotReader) {
val size = reader.readInt()
db.transaction {
map.clear()
// TODO: read & put entries in batches
for (i in 1..size) {
val bytes = ByteArray(reader.readUnsignedShort())
reader.read(bytes)
val (key, value) = bytes.deserialize<Pair<StateRef, Pair<Long, SecureHash>>>()
map[key] = value
}
// Clean notarisation request log
val deleteQuery = session.criteriaBuilder.createCriteriaDelete(PersistentUniquenessProvider.Request::class.java)
deleteQuery.from(PersistentUniquenessProvider.Request::class.java)
session.createQuery(deleteQuery).executeUpdate()
// Load and populate request log
for (i in 1..reader.readInt()) {
val bytes = ByteArray(reader.readUnsignedShort())
reader.read(bytes)
val request = bytes.deserialize<PersistentUniquenessProvider.Request>()
session.persist(request)
}
}
}
companion object {
private val log = contextLogger()
@VisibleForTesting
val serializer: Serializer by lazy {
Serializer().apply {
registerAbstract(SecureHash::class.java, CordaKryoSerializer::class.java)
registerAbstract(TimeWindow::class.java, CordaKryoSerializer::class.java)
registerAbstract(NotaryError::class.java, CordaKryoSerializer::class.java)
register(Commands.CommitTransaction::class.java, CordaKryoSerializer::class.java)
register(Commands.Get::class.java, CordaKryoSerializer::class.java)
register(StateRef::class.java, CordaKryoSerializer::class.java)
register(LinkedHashMap::class.java, CordaKryoSerializer::class.java)
}
}
class CordaKryoSerializer<T : Any> : TypeSerializer<T> {
private val context = CheckpointSerializationDefaults.CHECKPOINT_CONTEXT.withEncoding(CordaSerializationEncoding.SNAPPY)
private val checkpointSerializer = CheckpointSerializationDefaults.CHECKPOINT_SERIALIZER
override fun write(obj: T, buffer: BufferOutput<*>, serializer: Serializer) {
val serialized = obj.checkpointSerialize(context = context)
buffer.writeInt(serialized.size)
buffer.write(serialized.bytes)
}
override fun read(type: Class<T>, buffer: BufferInput<*>, serializer: Serializer): T {
val size = buffer.readInt()
val serialized = ByteArray(size)
buffer.read(serialized)
return checkpointSerializer.deserialize(ByteSequence.of(serialized), type, context)
}
}
}
}

View File

@ -0,0 +1,235 @@
package net.corda.notary.experimental.raft
import com.codahale.metrics.Gauge
import com.codahale.metrics.MetricRegistry
import io.atomix.catalyst.transport.Address
import io.atomix.catalyst.transport.Transport
import io.atomix.catalyst.transport.netty.NettyTransport
import io.atomix.catalyst.transport.netty.SslProtocol
import io.atomix.copycat.client.ConnectionStrategies
import io.atomix.copycat.client.CopycatClient
import io.atomix.copycat.client.RecoveryStrategies
import io.atomix.copycat.server.CopycatServer
import io.atomix.copycat.server.cluster.Member
import io.atomix.copycat.server.storage.Storage
import io.atomix.copycat.server.storage.StorageLevel
import net.corda.core.concurrent.CordaFuture
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.TimeWindow
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.NotarisationRequestSignature
import net.corda.core.identity.Party
import net.corda.core.internal.NamedCacheFactory
import net.corda.core.internal.concurrent.openFuture
import net.corda.core.internal.notary.UniquenessProvider
import net.corda.core.serialization.SerializationDefaults
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import net.corda.node.utilities.AppendOnlyPersistentMap
import net.corda.nodeapi.internal.config.MutualSslConfiguration
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
import net.corda.notary.experimental.raft.RaftTransactionCommitLog.Commands.CommitTransaction
import java.nio.file.Path
import java.time.Clock
import java.util.concurrent.CompletableFuture
import javax.annotation.concurrent.ThreadSafe
import javax.persistence.*
/**
* A uniqueness provider that records committed input states in a distributed collection replicated and
* persisted in a Raft cluster, using the Copycat framework (http://atomix.io/copycat/).
*
* The uniqueness provider maintains both a Copycat cluster node (server) and a client through which it can submit
* requests to the cluster. In Copycat, a client request is first sent to the server it's connected to and then redirected
* to the cluster leader to be actioned.
*/
@ThreadSafe
class RaftUniquenessProvider(
/** If *null* the Raft log will be stored in memory. */
private val storagePath: Path? = null,
private val transportConfiguration: MutualSslConfiguration,
private val db: CordaPersistence,
private val clock: Clock,
private val metrics: MetricRegistry,
private val cacheFactory: NamedCacheFactory,
private val raftConfig: RaftConfig
) : UniquenessProvider, SingletonSerializeAsToken() {
companion object {
private val log = contextLogger()
fun createMap(cacheFactory: NamedCacheFactory): AppendOnlyPersistentMap<StateRef, Pair<Long, SecureHash>, CommittedState, String> =
AppendOnlyPersistentMap(
cacheFactory = cacheFactory,
name = "RaftUniquenessProvider_transactions",
toPersistentEntityKey = { it.encoded() },
fromPersistentEntity = {
Pair(
it.key.parseStateRef(),
Pair(
it.index,
it.value.deserialize<SecureHash>(context = SerializationDefaults.STORAGE_CONTEXT)
)
)
},
toPersistentEntity = { k: StateRef, (first, second) ->
CommittedState().apply {
key = k.encoded()
value = second.serialize(context = SerializationDefaults.STORAGE_CONTEXT).bytes
index = first
}
},
persistentEntityClass = CommittedState::class.java
)
fun StateRef.encoded() = "$txhash:$index"
fun String.parseStateRef() = split(":").let { StateRef(SecureHash.parse(it[0]), it[1].toInt()) }
}
@Entity
@Table(name = "${NODE_DATABASE_PREFIX}raft_committed_states")
class CommittedState(
@Id
@Column(name = "id", nullable = false)
var key: String = "",
@Lob
@Column(name = "state_value", nullable = false)
var value: ByteArray = ByteArray(0),
@Column(name = "state_index")
var index: Long = 0
)
@Entity
@Table(name = "${NODE_DATABASE_PREFIX}raft_committed_txs")
class CommittedTransaction(
@Id
@Column(name = "transaction_id", nullable = false, length = 64)
val transactionId: String
)
private lateinit var _clientFuture: CompletableFuture<CopycatClient>
private lateinit var server: CopycatServer
/**
* Copycat clients are responsible for connecting to the cluster and submitting commands and queries that operate
* on the cluster's replicated state machine.
*/
private val client: CopycatClient
get() = _clientFuture.get()
fun start() {
log.info("Creating Copycat server, log stored in: ${storagePath?.toAbsolutePath() ?: " memory"}")
val stateMachineFactory = {
RaftTransactionCommitLog(db, clock) { createMap(cacheFactory) }
}
val address = raftConfig.nodeAddress.let { Address(it.host, it.port) }
val storage = buildStorage(storagePath)
val transport = buildTransport(transportConfiguration)
server = CopycatServer.builder(address)
.withStateMachine(stateMachineFactory)
.withStorage(storage)
.withServerTransport(transport)
.withSerializer(RaftTransactionCommitLog.serializer)
.build()
val serverFuture = if (raftConfig.clusterAddresses.isNotEmpty()) {
log.info("Joining an existing Copycat cluster at ${raftConfig.clusterAddresses}")
val cluster = raftConfig.clusterAddresses.map { Address(it.host, it.port) }
server.join(cluster)
} else {
log.info("Bootstrapping a Copycat cluster at $address")
server.bootstrap()
}
registerMonitoring()
val client = CopycatClient.builder(address)
.withTransport(transport) // TODO: use local transport for client-server communications
.withConnectionStrategy(ConnectionStrategies.EXPONENTIAL_BACKOFF)
.withSerializer(RaftTransactionCommitLog.serializer)
.withRecoveryStrategy(RecoveryStrategies.RECOVER)
.build()
_clientFuture = serverFuture.thenCompose { client.connect(address) }
}
fun stop() {
server.shutdown()
}
private fun buildStorage(storagePath: Path?): Storage? {
val builder = Storage.builder()
if (storagePath != null) {
builder.withDirectory(storagePath.toFile()).withStorageLevel(StorageLevel.DISK)
} else {
builder.withStorageLevel(StorageLevel.MEMORY)
}
return builder.build()
}
private fun buildTransport(config: MutualSslConfiguration): Transport? {
return NettyTransport.builder()
.withSsl()
.withSslProtocol(SslProtocol.TLSv1_2)
.withKeyStorePath(config.keyStore.path.toString())
.withKeyStorePassword(config.keyStore.storePassword)
.withTrustStorePath(config.trustStore.path.toString())
.withTrustStorePassword(config.trustStore.storePassword)
.build()
}
private fun registerMonitoring() {
metrics.register("RaftCluster.ThisServerStatus", Gauge<String> {
server.state().name
})
metrics.register("RaftCluster.MembersCount", Gauge<Int> {
server.cluster().members().size
})
metrics.register("RaftCluster.Members", Gauge<List<String>> {
server.cluster().members().map { it.address().toString() }
})
metrics.register("RaftCluster.AvailableMembers", Gauge<List<String>> {
server.cluster().members().filter { it.status() == Member.Status.AVAILABLE }.map { it.address().toString() }
})
metrics.register("RaftCluster.AvailableMembersCount", Gauge<Int> {
server.cluster().members().filter { it.status() == Member.Status.AVAILABLE }.size
})
}
override fun commit(
states: List<StateRef>,
txId: SecureHash,
callerIdentity: Party,
requestSignature: NotarisationRequestSignature,
timeWindow: TimeWindow?,
references: List<StateRef>
): CordaFuture<UniquenessProvider.Result> {
log.debug { "Attempting to commit input states: ${states.joinToString()}" }
val commitCommand = CommitTransaction(
states,
txId,
callerIdentity.name.toString(),
requestSignature.serialize().bytes,
timeWindow,
references
)
val future = openFuture<UniquenessProvider.Result>()
client.submit(commitCommand).thenAccept { commitError ->
val result = if (commitError != null) {
UniquenessProvider.Result.Failure(commitError)
} else {
log.debug { "All input states of transaction $txId have been committed" }
UniquenessProvider.Result.Success
}
future.set(result)
}
return future
}
}

View File

@ -0,0 +1,20 @@
package net.corda.notary.experimental.raft
import net.corda.core.schemas.MappedSchema
import net.corda.node.services.transactions.PersistentUniquenessProvider
object RaftNotarySchema
object RaftNotarySchemaV1 : MappedSchema(
schemaFamily = RaftNotarySchema.javaClass,
version = 1,
mappedTypes = listOf(
PersistentUniquenessProvider.BaseComittedState::class.java,
PersistentUniquenessProvider.Request::class.java,
RaftUniquenessProvider.CommittedState::class.java,
RaftUniquenessProvider.CommittedTransaction::class.java
)
) {
override val migrationResource: String?
get() = "notary-raft.changelog-master"
}

View File

@ -14,5 +14,5 @@
<include file="migration/node-core.changelog-tx-mapping.xml"/>
<include file="migration/node-core.changelog-v9.xml"/>
<include file="migration/node-core.changelog-v10.xml"/>
<include file="migration/node-core.changelog-v11.xml"/>
</databaseChangeLog>

View File

@ -0,0 +1,11 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd"
logicalFilePath="migration/node-services.changelog-init.xml">
<changeSet author="R3.Corda" id="update-version-of-whitelisted-jars">
<customChange class="net.corda.nodeapi.internal.persistence.AttachmentVersionNumberMigration">
</customChange>
</changeSet>
</databaseChangeLog>

View File

@ -0,0 +1,14 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
<changeSet author="R3.Corda" id="create-bft-committed-transactions-table">
<createTable tableName="node_bft_committed_txs">
<column name="transaction_id" type="NVARCHAR(64)">
<constraints nullable="false"/>
</column>
</createTable>
<addPrimaryKey columnNames="transaction_id" constraintName="node_bft_transactions_pkey" tableName="node_bft_committed_txs"/>
</changeSet>
</databaseChangeLog>

View File

@ -0,0 +1,45 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd"
logicalFilePath="migration/node-services.changelog-init.xml">
<changeSet author="R3.Corda" id="1511451595465-6">
<createTable tableName="node_bft_committed_states">
<column name="output_index" type="INT">
<constraints nullable="false"/>
</column>
<column name="transaction_id" type="NVARCHAR(64)">
<constraints nullable="false"/>
</column>
<column name="consuming_transaction_id" type="NVARCHAR(64)"/>
</createTable>
</changeSet>
<changeSet author="R3.Corda" id="1521131680317-17">
<createTable tableName="node_notary_request_log">
<column name="id" type="INT">
<constraints nullable="false"/>
</column>
<column name="consuming_transaction_id" type="NVARCHAR(64)">
<constraints nullable="false"/>
</column>
<column name="requesting_party_name" type="NVARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="request_timestamp" type="TIMESTAMP">
<constraints nullable="false"/>
</column>
<column name="request_signature" type="BLOB">
<constraints nullable="false"/>
</column>
</createTable>
</changeSet>
<changeSet author="R3.Corda" id="1511451595465-31">
<addPrimaryKey columnNames="output_index, transaction_id" constraintName="node_bft_states_pkey"
tableName="node_bft_committed_states"/>
</changeSet>
<changeSet author="R3.Corda" id="1521131680317-48">
<addPrimaryKey columnNames="id" constraintName="node_notary_request_log_pkey"
tableName="node_notary_request_log"/>
</changeSet>
</databaseChangeLog>

View File

@ -0,0 +1,10 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
<include file="migration/notary-bft-smart.changelog-init.xml"/>
<include file="migration/notary-bft-smart.changelog-v1.xml"/>
<include file="migration/notary-bft-smart.changelog-pkey.xml"/>
<include file="migration/notary-bft-smart.changelog-committed-transactions-table.xml"/>
</databaseChangeLog>

View File

@ -0,0 +1,11 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
<changeSet id="non-clustered_pk-bft_stae" author="R3.Corda" onValidationFail="MARK_RAN">
<dropPrimaryKey tableName="node_bft_committed_states" constraintName="node_bft_states_pkey"/>
<addPrimaryKey tableName="node_bft_committed_states" columnNames="output_index, transaction_id"
constraintName="node_bft_states_pkey" clustered="false"/>
</changeSet>
</databaseChangeLog>

View File

@ -0,0 +1,10 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd"
logicalFilePath="migration/node-services.changelog-init.xml">
<changeSet author="R3.Corda" id="nullability">
<addNotNullConstraint tableName="node_bft_committed_states" columnName="consuming_transaction_id" columnDataType="NVARCHAR(64)"/>
</changeSet>
</databaseChangeLog>

View File

@ -0,0 +1,14 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
<changeSet author="R3.Corda" id="create-raft-committed-transactions-table">
<createTable tableName="node_raft_committed_txs">
<column name="transaction_id" type="NVARCHAR(64)">
<constraints nullable="false"/>
</column>
</createTable>
<addPrimaryKey columnNames="transaction_id" constraintName="node_raft_transactions_pkey" tableName="node_raft_committed_txs"/>
</changeSet>
</databaseChangeLog>

View File

@ -0,0 +1,43 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd"
logicalFilePath="migration/node-services.changelog-init.xml">
<changeSet author="R3.Corda" id="1511451595465-18">
<createTable tableName="node_raft_committed_states">
<column name="id" type="NVARCHAR(128)">
<constraints nullable="false"/>
</column>
<column name="state_index" type="BIGINT"/>
<column name="state_value" type="BLOB"/>
</createTable>
</changeSet>
<changeSet author="R3.Corda" id="1521131680317-17">
<createTable tableName="node_notary_request_log">
<column name="id" type="INT">
<constraints nullable="false"/>
</column>
<column name="consuming_transaction_id" type="NVARCHAR(64)">
<constraints nullable="false"/>
</column>
<column name="requesting_party_name" type="NVARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="request_timestamp" type="TIMESTAMP">
<constraints nullable="false"/>
</column>
<column name="request_signature" type="BLOB">
<constraints nullable="false"/>
</column>
</createTable>
</changeSet>
<changeSet author="R3.Corda" id="1511451595465-43">
<addPrimaryKey columnNames="id" constraintName="node_raft_state_pkey"
tableName="node_raft_committed_states"/>
</changeSet>
<changeSet author="R3.Corda" id="1521131680317-48">
<addPrimaryKey columnNames="id" constraintName="node_notary_request_log_pkey"
tableName="node_notary_request_log"/>
</changeSet>
</databaseChangeLog>

View File

@ -0,0 +1,10 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
<include file="migration/notary-raft.changelog-init.xml"/>
<include file="migration/notary-raft.changelog-v1.xml"/>
<include file="migration/notary-raft.changelog-pkey.xml"/>
<include file="migration/notary-raft.changelog-committed-transactions-table.xml" />
</databaseChangeLog>

View File

@ -0,0 +1,11 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
<changeSet id="non-clustered_pk-raft_state" author="R3.Corda" onValidationFail="MARK_RAN">
<dropPrimaryKey tableName="node_raft_committed_states" constraintName="node_raft_state_pkey"/>
<addPrimaryKey tableName="node_raft_committed_states" columnNames="output_index, transaction_id"
constraintName="node_raft_state_pkey" clustered="false"/>
</changeSet>
</databaseChangeLog>

View File

@ -0,0 +1,11 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd"
logicalFilePath="migration/node-services.changelog-init.xml">
<changeSet author="R3.Corda" id="nullability">
<addNotNullConstraint tableName="node_raft_committed_states" columnName="state_index" columnDataType="BIGINT"/>
<addNotNullConstraint tableName="node_raft_committed_states" columnName="state_value" columnDataType="BLOB"/>
</changeSet>
</databaseChangeLog>

View File

@ -0,0 +1,118 @@
# Copyright (c) 2007-2013 Alysson Bessani, Eduardo Alchieri, Paulo Sousa, and the authors indicated in the @author tags
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
############################################
####### Communication Configurations #######
############################################
#HMAC algorithm used to authenticate messages between processes (HmacMD5 is the default value)
#This parameter is not currently being used being used
#system.authentication.hmacAlgorithm = HmacSHA1
#Specify if the communication system should use a thread to send data (true or false)
system.communication.useSenderThread = true
#Force all processes to use the same public/private keys pair and secret key. This is useful when deploying experiments
#and benchmarks, but must not be used in production systems.
system.communication.defaultkeys = true
############################################
### Replication Algorithm Configurations ###
############################################
#Number of servers in the group
system.servers.num = %s
#Maximum number of faulty replicas
system.servers.f = %s
#Timeout to asking for a client request
system.totalordermulticast.timeout = 2000
#Maximum batch size (in number of messages)
system.totalordermulticast.maxbatchsize = 400
#Number of nonces (for non-determinism actions) generated
system.totalordermulticast.nonces = 10
#if verification of leader-generated timestamps are increasing
#it can only be used on systems in which the network clocks
#are synchronized
system.totalordermulticast.verifyTimestamps = false
#Quantity of messages that can be stored in the receive queue of the communication system
system.communication.inQueueSize = 500000
# Quantity of messages that can be stored in the send queue of each replica
system.communication.outQueueSize = 500000
#Set to 1 if SMaRt should use signatures, set to 0 if otherwise
system.communication.useSignatures = 0
#Set to 1 if SMaRt should use MAC's, set to 0 if otherwise
system.communication.useMACs = 1
#Set to 1 if SMaRt should use the standard output to display debug messages, set to 0 if otherwise
system.debug = %s
#Print information about the replica when it is shutdown
system.shutdownhook = true
############################################
###### State Transfer Configurations #######
############################################
#Activate the state transfer protocol ('true' to activate, 'false' to de-activate)
system.totalordermulticast.state_transfer = false
#Maximum ahead-of-time message not discarded
system.totalordermulticast.highMark = 10000
#Maximum ahead-of-time message not discarded when the replica is still on EID 0 (after which the state transfer is triggered)
system.totalordermulticast.revival_highMark = 10
#Number of ahead-of-time messages necessary to trigger the state transfer after a request timeout occurs
system.totalordermulticast.timeout_highMark = 200
############################################
###### Log and Checkpoint Configurations ###
############################################
system.totalordermulticast.log = false
system.totalordermulticast.log_parallel = false
system.totalordermulticast.log_to_disk = false
system.totalordermulticast.sync_log = false
#Period at which BFT-SMaRt requests the state to the application (for the state transfer state protocol)
system.totalordermulticast.checkpoint_period = 1
system.totalordermulticast.global_checkpoint_period = 1
system.totalordermulticast.checkpoint_to_disk = false
system.totalordermulticast.sync_ckp = false
############################################
###### Reconfiguration Configurations ######
############################################
#Replicas ID for the initial view, separated by a comma.
# The number of replicas in this parameter should be equal to that specified in 'system.servers.num'
system.initial.view = %s
#The ID of the trust third party (TTP)
system.ttp.id = 7002
#This sets if the system will function in Byzantine or crash-only mode. Set to "true" to support Byzantine faults
system.bft = true

View File

@ -11,6 +11,7 @@ import net.corda.core.identity.AbstractParty;
import net.corda.core.identity.CordaX500Name;
import net.corda.core.identity.Party;
import net.corda.core.messaging.DataFeed;
import net.corda.core.node.ServicesForResolution;
import net.corda.core.node.services.AttachmentStorage;
import net.corda.core.node.services.IdentityService;
import net.corda.core.node.services.Vault;
@ -23,7 +24,7 @@ import net.corda.core.node.services.vault.AttachmentQueryCriteria.AttachmentsQue
import net.corda.finance.contracts.DealState;
import net.corda.finance.contracts.asset.Cash;
import net.corda.finance.schemas.CashSchemaV1;
import net.corda.finance.schemas.test.SampleCashSchemaV2;
import net.corda.finance.test.SampleCashSchemaV2;
import net.corda.node.services.api.IdentityServiceInternal;
import net.corda.node.services.persistence.NodeAttachmentService;
import net.corda.nodeapi.internal.persistence.CordaPersistence;
@ -58,12 +59,14 @@ import static net.corda.core.node.services.vault.Builder.equal;
import static net.corda.core.node.services.vault.Builder.sum;
import static net.corda.core.node.services.vault.QueryCriteriaUtils.*;
import static net.corda.core.utilities.ByteArrays.toHexString;
import static net.corda.testing.common.internal.ParametersUtilitiesKt.testNetworkParameters;
import static net.corda.testing.core.internal.ContractJarTestUtils.INSTANCE;
import static net.corda.testing.core.TestConstants.*;
import static net.corda.testing.internal.RigorousMockKt.rigorousMock;
import static net.corda.testing.node.MockServices.makeTestDatabaseAndMockServices;
import static net.corda.testing.node.MockServicesKt.makeTestIdentityService;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
public class VaultQueryJavaTests {
private static final TestIdentity BOC = new TestIdentity(BOC_NAME);
@ -82,19 +85,22 @@ public class VaultQueryJavaTests {
@Before
public void setUp() {
List<String> cordappPackages = asList("net.corda.testing.internal.vault", "net.corda.finance.contracts.asset", CashSchemaV1.class.getPackage().getName());
List<String> cordappPackages = asList("net.corda.testing.internal.vault", "net.corda.finance.contracts.asset", CashSchemaV1.class.getPackage().getName(), SampleCashSchemaV2.class.getPackage().getName());
IdentityService identitySvc = makeTestIdentityService(MEGA_CORP.getIdentity(), DUMMY_CASH_ISSUER_INFO.getIdentity(), DUMMY_NOTARY.getIdentity());
Pair<CordaPersistence, MockServices> databaseAndServices = makeTestDatabaseAndMockServices(
cordappPackages,
identitySvc,
MEGA_CORP,
DUMMY_NOTARY.getKeyPair());
issuerServices = new MockServices(cordappPackages, DUMMY_CASH_ISSUER_INFO, rigorousMock(IdentityServiceInternal.class), BOC.getKeyPair());
issuerServices = new MockServices(cordappPackages, DUMMY_CASH_ISSUER_INFO, mock(IdentityServiceInternal.class), BOC.getKeyPair());
database = databaseAndServices.getFirst();
MockServices services = databaseAndServices.getSecond();
vaultFiller = new VaultFiller(services, DUMMY_NOTARY);
vaultService = services.getVaultService();
storage = new NodeAttachmentService(new MetricRegistry(), new TestingNamedCacheFactory(100), database);
ServicesForResolution serviceForResolution = mock(ServicesForResolution.class);
((NodeAttachmentService) storage).servicesForResolution = serviceForResolution;
doReturn(testNetworkParameters()).when(serviceForResolution).getNetworkParameters();
}
@After

View File

@ -0,0 +1,156 @@
package net.corda.finance.contracts.asset.test
import net.corda.core.contracts.*
import net.corda.core.crypto.toStringShort
import net.corda.core.identity.AbstractParty
import net.corda.core.internal.Emoji
import net.corda.core.schemas.MappedSchema
import net.corda.core.schemas.PersistentState
import net.corda.core.schemas.QueryableState
import net.corda.core.transactions.LedgerTransaction
import net.corda.finance.contracts.asset.OnLedgerAsset
import net.corda.finance.test.SampleCashSchemaV1
import net.corda.finance.test.SampleCashSchemaV2
import net.corda.finance.test.SampleCashSchemaV3
import net.corda.finance.utils.sumCash
import net.corda.finance.utils.sumCashOrNull
import net.corda.finance.utils.sumCashOrZero
import java.security.PublicKey
import java.util.*
class DummyFungibleContract : OnLedgerAsset<Currency, DummyFungibleContract.Commands, DummyFungibleContract.State>() {
override fun extractCommands(commands: Collection<CommandWithParties<CommandData>>): List<CommandWithParties<Commands>>
= commands.select()
data class State(
override val amount: Amount<Issued<Currency>>,
override val owner: AbstractParty
) : FungibleAsset<Currency>, QueryableState {
constructor(deposit: PartyAndReference, amount: Amount<Currency>, owner: AbstractParty)
: this(Amount(amount.quantity, Issued(deposit, amount.token)), owner)
override val exitKeys = setOf(owner.owningKey, amount.token.issuer.party.owningKey)
override val participants = listOf(owner)
override fun withNewOwnerAndAmount(newAmount: Amount<Issued<Currency>>, newOwner: AbstractParty): FungibleAsset<Currency>
= copy(amount = amount.copy(newAmount.quantity), owner = newOwner)
override fun toString() = "${Emoji.bagOfCash}Cash($amount at ${amount.token.issuer} owned by $owner)"
override fun withNewOwner(newOwner: AbstractParty) = CommandAndState(Commands.Move(), copy(owner = newOwner))
/** Object Relational Mapping support. */
override fun generateMappedObject(schema: MappedSchema): PersistentState {
return when (schema) {
is SampleCashSchemaV1 -> SampleCashSchemaV1.PersistentCashState(
ownerHash = this.owner.owningKey.toStringShort(),
pennies = this.amount.quantity,
currency = this.amount.token.product.currencyCode,
issuerPartyHash = this.amount.token.issuer.party.owningKey.toStringShort(),
issuerRef = this.amount.token.issuer.reference.bytes
)
is SampleCashSchemaV2 -> SampleCashSchemaV2.PersistentCashState(
participants = this.participants.toMutableSet(),
owner = this.owner,
quantity = this.amount.quantity,
currency = this.amount.token.product.currencyCode,
issuerParty = this.amount.token.issuer.party,
issuerRef = this.amount.token.issuer.reference
)
is SampleCashSchemaV3 -> SampleCashSchemaV3.PersistentCashState(
participants = this.participants.toMutableSet(),
owner = this.owner,
pennies = this.amount.quantity,
currency = this.amount.token.product.currencyCode,
issuer = this.amount.token.issuer.party,
issuerRef = this.amount.token.issuer.reference.bytes
)
else -> throw IllegalArgumentException("Unrecognised schema $schema")
}
}
/** Object Relational Mapping support. */
override fun supportedSchemas(): Iterable<MappedSchema> = listOf(SampleCashSchemaV1, SampleCashSchemaV2, SampleCashSchemaV3)
}
interface Commands : CommandData {
data class Move(override val contract: Class<out Contract>? = null) : MoveCommand
class Issue : TypeOnlyCommandData()
data class Exit(val amount: Amount<Issued<Currency>>) : CommandData
}
override fun deriveState(txState: TransactionState<State>, amount: Amount<Issued<Currency>>, owner: AbstractParty)
= txState.copy(data = txState.data.copy(amount = amount, owner = owner))
override fun generateExitCommand(amount: Amount<Issued<Currency>>) = Commands.Exit(amount)
override fun generateMoveCommand() = Commands.Move()
override fun verify(tx: LedgerTransaction) {
val groups = tx.groupStates { it: State -> it.amount.token }
for ((inputs, outputs, key) in groups) {
// Either inputs or outputs could be empty.
val issuer = key.issuer
val currency = key.product
requireThat {
"there are no zero sized outputs" using (outputs.none { it.amount.quantity == 0L })
}
val issueCommand = tx.commands.select<Commands.Issue>().firstOrNull()
if (issueCommand != null) {
verifyIssueCommand(inputs, outputs, tx, issueCommand, currency, issuer)
} else {
val inputAmount = inputs.sumCashOrNull() ?: throw IllegalArgumentException("there is at least one input for this group")
val outputAmount = outputs.sumCashOrZero(Issued(issuer, currency))
val exitKeys: Set<PublicKey> = inputs.flatMap { it.exitKeys }.toSet()
val exitCommand = tx.commands.select<Commands.Exit>(parties = null, signers = exitKeys).singleOrNull { it.value.amount.token == key }
val amountExitingLedger = exitCommand?.value?.amount ?: Amount(0, Issued(issuer, currency))
requireThat {
"there are no zero sized inputs" using inputs.none { it.amount.quantity == 0L }
"for reference ${issuer.reference} at issuer ${issuer.party} the amounts balance: ${inputAmount.quantity} - ${amountExitingLedger.quantity} != ${outputAmount.quantity}" using
(inputAmount == outputAmount + amountExitingLedger)
}
verifyMoveCommand<Commands.Move>(inputs, tx.commands)
}
}
}
private fun verifyIssueCommand(inputs: List<State>,
outputs: List<State>,
tx: LedgerTransaction,
issueCommand: CommandWithParties<Commands.Issue>,
currency: Currency,
issuer: PartyAndReference) {
// If we have an issue command, perform special processing: the group is allowed to have no inputs,
// and the output states must have a deposit reference owned by the signer.
//
// Whilst the transaction *may* have no inputs, it can have them, and in this case the outputs must
// sum to more than the inputs. An issuance of zero size is not allowed.
//
// Note that this means literally anyone with access to the network can issue cash claims of arbitrary
// amounts! It is up to the recipient to decide if the backing party is trustworthy or not, via some
// as-yet-unwritten identity service. See ADP-22 for discussion.
// The grouping ensures that all outputs have the same deposit reference and currency.
val inputAmount = inputs.sumCashOrZero(Issued(issuer, currency))
val outputAmount = outputs.sumCash()
val cashCommands = tx.commands.select<Commands.Issue>()
requireThat {
// TODO: This doesn't work with the trader demo, so use the underlying key instead
// "output states are issued by a command signer" by (issuer.party in issueCommand.signingParties)
"output states are issued by a command signer" using (issuer.party.owningKey in issueCommand.signers)
"output values sum to more than the inputs" using (outputAmount > inputAmount)
"there is only a single issue command" using (cashCommands.count() == 1)
}
}
}

View File

@ -280,7 +280,7 @@ class CordaRPCOpsImplTest {
}
@Test
fun `can't upload the same attachment`() {
fun `cannot upload the same attachment`() {
withPermissions(invokeRpc(CordaRPCOps::uploadAttachment), invokeRpc(CordaRPCOps::attachmentExists)) {
val inputJar1 = Thread.currentThread().contextClassLoader.getResourceAsStream(testJar)
val inputJar2 = Thread.currentThread().contextClassLoader.getResourceAsStream(testJar)

View File

@ -10,6 +10,7 @@ import net.corda.core.identity.Party
import net.corda.core.utilities.unwrap
import net.corda.testing.core.singleIdentity
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.MockNetworkParameters
import net.corda.testing.node.MockNodeParameters
import net.corda.testing.node.StartedMockNode
import org.junit.After
@ -26,7 +27,7 @@ class FlowRegistrationTest {
@Before
fun setup() {
// no cordapps scanned so it can be tested in isolation
mockNetwork = MockNetwork(emptyList())
mockNetwork = MockNetwork(MockNetworkParameters())
initiator = mockNetwork.createNode(MockNodeParameters(legalName = CordaX500Name("initiator", "Reading", "GB")))
responder = mockNetwork.createNode(MockNodeParameters(legalName = CordaX500Name("responder", "Reading", "GB")))
mockNetwork.runNetwork()

View File

@ -15,10 +15,10 @@ import java.net.URL
class CordappProviderImplTests {
private companion object {
val isolatedJAR = this::class.java.getResource("isolated.jar")!!
val isolatedJAR: URL = this::class.java.getResource("/isolated.jar")
// TODO: Cordapp name should differ from the JAR name
const val isolatedCordappName = "isolated"
val emptyJAR = this::class.java.getResource("empty.jar")!!
val emptyJAR: URL = this::class.java.getResource("empty.jar")
val validConfig: Config = ConfigFactory.parseString("key=value")
val stubConfigProvider = object : CordappConfigProvider {
@ -52,7 +52,7 @@ class CordappProviderImplTests {
@Test
fun `test that we find a cordapp class that is loaded into the store`() {
val provider = newCordappProvider(isolatedJAR)
val className = "net.corda.finance.contracts.isolated.AnotherDummyContract"
val className = "net.corda.isolated.contracts.AnotherDummyContract"
val expected = provider.cordapps.first()
val actual = provider.getCordappForClass(className)
@ -62,9 +62,9 @@ class CordappProviderImplTests {
}
@Test
fun `test that we find an attachment for a cordapp contrat class`() {
fun `test that we find an attachment for a cordapp contract class`() {
val provider = newCordappProvider(isolatedJAR)
val className = "net.corda.finance.contracts.isolated.AnotherDummyContract"
val className = "net.corda.isolated.contracts.AnotherDummyContract"
val expected = provider.getAppContext(provider.cordapps.first()).attachmentId
val actual = provider.getContractAttachmentID(className)

View File

@ -36,8 +36,8 @@ class DummyRPCFlow : FlowLogic<Unit>() {
class JarScanningCordappLoaderTest {
private companion object {
const val isolatedContractId = "net.corda.finance.contracts.isolated.AnotherDummyContract"
const val isolatedFlowName = "net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator"
const val isolatedContractId = "net.corda.isolated.contracts.AnotherDummyContract"
const val isolatedFlowName = "net.corda.isolated.workflows.IsolatedIssuanceFlow"
}
@Test
@ -49,15 +49,15 @@ class JarScanningCordappLoaderTest {
@Test
fun `isolated JAR contains a CorDapp with a contract and plugin`() {
val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("isolated.jar")!!
val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("/isolated.jar")
val loader = JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR))
assertThat(loader.cordapps).hasSize(1)
val actualCordapp = loader.cordapps.single()
assertThat(actualCordapp.contractClassNames).isEqualTo(listOf(isolatedContractId))
assertThat(actualCordapp.initiatedFlows.first().name).isEqualTo("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Acceptor")
assertThat(actualCordapp.rpcFlows).isEmpty()
assertThat(actualCordapp.initiatedFlows).isEmpty()
assertThat(actualCordapp.rpcFlows.first().name).isEqualTo(isolatedFlowName)
assertThat(actualCordapp.schedulableFlows).isEmpty()
assertThat(actualCordapp.services).isEmpty()
assertThat(actualCordapp.serializationWhitelists).hasSize(1)
@ -83,7 +83,7 @@ class JarScanningCordappLoaderTest {
// being used internally. Later iterations will use a classloader per cordapp and this test can be retired.
@Test
fun `cordapp classloader can load cordapp classes`() {
val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("isolated.jar")!!
val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("/isolated.jar")
val loader = JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR), VersionInfo.UNKNOWN)
loader.appClassLoader.loadClass(isolatedContractId)

View File

@ -59,7 +59,7 @@ class RoundTripObservableSerializerTests {
@Test
fun roundTripTest1() {
val serializationScheme = AMQPRoundTripRPCSerializationScheme(
serializationContext, emptySet(), AccessOrderLinkedHashMap<SerializationFactoryCacheKey, SerializerFactory>(128).toSynchronised())
serializationContext, emptySet(), emptySet(), AccessOrderLinkedHashMap<SerializationFactoryCacheKey, SerializerFactory>(128).toSynchronised())
// Fake up a message ID, needs to be used on both "sides". The server setting it in the subscriptionMap,
// the client as a property of the deserializer which, in the actual RPC client, is pulled off of

View File

@ -4,6 +4,7 @@ import net.corda.client.rpc.internal.serialization.amqp.RpcClientObservableDeSer
import net.corda.core.context.Trace
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.SerializationCustomSerializer
import net.corda.core.serialization.SerializationWhitelist
import net.corda.node.serialization.amqp.RpcServerObservableSerializer
import net.corda.nodeapi.RPCApi
import net.corda.serialization.internal.CordaSerializationMagic
@ -20,9 +21,10 @@ import net.corda.client.rpc.internal.ObservableContext as ClientObservableContex
class AMQPRoundTripRPCSerializationScheme(
private val serializationContext: SerializationContext,
cordappCustomSerializers: Set<SerializationCustomSerializer<*, *>>,
cordappSerializationWhitelists: Set<SerializationWhitelist>,
serializerFactoriesForContexts: MutableMap<SerializationFactoryCacheKey, SerializerFactory>)
: AbstractAMQPSerializationScheme(
cordappCustomSerializers, serializerFactoriesForContexts
cordappCustomSerializers, cordappSerializationWhitelists, serializerFactoriesForContexts
) {
override fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory {
return SerializerFactoryBuilder.build(AllWhitelist, javaClass.classLoader).apply {

View File

@ -1,16 +1,15 @@
package net.corda.node.services
import co.paralleluniverse.fibers.Suspendable
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.concurrent.CordaFuture
import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.TimeWindow
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FinalityFlow
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSession
import net.corda.core.flows.NotarisationRequestSignature
import net.corda.core.flows.NotaryFlow
import net.corda.core.flows.*
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.internal.FlowIORequest
@ -26,6 +25,9 @@ import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.minutes
import net.corda.core.utilities.seconds
import net.corda.node.services.api.ServiceHubInternal
import net.corda.node.services.config.FlowTimeoutConfiguration
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.NotaryConfig
import net.corda.node.services.transactions.NonValidatingNotaryFlow
import net.corda.nodeapi.internal.DevIdentityGenerator
import net.corda.nodeapi.internal.network.NetworkParametersCopier
@ -35,15 +37,8 @@ import net.corda.testing.core.dummyCommand
import net.corda.testing.core.singleIdentity
import net.corda.testing.internal.LogHelper
import net.corda.testing.node.InMemoryMessagingNetwork
import net.corda.testing.node.MockNetFlowTimeOut
import net.corda.testing.node.MockNetNotaryConfig
import net.corda.testing.node.MockNetworkParameters
import net.corda.testing.node.MockNodeConfigOverrides
import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.InternalMockNodeParameters
import net.corda.testing.node.internal.TestStartedNode
import net.corda.testing.node.internal.cordappsForPackages
import net.corda.testing.node.internal.startFlow
import net.corda.testing.node.internal.*
import org.junit.AfterClass
import org.junit.Before
import org.junit.BeforeClass
@ -88,7 +83,6 @@ class TimedFlowTests {
notary = started.first
node = started.second
patientNode = started.third
}
@AfterClass
@ -105,33 +99,38 @@ class TimedFlowTests {
serviceLegalName)
val networkParameters = NetworkParametersCopier(testNetworkParameters(listOf(NotaryInfo(notaryIdentity, false))))
val notaryConfig = MockNetNotaryConfig(
serviceLegalName = serviceLegalName,
validating = false,
className = TestNotaryService::class.java.name
)
val notaryConfig = mock<NotaryConfig> {
whenever(it.serviceLegalName).thenReturn(serviceLegalName)
whenever(it.validating).thenReturn(true)
whenever(it.className).thenReturn(TestNotaryService::class.java.name)
}
val notaryNodes = (0 until CLUSTER_SIZE).map {
mockNet.createUnstartedNode(InternalMockNodeParameters(configOverrides = MockNodeConfigOverrides(
notary = notaryConfig
)))
mockNet.createUnstartedNode(InternalMockNodeParameters(configOverrides = {
doReturn(notaryConfig).whenever(it).notary
}))
}
val aliceNode = mockNet.createUnstartedNode(
InternalMockNodeParameters(
legalName = CordaX500Name("Alice", "AliceCorp", "GB"),
configOverrides = MockNodeConfigOverrides(flowTimeout = MockNetFlowTimeOut(2.seconds, 3, 1.0))
configOverrides = { conf: NodeConfiguration ->
val retryConfig = FlowTimeoutConfiguration(1.seconds, 3, 1.0)
doReturn(retryConfig).whenever(conf).flowTimeout
}
)
)
val patientNode = mockNet.createUnstartedNode(
InternalMockNodeParameters(
legalName = CordaX500Name("Bob", "BobCorp", "GB"),
configOverrides = MockNodeConfigOverrides(flowTimeout = MockNetFlowTimeOut(10.seconds, 3, 1.0))
configOverrides = { conf: NodeConfiguration ->
val retryConfig = FlowTimeoutConfiguration(10.seconds, 3, 1.0)
doReturn(retryConfig).whenever(conf).flowTimeout
}
)
)
// MockNetwork doesn't support notary clusters, so we create all the nodes we need unstarted, and then install the
// network-parameters in their directories before they're started.
val nodes = (notaryNodes + aliceNode + patientNode).map { node ->

View File

@ -7,7 +7,6 @@ import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.node.services.UnknownAnonymousPartyException
import net.corda.testing.internal.TestingNamedCacheFactory
import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.crypto.x509Certificates
@ -16,6 +15,7 @@ import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.core.*
import net.corda.testing.internal.DEV_INTERMEDIATE_CA
import net.corda.testing.internal.DEV_ROOT_CA
import net.corda.testing.internal.TestingNamedCacheFactory
import net.corda.testing.internal.configureDatabase
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import net.corda.testing.node.makeTestIdentityService
@ -96,21 +96,21 @@ class PersistentIdentityServiceTests {
}
@Test
fun `stripping others when none registered does not strip`() {
assertEquals(identityService.stripCachedPeerKeys(listOf(BOB_PUBKEY)).first(), BOB_PUBKEY)
fun `stripping others when none registered strips`() {
assertEquals(identityService.stripNotOurKeys(listOf(BOB_PUBKEY)).firstOrNull(), null)
}
@Test
fun `stripping others when only us registered does not strip`() {
fun `stripping others when only us registered strips`() {
identityService.verifyAndRegisterIdentity(ALICE_IDENTITY)
assertEquals(identityService.stripCachedPeerKeys(listOf(BOB_PUBKEY)).first(), BOB_PUBKEY)
assertEquals(identityService.stripNotOurKeys(listOf(BOB_PUBKEY)).firstOrNull(), null)
}
@Test
fun `stripping others when us and others registered does not strip us`() {
identityService.verifyAndRegisterIdentity(ALICE_IDENTITY)
identityService.verifyAndRegisterIdentity(BOB_IDENTITY)
val stripped = identityService.stripCachedPeerKeys(listOf(ALICE_PUBKEY, BOB_PUBKEY))
val stripped = identityService.stripNotOurKeys(listOf(ALICE_PUBKEY, BOB_PUBKEY))
assertEquals(stripped.single(), ALICE_PUBKEY)
}

View File

@ -8,7 +8,7 @@ import com.sun.xml.internal.messaging.saaj.util.ByteOutputStream
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.SignedDataWithCert
import net.corda.core.node.NetworkParameters
import net.corda.core.node.services.NetworkParametersStorage
import net.corda.core.node.services.NetworkParametersService
import net.corda.node.internal.DBNetworkParametersStorage
import net.corda.nodeapi.internal.createDevNetworkMapCa
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
@ -35,7 +35,7 @@ class DBNetworkParametersStorageTest {
val testSerialization = SerializationEnvironmentRule(true)
private lateinit var networkMapClient: NetworkMapClient
private lateinit var nodeParametersStorage: NetworkParametersStorage
private lateinit var networkParametersService: NetworkParametersService
private lateinit var database: CordaPersistence
private val certKeyPair: CertificateAndKeyPair = createDevNetworkMapCa()
@ -61,7 +61,7 @@ class DBNetworkParametersStorageTest {
{ null }
)
networkMapClient = createMockNetworkMapClient()
nodeParametersStorage = DBNetworkParametersStorage(TestingNamedCacheFactory(), database, networkMapClient).apply {
networkParametersService = DBNetworkParametersStorage(TestingNamedCacheFactory(), database, networkMapClient).apply {
database.transaction {
setCurrentParameters(netParams1, DEV_ROOT_CA.certificate)
}
@ -75,21 +75,21 @@ class DBNetworkParametersStorageTest {
@Test
fun `set current parameters`() {
assertThat(nodeParametersStorage.currentHash).isEqualTo(hash1)
assertThat(nodeParametersStorage.lookup(hash1)).isEqualTo(netParams1.verified())
assertThat(networkParametersService.currentHash).isEqualTo(hash1)
assertThat(networkParametersService.lookup(hash1)).isEqualTo(netParams1.verified())
}
@Test
fun `get default parameters`() {
// TODO After implementing default endpoint on network map check it is correct, for now we set it to current.
assertThat(nodeParametersStorage.defaultHash).isEqualTo(hash1)
assertThat(networkParametersService.defaultHash).isEqualTo(hash1)
}
@Test
fun `download parameters from network map server`() {
database.transaction {
val netParams = nodeParametersStorage.lookup(hash2)
assertThat(nodeParametersStorage.lookup(hash2)).isEqualTo(netParams)
val netParams = networkParametersService.lookup(hash2)
assertThat(networkParametersService.lookup(hash2)).isEqualTo(netParams)
verify(networkMapClient, times(1)).getNetworkParameters(hash2)
}
@ -99,7 +99,7 @@ class DBNetworkParametersStorageTest {
fun `try save parameters with incorrect signature`() {
database.transaction {
val consoleOutput = interceptConsoleOutput {
nodeParametersStorage.lookup(hash3)
networkParametersService.lookup(hash3)
}
assertThat(consoleOutput).anySatisfy {
it.contains("Caused by: java.security.cert.CertPathValidatorException: subject/issuer name chaining check failed")
@ -119,7 +119,7 @@ class DBNetworkParametersStorageTest {
private fun createMockNetworkMapClient(): NetworkMapClient {
return mock {
on { getNetworkParameters(any()) }.then {
val hash = it.getArguments()[0]
val hash = it.arguments[0]
when (hash) {
hash1 -> netParams1
hash2 -> netParams2

View File

@ -18,7 +18,7 @@ import net.corda.core.node.services.AttachmentId
import net.corda.core.serialization.serialize
import net.corda.core.utilities.millis
import net.corda.node.VersionInfo
import net.corda.node.internal.NetworkParametersStorageInternal
import net.corda.node.internal.NetworkParametersStorage
import net.corda.node.services.api.NetworkMapCacheInternal
import net.corda.node.services.config.NetworkParameterAcceptanceSettings
import net.corda.nodeapi.internal.NodeInfoAndSigned
@ -71,7 +71,7 @@ class NetworkMapUpdaterTest {
private val networkMapCache = createMockNetworkMapCache()
private lateinit var ourKeyPair: KeyPair
private lateinit var ourNodeInfo: SignedNodeInfo
private val networkParametersStorage: NetworkParametersStorageInternal = mock()
private val networkParametersStorage: NetworkParametersStorage = mock()
private lateinit var server: NetworkMapServer
private lateinit var networkMapClient: NetworkMapClient
private var updater: NetworkMapUpdater? = null

View File

@ -9,9 +9,7 @@ import net.corda.testing.internal.TestingNamedCacheFactory
import net.corda.testing.internal.configureDatabase
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@ -25,20 +23,20 @@ import javax.persistence.PersistenceException
class AppendOnlyPersistentMapTest(var scenario: Scenario) {
companion object {
private val scenarios = arrayOf<Scenario>(
Scenario(false, ReadOrWrite.Read, ReadOrWrite.Read, Outcome.Fail, Outcome.Fail, isCached = false),
Scenario(false, ReadOrWrite.Write, ReadOrWrite.Read, Outcome.Success, Outcome.Fail, Outcome.Success, null),
Scenario(false, ReadOrWrite.Read, ReadOrWrite.Write, Outcome.Fail, Outcome.Success, isCached = false),
Scenario(false, ReadOrWrite.Write, ReadOrWrite.Write, Outcome.Success, Outcome.SuccessButErrorOnCommit, isCached = null),
Scenario(false, ReadOrWrite.WriteDuplicateAllowed, ReadOrWrite.Read, Outcome.Success, Outcome.Fail, Outcome.Success, null),
Scenario(false, ReadOrWrite.Read, ReadOrWrite.WriteDuplicateAllowed, Outcome.Fail, Outcome.Success, isCached = true),
Scenario(false, ReadOrWrite.WriteDuplicateAllowed, ReadOrWrite.WriteDuplicateAllowed, Outcome.Success, Outcome.SuccessButErrorOnCommit, Outcome.Fail, null),
Scenario(true, ReadOrWrite.Read, ReadOrWrite.Read, Outcome.Success, Outcome.Success, isCached = false),
Scenario(true, ReadOrWrite.Write, ReadOrWrite.Read, Outcome.SuccessButErrorOnCommit, Outcome.Success, isCached = null),
Scenario(true, ReadOrWrite.Read, ReadOrWrite.Write, Outcome.Success, Outcome.Fail, isCached = true),
Scenario(true, ReadOrWrite.Write, ReadOrWrite.Write, Outcome.SuccessButErrorOnCommit, Outcome.SuccessButErrorOnCommit, isCached = null),
Scenario(true, ReadOrWrite.WriteDuplicateAllowed, ReadOrWrite.Read, Outcome.Fail, Outcome.Success, isCached = null),
Scenario(true, ReadOrWrite.Read, ReadOrWrite.WriteDuplicateAllowed, Outcome.Success, Outcome.Fail, isCached = true),
Scenario(true, ReadOrWrite.WriteDuplicateAllowed, ReadOrWrite.WriteDuplicateAllowed, Outcome.Fail, Outcome.Fail, isCached = null)
Scenario(false, ReadOrWrite.Read, ReadOrWrite.Read, Outcome.Fail, Outcome.Fail),
Scenario(false, ReadOrWrite.Write, ReadOrWrite.Read, Outcome.Success, Outcome.Fail, Outcome.Success),
Scenario(false, ReadOrWrite.Read, ReadOrWrite.Write, Outcome.Fail, Outcome.Success),
Scenario(false, ReadOrWrite.Write, ReadOrWrite.Write, Outcome.Success, Outcome.SuccessButErrorOnCommit),
Scenario(false, ReadOrWrite.WriteDuplicateAllowed, ReadOrWrite.Read, Outcome.Success, Outcome.Fail, Outcome.Success),
Scenario(false, ReadOrWrite.Read, ReadOrWrite.WriteDuplicateAllowed, Outcome.Fail, Outcome.Success),
Scenario(false, ReadOrWrite.WriteDuplicateAllowed, ReadOrWrite.WriteDuplicateAllowed, Outcome.Success, Outcome.SuccessButErrorOnCommit, Outcome.Fail),
Scenario(true, ReadOrWrite.Read, ReadOrWrite.Read, Outcome.Success, Outcome.Success),
Scenario(true, ReadOrWrite.Write, ReadOrWrite.Read, Outcome.SuccessButErrorOnCommit, Outcome.Success),
Scenario(true, ReadOrWrite.Read, ReadOrWrite.Write, Outcome.Success, Outcome.Fail),
Scenario(true, ReadOrWrite.Write, ReadOrWrite.Write, Outcome.SuccessButErrorOnCommit, Outcome.SuccessButErrorOnCommit),
Scenario(true, ReadOrWrite.WriteDuplicateAllowed, ReadOrWrite.Read, Outcome.Fail, Outcome.Success),
Scenario(true, ReadOrWrite.Read, ReadOrWrite.WriteDuplicateAllowed, Outcome.Success, Outcome.Fail),
Scenario(true, ReadOrWrite.WriteDuplicateAllowed, ReadOrWrite.WriteDuplicateAllowed, Outcome.Fail, Outcome.Fail)
)
@Parameterized.Parameters(name = "{0}")
@ -54,8 +52,7 @@ class AppendOnlyPersistentMapTest(var scenario: Scenario) {
val b: ReadOrWrite,
val aExpected: Outcome,
val bExpected: Outcome,
val bExpectedIfSingleThreaded: Outcome = bExpected,
val isCached: Boolean?)
val bExpectedIfSingleThreaded: Outcome = bExpected)
private val database = configureDatabase(makeTestDataSourceProperties(),
DatabaseConfig(),
@ -67,36 +64,6 @@ class AppendOnlyPersistentMapTest(var scenario: Scenario) {
database.close()
}
@Test
fun `getIfCached behaves as expected`() {
if (scenario.isCached != null) {
prepopulateIfRequired()
val map = createMap()
when (scenario.b) {
ReadOrWrite.Read -> { /* Do nothing */
}
ReadOrWrite.Write -> {
// Cause a read-thru
database.transaction { map.get(1) }
}
ReadOrWrite.WriteDuplicateAllowed -> {
// Write a value that overwrites anything pre-populated potentially.
database.transaction { map.addWithDuplicatesAllowed(1, "Y") }
}
}
val cachedValue = map.getIfCached(1)
val expectedValue = if (scenario.isCached!!) {
when (scenario.b) {
ReadOrWrite.Read -> throw IllegalStateException("Do nothing and isCached = true is not a valid combination.")
ReadOrWrite.Write -> "X"
ReadOrWrite.WriteDuplicateAllowed -> "Y"
}
} else null
assertEquals(expectedValue, cachedValue)
}
}
@Test
fun `concurrent test no purge between A and B`() {
prepopulateIfRequired()
@ -156,7 +123,7 @@ class AppendOnlyPersistentMapTest(var scenario: Scenario) {
@Test
fun `concurrent test purge between A and B`() {
// Writes intentionally do not check the database first, so purging between read and write changes behaviour
val remapped = mapOf(Scenario(true, ReadOrWrite.Read, ReadOrWrite.Write, Outcome.Success, Outcome.Fail, isCached = true) to Scenario(true, ReadOrWrite.Read, ReadOrWrite.Write, Outcome.Success, Outcome.SuccessButErrorOnCommit, isCached = true))
val remapped = mapOf(Scenario(true, ReadOrWrite.Read, ReadOrWrite.Write, Outcome.Success, Outcome.Fail) to Scenario(true, ReadOrWrite.Read, ReadOrWrite.Write, Outcome.Success, Outcome.SuccessButErrorOnCommit))
scenario = remapped[scenario] ?: scenario
prepopulateIfRequired()
val map = createMap()
@ -191,8 +158,8 @@ class AppendOnlyPersistentMapTest(var scenario: Scenario) {
fun `test purge mid-way in a single transaction`() {
// Writes intentionally do not check the database first, so purging between read and write changes behaviour
// Also, a purge after write causes the subsequent read to flush to the database, causing the read to generate a constraint violation when single threaded (in same database transaction).
val remapped = mapOf(Scenario(true, ReadOrWrite.Read, ReadOrWrite.Write, Outcome.Success, Outcome.Fail, isCached = true) to Scenario(true, ReadOrWrite.Read, ReadOrWrite.Write, Outcome.SuccessButErrorOnCommit, Outcome.SuccessButErrorOnCommit, isCached = true),
Scenario(true, ReadOrWrite.Write, ReadOrWrite.Read, Outcome.SuccessButErrorOnCommit, Outcome.Success, isCached = null) to Scenario(true, ReadOrWrite.Write, ReadOrWrite.Read, Outcome.SuccessButErrorOnCommit, Outcome.SuccessButErrorOnCommit, isCached = null))
val remapped = mapOf(Scenario(true, ReadOrWrite.Read, ReadOrWrite.Write, Outcome.Success, Outcome.Fail) to Scenario(true, ReadOrWrite.Read, ReadOrWrite.Write, Outcome.SuccessButErrorOnCommit, Outcome.SuccessButErrorOnCommit),
Scenario(true, ReadOrWrite.Write, ReadOrWrite.Read, Outcome.SuccessButErrorOnCommit, Outcome.Success) to Scenario(true, ReadOrWrite.Write, ReadOrWrite.Read, Outcome.SuccessButErrorOnCommit, Outcome.SuccessButErrorOnCommit))
scenario = remapped[scenario] ?: scenario
prepopulateIfRequired()
val map = createMap()

View File

@ -5,12 +5,15 @@ import com.esotericsoftware.kryo.KryoException
import net.corda.core.contracts.UniqueIdentifier
import net.corda.core.flows.FlowLogic
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.packageName
import net.corda.core.schemas.MappedSchema
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.TestIdentity
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.MockNetworkParameters
import net.corda.testing.node.MockServices
import net.corda.testing.node.internal.cordappsForPackages
import net.corda.testing.node.makeTestIdentityService
import org.junit.After
import org.junit.Before
@ -34,16 +37,15 @@ class ExposeJpaToFlowsTests {
}
val myself = TestIdentity(CordaX500Name("Me", "London", "GB"))
val cordapps = listOf("net.corda.node.services.persistence")
lateinit var mockNet: MockNetwork
lateinit var services: MockServices
lateinit var database: CordaPersistence
@Before
fun setUp() {
mockNet = MockNetwork(cordapps)
mockNet = MockNetwork(MockNetworkParameters(cordappsForAllNodes = cordappsForPackages(javaClass.packageName)))
val (db, mockServices) = MockServices.makeTestDatabaseAndMockServices(
cordappPackages = cordapps,
cordappPackages = listOf(javaClass.packageName),
identityService = makeTestIdentityService(myself.identity),
initialIdentity = myself,
networkParameters = testNetworkParameters(minimumPlatformVersion = 4)

View File

@ -2,24 +2,21 @@ package net.corda.node.services.persistence
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.Amount
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.Party
import net.corda.core.node.NotaryInfo
import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.getOrThrow
import net.corda.finance.DOLLARS
import net.corda.finance.`issued by`
import net.corda.finance.contracts.asset.Cash
import net.corda.finance.flows.AbstractCashFlow
import net.corda.finance.flows.CashIssueFlow
import net.corda.finance.issuedBy
import net.corda.node.internal.NetworkParametersStorageInternal
import net.corda.node.services.identity.PersistentIdentityService
import net.corda.node.services.keys.E2ETestKeyManagementService
import net.corda.testing.internal.TestingNamedCacheFactory
import net.corda.testing.core.BOC_NAME
import net.corda.testing.internal.TestingNamedCacheFactory
import net.corda.testing.node.InMemoryMessagingNetwork
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.StartedMockNode
@ -48,7 +45,7 @@ class HibernateColumnConverterTests {
return Result(tx, ourIdentity)
}
}
@Before
fun start() {
mockNet = MockNetwork(

View File

@ -25,9 +25,9 @@ import net.corda.finance.SWISS_FRANCS
import net.corda.finance.contracts.asset.Cash
import net.corda.finance.contracts.asset.test.DummyFungibleContract
import net.corda.finance.schemas.CashSchemaV1
import net.corda.finance.schemas.test.SampleCashSchemaV1
import net.corda.finance.schemas.test.SampleCashSchemaV2
import net.corda.finance.schemas.test.SampleCashSchemaV3
import net.corda.finance.test.SampleCashSchemaV1
import net.corda.finance.test.SampleCashSchemaV2
import net.corda.finance.test.SampleCashSchemaV3
import net.corda.finance.utils.sumCash
import net.corda.node.services.api.IdentityServiceInternal
import net.corda.node.services.api.WritableTransactionStorage
@ -100,12 +100,12 @@ class HibernateConfigurationTest {
@Before
fun setUp() {
val cordappPackages = listOf("net.corda.testing.internal.vault", "net.corda.finance.contracts.asset", "net.corda.finance.schemas")
bankServices = MockServices(cordappPackages, BOC.name, rigorousMock(), BOC_KEY)
issuerServices = MockServices(cordappPackages, dummyCashIssuer, rigorousMock<IdentityService>())
notaryServices = MockServices(cordappPackages, dummyNotary, rigorousMock<IdentityService>())
bankServices = MockServices(cordappPackages, BOC.name, mock(), BOC_KEY)
issuerServices = MockServices(cordappPackages, dummyCashIssuer, mock<IdentityService>())
notaryServices = MockServices(cordappPackages, dummyNotary, mock<IdentityService>())
notary = notaryServices.myInfo.singleIdentity()
val dataSourceProps = makeTestDataSourceProperties()
val identityService = rigorousMock<IdentityService>().also { mock ->
val identityService = mock<IdentityService>().also { mock ->
doReturn(null).whenever(mock).wellKnownPartyFromAnonymous(any<AbstractParty>())
listOf(dummyCashIssuer, dummyNotary).forEach {
doReturn(it.party).whenever(mock).wellKnownPartyFromAnonymous(it.party)
@ -118,10 +118,10 @@ class HibernateConfigurationTest {
hibernateConfig = database.hibernateConfig
// `consumeCash` expects we can self-notarise transactions
services = object : MockServices(cordappPackages, BOB_NAME, rigorousMock<IdentityServiceInternal>().also {
services = object : MockServices(cordappPackages, BOB_NAME, mock<IdentityServiceInternal>().also {
doNothing().whenever(it).justVerifyAndRegisterIdentity(argThat { name == BOB_NAME }, any())
}, generateKeyPair(), dummyNotary.keyPair) {
override val vaultService = NodeVaultService(Clock.systemUTC(), keyManagementService, servicesForResolution, database, schemaService).apply { start() }
override val vaultService = NodeVaultService(Clock.systemUTC(), keyManagementService, servicesForResolution, database, schemaService, cordappClassloader).apply { start() }
override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable<SignedTransaction>) {
for (stx in txs) {
(validatedTransactions as WritableTransactionStorage).addTransaction(stx)

View File

@ -15,8 +15,8 @@ import javax.persistence.Table
@CordaSerializable
data class MessageData(val value: String)
data class MessageChainState(val message: MessageData, val by: Party, override val linearId: UniqueIdentifier = UniqueIdentifier()) : LinearState, QueryableState {
override val participants: List<AbstractParty> = listOf(by)
data class MessageChainState(val message: MessageData, val by: Party, override val linearId: UniqueIdentifier = UniqueIdentifier(), val extraParty: Party? = null) : LinearState, QueryableState {
override val participants: List<AbstractParty> = if (extraParty == null) listOf(by) else listOf(by, extraParty)
override fun generateMappedObject(schema: MappedSchema): PersistentState {
return when (schema) {

View File

@ -4,6 +4,8 @@ import co.paralleluniverse.fibers.Suspendable
import com.codahale.metrics.MetricRegistry
import com.google.common.jimfs.Configuration
import com.google.common.jimfs.Jimfs
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.contracts.ContractAttachment
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256
@ -17,21 +19,22 @@ import net.corda.core.node.services.vault.Builder
import net.corda.core.node.services.vault.Sort
import net.corda.core.utilities.getOrThrow
import net.corda.node.services.transactions.PersistentUniquenessProvider
import net.corda.nodeapi.exceptions.DuplicateAttachmentException
import net.corda.nodeapi.exceptions.DuplicateContractClassException
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.internal.ContractJarTestUtils.makeTestContractJar
import net.corda.testing.core.internal.ContractJarTestUtils.makeTestJar
import net.corda.testing.core.internal.ContractJarTestUtils.makeTestSignedContractJar
import net.corda.testing.core.internal.SelfCleaningDir
import net.corda.testing.internal.LogHelper
import net.corda.testing.internal.TestingNamedCacheFactory
import net.corda.testing.internal.configureDatabase
import net.corda.testing.internal.rigorousMock
import net.corda.testing.internal.*
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.startFlow
import org.assertj.core.api.Assertions.assertThatIllegalArgumentException
import org.assertj.core.api.Assertions.*
import org.junit.After
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
@ -51,7 +54,9 @@ class NodeAttachmentServiceTest {
private lateinit var fs: FileSystem
private lateinit var database: CordaPersistence
private lateinit var storage: NodeAttachmentService
private val services = rigorousMock<ServicesForResolution>()
private val services = rigorousMock<ServicesForResolution>().also {
doReturn(testNetworkParameters()).whenever(it).networkParameters
}
@Before
fun setUp() {
@ -121,6 +126,43 @@ class NodeAttachmentServiceTest {
}
}
@Test
fun `attachment can be overridden by trusted uploader`() {
SelfCleaningDir().use { file ->
val contractJarName = makeTestContractJar(file.path, "com.example.MyContract")
val attachment = file.path.resolve(contractJarName)
val expectedAttachmentId = attachment.readAll().sha256()
val initialUploader = "test"
val attachmentId = attachment.read { storage.privilegedImportAttachment(it, initialUploader, null) }
assertThat(attachmentId).isEqualTo(expectedAttachmentId)
assertThat((storage.openAttachment(expectedAttachmentId) as ContractAttachment).uploader).isEqualTo(initialUploader)
val trustedUploader = TRUSTED_UPLOADERS.randomOrNull()!!
val overriddenAttachmentId = attachment.read { storage.privilegedImportAttachment(it, trustedUploader, null) }
assertThat(overriddenAttachmentId).isEqualTo(expectedAttachmentId)
assertThat((storage.openAttachment(expectedAttachmentId) as ContractAttachment).uploader).isEqualTo(trustedUploader)
}
}
@Test
fun `attachment cannot be overridden by untrusted uploader`() {
SelfCleaningDir().use { file ->
val contractJarName = makeTestContractJar(file.path, "com.example.MyContract")
val attachment = file.path.resolve(contractJarName)
val expectedAttachmentId = attachment.readAll().sha256()
val trustedUploader = TRUSTED_UPLOADERS.randomOrNull()!!
val attachmentId = attachment.read { storage.privilegedImportAttachment(it, trustedUploader, null) }
assertThat(attachmentId).isEqualTo(expectedAttachmentId)
assertThat((storage.openAttachment(expectedAttachmentId) as ContractAttachment).uploader).isEqualTo(trustedUploader)
val untrustedUploader = "test"
assertThatThrownBy { attachment.read { storage.privilegedImportAttachment(it, untrustedUploader, null) } }.isInstanceOf(DuplicateAttachmentException::class.java)
}
}
@Test
fun `insert contract attachment as an untrusted uploader and then as trusted CorDapp uploader`() {
SelfCleaningDir().use { file ->
@ -230,10 +272,18 @@ class NodeAttachmentServiceTest {
storage.queryAttachments(AttachmentsQueryCriteria(signersCondition = Builder.equal(listOf(publicKey)))).size
)
assertEquals(
3,
storage.queryAttachments(AttachmentsQueryCriteria(isSignedCondition = Builder.equal(true))).size
)
val allAttachments = storage.queryAttachments(AttachmentsQueryCriteria())
assertEquals(6, allAttachments.size)
val signedAttachments = storage.queryAttachments(AttachmentsQueryCriteria(isSignedCondition = Builder.equal(true)))
assertEquals(3, signedAttachments.size)
val unsignedAttachments = storage.queryAttachments(AttachmentsQueryCriteria(isSignedCondition = Builder.equal(false)))
assertEquals(3, unsignedAttachments.size)
assertNotEquals(signedAttachments.toSet(), unsignedAttachments.toSet())
assertEquals(signedAttachments.toSet() + unsignedAttachments.toSet(), allAttachments.toSet())
assertEquals(
1,
@ -277,6 +327,138 @@ class NodeAttachmentServiceTest {
}
}
@Test
fun `cannot import jar with duplicated contract class, version and signers for trusted uploader`() {
SelfCleaningDir().use { file ->
val (contractJar, _) = makeTestSignedContractJar(file.path, "com.example.MyContract")
val anotherContractJar = makeTestContractJar(file.path, listOf( "com.example.MyContract", "com.example.AnotherContract"), true, generateManifest = false, jarFileName = "another-sample.jar")
contractJar.read { storage.privilegedImportAttachment(it, "app", "sample.jar") }
assertThatExceptionOfType(DuplicateContractClassException::class.java).isThrownBy {
anotherContractJar.read { storage.privilegedImportAttachment(it, "app", "another-sample.jar") }
}
}
}
@Test
fun `can import jar with duplicated contract class, version and signers - when one uploader is trusted and other isnt`() {
SelfCleaningDir().use { file ->
val (contractJar, _) = makeTestSignedContractJar(file.path, "com.example.MyContract")
val anotherContractJar = makeTestContractJar(file.path, listOf( "com.example.MyContract", "com.example.AnotherContract"), true, generateManifest = false, jarFileName = "another-sample.jar")
val attachmentId = contractJar.read { storage.importAttachment(it, "uploaderA", "sample.jar") }
val anotherAttachmentId = anotherContractJar.read { storage.privilegedImportAttachment(it, "app", "another-sample.jar") }
assertNotEquals(attachmentId, anotherAttachmentId)
}
}
@Test
fun `can promote to trusted uploader for the same attachment`() {
SelfCleaningDir().use { file ->
val (contractJar, _) = makeTestSignedContractJar(file.path, "com.example.MyContract")
val attachmentId = contractJar.read { storage.importAttachment(it, "uploaderA", "sample.jar") }
val reimportedAttachmentId = contractJar.read { storage.privilegedImportAttachment(it, "app", "sample.jar") }
assertEquals(attachmentId, reimportedAttachmentId)
}
}
@Test
fun `cannot promote to trusted uploader if other trusted attachment already has duplicated contract class, version and signers`() {
SelfCleaningDir().use { file ->
val (contractJar, _) = makeTestSignedContractJar(file.path, "com.example.MyContract")
contractJar.read { storage.importAttachment(it, "uploaderA", "sample.jar") }
val anotherContractJar = makeTestContractJar(file.path, listOf( "com.example.MyContract", "com.example.AnotherContract"), true, generateManifest = false, jarFileName = "another-sample.jar")
anotherContractJar.read { storage.privilegedImportAttachment(it, "app", "another-sample.jar") }
assertThatExceptionOfType(DuplicateContractClassException::class.java).isThrownBy {
contractJar.read { storage.privilegedImportAttachment(it, "app", "sample.jar") }
}
}
}
@Test
fun `cannot promote to trusted uploder the same jar if other trusted uplodaer `() {
SelfCleaningDir().use { file ->
val (contractJar, _) = makeTestSignedContractJar(file.path, "com.example.MyContract")
val anotherContractJar = makeTestContractJar(file.path, listOf( "com.example.MyContract", "com.example.AnotherContract"), true, generateManifest = false, jarFileName = "another-sample.jar")
contractJar.read { storage.privilegedImportAttachment(it, "app", "sample.jar") }
assertThatExceptionOfType(DuplicateContractClassException::class.java).isThrownBy {
anotherContractJar.read { storage.privilegedImportAttachment(it, "app", "another-sample.jar") }
}
}
}
@Test
fun `can import duplicated contract class and signers if versions differ`() {
SelfCleaningDir().use { file ->
val (contractJar, _) = makeTestSignedContractJar(file.path, "com.example.MyContract", 2)
val anotherContractJar = makeTestContractJar(file.path, listOf( "com.example.MyContract", "com.example.AnotherContract"), true, generateManifest = false, jarFileName = "another-sample.jar")
contractJar.read { storage.importAttachment(it, "uploaderA", "sample.jar") }
anotherContractJar.read { storage.importAttachment(it, "uploaderA", "another-sample.jar") }
val attachments = storage.queryAttachments(AttachmentsQueryCriteria(contractClassNamesCondition = Builder.equal(listOf("com.example.MyContract"))))
assertEquals(2, attachments.size)
attachments.forEach {
assertTrue("com.example.MyContract" in (storage.openAttachment(it) as ContractAttachment).allContracts)
}
}
}
@Test
fun `can import duplicated contract class and version from unsiged attachment if a signed attachment already exists`() {
SelfCleaningDir().use { file ->
val (contractJar, _) = makeTestSignedContractJar(file.path, "com.example.MyContract")
val anotherContractJar = makeTestContractJar(file.path, listOf( "com.example.MyContract", "com.example.AnotherContract"), generateManifest = false, jarFileName = "another-sample.jar")
contractJar.read { storage.importAttachment(it, "uploaderA", "sample.jar") }
anotherContractJar.read { storage.importAttachment(it, "uploaderB", "another-sample.jar") }
val attachments = storage.queryAttachments(AttachmentsQueryCriteria(contractClassNamesCondition = Builder.equal(listOf("com.example.MyContract"))))
assertEquals(2, attachments.size)
attachments.forEach {
val att = storage.openAttachment(it)
assertTrue(att is ContractAttachment)
assertTrue("com.example.MyContract" in (att as ContractAttachment).allContracts)
}
}
}
@Test
fun `can import duplicated contract class and version from siged attachment if an unsigned attachment already exists`() {
SelfCleaningDir().use { file ->
val contractJar = makeTestContractJar(file.path, "com.example.MyContract")
val anotherContractJar = makeTestContractJar(file.path, listOf( "com.example.MyContract", "com.example.AnotherContract"), true, generateManifest = false, jarFileName = "another-sample.jar")
contractJar.read { storage.importAttachment(it, "uploaderA", "sample.jar") }
anotherContractJar.read { storage.importAttachment(it, "uploaderB", "another-sample.jar") }
val attachments = storage.queryAttachments(AttachmentsQueryCriteria(contractClassNamesCondition = Builder.equal(listOf("com.example.MyContract"))))
assertEquals(2, attachments.size)
attachments.forEach {
val att = storage.openAttachment(it)
assertTrue(att is ContractAttachment)
assertTrue("com.example.MyContract" in (att as ContractAttachment).allContracts)
}
}
}
@Test
fun `can import duplicated contract class and version for unsigned attachments`() {
SelfCleaningDir().use { file ->
val contractJar = makeTestContractJar(file.path, "com.example.MyContract")
val anotherContractJar = makeTestContractJar(file.path, listOf( "com.example.MyContract", "com.example.AnotherContract"), generateManifest = false, jarFileName = "another-sample.jar")
contractJar.read { storage.importAttachment(it, "uploaderA", "sample.jar") }
anotherContractJar.read { storage.importAttachment(it, "uploaderB", "another-sample.jar") }
val attachments = storage.queryAttachments(AttachmentsQueryCriteria(contractClassNamesCondition = Builder.equal(listOf("com.example.MyContract"))))
assertEquals(2, attachments.size)
attachments.forEach {
val att = storage.openAttachment(it)
assertTrue(att is ContractAttachment)
assertTrue("com.example.MyContract" in (att as ContractAttachment).allContracts)
}
}
}
@Test
fun `sorting and compound conditions work`() {
val (jarA, hashA) = makeTestJar(listOf(Pair("a", "a")))

View File

@ -0,0 +1,115 @@
package net.corda.node.services.persistence
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.Command
import net.corda.core.contracts.StateAndContract
import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.StateRef
import net.corda.core.flows.*
import net.corda.core.identity.Party
import net.corda.core.internal.packageName
import net.corda.core.node.StatesToRecord
import net.corda.core.node.services.Vault
import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.getOrThrow
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.cordappWithPackages
import net.corda.testing.node.internal.startFlow
import org.junit.After
import org.junit.Before
import org.junit.Test
import kotlin.test.assertEquals
class TransactionOrderingTests {
private lateinit var mockNet: InternalMockNetwork
@Before
fun start() {
mockNet = InternalMockNetwork(
cordappsForAllNodes = listOf(cordappWithPackages(MessageChainState::class.packageName)),
networkSendManuallyPumped = false,
threadPerNode = true)
}
@After
fun cleanUp() {
mockNet.stopNodes()
}
@Test
fun `Out of order transactions are recorded in vault correctly`() {
val alice = mockNet.createPartyNode(ALICE_NAME)
val aliceID = alice.info.identityFromX500Name(ALICE_NAME)
val bob = mockNet.createPartyNode(BOB_NAME)
val bobID = bob.info.identityFromX500Name(BOB_NAME)
bob.registerInitiatedFlow(ReceiveTx::class.java)
val notary = mockNet.defaultNotaryNode
val notaryID = mockNet.defaultNotaryIdentity
fun signTx(txBuilder: TransactionBuilder): SignedTransaction {
val first = alice.services.signInitialTransaction(txBuilder)
val second = bob.services.addSignature(first)
return notary.services.addSignature(second)
}
val state1 = MessageChainState(MessageData("A"), aliceID, extraParty = bobID)
val command = Command(MessageChainContract.Commands.Send(), state1.participants.map {it.owningKey})
val tx1Builder = TransactionBuilder(notaryID).withItems(
StateAndContract(state1, MESSAGE_CHAIN_CONTRACT_PROGRAM_ID),
command)
val stx1 = signTx(tx1Builder)
val state2 = MessageChainState(MessageData("AA"), aliceID, state1.linearId, extraParty = bobID)
val tx2Builder = TransactionBuilder(notaryID).withItems(
StateAndContract(state2, MESSAGE_CHAIN_CONTRACT_PROGRAM_ID),
command,
StateAndRef(stx1.coreTransaction.outputs[0], StateRef(stx1.coreTransaction.id, 0))
)
val stx2 = signTx(tx2Builder)
val state3 = MessageChainState(MessageData("AAA"), aliceID, state1.linearId, extraParty = bobID)
val tx3Builder = TransactionBuilder(notaryID).withItems(
StateAndContract(state3, MESSAGE_CHAIN_CONTRACT_PROGRAM_ID),
command,
StateAndRef(stx2.coreTransaction.outputs[0], StateRef(stx2.coreTransaction.id, 0))
)
val stx3 = signTx(tx3Builder)
alice.services.recordTransactions(listOf(stx1, stx2, stx3))
alice.services.startFlow(SendTx(bobID, stx3)).resultFuture.getOrThrow()
alice.services.startFlow(SendTx(bobID, stx1)).resultFuture.getOrThrow()
alice.services.startFlow(SendTx(bobID, stx2)).resultFuture.getOrThrow()
val queryCriteria = QueryCriteria.VaultQueryCriteria(Vault.StateStatus.ALL)
val bobStates = bob.services.vaultService.queryBy(MessageChainState::class.java, queryCriteria)
assertEquals(3, bobStates.states.size)
}
}
@InitiatingFlow
@StartableByRPC
class SendTx(private val party: Party,
private val stx: SignedTransaction) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
val session = initiateFlow(party)
subFlow(SendTransactionFlow(session, stx))
session.receive<Unit>()
}
}
@InitiatedBy(SendTx::class)
class ReceiveTx(private val otherSideSession: FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
subFlow(ReceiveTransactionFlow(otherSideSession, true, StatesToRecord.ONLY_RELEVANT))
otherSideSession.send(Unit)
}
}

View File

@ -1,6 +1,8 @@
package net.corda.node.services.statemachine
import co.paralleluniverse.fibers.Suspendable
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow
@ -9,8 +11,7 @@ import net.corda.core.internal.IdempotentFlow
import net.corda.core.internal.TimedFlow
import net.corda.core.internal.packageName
import net.corda.core.utilities.seconds
import net.corda.testing.node.MockNetFlowTimeOut
import net.corda.testing.node.MockNodeConfigOverrides
import net.corda.node.services.config.FlowTimeoutConfiguration
import net.corda.testing.node.internal.*
import org.junit.After
import org.junit.Before
@ -35,7 +36,10 @@ class IdempotentFlowTests {
mockNet = InternalMockNetwork(threadPerNode = true, cordappsForAllNodes = cordappsForPackages(this.javaClass.packageName))
nodeA = mockNet.createNode(InternalMockNodeParameters(
legalName = CordaX500Name("Alice", "AliceCorp", "GB"),
configOverrides = MockNodeConfigOverrides(flowTimeout = MockNetFlowTimeOut(1.seconds, 3, 1.0))
configOverrides = {
val retryConfig = FlowTimeoutConfiguration(1.seconds, 3, 1.0)
doReturn(retryConfig).whenever(it).flowTimeout
}
))
nodeB = mockNet.createNode()
mockNet.startNodes()
@ -71,7 +75,7 @@ class IdempotentFlowTests {
@Suspendable
override fun call() {
subFlowExecutionCounter.incrementAndGet() // No checkpoint should be taken before invoking IdempotentSubFlow,
// so this should be replayed when TimedSubFlow restarts.
// so this should be replayed when TimedSubFlow restarts.
subFlow(IdempotentSubFlow()) // Checkpoint shouldn't be taken before invoking the sub-flow.
}
}

View File

@ -22,7 +22,6 @@ import net.corda.testing.node.internal.*
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import kotlin.test.assertFailsWith
@ -89,7 +88,7 @@ class NotaryServiceTests {
private fun generateTransaction(node: TestStartedNode,
party: Party, notary: Party,
paramsHash: SecureHash? = node.services.networkParametersStorage.currentHash,
paramsHash: SecureHash? = node.services.networkParametersService.currentHash,
numberOfInputs: Int = 10_005): SignedTransaction {
val txHash = SecureHash.randomSHA256()
val inputs = (1..numberOfInputs).map { StateRef(txHash, it) }

View File

@ -222,7 +222,7 @@ class NotaryWhitelistTests(
listOf(inputState.ref),
fakeNotaryParty,
oldNotary,
aliceNode.services.networkParametersStorage.currentHash
aliceNode.services.networkParametersService.currentHash
).build()
val notaryChangeAliceSig = getAliceSig(notaryChangeTx)

View File

@ -87,7 +87,7 @@ class ResolveStatePointersTest {
@Test
fun `resolving nested pointers is possible`() {
// Create barOne.
createPointedToState(barOne)
val barOneStateAndRef = createPointedToState(barOne)
// Create another Bar - barTwo - which points to barOne.
val barTwoStateAndRef = createPointedToState(barTwo)
@ -105,6 +105,7 @@ class ResolveStatePointersTest {
// Check both Bar StateRefs have been added to the transaction.
assertEquals(2, tx.referenceStates().size)
assertEquals(setOf(barOneStateAndRef.ref, barTwoStateAndRef.ref), tx.referenceStates().toSet())
}
@Test

View File

@ -1,5 +1,6 @@
package net.corda.node.services.transactions
import com.codahale.metrics.MetricRegistry
import net.corda.core.contracts.TimeWindow
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.NullKeys
@ -10,16 +11,21 @@ import net.corda.core.flows.NotaryError
import net.corda.core.flows.StateConsumptionDetails
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.notary.UniquenessProvider
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.minutes
import net.corda.node.services.schema.NodeSchemaService
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.notary.experimental.raft.RaftConfig
import net.corda.notary.experimental.raft.RaftNotarySchemaV1
import net.corda.notary.experimental.raft.RaftUniquenessProvider
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.core.TestIdentity
import net.corda.testing.core.generateStateRef
import net.corda.testing.internal.LogHelper
import net.corda.testing.internal.TestingNamedCacheFactory
import net.corda.testing.internal.configureDatabase
import net.corda.testing.internal.configureTestSSL
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import net.corda.testing.node.TestClock
import org.junit.After
@ -39,7 +45,8 @@ class UniquenessProviderTests(
@JvmStatic
@Parameterized.Parameters(name = "{0}")
fun data(): Collection<UniquenessProviderFactory> = listOf(
PersistentUniquenessProviderFactory()
PersistentUniquenessProviderFactory(),
RaftUniquenessProviderFactory()
)
}
@ -152,11 +159,16 @@ class UniquenessProviderTests(
.get()
assertEquals(UniquenessProvider.Result.Success, result)
// Idempotency: can re-notarise successfully.
testClock.advanceBy(90.minutes)
val result2 = uniquenessProvider.commit(emptyList(), firstTxId, identity, requestSignature, timeWindow, references = listOf(referenceState))
// The reference state gets consumed.
val result2 = uniquenessProvider.commit(listOf(referenceState), SecureHash.randomSHA256(), identity, requestSignature, timeWindow)
.get()
assertEquals(UniquenessProvider.Result.Success, result2)
// Idempotency: can re-notarise successfully.
testClock.advanceBy(90.minutes)
val result3 = uniquenessProvider.commit(emptyList(), firstTxId, identity, requestSignature, timeWindow, references = listOf(referenceState))
.get()
assertEquals(UniquenessProvider.Result.Success, result3)
}
@Test
@ -389,3 +401,34 @@ class PersistentUniquenessProviderFactory : UniquenessProviderFactory {
database?.close()
}
}
class RaftUniquenessProviderFactory : UniquenessProviderFactory {
private var database: CordaPersistence? = null
private var provider: RaftUniquenessProvider? = null
override fun create(clock: Clock): UniquenessProvider {
database?.close()
database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(), { null }, { null }, NodeSchemaService(extraSchemas = setOf(RaftNotarySchemaV1)))
val testSSL = configureTestSSL(CordaX500Name("Raft", "London", "GB"))
val raftNodePort = 10987
return RaftUniquenessProvider(
null,
testSSL,
database!!,
clock,
MetricRegistry(),
TestingNamedCacheFactory(),
RaftConfig(NetworkHostAndPort("localhost", raftNodePort), emptyList())
).apply {
start()
provider = this
}
}
override fun cleanUp() {
provider?.stop()
database?.close()
}
}

View File

@ -1,6 +1,7 @@
package net.corda.node.services.vault
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
@ -46,7 +47,7 @@ class ExternalIdMappingTest {
fun setUp() {
val (db, mockServices) = MockServices.makeTestDatabaseAndMockServices(
cordappPackages = cordapps,
identityService = rigorousMock<IdentityServiceInternal>().also {
identityService = mock<IdentityServiceInternal>().also {
doReturn(notary.party).whenever(it).partyFromKey(notary.publicKey)
doReturn(notary.party).whenever(it).wellKnownPartyFromAnonymous(notary.party)
doReturn(notary.party).whenever(it).wellKnownPartyFromX500Name(notary.name)

View File

@ -1,10 +1,7 @@
package net.corda.node.services.vault
import co.paralleluniverse.fibers.Suspendable
import com.nhaarman.mockito_kotlin.any
import com.nhaarman.mockito_kotlin.argThat
import com.nhaarman.mockito_kotlin.doNothing
import com.nhaarman.mockito_kotlin.whenever
import com.nhaarman.mockito_kotlin.*
import net.corda.core.contracts.*
import net.corda.core.crypto.NullKeys
import net.corda.core.crypto.generateKeyPair
@ -35,16 +32,12 @@ import net.corda.testing.contracts.DummyContract
import net.corda.testing.contracts.DummyState
import net.corda.testing.core.*
import net.corda.testing.internal.LogHelper
import net.corda.testing.internal.rigorousMock
import net.corda.testing.internal.vault.*
import net.corda.testing.node.MockServices
import net.corda.testing.node.makeTestIdentityService
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.*
import rx.observers.TestSubscriber
import java.math.BigDecimal
import java.util.*
@ -103,8 +96,8 @@ class NodeVaultServiceTest {
vaultFiller = VaultFiller(services, dummyNotary)
// This is safe because MockServices only ever have a single identity
identity = services.myInfo.singleIdentityAndCert()
issuerServices = MockServices(cordappPackages, dummyCashIssuer, rigorousMock<IdentityService>(), parameters)
bocServices = MockServices(cordappPackages, bankOfCorda, rigorousMock<IdentityService>(), parameters)
issuerServices = MockServices(cordappPackages, dummyCashIssuer, mock<IdentityService>(), parameters)
bocServices = MockServices(cordappPackages, bankOfCorda, mock<IdentityService>(), parameters)
services.identityService.verifyAndRegisterIdentity(DUMMY_CASH_ISSUER_IDENTITY)
services.identityService.verifyAndRegisterIdentity(BOC_IDENTITY)
}
@ -133,10 +126,10 @@ class NodeVaultServiceTest {
return tryLockFungibleStatesForSpending(lockId, baseCriteria, amount, Cash.State::class.java)
}
class FungibleFoo(override val amount: Amount<Currency>, override val participants: List<AbstractParty>) : FungibleState<Currency>
@Test
fun `fungible state selection test`() {
val issuerParty = services.myInfo.legalIdentities.first()
class FungibleFoo(override val amount: Amount<Currency>, override val participants: List<AbstractParty>) : FungibleState<Currency>
val fungibleFoo = FungibleFoo(100.DOLLARS, listOf(issuerParty))
services.apply {
val tx = signInitialTransaction(TransactionBuilder(DUMMY_NOTARY).apply {
@ -182,7 +175,7 @@ class NodeVaultServiceTest {
assertThat(w1).hasSize(3)
val originalVault = vaultService
val services2 = object : MockServices(emptyList(), MEGA_CORP.name, rigorousMock()) {
val services2 = object : MockServices(emptyList(), MEGA_CORP.name, mock()) {
override val vaultService: NodeVaultService get() = originalVault
override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable<SignedTransaction>) {
for (stx in txs) {
@ -525,7 +518,7 @@ class NodeVaultServiceTest {
@Test
fun addNoteToTransaction() {
val megaCorpServices = MockServices(cordappPackages, MEGA_CORP.name, rigorousMock(), MEGA_CORP_KEY)
val megaCorpServices = MockServices(cordappPackages, MEGA_CORP.name, mock(), MEGA_CORP_KEY)
database.transaction {
val freshKey = identity.owningKey
@ -631,7 +624,7 @@ class NodeVaultServiceTest {
val identity = services.myInfo.singleIdentityAndCert()
assertEquals(services.identityService.partyFromKey(identity.owningKey), identity.party)
val anonymousIdentity = services.keyManagementService.freshKeyAndCert(identity, false)
val thirdPartyServices = MockServices(emptyList(), MEGA_CORP.name, rigorousMock<IdentityServiceInternal>().also {
val thirdPartyServices = MockServices(emptyList(), MEGA_CORP.name, mock<IdentityServiceInternal>().also {
doNothing().whenever(it).justVerifyAndRegisterIdentity(argThat { name == MEGA_CORP.name }, any())
})
val thirdPartyIdentity = thirdPartyServices.keyManagementService.freshKeyAndCert(thirdPartyServices.myInfo.singleIdentityAndCert(), false)
@ -650,7 +643,7 @@ class NodeVaultServiceTest {
// Change notary
services.identityService.verifyAndRegisterIdentity(DUMMY_NOTARY_IDENTITY)
val newNotary = DUMMY_NOTARY
val changeNotaryTx = NotaryChangeTransactionBuilder(listOf(initialCashState.ref), issueStx.notary!!, newNotary, services.networkParametersStorage.currentHash).build()
val changeNotaryTx = NotaryChangeTransactionBuilder(listOf(initialCashState.ref), issueStx.notary!!, newNotary, services.networkParametersService.currentHash).build()
val cashStateWithNewNotary = StateAndRef(initialCashState.state.copy(notary = newNotary), StateRef(changeNotaryTx.id, 0))
database.transaction {
@ -861,4 +854,43 @@ class NodeVaultServiceTest {
vaultService.queryBy<DummyDealContract.State>().states.size
})
}
@Test
@Ignore
fun `trackByCriteria filters updates and snapshots`() {
/*
* This test is ignored as the functionality it tests is not yet implemented - see CORDA-2389
*/
fun addCashToVault() {
database.transaction {
vaultFiller.fillWithSomeTestCash(100.DOLLARS, issuerServices, 1, DUMMY_CASH_ISSUER)
}
}
fun addDummyToVault() {
database.transaction {
vaultFiller.fillWithDummyState()
}
}
addCashToVault()
addDummyToVault()
val criteria = VaultQueryCriteria(contractStateTypes = setOf(Cash.State::class.java))
val data = vaultService.trackBy<ContractState>(criteria)
for (state in data.snapshot.states) {
assertEquals(Cash.PROGRAM_ID, state.state.contract)
}
val allCash = data.updates.all {
it.produced.all {
it.state.contract == Cash.PROGRAM_ID
}
}
addCashToVault()
addDummyToVault()
addCashToVault()
allCash.subscribe {
assertTrue(it)
}
}
}

View File

@ -6,7 +6,7 @@ import net.corda.core.node.services.vault.*
import net.corda.core.node.services.vault.QueryCriteria.*
import net.corda.finance.*
import net.corda.finance.contracts.asset.Cash
import net.corda.finance.schemas.test.SampleCashSchemaV3
import net.corda.finance.test.SampleCashSchemaV3
import net.corda.testing.core.*
import net.corda.testing.internal.vault.DummyLinearStateSchemaV1
import org.assertj.core.api.Assertions.assertThatThrownBy

View File

@ -1,5 +1,6 @@
package net.corda.node.services.vault
import com.nhaarman.mockito_kotlin.mock
import net.corda.core.contracts.*
import net.corda.core.crypto.*
import net.corda.core.identity.AbstractParty
@ -22,8 +23,8 @@ import net.corda.finance.contracts.asset.AbstractCashSelection
import net.corda.finance.schemas.CashSchemaV1
import net.corda.finance.schemas.CashSchemaV1.PersistentCashState
import net.corda.finance.schemas.CommercialPaperSchemaV1
import net.corda.finance.schemas.test.SampleCashSchemaV2
import net.corda.finance.schemas.test.SampleCashSchemaV3
import net.corda.finance.test.SampleCashSchemaV2
import net.corda.finance.test.SampleCashSchemaV3
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.nodeapi.internal.persistence.DatabaseTransaction
@ -141,7 +142,7 @@ open class VaultQueryTestRule : ExternalResource(), VaultQueryParties {
services = databaseAndServices.second
vaultFiller = VaultFiller(services, dummyNotary)
vaultFillerCashNotary = VaultFiller(services, dummyNotary, CASH_NOTARY)
notaryServices = MockServices(cordappPackages, dummyNotary, rigorousMock(), dummyCashIssuer.keyPair, BOC_KEY, MEGA_CORP_KEY)
notaryServices = MockServices(cordappPackages, dummyNotary, mock(), dummyCashIssuer.keyPair, BOC_KEY, MEGA_CORP_KEY)
identitySvc = services.identityService
// Register all of the identities we're going to use
(notaryServices.myInfo.legalIdentitiesAndCerts + BOC_IDENTITY + CASH_NOTARY_IDENTITY + MINI_CORP_IDENTITY + MEGA_CORP_IDENTITY).forEach { identity ->
@ -1907,15 +1908,15 @@ abstract class VaultQueryTestsBase : VaultQueryParties {
fun `unconsumed fungible assets for selected issuer parties`() {
// GBP issuer
val gbpCashIssuerName = CordaX500Name(organisation = "British Pounds Cash Issuer", locality = "London", country = "GB")
val gbpCashIssuerServices = MockServices(cordappPackages, gbpCashIssuerName, rigorousMock(), generateKeyPair())
val gbpCashIssuerServices = MockServices(cordappPackages, gbpCashIssuerName, mock(), generateKeyPair())
val gbpCashIssuer = gbpCashIssuerServices.myInfo.singleIdentityAndCert()
// USD issuer
val usdCashIssuerName = CordaX500Name(organisation = "US Dollars Cash Issuer", locality = "New York", country = "US")
val usdCashIssuerServices = MockServices(cordappPackages, usdCashIssuerName, rigorousMock(), generateKeyPair())
val usdCashIssuerServices = MockServices(cordappPackages, usdCashIssuerName, mock(), generateKeyPair())
val usdCashIssuer = usdCashIssuerServices.myInfo.singleIdentityAndCert()
// CHF issuer
val chfCashIssuerName = CordaX500Name(organisation = "Swiss Francs Cash Issuer", locality = "Zurich", country = "CH")
val chfCashIssuerServices = MockServices(cordappPackages, chfCashIssuerName, rigorousMock(), generateKeyPair())
val chfCashIssuerServices = MockServices(cordappPackages, chfCashIssuerName, mock(), generateKeyPair())
val chfCashIssuer = chfCashIssuerServices.myInfo.singleIdentityAndCert()
listOf(gbpCashIssuer, usdCashIssuer, chfCashIssuer).forEach { identity ->
services.identityService.verifyAndRegisterIdentity(identity)

View File

@ -20,6 +20,7 @@ import net.corda.core.utilities.NonEmptySet
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.unwrap
import net.corda.node.cordapp.CordappLoader
import net.corda.node.services.api.VaultServiceInternal
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.testing.core.singleIdentity
@ -82,9 +83,10 @@ class VaultSoftLockManagerTest {
object : InternalMockNetwork.MockNode(args) {
override fun makeVaultService(keyManagementService: KeyManagementService,
services: ServicesForResolution,
database: CordaPersistence): VaultServiceInternal {
database: CordaPersistence,
cordappLoader: CordappLoader): VaultServiceInternal {
val node = this
val realVault = super.makeVaultService(keyManagementService, services, database)
val realVault = super.makeVaultService(keyManagementService, services, database, cordappLoader)
return object : VaultServiceInternal by realVault {
override fun softLockRelease(lockId: UUID, stateRefs: NonEmptySet<StateRef>?) {
// Should be called before flow is removed

View File

@ -1,5 +1,6 @@
package net.corda.node.services.vault
import com.nhaarman.mockito_kotlin.mock
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.InsufficientBalanceException
import net.corda.core.contracts.LinearState
@ -27,7 +28,6 @@ import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.common.internal.addNotary
import net.corda.testing.core.*
import net.corda.testing.internal.LogHelper
import net.corda.testing.internal.rigorousMock
import net.corda.testing.internal.vault.*
import net.corda.testing.node.MockServices
import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices
@ -88,8 +88,8 @@ class VaultWithCashTest {
vaultFiller = VaultFiller(services, dummyNotary)
issuerServices = MockServices(cordappPackages, dummyCashIssuer, rigorousMock(), networkParameters, MEGA_CORP_KEY)
notaryServices = MockServices(cordappPackages, dummyNotary, rigorousMock(), networkParameters)
issuerServices = MockServices(cordappPackages, dummyCashIssuer, mock(), networkParameters, MEGA_CORP_KEY)
notaryServices = MockServices(cordappPackages, dummyNotary, mock(), networkParameters)
notary = notaryServices.myInfo.legalIdentitiesAndCerts.single().party
}
@ -119,7 +119,7 @@ class VaultWithCashTest {
@Test
fun `issue and spend total correctly and irrelevant ignored`() {
val megaCorpServices = MockServices(cordappPackages, MEGA_CORP.name, rigorousMock(), MEGA_CORP_KEY)
val megaCorpServices = MockServices(cordappPackages, MEGA_CORP.name, mock(), MEGA_CORP_KEY)
val freshKey = services.keyManagementService.freshKey()
val usefulTX =

View File

@ -0,0 +1,229 @@
package net.corda.notary.experimental.bftsmart
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.TimeWindow
import net.corda.core.crypto.*
import net.corda.core.flows.NotaryError
import net.corda.core.flows.NotaryException
import net.corda.core.flows.NotaryFlow
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.internal.deleteIfExists
import net.corda.core.internal.div
import net.corda.core.node.NotaryInfo
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.Try
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.seconds
import net.corda.node.services.config.NotaryConfig
import net.corda.nodeapi.internal.DevIdentityGenerator
import net.corda.nodeapi.internal.network.NetworkParametersCopier
import net.corda.notary.experimental.bftsmart.BFTSmartConfig
import net.corda.notary.experimental.bftsmart.minClusterSize
import net.corda.notary.experimental.bftsmart.minCorrectReplicas
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.contracts.DummyContract
import net.corda.testing.core.dummyCommand
import net.corda.testing.core.singleIdentity
import net.corda.testing.node.TestClock
import net.corda.testing.node.internal.*
import org.hamcrest.Matchers.instanceOf
import org.junit.AfterClass
import org.junit.Assert.assertThat
import org.junit.BeforeClass
import org.junit.Ignore
import org.junit.Test
import java.nio.file.Paths
import java.time.Duration
import java.time.Instant
import java.util.concurrent.ExecutionException
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
// This test is excluded from CI due to the experimental nature of the BFT notary
@Ignore
class BFTNotaryServiceTests {
companion object {
private lateinit var mockNet: InternalMockNetwork
private lateinit var notary: Party
private lateinit var node: TestStartedNode
@BeforeClass
@JvmStatic
fun before() {
mockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForPackages("net.corda.testing.contracts"))
val clusterSize = minClusterSize(1)
val started = startBftClusterAndNode(clusterSize, mockNet)
notary = started.first
node = started.second
}
@AfterClass
@JvmStatic
fun stopNodes() {
mockNet.stopNodes()
}
fun startBftClusterAndNode(clusterSize: Int, mockNet: InternalMockNetwork, exposeRaces: Boolean = false): Pair<Party, TestStartedNode> {
(Paths.get("config") / "currentView").deleteIfExists() // XXX: Make config object warn if this exists?
val replicaIds = (0 until clusterSize)
val serviceLegalName = CordaX500Name("BFT", "Zurich", "CH")
val notaryIdentity = DevIdentityGenerator.generateDistributedNotaryCompositeIdentity(
replicaIds.map { mockNet.baseDirectory(mockNet.nextNodeId + it) },
serviceLegalName)
val networkParameters = NetworkParametersCopier(testNetworkParameters(listOf(NotaryInfo(notaryIdentity, false))))
val clusterAddresses = replicaIds.map { NetworkHostAndPort("localhost", 11000 + it * 10) }
val nodes = replicaIds.map { replicaId ->
mockNet.createUnstartedNode(InternalMockNodeParameters(configOverrides = {
val notary = NotaryConfig(
validating = false,
bftSMaRt = BFTSmartConfig(replicaId, clusterAddresses, exposeRaces = exposeRaces),
serviceLegalName = serviceLegalName
)
doReturn(notary).whenever(it).notary
}))
} + mockNet.createUnstartedNode()
// MockNetwork doesn't support BFT clusters, so we create all the nodes we need unstarted, and then install the
// network-parameters in their directories before they're started.
val node = nodes.map { node ->
networkParameters.install(mockNet.baseDirectory(node.id))
node.start()
}.last()
return Pair(notaryIdentity, node)
}
}
@Test
fun `detect double spend`() {
node.run {
val issueTx = signInitialTransaction(notary) {
addOutputState(DummyContract.SingleOwnerState(owner = info.singleIdentity()), DummyContract.PROGRAM_ID, AlwaysAcceptAttachmentConstraint)
}
services.recordTransactions(issueTx)
val spendTxs = (1..10).map {
signInitialTransaction(notary) {
addInputState(issueTx.tx.outRef<ContractState>(0))
}
}
assertEquals(spendTxs.size, spendTxs.map { it.id }.distinct().size)
val flows = spendTxs.map { NotaryFlow.Client(it) }
val stateMachines = flows.map { services.startFlow(it) }
mockNet.runNetwork()
val results = stateMachines.map { Try.on { it.resultFuture.getOrThrow() } }
val successfulIndex = results.mapIndexedNotNull { index, result ->
if (result is Try.Success) {
val signers = result.value.map { it.by }
assertEquals(minCorrectReplicas(3), signers.size)
signers.forEach {
assertTrue(it in (notary.owningKey as CompositeKey).leafKeys)
}
index
} else {
null
}
}.single()
spendTxs.zip(results).forEach { (tx, result) ->
if (result is Try.Failure) {
val exception = result.exception as NotaryException
val error = exception.error as NotaryError.Conflict
assertEquals(tx.id, error.txId)
val (stateRef, cause) = error.consumedStates.entries.single()
assertEquals(StateRef(issueTx.id, 0), stateRef)
assertEquals(spendTxs[successfulIndex].id.sha256(), cause.hashOfTransactionId)
}
}
}
}
@Test
fun `transactions outside their time window are rejected`() {
node.run {
val issueTx = signInitialTransaction(notary) {
addOutputState(DummyContract.SingleOwnerState(owner = info.singleIdentity()), DummyContract.PROGRAM_ID, AlwaysAcceptAttachmentConstraint)
}
services.recordTransactions(issueTx)
val spendTx = signInitialTransaction(notary) {
addInputState(issueTx.tx.outRef<ContractState>(0))
setTimeWindow(TimeWindow.fromOnly(Instant.MAX))
}
val flow = NotaryFlow.Client(spendTx)
val resultFuture = services.startFlow(flow).resultFuture
mockNet.runNetwork()
val exception = assertFailsWith<ExecutionException> { resultFuture.get() }
assertThat(exception.cause, instanceOf(NotaryException::class.java))
val error = (exception.cause as NotaryException).error
assertThat(error, instanceOf(NotaryError.TimeWindowInvalid::class.java))
}
}
@Test
fun `notarise issue tx with time-window`() {
node.run {
val issueTx = signInitialTransaction(notary) {
setTimeWindow(services.clock.instant(), 30.seconds)
addOutputState(DummyContract.SingleOwnerState(owner = info.singleIdentity()), DummyContract.PROGRAM_ID, AlwaysAcceptAttachmentConstraint)
}
val resultFuture = services.startFlow(NotaryFlow.Client(issueTx)).resultFuture
mockNet.runNetwork()
val signatures = resultFuture.get()
verifySignatures(signatures, issueTx.id)
}
}
@Test
fun `transactions can be re-notarised outside their time window`() {
node.run {
val issueTx = signInitialTransaction(notary) {
addOutputState(DummyContract.SingleOwnerState(owner = info.singleIdentity()), DummyContract.PROGRAM_ID, AlwaysAcceptAttachmentConstraint)
}
services.recordTransactions(issueTx)
val spendTx = signInitialTransaction(notary) {
addInputState(issueTx.tx.outRef<ContractState>(0))
setTimeWindow(TimeWindow.untilOnly(Instant.now() + Duration.ofHours(1)))
}
val resultFuture = services.startFlow(NotaryFlow.Client(spendTx)).resultFuture
mockNet.runNetwork()
val signatures = resultFuture.get()
verifySignatures(signatures, spendTx.id)
for (node in mockNet.nodes) {
(node.started!!.services.clock as TestClock).advanceBy(Duration.ofDays(1))
}
val resultFuture2 = services.startFlow(NotaryFlow.Client(spendTx)).resultFuture
mockNet.runNetwork()
val signatures2 = resultFuture2.get()
verifySignatures(signatures2, spendTx.id)
}
}
private fun verifySignatures(signatures: List<TransactionSignature>, txId: SecureHash) {
notary.owningKey.isFulfilledBy(signatures.map { it.by })
signatures.forEach { it.verify(txId) }
}
private fun TestStartedNode.signInitialTransaction(notary: Party, block: TransactionBuilder.() -> Any?): SignedTransaction {
return services.signInitialTransaction(
TransactionBuilder(notary).apply {
addCommand(dummyCommand(services.myInfo.singleIdentity().owningKey))
block()
}
)
}
}

View File

@ -0,0 +1,44 @@
package net.corda.notary.experimental.bftsmart
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.notary.experimental.bftsmart.BFTSmartConfigInternal
import net.corda.notary.experimental.bftsmart.BFTSmartConfigInternal.Companion.portIsClaimedFormat
import net.corda.notary.experimental.bftsmart.maxFaultyReplicas
import net.corda.notary.experimental.bftsmart.minClusterSize
import net.corda.notary.experimental.bftsmart.minCorrectReplicas
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Test
import kotlin.test.assertEquals
class BFTSmartConfigTests {
@Test
fun `replica arithmetic`() {
(1..20).forEach { n ->
assertEquals(n, maxFaultyReplicas(n) + minCorrectReplicas(n))
}
(1..3).forEach { n -> assertEquals(0, maxFaultyReplicas(n)) }
(4..6).forEach { n -> assertEquals(1, maxFaultyReplicas(n)) }
(7..9).forEach { n -> assertEquals(2, maxFaultyReplicas(n)) }
10.let { n -> assertEquals(3, maxFaultyReplicas(n)) }
}
@Test
fun `min cluster size`() {
assertEquals(1, minClusterSize(0))
assertEquals(4, minClusterSize(1))
assertEquals(7, minClusterSize(2))
assertEquals(10, minClusterSize(3))
}
@Test
fun `overlapping port ranges are rejected`() {
fun config(vararg ports: Int) = BFTSmartConfigInternal(ports.map { NetworkHostAndPort("localhost", it) }, false, false)
assertThatThrownBy { config(11000, 11001).use {} }
.isInstanceOf(IllegalArgumentException::class.java)
.hasMessage(portIsClaimedFormat.format("localhost:11001", setOf("localhost:11000", "localhost:11001")))
assertThatThrownBy { config(11001, 11000).use {} }
.isInstanceOf(IllegalArgumentException::class.java)
.hasMessage(portIsClaimedFormat.format("localhost:11001", setOf("localhost:11001", "localhost:11002", "localhost:11000")))
config(11000, 11002).use {} // Non-overlapping.
}
}

View File

@ -0,0 +1,86 @@
package net.corda.notary.experimental.raft
import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.StateRef
import net.corda.core.flows.NotaryError
import net.corda.core.flows.NotaryException
import net.corda.core.flows.NotaryFlow
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.internal.concurrent.map
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.seconds
import net.corda.testing.contracts.DummyContract
import net.corda.testing.core.DUMMY_BANK_A_NAME
import net.corda.testing.core.dummyCommand
import net.corda.testing.core.singleIdentity
import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.InProcess
import net.corda.testing.driver.driver
import net.corda.testing.node.ClusterSpec
import net.corda.testing.node.NotarySpec
import org.junit.Test
import java.util.*
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class RaftNotaryServiceTests {
private val notaryName = CordaX500Name("RAFT Notary Service", "London", "GB")
@Test
fun `detect double spend`() {
driver(DriverParameters(
startNodesInProcess = true,
extraCordappPackagesToScan = listOf("net.corda.testing.contracts"),
notarySpecs = listOf(NotarySpec(notaryName, cluster = ClusterSpec.Raft(clusterSize = 3)))
)) {
val bankA = startNode(providedName = DUMMY_BANK_A_NAME).map { (it as InProcess) }.getOrThrow()
val inputState = issueState(bankA, defaultNotaryIdentity)
val firstTxBuilder = TransactionBuilder(defaultNotaryIdentity)
.addInputState(inputState)
.addCommand(dummyCommand(bankA.services.myInfo.singleIdentity().owningKey))
val firstSpendTx = bankA.services.signInitialTransaction(firstTxBuilder)
val firstSpend = bankA.startFlow(NotaryFlow.Client(firstSpendTx))
firstSpend.getOrThrow()
val secondSpendBuilder = TransactionBuilder(defaultNotaryIdentity).withItems(inputState).run {
val dummyState = DummyContract.SingleOwnerState(0, bankA.services.myInfo.singleIdentity())
addOutputState(dummyState, DummyContract.PROGRAM_ID)
addCommand(dummyCommand(bankA.services.myInfo.singleIdentity().owningKey))
this
}
val secondSpendTx = bankA.services.signInitialTransaction(secondSpendBuilder)
val secondSpend = bankA.startFlow(NotaryFlow.Client(secondSpendTx))
val ex = assertFailsWith(NotaryException::class) { secondSpend.getOrThrow() }
val error = ex.error as NotaryError.Conflict
assertEquals(error.txId, secondSpendTx.id)
}
}
@Test
fun `notarise issue tx with time-window`() {
driver(DriverParameters(
startNodesInProcess = true,
extraCordappPackagesToScan = listOf("net.corda.testing.contracts"),
notarySpecs = listOf(NotarySpec(notaryName, cluster = ClusterSpec.Raft(clusterSize = 3)))
)) {
val bankA = startNode(providedName = DUMMY_BANK_A_NAME).map { (it as InProcess) }.getOrThrow()
val builder = DummyContract.generateInitial(Random().nextInt(), defaultNotaryIdentity, bankA.services.myInfo.singleIdentity().ref(0))
.setTimeWindow(bankA.services.clock.instant(), 30.seconds)
val issueTx = bankA.services.signInitialTransaction(builder)
bankA.startFlow(NotaryFlow.Client(issueTx)).getOrThrow()
}
}
private fun issueState(nodeHandle: InProcess, notary: Party): StateAndRef<*> {
val builder = DummyContract.generateInitial(Random().nextInt(), notary, nodeHandle.services.myInfo.singleIdentity().ref(0))
val stx = nodeHandle.services.signInitialTransaction(builder)
nodeHandle.services.recordTransactions(stx)
return StateAndRef(stx.coreTransaction.outputs.first(), StateRef(stx.id, 0))
}
}

Some files were not shown because too many files have changed in this diff Show More