mirror of
https://github.com/corda/corda.git
synced 2025-06-17 22:58:19 +00:00
Merge branch 'master' into wn-redo-node-conf-docs
This commit is contained in:
committed by
GitHub
commit
2f18ce9440
@ -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) {
|
||||
|
@ -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 ->
|
||||
|
@ -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 ...")
|
||||
|
@ -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)),
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
@ -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")
|
@ -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()
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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?>()
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
BIN
node/src/integration-test/resources/isolated.jar
Normal file
BIN
node/src/integration-test/resources/isolated.jar
Normal file
Binary file not shown.
Binary file not shown.
@ -8,7 +8,7 @@ import net.corda.core.schemas.MappedSchema
|
||||
/**
|
||||
* Handles loading [Cordapp]s.
|
||||
*/
|
||||
interface CordappLoader {
|
||||
interface CordappLoader : AutoCloseable {
|
||||
|
||||
/**
|
||||
* Returns all [Cordapp]s found.
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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()})")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
)
|
||||
|
||||
/**
|
||||
|
@ -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)
|
||||
|
@ -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 }
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
@ -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() })
|
||||
|
@ -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() {}
|
||||
}
|
||||
|
@ -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.")
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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? {
|
||||
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
@ -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>
|
||||
)
|
@ -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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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 ->
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -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")))
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
@ -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) }
|
||||
|
@ -222,7 +222,7 @@ class NotaryWhitelistTests(
|
||||
listOf(inputState.ref),
|
||||
fakeNotaryParty,
|
||||
oldNotary,
|
||||
aliceNode.services.networkParametersStorage.currentHash
|
||||
aliceNode.services.networkParametersService.currentHash
|
||||
).build()
|
||||
|
||||
val notaryChangeAliceSig = getAliceSig(notaryChangeTx)
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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 =
|
||||
|
@ -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()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
@ -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.
|
||||
}
|
||||
}
|
@ -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
Reference in New Issue
Block a user