diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 918e6c3f9a..56c1bd80e4 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -3052,21 +3052,21 @@ public final class net.corda.core.flows.FlowRecoveryException extends net.corda. @CordaSerializable public final class net.corda.core.flows.FlowRecoveryQuery extends java.lang.Object public () - public (net.corda.core.flows.FlowTimeWindow, net.corda.core.identity.CordaX500Name, java.util.List) - public (net.corda.core.flows.FlowTimeWindow, net.corda.core.identity.CordaX500Name, java.util.List, int, kotlin.jvm.internal.DefaultConstructorMarker) + public (net.corda.core.flows.FlowTimeWindow, java.util.List, java.util.List) + public (net.corda.core.flows.FlowTimeWindow, java.util.List, java.util.List, int, kotlin.jvm.internal.DefaultConstructorMarker) @Nullable public final net.corda.core.flows.FlowTimeWindow component1() @Nullable - public final net.corda.core.identity.CordaX500Name component2() + public final java.util.List component2() @Nullable public final java.util.List component3() @NotNull - public final net.corda.core.flows.FlowRecoveryQuery copy(net.corda.core.flows.FlowTimeWindow, net.corda.core.identity.CordaX500Name, java.util.List) + public final net.corda.core.flows.FlowRecoveryQuery copy(net.corda.core.flows.FlowTimeWindow, java.util.List, java.util.List) public boolean equals(Object) @Nullable public final java.util.List getCounterParties() @Nullable - public final net.corda.core.identity.CordaX500Name getInitiatedBy() + public final java.util.List getInitiatedBy() @Nullable public final net.corda.core.flows.FlowTimeWindow getTimeframe() public int hashCode() diff --git a/.ci/dev/forward-merge/Jenkinsfile b/.ci/dev/forward-merge/Jenkinsfile index a7ba84d869..4c335c50f4 100644 --- a/.ci/dev/forward-merge/Jenkinsfile +++ b/.ci/dev/forward-merge/Jenkinsfile @@ -28,4 +28,5 @@ forwardMerger( targetBranch: targetBranch, originBranch: originBranch, slackChannel: '#c4-forward-merge-bot-notifications', + cloneTickets: true, ) diff --git a/.ci/dev/regression/Jenkinsfile b/.ci/dev/regression/Jenkinsfile index 0f49cc4d4c..d66d647198 100644 --- a/.ci/dev/regression/Jenkinsfile +++ b/.ci/dev/regression/Jenkinsfile @@ -29,7 +29,8 @@ String COMMON_GRADLE_PARAMS = [ '--info', '-Pcompilation.warningsAsErrors=false', '-Ptests.failFast=true', - '-DexcludeShell', + '--build-cache', + '-DexcludeShell' ].join(' ') pipeline { @@ -55,8 +56,12 @@ pipeline { environment { ARTIFACTORY_BUILD_NAME = "Corda :: Publish :: Publish Release to Artifactory :: ${env.BRANCH_NAME}" ARTIFACTORY_CREDENTIALS = credentials('artifactory-credentials') + BUILD_CACHE_CREDENTIALS = credentials('gradle-ent-cache-credentials') + BUILD_CACHE_PASSWORD = "${env.BUILD_CACHE_CREDENTIALS_PSW}" + BUILD_CACHE_USERNAME = "${env.BUILD_CACHE_CREDENTIALS_USR}" CORDA_ARTIFACTORY_PASSWORD = "${env.ARTIFACTORY_CREDENTIALS_PSW}" CORDA_ARTIFACTORY_USERNAME = "${env.ARTIFACTORY_CREDENTIALS_USR}" + CORDA_GRADLE_SCAN_KEY = credentials('gradle-build-scans-key') CORDA_BUILD_EDITION = "${buildEdition}" CORDA_USE_CACHE = "corda-remotes" DOCKER_URL = "https://index.docker.io/v1/" @@ -76,7 +81,8 @@ pipeline { './gradlew', COMMON_GRADLE_PARAMS, 'clean', - 'jar' + 'jar', + '--parallel' ].join(' ') } } @@ -136,8 +142,8 @@ pipeline { } post { always { - archiveArtifacts artifacts: '**/*.log', fingerprint: false - junit testResults: '**/build/test-results/**/*.xml', keepLongStdio: true + archiveArtifacts artifacts: '**/*.log', allowEmptyArchive: true, fingerprint: false + junit testResults: '**/build/test-results/**/*.xml', keepLongStdio: true, allowEmptyResults: true /* * Copy all JUnit results files into a single top level directory. * This is necessary to stop the allure plugin from hitting out @@ -173,7 +179,8 @@ pipeline { sh script: [ './gradlew', COMMON_GRADLE_PARAMS, - 'jar' + 'jar', + '--parallel' ].join(' ') } } @@ -209,8 +216,8 @@ pipeline { stage('Same agent') { post { always { - archiveArtifacts artifacts: '**/*.log', fingerprint: false - junit testResults: '**/build/test-results/**/*.xml', keepLongStdio: true + archiveArtifacts artifacts: '**/*.log', allowEmptyArchive: true, fingerprint: false + junit testResults: '**/build/test-results/**/*.xml', keepLongStdio: true, allowEmptyResults: true /* * Copy all JUnit results files into a single top level directory. * This is necessary to stop the allure plugin from hitting out @@ -332,6 +339,7 @@ pipeline { post { always { script { + findBuildScans() if (gitUtils.isReleaseTag()) { gitUtils.getGitLog(env.TAG_NAME, env.GIT_URL.replace('https://github.com/corda/', '')) } diff --git a/.github/workflows/jira_assign_issue.yml b/.github/workflows/jira_assign_issue.yml index 7d8b4fba96..9efd0f87a0 100644 --- a/.github/workflows/jira_assign_issue.yml +++ b/.github/workflows/jira_assign_issue.yml @@ -8,12 +8,18 @@ jobs: sync_assigned: runs-on: ubuntu-latest steps: + - name: Generate a token + id: generate_token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.AUTH_APP_ID }} + private-key: ${{ secrets.AUTH_APP_PK }} - name: Assign uses: corda/jira-sync-assigned-action@master with: jiraBaseUrl: ${{ secrets.JIRA_BASE_URL }} jiraEmail: ${{ secrets.JIRA_USER_EMAIL }} jiraToken: ${{ secrets.JIRA_API_TOKEN }} - token: ${{ secrets.GH_TOKEN }} + token: ${{ steps.generate_token.outputs.token }} owner: corda repository: corda diff --git a/.github/workflows/jira_close_issue.yml b/.github/workflows/jira_close_issue.yml index dfe3d2443a..00f5b06363 100644 --- a/.github/workflows/jira_close_issue.yml +++ b/.github/workflows/jira_close_issue.yml @@ -8,12 +8,18 @@ jobs: sync_closed: runs-on: ubuntu-latest steps: + - name: Generate a token + id: generate_token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.AUTH_APP_ID }} + private-key: ${{ secrets.AUTH_APP_PK }} - name: Close uses: corda/jira-sync-closed-action@master with: jiraBaseUrl: https://r3-cev.atlassian.net jiraEmail: ${{ secrets.JIRA_USER_EMAIL }} jiraToken: ${{ secrets.JIRA_API_TOKEN }} - token: ${{ secrets.GH_TOKEN }} + token: ${{ steps.generate_token.outputs.token }} owner: corda repository: corda diff --git a/.github/workflows/jira_create_issue.yml b/.github/workflows/jira_create_issue.yml index 66a3bbdc37..066fb8ca45 100644 --- a/.github/workflows/jira_create_issue.yml +++ b/.github/workflows/jira_create_issue.yml @@ -10,6 +10,13 @@ jobs: steps: - uses: actions/checkout@v2 + - name: Generate a token + id: generate_token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.AUTH_APP_ID }} + private-key: ${{ secrets.AUTH_APP_PK }} + - name: Jira Create issue id: create uses: corda/jira-create-issue-action@master @@ -30,7 +37,7 @@ jobs: - name: Create comment uses: peter-evans/create-or-update-comment@v1 with: - token: ${{ secrets.GH_TOKEN }} + token: ${{ steps.generate_token.outputs.token }} issue-number: ${{ github.event.issue.number }} body: | Automatically created Jira issue: ${{ steps.create.outputs.issue }} diff --git a/Jenkinsfile b/Jenkinsfile index b6e7978642..8cdbb77c9e 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -24,6 +24,7 @@ String COMMON_GRADLE_PARAMS = [ '-Ptests.failFast=true', '-Ddependx.branch.origin="${GIT_COMMIT}"', // DON'T change quotation - GIT_COMMIT variable is substituted by SHELL!!!! '-Ddependx.branch.target="${CHANGE_TARGET}"', // DON'T change quotation - CHANGE_TARGET variable is substituted by SHELL!!!! + '--build-cache', ].join(' ') pipeline { @@ -45,8 +46,12 @@ pipeline { */ environment { ARTIFACTORY_CREDENTIALS = credentials('artifactory-credentials') + BUILD_CACHE_CREDENTIALS = credentials('gradle-ent-cache-credentials') + BUILD_CACHE_PASSWORD = "${env.BUILD_CACHE_CREDENTIALS_PSW}" + BUILD_CACHE_USERNAME = "${env.BUILD_CACHE_CREDENTIALS_USR}" CORDA_ARTIFACTORY_PASSWORD = "${env.ARTIFACTORY_CREDENTIALS_PSW}" CORDA_ARTIFACTORY_USERNAME = "${env.ARTIFACTORY_CREDENTIALS_USR}" + CORDA_GRADLE_SCAN_KEY = credentials('gradle-build-scans-key') CORDA_USE_CACHE = "corda-remotes" JAVA_HOME="/usr/lib/jvm/java-17-amazon-corretto" } @@ -59,7 +64,8 @@ pipeline { './gradlew', COMMON_GRADLE_PARAMS, 'clean', - 'jar' + 'jar', + '--parallel' ].join(' ') } } @@ -81,8 +87,8 @@ pipeline { } post { always { - archiveArtifacts artifacts: '**/*.log', fingerprint: false - junit testResults: '**/build/test-results/**/*.xml', keepLongStdio: true + archiveArtifacts artifacts: '**/*.log', allowEmptyArchive: true, fingerprint: true + junit testResults: '**/build/test-results/**/*.xml', keepLongStdio: true,allowEmptyResults: true } cleanup { deleteDir() /* clean up our workspace */ @@ -100,7 +106,8 @@ pipeline { sh script: [ './gradlew', COMMON_GRADLE_PARAMS, - 'jar' + 'jar', + '--parallel' ].join(' ') } } @@ -136,8 +143,8 @@ pipeline { stage('Same agent') { post { always { - archiveArtifacts artifacts: '**/*.log', fingerprint: false - junit testResults: '**/build/test-results/**/*.xml', keepLongStdio: true + archiveArtifacts artifacts: '**/*.log', allowEmptyArchive: true, fingerprint: true + junit testResults: '**/build/test-results/**/*.xml', keepLongStdio: true, allowEmptyResults: true } } stages { @@ -155,8 +162,10 @@ pipeline { } } } - post { + always { + findBuildScans() + } cleanup { deleteDir() /* clean up our workspace */ } diff --git a/core/src/main/kotlin/net/corda/core/flows/FinalityRecoveryFlow.kt b/core/src/main/kotlin/net/corda/core/flows/FinalityRecoveryFlow.kt index 2e0adf9eb2..1d03305243 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FinalityRecoveryFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FinalityRecoveryFlow.kt @@ -56,7 +56,7 @@ class FlowRecoveryException(message: String, cause: Throwable? = null) : FlowExc @CordaSerializable data class FlowRecoveryQuery( val timeframe: FlowTimeWindow? = null, - val initiatedBy: CordaX500Name? = null, + val initiatedBy: List? = null, val counterParties: List? = null) { init { require(timeframe != null || initiatedBy != null || counterParties != null) { diff --git a/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt b/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt index 5153f98e91..3e16d044ad 100644 --- a/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt @@ -11,8 +11,42 @@ import net.corda.core.utilities.ProgressTracker */ @StartableByRPC class LedgerRecoveryFlow( - private val parameters: LedgerRecoveryParameters, - override val progressTracker: ProgressTracker = ProgressTracker()) : FlowLogic() { + private val parameters: LedgerRecoveryParameters, + override val progressTracker: ProgressTracker = ProgressTracker()) : FlowLogic() { + + // constructors added to aid Corda Node Shell flow command invocation + constructor(recoveryPeer: Party) : this(LedgerRecoveryParameters(setOf(recoveryPeer))) + constructor(recoveryPeers: Collection) : this(LedgerRecoveryParameters(recoveryPeers)) + constructor(useAllNetworkNodes: Boolean) : this(LedgerRecoveryParameters(emptySet(), useAllNetworkNodes = useAllNetworkNodes)) + constructor(recoveryPeer: Party, timeWindow: RecoveryTimeWindow) : + this(LedgerRecoveryParameters(setOf(recoveryPeer), timeWindow)) + constructor(recoveryPeer: Party, timeWindow: RecoveryTimeWindow, dryRun: Boolean) : + this(LedgerRecoveryParameters(setOf(recoveryPeer), timeWindow, dryRun = dryRun)) + constructor(recoveryPeer: Party, timeWindow: RecoveryTimeWindow, dryRun: Boolean, verboseLogging: Boolean) : + this(LedgerRecoveryParameters(setOf(recoveryPeer), timeWindow, dryRun = dryRun, verboseLogging = verboseLogging)) + constructor(recoveryPeer: Party, timeWindow: RecoveryTimeWindow, dryRun: Boolean, verboseLogging: Boolean, alsoFinalize: Boolean) : + this(LedgerRecoveryParameters(setOf(recoveryPeer), timeWindow, dryRun = dryRun, verboseLogging = verboseLogging, alsoFinalize = alsoFinalize)) + constructor(recoveryPeers: Collection, timeWindow: RecoveryTimeWindow) : + this(LedgerRecoveryParameters(recoveryPeers, timeWindow)) + constructor(recoveryPeers: Collection, timeWindow: RecoveryTimeWindow, dryRun: Boolean) : + this(LedgerRecoveryParameters(recoveryPeers, timeWindow, dryRun = dryRun)) + constructor(recoveryPeers: Collection, timeWindow: RecoveryTimeWindow, dryRun: Boolean, verboseLogging: Boolean) : + this(LedgerRecoveryParameters(recoveryPeers, timeWindow, dryRun = dryRun, verboseLogging = verboseLogging)) + constructor(recoveryPeers: Collection, timeWindow: RecoveryTimeWindow, dryRun: Boolean, verboseLogging: Boolean, alsoFinalize: Boolean) : + this(LedgerRecoveryParameters(recoveryPeers, timeWindow, dryRun = dryRun, verboseLogging = verboseLogging, alsoFinalize = alsoFinalize)) + constructor(useAllNetworkNodes: Boolean, timeWindow: RecoveryTimeWindow) : + this(LedgerRecoveryParameters(emptySet(), timeWindow, useAllNetworkNodes = useAllNetworkNodes)) + constructor(useAllNetworkNodes: Boolean, timeWindow: RecoveryTimeWindow, dryRun: Boolean) : + this(LedgerRecoveryParameters(emptySet(), timeWindow, useAllNetworkNodes = useAllNetworkNodes, dryRun = dryRun)) + constructor(useAllNetworkNodes: Boolean, timeWindow: RecoveryTimeWindow, dryRun: Boolean, verboseLogging: Boolean) : + this(LedgerRecoveryParameters(emptySet(), timeWindow, useAllNetworkNodes = useAllNetworkNodes, dryRun = dryRun, verboseLogging = verboseLogging)) + constructor(useAllNetworkNodes: Boolean, timeWindow: RecoveryTimeWindow, dryRun: Boolean, verboseLogging: Boolean, recoveryBatchSize: Int, alsoFinalize: Boolean) : + this(LedgerRecoveryParameters(emptySet(), timeWindow, useAllNetworkNodes = useAllNetworkNodes, dryRun = dryRun, verboseLogging = verboseLogging, recoveryBatchSize = recoveryBatchSize, alsoFinalize = alsoFinalize)) + constructor(useAllNetworkNodes: Boolean, timeWindow: RecoveryTimeWindow, dryRun: Boolean, verboseLogging: Boolean, recoveryBatchSize: Int) : + this(LedgerRecoveryParameters(emptySet(), timeWindow, useAllNetworkNodes = useAllNetworkNodes, dryRun = dryRun, verboseLogging = verboseLogging, recoveryBatchSize = recoveryBatchSize)) + constructor(recoveryPeers: Collection, timeWindow: RecoveryTimeWindow, useAllNetworkNodes: Boolean, dryRun: Boolean, useTimeWindowNarrowing: Boolean, verboseLogging: Boolean, recoveryBatchSize: Int) : + this(LedgerRecoveryParameters(recoveryPeers, timeWindow, useAllNetworkNodes, + dryRun = dryRun, useTimeWindowNarrowing = useTimeWindowNarrowing, verboseLogging = verboseLogging, recoveryBatchSize = recoveryBatchSize)) @CordaInternal data class ExtraConstructorArgs(val parameters: LedgerRecoveryParameters) diff --git a/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt b/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt index 07564bcbf4..441a0e669a 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt @@ -80,7 +80,8 @@ sealed class FetchDataFlow( // against not having unknown by using the platform version as a guard. @CordaSerializationTransformEnumDefaults( CordaSerializationTransformEnumDefault("BATCH_TRANSACTION", "TRANSACTION"), - CordaSerializationTransformEnumDefault("UNKNOWN", "TRANSACTION") + CordaSerializationTransformEnumDefault("UNKNOWN", "TRANSACTION"), + CordaSerializationTransformEnumDefault("TRANSACTION_RECOVERY", "TRANSACTION") ) @CordaSerializable enum class DataType { diff --git a/node/src/integration-test/kotlin/net/corda/node/VaultUpdateDeserializationTest.kt b/node/src/integration-test/kotlin/net/corda/node/VaultUpdateDeserializationTest.kt index ff212612c6..e77da5a9c1 100644 --- a/node/src/integration-test/kotlin/net/corda/node/VaultUpdateDeserializationTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/VaultUpdateDeserializationTest.kt @@ -4,7 +4,6 @@ import co.paralleluniverse.strands.Strand import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.assertTrue -import net.corda.core.flows.UnexpectedFlowEndException import net.corda.core.internal.InputStreamAndHash import net.corda.core.internal.deleteRecursively import net.corda.core.messaging.startFlow @@ -55,13 +54,15 @@ class VaultUpdateDeserializationTest { /* * Transaction sent from A -> B with Notarisation * Test that a deserialization error is raised where the receiver node of a transaction has an incompatible contract jar. - * In the case of a notarised transaction, a deserialisation error is thrown in the receiver SignTransactionFlow (before finality) - * upon receiving the transaction to be signed and attempting to record its dependencies. - * The ledger will not record any transactions, and the flow must be retried by the sender upon installing the correct contract jar + * But only on new transactions, and not in the back chain. + * In the case of a notarised transaction, a deserialisation error is thrown in the receiver in the second phase of finality + * when updating the vault. The sender will not block, and the back chain is successfully recorded + * on the receiver even though those states have deserialization errors too. The flow on the receiver is hospitalised. + * The flow will be retried by the receiver upon installing the correct contract jar * version at the receiver and re-starting the node. */ - @Test(timeout=300_000) - fun `Notarised transaction fails completely upon receiver deserialization failure collecting signatures when using incompatible contract jar`() { + @Test(timeout = 300_000) + fun `Notarised transaction fails but back chain succeeds upon receiver deserialization failure when using incompatible contract jar`() { driver(driverParameters(listOf(flowVersion1, contractVersion1))) { val alice = startNode(NodeParameters(additionalCordapps = listOf(flowVersion1, contractVersion1)), providedName = ALICE_NAME).getOrThrow() @@ -75,20 +76,15 @@ class VaultUpdateDeserializationTest { val stx = alice.rpc.startFlow(::AttachmentIssueFlow, hash, defaultNotaryIdentity).returnValue.getOrThrow(30.seconds) val spendableState = stx.coreTransaction.outRef(0) - // NOTE: exception is propagated from Receiver - try { - alice.rpc.startFlow(::AttachmentFlowV1, bob.nodeInfo.singleIdentity(), defaultNotaryIdentity, hash, spendableState).returnValue.getOrThrow(30.seconds) - } - catch(e: UnexpectedFlowEndException) { - println("Bob fails to deserialise transaction upon receipt of transaction for signing.") - } + alice.rpc.startFlow(::AttachmentFlowV1, bob.nodeInfo.singleIdentity(), defaultNotaryIdentity, hash, spendableState).returnValue.getOrThrow(30.seconds) + assertEquals(0, bob.rpc.vaultQueryBy().states.size) assertEquals(1, alice.rpc.vaultQueryBy().states.size) // check transaction records @Suppress("DEPRECATION") - assertEquals(1, alice.rpc.internalVerifiedTransactionsSnapshot().size) // issuance only + assertEquals(2, alice.rpc.internalVerifiedTransactionsSnapshot().size) // both @Suppress("DEPRECATION") - assertTrue(bob.rpc.internalVerifiedTransactionsSnapshot().isEmpty()) + assertEquals(1, bob.rpc.internalVerifiedTransactionsSnapshot().size) // issuance only // restart Bob with correct contract jar version (bob as OutOfProcess).process.destroyForcibly() @@ -97,13 +93,12 @@ class VaultUpdateDeserializationTest { val restartedBob = startNode(NodeParameters(additionalCordapps = listOf(flowVersion1, contractVersion1)), providedName = BOB_NAME).getOrThrow() - // re-run failed flow - alice.rpc.startFlow(::AttachmentFlowV1, restartedBob.nodeInfo.singleIdentity(), defaultNotaryIdentity, hash, spendableState).returnValue.getOrThrow(30.seconds) - + // original hospitalized transaction should now have been re-processed with correct contract jar assertEquals(1, waitForVaultUpdate(restartedBob)) assertEquals(1, alice.rpc.vaultQueryBy().states.size) @Suppress("DEPRECATION") - assertTrue(restartedBob.rpc.internalVerifiedTransactionsSnapshot().isNotEmpty()) + assertEquals(2, restartedBob.rpc.internalVerifiedTransactionsSnapshot().size) // both + assertEquals(1, restartedBob.rpc.vaultQueryBy().states.size) } } diff --git a/node/src/main/kotlin/net/corda/node/services/DbTransactionsResolver.kt b/node/src/main/kotlin/net/corda/node/services/DbTransactionsResolver.kt index 668b95ff92..9a8e9d5eda 100644 --- a/node/src/main/kotlin/net/corda/node/services/DbTransactionsResolver.kt +++ b/node/src/main/kotlin/net/corda/node/services/DbTransactionsResolver.kt @@ -13,6 +13,7 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.debug import net.corda.core.utilities.seconds import net.corda.core.utilities.trace +import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.api.WritableTransactionStorage import java.util.* @@ -107,7 +108,7 @@ class DbTransactionsResolver(private val flow: ResolveTransactionsFlow) : Transa } if (txStatus == TransactionStatus.UNVERIFIED) { tx.verify(flow.serviceHub) - flow.serviceHub.recordTransactions(usedStatesToRecord, listOf(tx)) + (flow.serviceHub as ServiceHubInternal).recordTransactions(usedStatesToRecord, listOf(tx), false, disableSoftLocking = true) } else { logger.debug { "No need to record $txId as it's already been verified" } } diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index bea46121fc..93df592800 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -88,6 +88,7 @@ interface ServiceHubInternal : ServiceHubCoreInternal { stateMachineRecordedTransactionMapping: StateMachineRecordedTransactionMappingStorage, vaultService: VaultServiceInternal, database: CordaPersistence, + disableSoftLocking: Boolean = false, updateFn: (SignedTransaction) -> Boolean = validatedTransactions::addTransaction ) { @@ -147,7 +148,7 @@ interface ServiceHubInternal : ServiceHubCoreInternal { // // Because the primary use case for recording irrelevant states is observer/regulator nodes, who are unlikely // to make writes to the ledger very often or at all, we choose to punt this issue for the time being. - vaultService.notifyAll(statesToRecord, recordedTransactions.map { it.coreTransaction }, previouslySeenTxs.map { it.coreTransaction }) + vaultService.notifyAll(statesToRecord, recordedTransactions.map { it.coreTransaction }, previouslySeenTxs.map { it.coreTransaction }, disableSoftLocking) } } @@ -205,15 +206,14 @@ interface ServiceHubInternal : ServiceHubCoreInternal { @Suppress("NestedBlockDepth") @VisibleForTesting - fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable, disableSignatureVerification: Boolean) { + fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable, disableSignatureVerification: Boolean, disableSoftLocking: Boolean = false) { txs.forEach { requireSupportedHashType(it) if (it.coreTransaction is WireTransaction) { if (disableSignatureVerification) { log.warnOnce("The current usage of recordTransactions is unsafe." + "Recording transactions without signature verification may lead to severe problems with ledger consistency.") - } - else { + } else { try { it.verifyRequiredSignatures() } @@ -229,7 +229,8 @@ interface ServiceHubInternal : ServiceHubCoreInternal { validatedTransactions, stateMachineRecordedTransactionMapping, vaultService, - database + database, + disableSoftLocking ) } diff --git a/node/src/main/kotlin/net/corda/node/services/api/VaultServiceInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/VaultServiceInternal.kt index e95ba4b015..3c54a3321d 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/VaultServiceInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/VaultServiceInternal.kt @@ -15,7 +15,8 @@ interface VaultServiceInternal : VaultService { * indicate whether an update consists entirely of regular or notary change transactions, which may require * different processing logic. */ - fun notifyAll(statesToRecord: StatesToRecord, txns: Iterable, previouslySeenTxns: Iterable = emptyList()) + fun notifyAll(statesToRecord: StatesToRecord, txns: Iterable, previouslySeenTxns: Iterable = emptyList(), + disableSoftLocking: Boolean = false) /** * Same as notifyAll but with a single transaction. diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 6a20f2c4e5..ce21da56dd 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -70,7 +70,7 @@ import java.security.PublicKey import java.sql.SQLException import java.time.Clock import java.time.Instant -import java.util.* +import java.util.UUID import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArraySet import java.util.stream.Stream @@ -283,12 +283,13 @@ class NodeVaultService( internal val publishUpdates get() = mutex.locked { updatesPublisher } /** Groups adjacent transactions into batches to generate separate net updates per transaction type. */ - override fun notifyAll(statesToRecord: StatesToRecord, txns: Iterable, previouslySeenTxns: Iterable) { + override fun notifyAll(statesToRecord: StatesToRecord, txns: Iterable, previouslySeenTxns: Iterable, + disableSoftLocking: Boolean) { if (statesToRecord == StatesToRecord.NONE || (!txns.any() && !previouslySeenTxns.any())) return val batch = mutableListOf() fun flushBatch(previouslySeen: Boolean) { - val updates = makeUpdates(batch, statesToRecord, previouslySeen) + val updates = makeUpdates(batch, statesToRecord, previouslySeen, disableSoftLocking) processAndNotify(updates) batch.clear() } @@ -307,7 +308,8 @@ class NodeVaultService( processTransactions(txns, false) } - private fun makeUpdates(batch: Iterable, statesToRecord: StatesToRecord, previouslySeen: Boolean): List> { + @Suppress("ComplexMethod", "ThrowsCount") + private fun makeUpdates(batch: Iterable, statesToRecord: StatesToRecord, previouslySeen: Boolean, disableSoftLocking: Boolean): List> { fun withValidDeserialization(list: List, txId: SecureHash): Map { var error: TransactionDeserialisationException? = null @@ -319,13 +321,15 @@ class NodeVaultService( // This will cause a failure as we can't deserialize such states in the context of the `appClassloader`. // For now we ignore these states. // In the future we will use the AttachmentsClassloader to correctly deserialize and asses the relevancy. - if (IGNORE_TRANSACTION_DESERIALIZATION_ERRORS) { + // Disabled if soft locking disabled, as assumes you are in the back chain and that maybe it is less important than top + // level transaction. + if (IGNORE_TRANSACTION_DESERIALIZATION_ERRORS || disableSoftLocking) { log.warnOnce("The current usage of transaction deserialization for the vault is unsafe." + "Ignoring vault updates due to failed deserialized states may lead to severe problems with ledger consistency. ") log.warn("Could not deserialize state $idx from transaction $txId. Cause: $e") } else { log.error("Could not deserialize state $idx from transaction $txId. Cause: $e") - if(error == null) error = e + if (error == null) error = e } null } diff --git a/node/src/main/resources/migration/node-core.changelog-v25.xml b/node/src/main/resources/migration/node-core.changelog-v25.xml index 75a4144667..28ae879bc1 100644 --- a/node/src/main/resources/migration/node-core.changelog-v25.xml +++ b/node/src/main/resources/migration/node-core.changelog-v25.xml @@ -75,7 +75,7 @@ - +