mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
CORDA-4054: combine different identities of the same notary after its key rotation (#6734)
This commit is contained in:
parent
d0cfeb1fbe
commit
551b3f0811
@ -26,6 +26,7 @@ import net.corda.core.internal.DigitalSignatureWithCert
|
|||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.node.ServiceHub
|
import net.corda.core.node.ServiceHub
|
||||||
import net.corda.core.node.services.AttachmentStorage
|
import net.corda.core.node.services.AttachmentStorage
|
||||||
|
import net.corda.core.node.services.IdentityService
|
||||||
import net.corda.core.node.services.NetworkParametersService
|
import net.corda.core.node.services.NetworkParametersService
|
||||||
import net.corda.core.node.services.TransactionStorage
|
import net.corda.core.node.services.TransactionStorage
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
@ -242,6 +243,7 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
|
|||||||
val attachmentStorage = rigorousMock<AttachmentStorage>()
|
val attachmentStorage = rigorousMock<AttachmentStorage>()
|
||||||
doReturn(attachmentStorage).whenever(services).attachments
|
doReturn(attachmentStorage).whenever(services).attachments
|
||||||
doReturn(mock<TransactionStorage>()).whenever(services).validatedTransactions
|
doReturn(mock<TransactionStorage>()).whenever(services).validatedTransactions
|
||||||
|
doReturn(mock<IdentityService>()).whenever(services).identityService
|
||||||
val attachment = rigorousMock<ContractAttachment>()
|
val attachment = rigorousMock<ContractAttachment>()
|
||||||
doReturn(attachment).whenever(attachmentStorage).openAttachment(attachmentId)
|
doReturn(attachment).whenever(attachmentStorage).openAttachment(attachmentId)
|
||||||
doReturn(attachmentId).whenever(attachment).id
|
doReturn(attachmentId).whenever(attachment).id
|
||||||
|
@ -20,7 +20,6 @@ import net.corda.core.identity.Party
|
|||||||
import net.corda.core.identity.PartyAndCertificate
|
import net.corda.core.identity.PartyAndCertificate
|
||||||
import net.corda.core.identity.excludeHostNode
|
import net.corda.core.identity.excludeHostNode
|
||||||
import net.corda.core.identity.groupAbstractPartyByWellKnownParty
|
import net.corda.core.identity.groupAbstractPartyByWellKnownParty
|
||||||
import net.corda.core.node.services.IdentityService
|
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
@ -32,7 +31,6 @@ import net.corda.testing.core.TestIdentity
|
|||||||
import net.corda.testing.core.singleIdentity
|
import net.corda.testing.core.singleIdentity
|
||||||
import net.corda.coretesting.internal.matchers.flow.willReturn
|
import net.corda.coretesting.internal.matchers.flow.willReturn
|
||||||
import net.corda.coretesting.internal.matchers.flow.willThrow
|
import net.corda.coretesting.internal.matchers.flow.willThrow
|
||||||
import net.corda.coretesting.internal.rigorousMock
|
|
||||||
import net.corda.testing.node.MockServices
|
import net.corda.testing.node.MockServices
|
||||||
import net.corda.testing.node.internal.DUMMY_CONTRACTS_CORDAPP
|
import net.corda.testing.node.internal.DUMMY_CONTRACTS_CORDAPP
|
||||||
import net.corda.testing.node.internal.InternalMockNetwork
|
import net.corda.testing.node.internal.InternalMockNetwork
|
||||||
@ -47,7 +45,7 @@ import java.security.PublicKey
|
|||||||
class CollectSignaturesFlowTests : WithContracts {
|
class CollectSignaturesFlowTests : WithContracts {
|
||||||
companion object {
|
companion object {
|
||||||
private val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB"))
|
private val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB"))
|
||||||
private val miniCorpServices = MockServices(listOf("net.corda.testing.contracts"), miniCorp, rigorousMock<IdentityService>())
|
private val miniCorpServices = MockServices(listOf("net.corda.testing.contracts"), miniCorp)
|
||||||
private val classMockNet = InternalMockNetwork(cordappsForAllNodes = listOf(DUMMY_CONTRACTS_CORDAPP, enclosedCordapp()))
|
private val classMockNet = InternalMockNetwork(cordappsForAllNodes = listOf(DUMMY_CONTRACTS_CORDAPP, enclosedCordapp()))
|
||||||
|
|
||||||
private const val MAGIC_NUMBER = 1337
|
private const val MAGIC_NUMBER = 1337
|
||||||
|
@ -80,7 +80,7 @@ class TransactionSerializationTests {
|
|||||||
//override mock implementation with a real one
|
//override mock implementation with a real one
|
||||||
override fun loadContractAttachment(stateRef: StateRef): Attachment = servicesForResolution.loadContractAttachment(stateRef)
|
override fun loadContractAttachment(stateRef: StateRef): Attachment = servicesForResolution.loadContractAttachment(stateRef)
|
||||||
}
|
}
|
||||||
val notaryServices = MockServices(listOf("net.corda.coretests.serialization"), DUMMY_NOTARY.name, rigorousMock(), DUMMY_NOTARY_KEY)
|
val notaryServices = MockServices(listOf("net.corda.coretests.serialization"), DUMMY_NOTARY.name, key = DUMMY_NOTARY_KEY)
|
||||||
lateinit var tx: TransactionBuilder
|
lateinit var tx: TransactionBuilder
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
|
@ -13,6 +13,7 @@ import net.corda.core.internal.PLATFORM_VERSION
|
|||||||
import net.corda.core.node.ServicesForResolution
|
import net.corda.core.node.ServicesForResolution
|
||||||
import net.corda.core.node.ZoneVersionTooLowException
|
import net.corda.core.node.ZoneVersionTooLowException
|
||||||
import net.corda.core.node.services.AttachmentStorage
|
import net.corda.core.node.services.AttachmentStorage
|
||||||
|
import net.corda.core.node.services.IdentityService
|
||||||
import net.corda.core.node.services.NetworkParametersService
|
import net.corda.core.node.services.NetworkParametersService
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
@ -51,6 +52,7 @@ class TransactionBuilderTest {
|
|||||||
doReturn(cordappProvider).whenever(services).cordappProvider
|
doReturn(cordappProvider).whenever(services).cordappProvider
|
||||||
doReturn(contractAttachmentId).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID)
|
doReturn(contractAttachmentId).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID)
|
||||||
doReturn(networkParameters).whenever(services).networkParameters
|
doReturn(networkParameters).whenever(services).networkParameters
|
||||||
|
doReturn(mock<IdentityService>()).whenever(services).identityService
|
||||||
|
|
||||||
val attachmentStorage = rigorousMock<AttachmentStorage>()
|
val attachmentStorage = rigorousMock<AttachmentStorage>()
|
||||||
doReturn(attachmentStorage).whenever(services).attachments
|
doReturn(attachmentStorage).whenever(services).attachments
|
||||||
|
@ -82,9 +82,13 @@ class NotaryFlow {
|
|||||||
*/
|
*/
|
||||||
protected fun checkTransaction(): Party {
|
protected fun checkTransaction(): Party {
|
||||||
val notaryParty = stx.notary ?: throw IllegalStateException("Transaction does not specify a Notary")
|
val notaryParty = stx.notary ?: throw IllegalStateException("Transaction does not specify a Notary")
|
||||||
check(serviceHub.networkMapCache.isNotary(notaryParty)) { "$notaryParty is not a notary on the network" }
|
val notaries = setOf(notaryParty) + serviceHub.loadStates(stx.inputs.toSet() + stx.references.toSet()).map { it.state.notary }
|
||||||
check(serviceHub.loadStates(stx.inputs.toSet() + stx.references.toSet()).all { it.state.notary == notaryParty }) {
|
notaries.forEach {
|
||||||
"Input states and reference input states must have the same Notary"
|
check(serviceHub.networkMapCache.isNotary(it)) { "${it.description()} is not a notary on the network" }
|
||||||
|
// Transaction can combine different identities of the same notary after key rotation.
|
||||||
|
check(it.name == notaryParty.name) {
|
||||||
|
"Input states and reference input states must have the same Notary as the transaction Notary"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!skipVerification) {
|
if (!skipVerification) {
|
||||||
@ -117,10 +121,10 @@ class NotaryFlow {
|
|||||||
and can't be used for regular transactions.
|
and can't be used for regular transactions.
|
||||||
*/
|
*/
|
||||||
check(stx.coreTransaction is NotaryChangeWireTransaction) {
|
check(stx.coreTransaction is NotaryChangeWireTransaction) {
|
||||||
"Notary $notaryParty is not on the network parameter whitelist. A non-whitelisted notary can only be used for notary change transactions"
|
"Notary ${notaryParty.description()} is not on the network parameter whitelist. A non-whitelisted notary can only be used for notary change transactions"
|
||||||
}
|
}
|
||||||
val historicNotary = (serviceHub.networkParametersService as NetworkParametersStorage).getHistoricNotary(notaryParty)
|
val historicNotary = (serviceHub.networkParametersService as NetworkParametersStorage).getHistoricNotary(notaryParty)
|
||||||
?: throw IllegalStateException("The notary party $notaryParty specified by transaction ${stx.id}, is not recognised as a current or historic notary.")
|
?: throw IllegalStateException("The notary party ${notaryParty.description()} specified by transaction ${stx.id}, is not recognised as a current or historic notary.")
|
||||||
historicNotary.validating
|
historicNotary.validating
|
||||||
} else serviceHub.networkMapCache.isValidatingNotary(notaryParty)
|
} else serviceHub.networkMapCache.isValidatingNotary(notaryParty)
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import net.corda.core.contracts.StateRef
|
|||||||
import net.corda.core.contracts.TimeWindow
|
import net.corda.core.contracts.TimeWindow
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.TransactionSignature
|
import net.corda.core.crypto.TransactionSignature
|
||||||
|
import net.corda.core.crypto.toStringShort
|
||||||
import net.corda.core.flows.*
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.IdempotentFlow
|
import net.corda.core.internal.IdempotentFlow
|
||||||
@ -108,7 +109,8 @@ abstract class NotaryServiceFlow(val otherSideSession: FlowSession, val service:
|
|||||||
@Suspendable
|
@Suspendable
|
||||||
private fun checkNotary(notary: Party?) {
|
private fun checkNotary(notary: Party?) {
|
||||||
require(notary?.owningKey == service.notaryIdentityKey) {
|
require(notary?.owningKey == service.notaryIdentityKey) {
|
||||||
"The notary specified on the transaction: [$notary] does not match the notary service's identity: [${service.notaryIdentityKey}] "
|
"The notary specified on the transaction: [${notary?.description()}] does not match the notary service's identity:" +
|
||||||
|
" [${service.notaryIdentityKey.toStringShort()}] "
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,7 +40,9 @@ fun NotarisationRequest.verifySignature(requestSignature: NotarisationRequestSig
|
|||||||
*/
|
*/
|
||||||
fun NotarisationResponse.validateSignatures(txId: SecureHash, notary: Party) {
|
fun NotarisationResponse.validateSignatures(txId: SecureHash, notary: Party) {
|
||||||
val signingKeys = signatures.map { it.by }
|
val signingKeys = signatures.map { it.by }
|
||||||
require(notary.owningKey.isFulfilledBy(signingKeys)) { "Insufficient signatures to fulfill the notary signing requirement for $notary" }
|
require(notary.owningKey.isFulfilledBy(signingKeys)) {
|
||||||
|
"Insufficient signatures to fulfill the notary signing requirement for ${notary.description()}"
|
||||||
|
}
|
||||||
signatures.forEach { it.verify(txId) }
|
signatures.forEach { it.verify(txId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,7 +120,7 @@ interface NetworkMapCacheBase {
|
|||||||
// DOCEND 2
|
// DOCEND 2
|
||||||
|
|
||||||
/** Returns true if and only if the given [Party] is a notary, which is defined by the network parameters. */
|
/** Returns true if and only if the given [Party] is a notary, which is defined by the network parameters. */
|
||||||
fun isNotary(party: Party): Boolean = party in notaryIdentities
|
fun isNotary(party: Party): Boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if and only if the given [Party] is validating notary. For every party that is a validating notary,
|
* Returns true if and only if the given [Party] is validating notary. For every party that is a validating notary,
|
||||||
|
@ -42,9 +42,12 @@ abstract class FullTransaction : BaseTransaction() {
|
|||||||
|
|
||||||
private fun checkInputsAndReferencesHaveSameNotary() {
|
private fun checkInputsAndReferencesHaveSameNotary() {
|
||||||
if (inputs.isEmpty() && references.isEmpty()) return
|
if (inputs.isEmpty() && references.isEmpty()) return
|
||||||
val notaries = (inputs + references).map { it.state.notary }.toHashSet()
|
// Transaction can combine different identities of the same notary after key rotation.
|
||||||
|
val notaries = (inputs + references).map { it.state.notary.name }.toHashSet()
|
||||||
check(notaries.size == 1) { "All inputs and reference inputs must point to the same notary" }
|
check(notaries.size == 1) { "All inputs and reference inputs must point to the same notary" }
|
||||||
check(notaries.single() == notary) { "The specified notary must be the one specified by all inputs and input references" }
|
check(notaries.single() == notary?.name) {
|
||||||
|
"The specified transaction notary must be the one specified by all inputs and input references"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Make sure the assigned notary is part of the network parameter whitelist. */
|
/** Make sure the assigned notary is part of the network parameter whitelist. */
|
||||||
@ -53,8 +56,14 @@ abstract class FullTransaction : BaseTransaction() {
|
|||||||
// Network parameters will never be null if the transaction is resolved from a CoreTransaction rather than constructed directly.
|
// Network parameters will never be null if the transaction is resolved from a CoreTransaction rather than constructed directly.
|
||||||
networkParameters?.let { parameters ->
|
networkParameters?.let { parameters ->
|
||||||
val notaryWhitelist = parameters.notaries.map { it.identity }
|
val notaryWhitelist = parameters.notaries.map { it.identity }
|
||||||
check(notaryParty in notaryWhitelist) {
|
// Transaction can combine different identities of the same notary after key rotation.
|
||||||
"Notary ($notaryParty) specified by the transaction is not on the network parameter whitelist: [${notaryWhitelist.joinToString()}]"
|
// Each of these identities should be whitelisted.
|
||||||
|
val notaries = setOf(notaryParty) + (inputs + references).map { it.state.notary }
|
||||||
|
notaries.forEach {
|
||||||
|
check(it in notaryWhitelist) {
|
||||||
|
"Notary [${it.description()}] specified by the transaction is not on the network parameter whitelist: " +
|
||||||
|
" [${notaryWhitelist.joinToString { party -> party.description() }}]"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -156,7 +156,7 @@ private constructor(
|
|||||||
networkParameters?.let { parameters ->
|
networkParameters?.let { parameters ->
|
||||||
val notaryWhitelist = parameters.notaries.map { it.identity }
|
val notaryWhitelist = parameters.notaries.map { it.identity }
|
||||||
check(newNotary in notaryWhitelist) {
|
check(newNotary in notaryWhitelist) {
|
||||||
"The output notary $newNotary is not whitelisted in the attached network parameters."
|
"The output notary ${newNotary.description()} is not whitelisted in the attached network parameters."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -154,6 +154,7 @@ open class TransactionBuilder(
|
|||||||
if (referenceStates.isNotEmpty()) {
|
if (referenceStates.isNotEmpty()) {
|
||||||
services.ensureMinimumPlatformVersion(4, "Reference states")
|
services.ensureMinimumPlatformVersion(4, "Reference states")
|
||||||
}
|
}
|
||||||
|
resolveNotary(services)
|
||||||
|
|
||||||
val (allContractAttachments: Collection<AttachmentId>, resolvedOutputs: List<TransactionState<ContractState>>)
|
val (allContractAttachments: Collection<AttachmentId>, resolvedOutputs: List<TransactionState<ContractState>>)
|
||||||
= selectContractAttachmentsAndOutputStateConstraints(services, serializationContext)
|
= selectContractAttachmentsAndOutputStateConstraints(services, serializationContext)
|
||||||
@ -635,8 +636,10 @@ open class TransactionBuilder(
|
|||||||
|
|
||||||
private fun checkNotary(stateAndRef: StateAndRef<*>) {
|
private fun checkNotary(stateAndRef: StateAndRef<*>) {
|
||||||
val notary = stateAndRef.state.notary
|
val notary = stateAndRef.state.notary
|
||||||
require(notary == this.notary) {
|
// Transaction can combine different identities of the same notary after key rotation.
|
||||||
"Input state requires notary \"$notary\" which does not match the transaction notary \"${this.notary}\"."
|
require(notary.name == this.notary?.name) {
|
||||||
|
"Input state requires notary \"${notary.description()}\" which does not match" +
|
||||||
|
" the transaction notary \"${this.notary?.description()}\"."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -648,7 +651,25 @@ open class TransactionBuilder(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkReferencesUseSameNotary() = referencesWithTransactionState.map { it.notary }.toSet().size == 1
|
// Transaction can combine different identities of the same notary after key rotation.
|
||||||
|
private fun checkReferencesUseSameNotary() = referencesWithTransactionState.map { it.notary.name }.toSet().size == 1
|
||||||
|
|
||||||
|
// Automatically correct notary after its key rotation
|
||||||
|
private fun resolveNotary(services: ServicesForResolution) {
|
||||||
|
notary?.let {
|
||||||
|
val activeNotary = services.identityService.wellKnownPartyFromX500Name(it.name)
|
||||||
|
if (activeNotary != null && activeNotary != it) {
|
||||||
|
log.warn("Replacing notary on the transaction '${it.description()}' with '${activeNotary.description()}'.")
|
||||||
|
notary = activeNotary
|
||||||
|
}
|
||||||
|
outputs.forEachIndexed { index, transactionState ->
|
||||||
|
if (activeNotary != null && activeNotary != transactionState.notary) {
|
||||||
|
log.warn("Replacing notary on the transaction output '${it.description()}' with '${activeNotary.description()}'.")
|
||||||
|
outputs[index] = transactionState.copy(notary = activeNotary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If any inputs or outputs added to the [TransactionBuilder] contain [StatePointer]s, then this method is used
|
* If any inputs or outputs added to the [TransactionBuilder] contain [StatePointer]s, then this method is used
|
||||||
|
@ -11,6 +11,7 @@ import net.corda.core.identity.CordaX500Name
|
|||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.node.ServicesForResolution
|
import net.corda.core.node.ServicesForResolution
|
||||||
import net.corda.core.node.services.AttachmentStorage
|
import net.corda.core.node.services.AttachmentStorage
|
||||||
|
import net.corda.core.node.services.IdentityService
|
||||||
import net.corda.core.node.services.NetworkParametersService
|
import net.corda.core.node.services.NetworkParametersService
|
||||||
import net.corda.core.serialization.deserialize
|
import net.corda.core.serialization.deserialize
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
@ -91,6 +92,7 @@ class AttachmentsClassLoaderStaticContractTests {
|
|||||||
val contractAttachmentId = SecureHash.randomSHA256()
|
val contractAttachmentId = SecureHash.randomSHA256()
|
||||||
doReturn(listOf(contractAttachmentId)).whenever(attachmentStorage)
|
doReturn(listOf(contractAttachmentId)).whenever(attachmentStorage)
|
||||||
.getLatestContractAttachments(AttachmentDummyContract.ATTACHMENT_PROGRAM_ID)
|
.getLatestContractAttachments(AttachmentDummyContract.ATTACHMENT_PROGRAM_ID)
|
||||||
|
doReturn(mock<IdentityService>()).whenever(it).identityService
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
@Test(timeout=300_000)
|
||||||
|
@ -0,0 +1,129 @@
|
|||||||
|
package net.corda.node.services.identity
|
||||||
|
|
||||||
|
import com.nhaarman.mockito_kotlin.doReturn
|
||||||
|
import com.nhaarman.mockito_kotlin.whenever
|
||||||
|
import net.corda.core.internal.createDirectories
|
||||||
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
|
import net.corda.finance.DOLLARS
|
||||||
|
import net.corda.finance.USD
|
||||||
|
import net.corda.finance.flows.CashIssueAndPaymentFlow
|
||||||
|
import net.corda.finance.flows.CashIssueFlow
|
||||||
|
import net.corda.finance.flows.CashPaymentFlow
|
||||||
|
import net.corda.finance.workflows.getCashBalance
|
||||||
|
import net.corda.node.services.config.NotaryConfig
|
||||||
|
import net.corda.nodeapi.internal.DevIdentityGenerator
|
||||||
|
import net.corda.nodeapi.internal.createDevNetworkMapCa
|
||||||
|
import net.corda.nodeapi.internal.network.NetworkParametersCopier
|
||||||
|
import net.corda.testing.common.internal.addNotary
|
||||||
|
import net.corda.testing.common.internal.testNetworkParameters
|
||||||
|
import net.corda.testing.core.ALICE_NAME
|
||||||
|
import net.corda.testing.core.BOB_NAME
|
||||||
|
import net.corda.testing.core.CHARLIE_NAME
|
||||||
|
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||||
|
import net.corda.testing.node.MockNetworkNotarySpec
|
||||||
|
import net.corda.testing.node.internal.FINANCE_CORDAPPS
|
||||||
|
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.startFlow
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.junit.runners.Parameterized
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
@RunWith(Parameterized::class)
|
||||||
|
class NotaryCertificateRotationTest(private val validating: Boolean) {
|
||||||
|
private val ref = OpaqueBytes.of(0x01)
|
||||||
|
|
||||||
|
private val TestStartedNode.party get() = info.legalIdentities.first()
|
||||||
|
|
||||||
|
private lateinit var mockNet: InternalMockNetwork
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
@Parameterized.Parameters(name = "validating = {0}")
|
||||||
|
fun data() = listOf(false, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
mockNet.stopNodes()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(timeout = 300_000)
|
||||||
|
fun `rotate notary identity`() {
|
||||||
|
mockNet = InternalMockNetwork(
|
||||||
|
cordappsForAllNodes = FINANCE_CORDAPPS,
|
||||||
|
notarySpecs = listOf(MockNetworkNotarySpec(DUMMY_NOTARY_NAME, validating))
|
||||||
|
)
|
||||||
|
val alice = mockNet.createPartyNode(ALICE_NAME)
|
||||||
|
val bob = mockNet.createPartyNode(BOB_NAME)
|
||||||
|
|
||||||
|
// Issue states and notarize them with initial notary identity.
|
||||||
|
alice.services.startFlow(CashIssueFlow(1000.DOLLARS, ref, mockNet.defaultNotaryIdentity))
|
||||||
|
alice.services.startFlow(CashIssueAndPaymentFlow(2000.DOLLARS, ref, alice.party, false, mockNet.defaultNotaryIdentity))
|
||||||
|
alice.services.startFlow(CashIssueAndPaymentFlow(4000.DOLLARS, ref, bob.party, false, mockNet.defaultNotaryIdentity))
|
||||||
|
mockNet.runNetwork()
|
||||||
|
|
||||||
|
// Rotate notary identity and update network parameters.
|
||||||
|
val newNotaryIdentity = DevIdentityGenerator.installKeyStoreWithNodeIdentity(
|
||||||
|
mockNet.baseDirectory(mockNet.nextNodeId),
|
||||||
|
DUMMY_NOTARY_NAME
|
||||||
|
)
|
||||||
|
val newNetworkParameters = testNetworkParameters(epoch = 2)
|
||||||
|
.addNotary(mockNet.defaultNotaryIdentity, validating)
|
||||||
|
.addNotary(newNotaryIdentity, validating)
|
||||||
|
val ca = createDevNetworkMapCa()
|
||||||
|
NetworkParametersCopier(newNetworkParameters, ca, overwriteFile = true).apply {
|
||||||
|
install(mockNet.baseDirectory(alice))
|
||||||
|
install(mockNet.baseDirectory(bob))
|
||||||
|
install(mockNet.baseDirectory(mockNet.nextNodeId))
|
||||||
|
install(mockNet.baseDirectory(mockNet.nextNodeId + 1).apply { createDirectories() })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start notary with new identity and restart nodes.
|
||||||
|
val notary2 = mockNet.createNode(InternalMockNodeParameters(
|
||||||
|
legalName = DUMMY_NOTARY_NAME,
|
||||||
|
configOverrides = { doReturn(NotaryConfig(validating)).whenever(it).notary }
|
||||||
|
))
|
||||||
|
val alice2 = mockNet.restartNode(alice)
|
||||||
|
val bob2 = mockNet.restartNode(bob)
|
||||||
|
val charlie = mockNet.createPartyNode(CHARLIE_NAME)
|
||||||
|
|
||||||
|
// Save previous network parameters for subsequent backchain verification.
|
||||||
|
mockNet.nodes.forEach { it.services.networkParametersService.saveParameters(ca.sign(mockNet.networkParameters)) }
|
||||||
|
|
||||||
|
// Verify that notary identity has been changed.
|
||||||
|
assertEquals(listOf(newNotaryIdentity), alice2.services.networkMapCache.notaryIdentities)
|
||||||
|
assertEquals(listOf(newNotaryIdentity), bob2.services.networkMapCache.notaryIdentities)
|
||||||
|
assertEquals(listOf(newNotaryIdentity), charlie.services.networkMapCache.notaryIdentities)
|
||||||
|
|
||||||
|
assertEquals(newNotaryIdentity, alice2.services.identityService.wellKnownPartyFromX500Name(DUMMY_NOTARY_NAME))
|
||||||
|
assertEquals(newNotaryIdentity, bob2.services.identityService.wellKnownPartyFromX500Name(DUMMY_NOTARY_NAME))
|
||||||
|
assertEquals(newNotaryIdentity, charlie.services.identityService.wellKnownPartyFromX500Name(DUMMY_NOTARY_NAME))
|
||||||
|
|
||||||
|
assertEquals(newNotaryIdentity, alice2.services.identityService.wellKnownPartyFromAnonymous(mockNet.defaultNotaryIdentity))
|
||||||
|
assertEquals(newNotaryIdentity, bob2.services.identityService.wellKnownPartyFromAnonymous(mockNet.defaultNotaryIdentity))
|
||||||
|
assertEquals(newNotaryIdentity, charlie.services.identityService.wellKnownPartyFromAnonymous(mockNet.defaultNotaryIdentity))
|
||||||
|
|
||||||
|
// Move states notarized with previous notary identity.
|
||||||
|
alice2.services.startFlow(CashPaymentFlow(3000.DOLLARS, bob2.party, false))
|
||||||
|
mockNet.runNetwork()
|
||||||
|
bob2.services.startFlow(CashPaymentFlow(7000.DOLLARS, charlie.party, false))
|
||||||
|
mockNet.runNetwork()
|
||||||
|
charlie.services.startFlow(CashPaymentFlow(7000.DOLLARS, alice2.party, false))
|
||||||
|
mockNet.runNetwork()
|
||||||
|
|
||||||
|
// Combine states notarized with previous and present notary identities.
|
||||||
|
bob2.services.startFlow(CashIssueAndPaymentFlow(300.DOLLARS, ref, alice2.party, false, notary2.party))
|
||||||
|
mockNet.runNetwork()
|
||||||
|
alice2.services.startFlow(CashPaymentFlow(7300.DOLLARS, charlie.party, false))
|
||||||
|
mockNet.runNetwork()
|
||||||
|
|
||||||
|
// Verify that the ledger contains expected states.
|
||||||
|
assertEquals(0.DOLLARS, alice2.services.getCashBalance(USD))
|
||||||
|
assertEquals(0.DOLLARS, bob2.services.getCashBalance(USD))
|
||||||
|
assertEquals(7300.DOLLARS, charlie.services.getCashBalance(USD))
|
||||||
|
}
|
||||||
|
}
|
@ -58,7 +58,13 @@ open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory,
|
|||||||
@Volatile
|
@Volatile
|
||||||
private lateinit var notaries: List<NotaryInfo>
|
private lateinit var notaries: List<NotaryInfo>
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private lateinit var rotatedNotaries: Set<CordaX500Name>
|
||||||
|
|
||||||
|
// Notary whitelist may contain multiple identities with the same X.500 name after certificate rotation.
|
||||||
|
// Exclude duplicated entries, which are not present in the network map.
|
||||||
override val notaryIdentities: List<Party> get() = notaries.map { it.identity }
|
override val notaryIdentities: List<Party> get() = notaries.map { it.identity }
|
||||||
|
.filterNot { it.name in rotatedNotaries && it != getPeerCertificateByLegalName(it.name)?.party }
|
||||||
|
|
||||||
override val allNodeHashes: List<SecureHash>
|
override val allNodeHashes: List<SecureHash>
|
||||||
get() {
|
get() {
|
||||||
@ -74,7 +80,7 @@ open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory,
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun start(notaries: List<NotaryInfo>) {
|
fun start(notaries: List<NotaryInfo>) {
|
||||||
this.notaries = notaries
|
onNewNotaryList(notaries)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo? {
|
override fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo? {
|
||||||
@ -98,6 +104,8 @@ open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun isNotary(party: Party): Boolean = notaries.any { it.identity == party }
|
||||||
|
|
||||||
override fun isValidatingNotary(party: Party): Boolean = notaries.any { it.validating && it.identity == party }
|
override fun isValidatingNotary(party: Party): Boolean = notaries.any { it.validating && it.identity == party }
|
||||||
|
|
||||||
override fun getPartyInfo(party: Party): PartyInfo? {
|
override fun getPartyInfo(party: Party): PartyInfo? {
|
||||||
@ -426,5 +434,6 @@ open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory,
|
|||||||
|
|
||||||
override fun onNewNotaryList(notaries: List<NotaryInfo>) {
|
override fun onNewNotaryList(notaries: List<NotaryInfo>) {
|
||||||
this.notaries = notaries
|
this.notaries = notaries
|
||||||
|
this.rotatedNotaries = notaries.groupBy { it.identity.name }.filter { it.value.size > 1 }.keys
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -91,7 +91,8 @@ class NonValidatingNotaryFlow(otherSideSession: FlowSession, service: SinglePart
|
|||||||
val notaryWhitelist = networkParameters.notaries.map { it.identity }
|
val notaryWhitelist = networkParameters.notaries.map { it.identity }
|
||||||
|
|
||||||
check(notary in notaryWhitelist) {
|
check(notary in notaryWhitelist) {
|
||||||
"Notary specified by the transaction ($notary) is not on the network parameter whitelist: ${notaryWhitelist.joinToString()}"
|
"Notary [${notary.description()}] specified by the transaction is not on the network parameter whitelist: " +
|
||||||
|
" [${notaryWhitelist.joinToString { party -> party.description() }}]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,10 +14,10 @@ import net.corda.nodeapi.internal.persistence.CordaPersistence
|
|||||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||||
import net.corda.testing.core.*
|
import net.corda.testing.core.*
|
||||||
import net.corda.testing.internal.configureDatabase
|
import net.corda.testing.internal.configureDatabase
|
||||||
import net.corda.coretesting.internal.rigorousMock
|
|
||||||
import net.corda.testing.node.MockServices
|
import net.corda.testing.node.MockServices
|
||||||
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
|
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
|
||||||
import net.corda.testing.node.createMockCordaService
|
import net.corda.testing.node.createMockCordaService
|
||||||
|
import net.corda.testing.node.makeTestIdentityService
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Assert.*
|
import org.junit.Assert.*
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
@ -50,7 +50,7 @@ class NodeInterestRatesTest {
|
|||||||
EURIBOR 2016-03-15 2M = 0.111
|
EURIBOR 2016-03-15 2M = 0.111
|
||||||
""".trimIndent())
|
""".trimIndent())
|
||||||
private val dummyCashIssuer = TestIdentity(CordaX500Name("Cash issuer", "London", "GB"))
|
private val dummyCashIssuer = TestIdentity(CordaX500Name("Cash issuer", "London", "GB"))
|
||||||
private val services = MockServices(listOf("net.corda.finance.contracts.asset"), dummyCashIssuer, rigorousMock(), MEGA_CORP_KEY)
|
private val services = MockServices(listOf("net.corda.finance.contracts.asset"), dummyCashIssuer, makeTestIdentityService(), MEGA_CORP_KEY)
|
||||||
// This is safe because MockServices only ever have a single identity
|
// This is safe because MockServices only ever have a single identity
|
||||||
private val identity = services.myInfo.singleIdentity()
|
private val identity = services.myInfo.singleIdentity()
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ package net.corda.traderdemo.flow
|
|||||||
import net.corda.core.contracts.CommandData
|
import net.corda.core.contracts.CommandData
|
||||||
import net.corda.core.crypto.newSecureRandom
|
import net.corda.core.crypto.newSecureRandom
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.node.services.IdentityService
|
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.core.transactions.WireTransaction
|
import net.corda.core.transactions.WireTransaction
|
||||||
@ -13,7 +12,6 @@ import net.corda.testing.core.DUMMY_NOTARY_NAME
|
|||||||
import net.corda.testing.core.SerializationEnvironmentRule
|
import net.corda.testing.core.SerializationEnvironmentRule
|
||||||
import net.corda.testing.core.TestIdentity
|
import net.corda.testing.core.TestIdentity
|
||||||
import net.corda.testing.core.dummyCommand
|
import net.corda.testing.core.dummyCommand
|
||||||
import net.corda.coretesting.internal.rigorousMock
|
|
||||||
import net.corda.testing.node.MockServices
|
import net.corda.testing.node.MockServices
|
||||||
import net.corda.testing.node.internal.MockTransactionStorage
|
import net.corda.testing.node.internal.MockTransactionStorage
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
@ -47,8 +45,8 @@ class TransactionGraphSearchTests {
|
|||||||
* @param signer signer for the two transactions and their commands.
|
* @param signer signer for the two transactions and their commands.
|
||||||
*/
|
*/
|
||||||
fun buildTransactions(command: CommandData): GraphTransactionStorage {
|
fun buildTransactions(command: CommandData): GraphTransactionStorage {
|
||||||
val megaCorpServices = MockServices(listOf("net.corda.testing.contracts"), megaCorp, rigorousMock<IdentityService>())
|
val megaCorpServices = MockServices(listOf("net.corda.testing.contracts"), megaCorp)
|
||||||
val notaryServices = MockServices(listOf("net.corda.testing.contracts"), dummyNotary, rigorousMock<IdentityService>())
|
val notaryServices = MockServices(listOf("net.corda.testing.contracts"), dummyNotary)
|
||||||
val originBuilder = TransactionBuilder(dummyNotary.party)
|
val originBuilder = TransactionBuilder(dummyNotary.party)
|
||||||
.addOutputState(DummyState(random31BitValue()), DummyContract.PROGRAM_ID)
|
.addOutputState(DummyState(random31BitValue()), DummyContract.PROGRAM_ID)
|
||||||
.addCommand(command, megaCorp.publicKey)
|
.addCommand(command, megaCorp.publicKey)
|
||||||
|
Loading…
Reference in New Issue
Block a user