From fd491d91ff44c2e4e9c73e12a197cc4625967afa Mon Sep 17 00:00:00 2001 From: igor nitto Date: Fri, 23 Feb 2018 18:29:49 +0000 Subject: [PATCH 1/5] WebServer init failure on config produced by plugins >= 4.0.2 [CORDA-877] (#2624) --- .../kotlin/net/corda/webserver/WebServerConfig.kt | 14 +++++++++++++- .../net/corda/webserver/internal/NodeWebServer.kt | 5 ++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt b/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt index 157b5f3cdd..28a960a224 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt @@ -5,6 +5,7 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.nodeapi.internal.config.NodeSSLConfiguration import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.getValue +import net.corda.nodeapi.internal.config.parseAs import java.nio.file.Path /** @@ -25,5 +26,16 @@ class WebServerConfig(override val baseDirectory: Path, val config: Config) : No throw Exception("Missing rpc address property. Either 'rpcSettings' or 'rpcAddress' must be specified.") } val webAddress: NetworkHostAndPort by config - val rpcUsers: List by config + val runAs: User + + init { + // TODO: replace with credentials supplied by a user + val users = if (config.hasPath("rpcUsers")) { + // TODO: remove this once config format is updated + config.getConfigList("rpcUsers") + } else { + config.getConfigList("security.authService.dataSource.users") + } + runAs = users.first().parseAs() + } } diff --git a/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt b/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt index 4938ca99c1..958c540f61 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt @@ -189,10 +189,9 @@ class NodeWebServer(val config: WebServerConfig) { } private fun connectLocalRpcAsNodeUser(): CordaRPCOps { - val rpcUser = config.rpcUsers.firstOrNull() ?: throw IllegalArgumentException("The node config has not specified any RPC users") - log.info("Connecting to node at ${config.rpcAddress} as $rpcUser") + log.info("Connecting to node at ${config.rpcAddress} as ${config.runAs}") val client = CordaRPCClient(config.rpcAddress) - val connection = client.start(rpcUser.username, rpcUser.password) + val connection = client.start(config.runAs.username, config.runAs.password) return connection.proxy } From 1bca591dd1d2874ef1bb51dd47d8c0d45d4f81c0 Mon Sep 17 00:00:00 2001 From: Maksymilian Pawlak <120831+m4ksio@users.noreply.github.com> Date: Fri, 23 Feb 2018 20:48:39 +0000 Subject: [PATCH 2/5] Disable unit test for experimental projects (#2627) * Disable unit test for experimental projects --- build.gradle | 4 ++++ experimental/README.md | 3 +++ 2 files changed, 7 insertions(+) diff --git a/build.gradle b/build.gradle index 436efdab22..0a9307d4c6 100644 --- a/build.gradle +++ b/build.gradle @@ -173,6 +173,10 @@ allprojects { if (System.getProperty("test.maxParallelForks") != null) { maxParallelForks = Integer.valueOf(System.getProperty("test.maxParallelForks")) } + + if (project.path.startsWith(':experimental') && System.getProperty("experimental.test.enable") == null) { + enabled = false + } } group 'net.corda' diff --git a/experimental/README.md b/experimental/README.md index f58401d525..99a8957223 100644 --- a/experimental/README.md +++ b/experimental/README.md @@ -7,3 +7,6 @@ either be moved into the main modules and go through code review, or be deleted. Code placed here can be committed to directly onto master at any time as long as it doesn't break the build (no compile failures or unit test failures). Any commits here that break the build will simply be rolled back. +To help reduce the build times, unit tests for experimental projects have been disabled and will NOT run alongside +the whole project tests run via Gradle. Add parameter ```experimental.test.enable``` (example command is ```gradlew test -Dexperimental.test.enable``` +to enable those tests. \ No newline at end of file From 50ccb327008b60a47db1498104b02f14a1fe0da0 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Mon, 26 Feb 2018 09:21:27 +0000 Subject: [PATCH 3/5] Adds a 'provision the certificates' step to the Windows deployment instructions. --- docs/source/deploying-a-node.rst | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/source/deploying-a-node.rst b/docs/source/deploying-a-node.rst index 0643ff1332..bfc3eaa129 100644 --- a/docs/source/deploying-a-node.rst +++ b/docs/source/deploying-a-node.rst @@ -267,13 +267,16 @@ at boot, and means the Corda service stays running with no users connected to th * Set the amount of Java heap memory available to this node by modifying the -Xmx argument * Set an informative description -10. Run the batch file by clicking on it or from a command prompt +10. Provision the required certificates to your node. Contact the network permissioning service or see + :doc:`permissioning` -11. Run ``services.msc`` and verify that a service called ``cordanode1`` is present and running +11. Run the batch file by clicking on it or from a command prompt -12. Run ``netstat -ano`` and check for the ports you configured in ``node.conf`` +12. Run ``services.msc`` and verify that a service called ``cordanode1`` is present and running -13. You may need to open the ports on the Windows firewall +13. Run ``netstat -ano`` and check for the ports you configured in ``node.conf`` + + * You may need to open the ports on the Windows firewall Testing your installation ------------------------- From a483e7e8ce2f0b5e7bbf6847bfde9a4e611b4ff5 Mon Sep 17 00:00:00 2001 From: Christian Sailer Date: Mon, 26 Feb 2018 09:37:32 +0000 Subject: [PATCH 4/5] CORDA-1096 - Performance when loading multiple states from the vault (#2614) * CORDA-1096 - Performance when loading multiple states from the vault (#2609) * Provide efficient `loadStates()` implementation * Replace loops using `loadState` with calls to `loadStates` * Replace `map`/`flatMap` with just a single `flatMap` --- .../net/corda/confidential/IdentitySyncFlow.kt | 2 +- .../main/kotlin/net/corda/core/flows/NotaryFlow.kt | 2 +- .../main/kotlin/net/corda/core/node/ServiceHub.kt | 8 +++++--- .../corda/core/node/services/TransactionStorage.kt | 13 ++++++++++--- .../core/transactions/NotaryChangeTransactions.kt | 4 +--- .../net/corda/finance/flows/TwoPartyTradeFlow.kt | 2 +- .../corda/node/services/AttachmentLoadingTests.kt | 6 ++---- .../main/kotlin/net/corda/testing/dsl/TestDSL.kt | 3 +++ .../kotlin/net/corda/verifier/GeneratedLedger.kt | 7 +++++++ 9 files changed, 31 insertions(+), 16 deletions(-) diff --git a/confidential-identities/src/main/kotlin/net/corda/confidential/IdentitySyncFlow.kt b/confidential-identities/src/main/kotlin/net/corda/confidential/IdentitySyncFlow.kt index b454556bf3..bffff0abf1 100644 --- a/confidential-identities/src/main/kotlin/net/corda/confidential/IdentitySyncFlow.kt +++ b/confidential-identities/src/main/kotlin/net/corda/confidential/IdentitySyncFlow.kt @@ -53,7 +53,7 @@ object IdentitySyncFlow { } private fun extractOurConfidentialIdentities(): Map { - val states: List = (tx.inputs.map { serviceHub.loadState(it) }.requireNoNulls().map { it.data } + tx.outputs.map { it.data }) + val states: List = (serviceHub.loadStates(tx.inputs.toSet()).map { it.state.data } + tx.outputs.map { it.data }) val identities: Set = states.flatMap(ContractState::participants).toSet() // Filter participants down to the set of those not in the network map (are not well known) val confidentialIdentities = identities diff --git a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt index 9c2cbee976..78dd436eb6 100644 --- a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt @@ -63,7 +63,7 @@ class NotaryFlow { protected fun checkTransaction(): Party { 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" } - check(stx.inputs.all { stateRef -> serviceHub.loadState(stateRef).notary == notaryParty }) { + check(serviceHub.loadStates(stx.inputs.toSet()).all { it.state.notary == notaryParty }) { "Input states must have the same Notary" } diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index 0d2f36ff43..80e1065c99 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -26,6 +26,10 @@ interface StateLoader { /** * Given a [StateRef] loads the referenced transaction and looks up the specified output [ContractState]. * + * *WARNING* Do not use this method unless you really only want a single state - any batch loading should + * go through [loadStates] as repeatedly calling [loadState] can lead to repeat deserialsiation work and + * severe performance degradation. + * * @throws TransactionResolutionException if [stateRef] points to a non-existent transaction. */ @Throws(TransactionResolutionException::class) @@ -39,9 +43,7 @@ interface StateLoader { // TODO: future implementation to use a Vault state ref -> contract state BLOB table and perform single query bulk load // as the existing transaction store will become encrypted at some point @Throws(TransactionResolutionException::class) - fun loadStates(stateRefs: Set): Set> { - return stateRefs.map { StateAndRef(loadState(it), it) }.toSet() - } + fun loadStates(stateRefs: Set): Set> } /** diff --git a/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt b/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt index 25053adb67..07240e9f30 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt @@ -1,9 +1,7 @@ package net.corda.core.node.services import net.corda.core.DoNotImplement -import net.corda.core.contracts.StateRef -import net.corda.core.contracts.TransactionResolutionException -import net.corda.core.contracts.TransactionState +import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.messaging.DataFeed import net.corda.core.node.StateLoader @@ -26,6 +24,15 @@ interface TransactionStorage : StateLoader { return stx.resolveBaseTransaction(this).outputs[stateRef.index] } + @Throws(TransactionResolutionException::class) + override fun loadStates(stateRefs: Set): Set> { + return stateRefs.groupBy { it.txhash }.flatMap { + val stx = getTransaction(it.key) ?: throw TransactionResolutionException(it.key) + val baseTx = stx.resolveBaseTransaction(this) + it.value.map { StateAndRef(baseTx.outputs[it.index], it) } + }.toSet() + } + /** * Get a synchronous Observable of updates. When observations are pushed to the Observer, the vault will already * incorporate the update. diff --git a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt index 332718fd94..f0a6c7cc18 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt @@ -42,9 +42,7 @@ data class NotaryChangeWireTransaction( fun resolve(services: ServiceHub, sigs: List) = resolve(services as StateLoader, sigs) fun resolve(stateLoader: StateLoader, sigs: List): NotaryChangeLedgerTransaction { - val resolvedInputs = inputs.map { ref -> - stateLoader.loadState(ref).let { StateAndRef(it, ref) } - } + val resolvedInputs = stateLoader.loadStates(inputs.toSet()).toList() return NotaryChangeLedgerTransaction(resolvedInputs, notary, newNotary, id, sigs) } } diff --git a/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt index 9d18c2d3a5..28e98035c7 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt @@ -97,7 +97,7 @@ object TwoPartyTradeFlow { val signTransactionFlow = object : SignTransactionFlow(otherSideSession, VERIFYING_AND_SIGNING.childProgressTracker()) { override fun checkTransaction(stx: SignedTransaction) { // Verify that we know who all the participants in the transaction are - val states: Iterable = stx.tx.inputs.map { serviceHub.loadState(it).data } + stx.tx.outputs.map { it.data } + val states: Iterable = serviceHub.loadStates(stx.tx.inputs.toSet()).map { it.state.data } + stx.tx.outputs.map { it.data } states.forEach { state -> state.participants.forEach { anon -> require(serviceHub.identityService.wellKnownPartyFromAnonymous(anon) != null) { diff --git a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt index 4a911cb0d3..166056f0bf 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt @@ -2,10 +2,7 @@ package net.corda.node.services import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.whenever -import net.corda.core.contracts.Contract -import net.corda.core.contracts.PartyAndReference -import net.corda.core.contracts.StateRef -import net.corda.core.contracts.TransactionState +import net.corda.core.contracts.* import net.corda.core.cordapp.CordappProvider import net.corda.core.flows.FlowLogic import net.corda.core.flows.UnexpectedFlowEndException @@ -87,6 +84,7 @@ class AttachmentLoadingTests { private val services = object : ServicesForResolution { override fun loadState(stateRef: StateRef): TransactionState<*> = throw NotImplementedError() + override fun loadStates(stateRefs: Set): Set> = throw NotImplementedError() override val identityService = rigorousMock().apply { doReturn(null).whenever(this).partyFromKey(DUMMY_BANK_A.owningKey) } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt index bf2745f8e3..24ba8921c8 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt @@ -77,6 +77,9 @@ data class TestTransactionDSLInterpreter private constructor( val services = object : ServicesForResolution by ledgerInterpreter.services { override fun loadState(stateRef: StateRef) = ledgerInterpreter.resolveStateRef(stateRef) + override fun loadStates(stateRefs: Set): Set> { + return stateRefs.map { StateAndRef(loadState(it), it) }.toSet() + } override val cordappProvider: CordappProvider = ledgerInterpreter.services.cordappProvider } diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt index bd33ea9efc..68c85a65bf 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt @@ -50,6 +50,13 @@ data class GeneratedLedger( override fun loadState(stateRef: StateRef): TransactionState<*> { return hashTransactionMap[stateRef.txhash]?.outputs?.get(stateRef.index) ?: throw TransactionResolutionException(stateRef.txhash) } + + override fun loadStates(stateRefs: Set): Set> { + return stateRefs.groupBy { it.txhash }.flatMap { + val outputs = hashTransactionMap[it.key]?.outputs ?: throw TransactionResolutionException(it.key) + it.value.map { StateAndRef(outputs[it.index], it) } + }.toSet() + } override val identityService = rigorousMock().apply { doAnswer { identityMap[it.arguments[0]] }.whenever(this).partyFromKey(any()) } From 0edfef24090ec6068ccc9337595dc7ff7e7a45d2 Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Thu, 22 Feb 2018 17:51:41 +0000 Subject: [PATCH 5/5] CORDA-696 - Create separate transaction types for contract upgrade transactions (#2589) * CORDA-986 and CORDA-985 CompositeKey and Signature verification performance fixes (#2467) * CORDA-696: Create separate transaction types for contract upgrade transactions. Add rationale around upgrade transactions Move contract upgrade transaction resolution logic into internal until it's stabilised. Throw a better exception when contract attachment not found Default legacy contract constraint to always accepting - needs to be changed to whitelist constraint before merging Introduce a new upgraded contract interface that allows specifying the legacy constraint. Remove StateLoader, make all tx resolution functions take in ServicesForResolution Contract upgrade transactions can handle whitelist by zone constraints When creating a contract upgrade transaction, make sure the attachment of the old cordapp gets attached when using hash constraints. Attachment lookup for a given contract class name only scans currently loaded cordapps, and we don't load old versions of cordapps. CORDA-696: Update upgrade docs --- .ci/api-current.txt | 26 +-- .../corda/client/jackson/JacksonSupport.kt | 5 +- .../net/corda/core/contracts/Structures.kt | 21 ++ .../corda/core/flows/ContractUpgradeFlow.kt | 11 +- .../kotlin/net/corda/core/flows/NotaryFlow.kt | 17 +- .../core/internal/ContractUpgradeUtils.kt | 43 ++-- .../core/internal/ResolveTransactionsFlow.kt | 14 +- .../net/corda/core/internal/UpgradeCommand.kt | 7 - .../kotlin/net/corda/core/node/ServiceHub.kt | 52 +++-- .../core/node/services/TransactionStorage.kt | 19 +- .../corda/core/node/services/VaultService.kt | 18 +- .../ContractUpgradeTransactions.kt | 183 ++++++++++++++++++ .../core/transactions/LedgerTransaction.kt | 29 +-- .../transactions/NotaryChangeTransactions.kt | 16 +- .../core/transactions/SignedTransaction.kt | 104 ++++++---- .../core/contracts/DummyContractV2Tests.kt | 58 ------ .../core/flows/ContractUpgradeFlowTest.kt | 65 ++++--- .../net/corda/core/node/VaultUpdateTests.kt | 9 +- docs/source/upgrading-cordapps.rst | 17 ++ .../kryo/DefaultKryoCustomizer.kt | 2 + .../internal/serialization/kryo/Kryo.kt | 26 +++ .../net/corda/node/internal/AbstractNode.kt | 30 ++- .../node/internal/ServiesForResolutionImpl.kt | 32 +++ .../node/internal/cordapp/CordappLoader.kt | 9 +- .../corda/node/services/CoreFlowHandlers.kt | 7 +- .../node/services/api/ServiceHubInternal.kt | 3 +- .../services/events/NodeSchedulerService.kt | 6 +- .../transactions/NonValidatingNotaryFlow.kt | 2 + .../transactions/ValidatingNotaryFlow.kt | 8 +- .../node/services/vault/NodeVaultService.kt | 105 +++++----- .../events/NodeSchedulerServiceTest.kt | 6 +- .../persistence/HibernateConfigurationTest.kt | 2 +- .../vault/VaultSoftLockManagerTest.kt | 6 +- .../corda/notarydemo/MyCustomNotaryService.kt | 9 +- .../net/corda/testing/node/MockServices.kt | 21 +- .../testing/contracts/DummyContractV2.kt | 29 +-- 36 files changed, 622 insertions(+), 395 deletions(-) delete mode 100644 core/src/main/kotlin/net/corda/core/internal/UpgradeCommand.kt create mode 100644 core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt create mode 100644 node/src/main/kotlin/net/corda/node/internal/ServiesForResolutionImpl.kt diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 6954e313f5..c9561b6ab2 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -627,6 +627,9 @@ public static final class net.corda.core.contracts.UniqueIdentifier$Companion ex @org.jetbrains.annotations.NotNull public abstract String getLegacyContract() @org.jetbrains.annotations.NotNull public abstract net.corda.core.contracts.ContractState upgrade(net.corda.core.contracts.ContractState) ## +@net.corda.core.serialization.CordaSerializable public interface net.corda.core.contracts.UpgradedContractWithLegacyConstraint extends net.corda.core.contracts.UpgradedContract + @org.jetbrains.annotations.NotNull public abstract net.corda.core.contracts.AttachmentConstraint getLegacyContractConstraint() +## @net.corda.core.serialization.CordaSerializable @net.corda.core.DoNotImplement public final class net.corda.core.contracts.WhitelistedByZoneAttachmentConstraint extends java.lang.Object implements net.corda.core.contracts.AttachmentConstraint public boolean isSatisfiedBy(net.corda.core.contracts.Attachment) public static final net.corda.core.contracts.WhitelistedByZoneAttachmentConstraint INSTANCE @@ -1894,16 +1897,12 @@ public @interface net.corda.core.messaging.RPCReturnsObservables @org.jetbrains.annotations.NotNull public abstract net.corda.core.transactions.SignedTransaction signInitialTransaction(net.corda.core.transactions.TransactionBuilder, java.security.PublicKey) @org.jetbrains.annotations.NotNull public abstract net.corda.core.contracts.StateAndRef toStateAndRef(net.corda.core.contracts.StateRef) ## -@net.corda.core.DoNotImplement public interface net.corda.core.node.ServicesForResolution extends net.corda.core.node.StateLoader +@net.corda.core.DoNotImplement public interface net.corda.core.node.ServicesForResolution @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.AttachmentStorage getAttachments() @org.jetbrains.annotations.NotNull public abstract net.corda.core.cordapp.CordappProvider getCordappProvider() @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.IdentityService getIdentityService() @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.NetworkParameters getNetworkParameters() ## -@net.corda.core.DoNotImplement public interface net.corda.core.node.StateLoader - @org.jetbrains.annotations.NotNull public abstract net.corda.core.contracts.TransactionState loadState(net.corda.core.contracts.StateRef) - @org.jetbrains.annotations.NotNull public abstract Set loadStates(Set) -## public final class net.corda.core.node.StatesToRecord extends java.lang.Enum protected (String, int) public static net.corda.core.node.StatesToRecord valueOf(String) @@ -2052,10 +2051,9 @@ public final class net.corda.core.node.services.TimeWindowChecker extends java.l @org.jetbrains.annotations.NotNull public final java.time.Clock getClock() public final boolean isValid(net.corda.core.contracts.TimeWindow) ## -@net.corda.core.DoNotImplement public interface net.corda.core.node.services.TransactionStorage extends net.corda.core.node.StateLoader +@net.corda.core.DoNotImplement public interface net.corda.core.node.services.TransactionStorage @org.jetbrains.annotations.Nullable public abstract net.corda.core.transactions.SignedTransaction getTransaction(net.corda.core.crypto.SecureHash) @org.jetbrains.annotations.NotNull public abstract rx.Observable getUpdates() - @org.jetbrains.annotations.NotNull public abstract net.corda.core.contracts.TransactionState loadState(net.corda.core.contracts.StateRef) @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed track() ## @net.corda.core.DoNotImplement public interface net.corda.core.node.services.TransactionVerifierService @@ -3137,7 +3135,6 @@ public static final class net.corda.core.transactions.LedgerTransaction$InOutGro @org.jetbrains.annotations.NotNull public List getOutputs() public int hashCode() @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.NotaryChangeLedgerTransaction resolve(net.corda.core.node.ServiceHub, List) - @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.NotaryChangeLedgerTransaction resolve(net.corda.core.node.StateLoader, List) public String toString() ## @net.corda.core.serialization.CordaSerializable @net.corda.core.DoNotImplement public final class net.corda.core.transactions.SignedTransaction extends java.lang.Object implements net.corda.core.transactions.TransactionWithSignatures @@ -3160,13 +3157,11 @@ public static final class net.corda.core.transactions.LedgerTransaction$InOutGro @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.WireTransaction getTx() @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializedBytes getTxBits() public int hashCode() - public final boolean isNotaryChangeTransaction() + @kotlin.Deprecated public final boolean isNotaryChangeTransaction() @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction plus(Collection) @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction plus(net.corda.core.crypto.TransactionSignature) - @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.BaseTransaction resolveBaseTransaction(net.corda.core.node.StateLoader) @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.NotaryChangeLedgerTransaction resolveNotaryChangeTransaction(net.corda.core.node.ServiceHub) - @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.NotaryChangeLedgerTransaction resolveNotaryChangeTransaction(net.corda.core.node.StateLoader) - @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionWithSignatures resolveTransactionWithSignatures(net.corda.core.node.ServiceHub) + @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionWithSignatures resolveTransactionWithSignatures(net.corda.core.node.ServicesForResolution) @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.LedgerTransaction toLedgerTransaction(net.corda.core.node.ServiceHub) @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.LedgerTransaction toLedgerTransaction(net.corda.core.node.ServiceHub, boolean) @org.jetbrains.annotations.NotNull public String toString() @@ -4075,7 +4070,7 @@ public final class net.corda.testing.node.MockNodeParameters extends java.lang.O @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNodeParameters withForcedID(Integer) @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNodeParameters withLegalName(net.corda.core.identity.CordaX500Name) ## -public class net.corda.testing.node.MockServices extends java.lang.Object implements net.corda.core.node.StateLoader, net.corda.core.node.ServiceHub +public class net.corda.testing.node.MockServices extends java.lang.Object implements net.corda.core.node.ServiceHub public () public (List) public (List, net.corda.core.identity.CordaX500Name) @@ -4174,8 +4169,6 @@ public class net.corda.testing.node.MockTransactionStorage extends net.corda.cor public boolean addTransaction(net.corda.core.transactions.SignedTransaction) @org.jetbrains.annotations.Nullable public net.corda.core.transactions.SignedTransaction getTransaction(net.corda.core.crypto.SecureHash) @org.jetbrains.annotations.NotNull public rx.Observable getUpdates() - @org.jetbrains.annotations.NotNull public net.corda.core.contracts.TransactionState loadState(net.corda.core.contracts.StateRef) - @org.jetbrains.annotations.NotNull public Set loadStates(Set) @org.jetbrains.annotations.NotNull public net.corda.core.messaging.DataFeed track() ## public final class net.corda.testing.node.NodeTestUtils extends java.lang.Object @@ -4351,9 +4344,10 @@ public static final class net.corda.testing.contracts.DummyContract$SingleOwnerS @net.corda.core.DoNotImplement public static interface net.corda.testing.contracts.DummyContract$State extends net.corda.core.contracts.ContractState public abstract int getMagicNumber() ## -public final class net.corda.testing.contracts.DummyContractV2 extends java.lang.Object implements net.corda.core.contracts.UpgradedContract +public final class net.corda.testing.contracts.DummyContractV2 extends java.lang.Object implements net.corda.core.contracts.UpgradedContractWithLegacyConstraint public () @org.jetbrains.annotations.NotNull public String getLegacyContract() + @org.jetbrains.annotations.NotNull public net.corda.core.contracts.AttachmentConstraint getLegacyContractConstraint() @org.jetbrains.annotations.NotNull public net.corda.testing.contracts.DummyContractV2$State upgrade(net.corda.testing.contracts.DummyContract$State) public void verify(net.corda.core.transactions.LedgerTransaction) public static final net.corda.testing.contracts.DummyContractV2$Companion Companion diff --git a/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt b/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt index 65465e04bb..51f39f967e 100644 --- a/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt +++ b/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt @@ -24,10 +24,7 @@ import net.corda.core.node.services.IdentityService import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize -import net.corda.core.transactions.CoreTransaction -import net.corda.core.transactions.NotaryChangeWireTransaction -import net.corda.core.transactions.SignedTransaction -import net.corda.core.transactions.WireTransaction +import net.corda.core.transactions.* import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.base58ToByteArray import net.corda.core.utilities.base64ToByteArray diff --git a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt index 42b8eff91f..6ee1e7152f 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt @@ -48,6 +48,7 @@ data class Issued(val issuer: PartyAndReference, val product: P) { init { require(issuer.reference.size <= MAX_ISSUER_REF_SIZE) { "Maximum issuer reference size is $MAX_ISSUER_REF_SIZE." } } + override fun toString() = "$product issued by $issuer" } @@ -248,12 +249,21 @@ annotation class LegalProseReference(val uri: String) /** * Interface which can upgrade state objects issued by a contract to a new state object issued by a different contract. + * The upgraded contract should specify the legacy contract class name, and provide an upgrade function that will convert + * legacy contract states into states defined by this contract. + * + * In addition to the legacy contract class name, you can also specify the legacy contract constraint by implementing + * [UpgradedContractWithLegacyConstraint] instead. Otherwise, the default [WhitelistedByZoneAttachmentConstraint] will + * be used for verifying the validity of an upgrade transaction. * * @param OldState the old contract state (can be [ContractState] or other common supertype if this supports upgrading * more than one state). * @param NewState the upgraded contract state. */ interface UpgradedContract : Contract { + /** + * Name of the contract this is an upgraded version of, used as part of verification of upgrade transactions. + */ val legacyContract: ContractClassName /** * Upgrade contract's state object to a new state object. @@ -264,6 +274,17 @@ interface UpgradedContract : UpgradedContract { + /** + * A validator for the legacy (pre-upgrade) contract attachments on the transaction. + */ + val legacyContractConstraint: AttachmentConstraint +} + /** * A privacy salt is required to compute nonces per transaction component in order to ensure that an adversary cannot * use brute force techniques and reveal the content of a Merkle-leaf hashed value. diff --git a/core/src/main/kotlin/net/corda/core/flows/ContractUpgradeFlow.kt b/core/src/main/kotlin/net/corda/core/flows/ContractUpgradeFlow.kt index 423b6c1fa6..6c286be8b2 100644 --- a/core/src/main/kotlin/net/corda/core/flows/ContractUpgradeFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/ContractUpgradeFlow.kt @@ -2,7 +2,11 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.* +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.SignableData +import net.corda.core.crypto.SignatureMetadata import net.corda.core.internal.ContractUpgradeUtils +import net.corda.core.transactions.SignedTransaction /** * A flow to be used for authorising and upgrading state objects of an old contract to a new contract. @@ -38,7 +42,6 @@ object ContractUpgradeFlow { serviceHub.contractUpgradeService.storeAuthorisedContractUpgrade(stateAndRef.ref, upgradedContractClass) return null } - } /** @@ -68,11 +71,13 @@ object ContractUpgradeFlow { @Suspendable override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx { - val baseTx = ContractUpgradeUtils.assembleBareTx(originalState, modification, PrivacySalt()) + val tx = ContractUpgradeUtils.assembleUpgradeTx(originalState, modification, PrivacySalt(), serviceHub) val participantKeys = originalState.state.data.participants.map { it.owningKey }.toSet() // TODO: We need a much faster way of finding our key in the transaction val myKey = serviceHub.keyManagementService.filterMyKeys(participantKeys).single() - val stx = serviceHub.signInitialTransaction(baseTx, myKey) + val signableData = SignableData(tx.id, SignatureMetadata(serviceHub.myInfo.platformVersion, Crypto.findSignatureScheme(myKey).schemeNumberID)) + val mySignature = serviceHub.keyManagementService.sign(signableData, myKey) + val stx = SignedTransaction(tx, listOf(mySignature)) return AbstractStateReplacementFlow.UpgradeTx(stx) } } diff --git a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt index 78dd436eb6..ccb823897b 100644 --- a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt @@ -15,7 +15,9 @@ import net.corda.core.node.services.TrustedAuthorityNotaryService import net.corda.core.node.services.UniquenessProvider import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.CoreTransaction +import net.corda.core.transactions.ContractUpgradeWireTransaction import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.UntrustworthyData import net.corda.core.utilities.unwrap @@ -104,10 +106,11 @@ class NotaryFlow { @Suspendable private fun sendAndReceiveNonValidating(notaryParty: Party, session: FlowSession, signature: NotarisationRequestSignature): UntrustworthyData> { - val tx: CoreTransaction = if (stx.isNotaryChangeTransaction()) { - stx.notaryChangeTx // Notary change transactions do not support filtering - } else { - stx.buildFilteredTransaction(Predicate { it is StateRef || it is TimeWindow || it == notaryParty }) + val ctx = stx.coreTransaction + val tx = when (ctx) { + is ContractUpgradeWireTransaction -> ctx.buildFilteredTransaction() + is WireTransaction -> ctx.buildFilteredTransaction(Predicate { it is StateRef || it is TimeWindow || it == notaryParty }) + else -> ctx } return session.sendAndReceiveWithRetry(NotarisationPayload(tx, signature)) } @@ -168,12 +171,10 @@ class NotaryFlow { @Suspendable abstract fun receiveAndVerifyTx(): TransactionParts - // Check if transaction is intended to be signed by this notary. + /** Check if transaction is intended to be signed by this notary. */ @Suspendable protected fun checkNotary(notary: Party?) { - // TODO This check implies that it's OK to use the node's main identity. Shouldn't it be just limited to the - // notary identities? - if (notary == null || !serviceHub.myInfo.isLegalIdentity(notary)) { + if (notary?.owningKey != service.notaryIdentityKey) { throw NotaryException(NotaryError.WrongNotary) } } diff --git a/core/src/main/kotlin/net/corda/core/internal/ContractUpgradeUtils.kt b/core/src/main/kotlin/net/corda/core/internal/ContractUpgradeUtils.kt index e2e36294e7..b085f51391 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ContractUpgradeUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ContractUpgradeUtils.kt @@ -1,21 +1,38 @@ package net.corda.core.internal import net.corda.core.contracts.* -import net.corda.core.transactions.TransactionBuilder +import net.corda.core.node.ServicesForResolution +import net.corda.core.node.services.AttachmentId +import net.corda.core.transactions.ContractUpgradeWireTransaction object ContractUpgradeUtils { - fun assembleBareTx( - stateRef: StateAndRef, + fun assembleUpgradeTx( + stateAndRef: StateAndRef, upgradedContractClass: Class>, - privacySalt: PrivacySalt - ): TransactionBuilder { - val contractUpgrade = upgradedContractClass.newInstance() - return TransactionBuilder(stateRef.state.notary) - .withItems( - stateRef, - StateAndContract(contractUpgrade.upgrade(stateRef.state.data), upgradedContractClass.name), - Command(UpgradeCommand(upgradedContractClass.name), stateRef.state.data.participants.map { it.owningKey }), - privacySalt - ) + privacySalt: PrivacySalt, + services: ServicesForResolution + ): ContractUpgradeWireTransaction { + require(stateAndRef.state.encumbrance == null) { "Upgrading an encumbered state is not yet supported" } + val legacyConstraint = stateAndRef.state.constraint + val legacyContractAttachmentId = when (legacyConstraint) { + is HashAttachmentConstraint -> legacyConstraint.attachmentId + else -> getContractAttachmentId(stateAndRef.state.contract, services) + } + val upgradedContractAttachmentId = getContractAttachmentId(upgradedContractClass.name, services) + + val inputs = listOf(stateAndRef.ref) + return ContractUpgradeWireTransaction( + inputs, + stateAndRef.state.notary, + legacyContractAttachmentId, + upgradedContractClass.name, + upgradedContractAttachmentId, + privacySalt + ) + } + + private fun getContractAttachmentId(name: ContractClassName, services: ServicesForResolution): AttachmentId { + return services.cordappProvider.getContractAttachmentID(name) + ?: throw IllegalStateException("Attachment not found for contract: $name") } } diff --git a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt index 3b531d3cf6..5ceb17d094 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt @@ -7,7 +7,9 @@ import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowSession import net.corda.core.node.StatesToRecord import net.corda.core.serialization.CordaSerializable +import net.corda.core.transactions.ContractUpgradeWireTransaction import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.exactAdd import java.util.* @@ -157,13 +159,17 @@ class ResolveTransactionsFlow(private val txHashes: Set, * Returns a list of all the dependencies of the given transactions, deepest first i.e. the last downloaded comes * first in the returned list and thus doesn't have any unverified dependencies. */ + // TODO: This could be done in parallel with other fetches for extra speed. @Suspendable private fun fetchMissingAttachments(downloads: List) { - // TODO: This could be done in parallel with other fetches for extra speed. - val wireTransactions = downloads.filterNot { it.isNotaryChangeTransaction() }.map { it.tx } - val missingAttachments = wireTransactions.flatMap { wtx -> - wtx.attachments.filter { serviceHub.attachments.openAttachment(it) == null } + val attachments = downloads.map(SignedTransaction::coreTransaction).flatMap { tx -> + when (tx) { + is WireTransaction -> tx.attachments + is ContractUpgradeWireTransaction -> listOf(tx.legacyContractAttachmentId, tx.upgradedContractAttachmentId) + else -> emptyList() + } } + val missingAttachments = attachments.filter { serviceHub.attachments.openAttachment(it) == null } if (missingAttachments.isNotEmpty()) subFlow(FetchAttachmentsFlow(missingAttachments.toSet(), otherSide)) } diff --git a/core/src/main/kotlin/net/corda/core/internal/UpgradeCommand.kt b/core/src/main/kotlin/net/corda/core/internal/UpgradeCommand.kt deleted file mode 100644 index 9c4e3abb7f..0000000000 --- a/core/src/main/kotlin/net/corda/core/internal/UpgradeCommand.kt +++ /dev/null @@ -1,7 +0,0 @@ -package net.corda.core.internal - -import net.corda.core.contracts.CommandData -import net.corda.core.contracts.ContractClassName - -/** Indicates that this transaction replaces the inputs contract state to another contract state */ -data class UpgradeCommand(val upgradedContractClass: ContractClassName) : CommandData \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index 80e1065c99..30badc8807 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -18,39 +18,12 @@ import java.security.PublicKey import java.sql.Connection import java.time.Clock -/** - * Part of [ServiceHub]. - */ -@DoNotImplement -interface StateLoader { - /** - * Given a [StateRef] loads the referenced transaction and looks up the specified output [ContractState]. - * - * *WARNING* Do not use this method unless you really only want a single state - any batch loading should - * go through [loadStates] as repeatedly calling [loadState] can lead to repeat deserialsiation work and - * severe performance degradation. - * - * @throws TransactionResolutionException if [stateRef] points to a non-existent transaction. - */ - @Throws(TransactionResolutionException::class) - fun loadState(stateRef: StateRef): TransactionState<*> - - /** - * Given a [Set] of [StateRef]'s loads the referenced transaction and looks up the specified output [ContractState]. - * - * @throws TransactionResolutionException if [stateRef] points to a non-existent transaction. - */ - // TODO: future implementation to use a Vault state ref -> contract state BLOB table and perform single query bulk load - // as the existing transaction store will become encrypted at some point - @Throws(TransactionResolutionException::class) - fun loadStates(stateRefs: Set): Set> -} - /** * Subset of node services that are used for loading transactions from the wire into fully resolved, looked up * forms ready for verification. */ -interface ServicesForResolution : StateLoader { +@DoNotImplement +interface ServicesForResolution { /** * An identity service maintains a directory of parties by their associated distinguished name/public keys and thus * supports lookup of a party given its key, or name. The service also manages the certificates linking confidential @@ -66,6 +39,27 @@ interface ServicesForResolution : StateLoader { /** Returns the network parameters the node is operating under. */ val networkParameters: NetworkParameters + + /** + * Given a [StateRef] loads the referenced transaction and looks up the specified output [ContractState]. + * + * *WARNING* Do not use this method unless you really only want a single state - any batch loading should + * go through [loadStates] as repeatedly calling [loadState] can lead to repeat deserialsiation work and + * severe performance degradation. + * + * @throws TransactionResolutionException if [stateRef] points to a non-existent transaction. + */ + @Throws(TransactionResolutionException::class) + fun loadState(stateRef: StateRef): TransactionState<*> + /** + * Given a [Set] of [StateRef]'s loads the referenced transaction and looks up the specified output [ContractState]. + * + * @throws TransactionResolutionException if [stateRef] points to a non-existent transaction. + */ + // TODO: future implementation to use a Vault state ref -> contract state BLOB table and perform single query bulk load + // as the existing transaction store will become encrypted at some point + @Throws(TransactionResolutionException::class) + fun loadStates(stateRefs: Set): Set> } /** diff --git a/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt b/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt index 07240e9f30..9b6b713ed2 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt @@ -1,10 +1,8 @@ package net.corda.core.node.services import net.corda.core.DoNotImplement -import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.messaging.DataFeed -import net.corda.core.node.StateLoader import net.corda.core.transactions.SignedTransaction import rx.Observable @@ -12,27 +10,12 @@ import rx.Observable * Thread-safe storage of transactions. */ @DoNotImplement -interface TransactionStorage : StateLoader { +interface TransactionStorage { /** * Return the transaction with the given [id], or null if no such transaction exists. */ fun getTransaction(id: SecureHash): SignedTransaction? - @Throws(TransactionResolutionException::class) - override fun loadState(stateRef: StateRef): TransactionState<*> { - val stx = getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash) - return stx.resolveBaseTransaction(this).outputs[stateRef.index] - } - - @Throws(TransactionResolutionException::class) - override fun loadStates(stateRefs: Set): Set> { - return stateRefs.groupBy { it.txhash }.flatMap { - val stx = getTransaction(it.key) ?: throw TransactionResolutionException(it.key) - val baseTx = stx.resolveBaseTransaction(this) - it.value.map { StateAndRef(baseTx.outputs[it.index], it) } - }.toSet() - } - /** * Get a synchronous Observable of updates. When observations are pushed to the Observer, the vault will already * incorporate the update. diff --git a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt index 2440d80f77..17586bfc69 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt @@ -46,8 +46,8 @@ class Vault(val states: Iterable>) { val produced: Set>, val flowId: UUID? = null, /** - * Specifies the type of update, currently supported types are general and notary change. Notary - * change transactions only modify the notary field on states, and potentially need to be handled + * Specifies the type of update, currently supported types are general and, contract upgrade and notary change. + * Notary change transactions only modify the notary field on states, and potentially need to be handled * differently. */ val type: UpdateType = UpdateType.GENERAL @@ -97,11 +97,6 @@ class Vault(val states: Iterable>) { } } - companion object { - val NoUpdate = Update(emptySet(), emptySet(), type = Vault.UpdateType.GENERAL) - val NoNotaryUpdate = Vault.Update(emptySet(), emptySet(), type = Vault.UpdateType.NOTARY_CHANGE) - } - @CordaSerializable enum class StateStatus { UNCONSUMED, CONSUMED, ALL @@ -109,7 +104,7 @@ class Vault(val states: Iterable>) { @CordaSerializable enum class UpdateType { - GENERAL, NOTARY_CHANGE + GENERAL, NOTARY_CHANGE, CONTRACT_UPGRADE } /** @@ -141,6 +136,13 @@ class Vault(val states: Iterable>) { val notary: AbstractParty?, val lockId: String?, val lockUpdateTime: Instant?) + + companion object { + @Deprecated("No longer used. The vault does not emit empty updates") + val NoUpdate = Update(emptySet(), emptySet(), type = Vault.UpdateType.GENERAL) + @Deprecated("No longer used. The vault does not emit empty updates") + val NoNotaryUpdate = Vault.Update(emptySet(), emptySet(), type = Vault.UpdateType.NOTARY_CHANGE) + } } /** diff --git a/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt new file mode 100644 index 0000000000..7d919316d4 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt @@ -0,0 +1,183 @@ +package net.corda.core.transactions + +import net.corda.core.contracts.* +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.TransactionSignature +import net.corda.core.crypto.serializedHash +import net.corda.core.identity.Party +import net.corda.core.internal.AttachmentWithContext +import net.corda.core.node.NetworkParameters +import net.corda.core.node.ServicesForResolution +import net.corda.core.serialization.CordaSerializable +import net.corda.core.utilities.toBase58String +import java.security.PublicKey + +// TODO: copy across encumbrances when performing contract upgrades +// TODO: check transaction size is within limits + +/** A special transaction for upgrading the contract of a state. */ +@CordaSerializable +data class ContractUpgradeWireTransaction( + override val inputs: List, + override val notary: Party, + val legacyContractAttachmentId: SecureHash, + val upgradeContractClassName: ContractClassName, + val upgradedContractAttachmentId: SecureHash, + val privacySalt: PrivacySalt = PrivacySalt() +) : CoreTransaction() { + + init { + check(inputs.isNotEmpty()) { "A contract upgrade transaction must have inputs" } + } + + /** + * This transaction does not contain any output states, outputs can be obtained by resolving a + * [ContractUpgradeLedgerTransaction] – outputs will be calculated on demand by applying the contract + * upgrade operation to inputs. + */ + override val outputs: List> + get() = throw UnsupportedOperationException("ContractUpgradeWireTransaction does not contain output states, " + + "outputs can only be obtained from a resolved ContractUpgradeLedgerTransaction") + + /** Hash of the list of components that are hidden in the [ContractUpgradeFilteredTransaction]. */ + private val hiddenComponentHash: SecureHash + get() = serializedHash(listOf(legacyContractAttachmentId, upgradeContractClassName, privacySalt)) + + override val id: SecureHash by lazy { serializedHash(inputs + notary).hashConcat(hiddenComponentHash) } + + /** Resolves input states and contract attachments, and builds a ContractUpgradeLedgerTransaction. */ + fun resolve(services: ServicesForResolution, sigs: List): ContractUpgradeLedgerTransaction { + val resolvedInputs = services.loadStates(inputs.toSet()).toList() + val legacyContractClassName = resolvedInputs.first().state.contract + val legacyContractAttachment = services.attachments.openAttachment(legacyContractAttachmentId) + ?: throw AttachmentResolutionException(legacyContractAttachmentId) + val upgradedContractAttachment = services.attachments.openAttachment(upgradedContractAttachmentId) + ?: throw AttachmentResolutionException(upgradedContractAttachmentId) + return ContractUpgradeLedgerTransaction( + resolvedInputs, + notary, + ContractAttachment(legacyContractAttachment, legacyContractClassName), + ContractAttachment(upgradedContractAttachment, upgradeContractClassName), + id, + privacySalt, + sigs, + services.networkParameters + ) + } + + fun buildFilteredTransaction(): ContractUpgradeFilteredTransaction { + return ContractUpgradeFilteredTransaction(inputs, notary, hiddenComponentHash) + } +} + +/** + * A filtered version of the [ContractUpgradeWireTransaction]. In comparison with a regular [FilteredTransaction], there + * is no flexibility on what parts of the transaction to reveal – the inputs and notary field are always visible and the + * rest of the transaction is always hidden. Its only purpose is to hide transaction data when using a non-validating notary. + * + * @property inputs The inputs of this transaction. + * @property notary The notary for this transaction. + * @property rest Hash of the hidden components of the [ContractUpgradeWireTransaction]. + */ +@CordaSerializable +data class ContractUpgradeFilteredTransaction( + override val inputs: List, + override val notary: Party, + val rest: SecureHash +) : CoreTransaction() { + override val id: SecureHash get() = serializedHash(inputs + notary).hashConcat(rest) + override val outputs: List> get() = emptyList() +} + +/** + * A contract upgrade transaction with fully resolved inputs and signatures. Contract upgrade transactions are separate + * to regular transactions because their validation logic is specialised; the original contract by definition cannot be + * aware of the upgraded contract (it was written after the original contract was developed), so its validation logic + * cannot succeed. Instead alternative verification logic is used which verifies that the outputs correspond to the + * inputs after upgrading. + * + * In contrast with a regular transaction, signatures are checked against the signers specified by input states' + * *participants* fields, so full resolution is needed for signature verification. + */ +data class ContractUpgradeLedgerTransaction( + override val inputs: List>, + override val notary: Party, + val legacyContractAttachment: ContractAttachment, + val upgradedContractAttachment: ContractAttachment, + override val id: SecureHash, + val privacySalt: PrivacySalt, + override val sigs: List, + private val networkParameters: NetworkParameters +) : FullTransaction(), TransactionWithSignatures { + private val upgradedContract: UpgradedContract = loadUpgradedContract() + + init { + // TODO: relax this constraint once upgrading encumbered states is supported + check(inputs.all { it.state.contract == legacyContractAttachment.contract }) { + "All input states must point to the legacy contract" + } + check(inputs.all { it.state.constraint.isSatisfiedBy(legacyContractAttachment) }) { + "Legacy contract constraint does not satisfy the constraint of the input states" + } + verifyLegacyContractConstraint() + } + + private fun verifyLegacyContractConstraint() { + check(upgradedContract.legacyContract == legacyContractAttachment.contract) { + "Outputs' contract must be an upgraded version of the inputs' contract" + } + val attachmentWithContext = AttachmentWithContext( + legacyContractAttachment, + upgradedContract.legacyContract, + networkParameters.whitelistedContractImplementations + ) + val constraintCheck = if (upgradedContract is UpgradedContractWithLegacyConstraint) { + upgradedContract.legacyContractConstraint.isSatisfiedBy(attachmentWithContext) + } else { + // If legacy constraint not specified, defaulting to WhitelistedByZoneAttachmentConstraint + WhitelistedByZoneAttachmentConstraint.isSatisfiedBy(attachmentWithContext) + } + check(constraintCheck) { + "Legacy contract does not satisfy the upgraded contract's constraint" + } + } + + /** + * Outputs are computed by running the contract upgrade logic on input states. This is done eagerly so that the + * transaction is verified during construction. + */ + override val outputs: List> = inputs.map { input -> + // TODO: if there are encumbrance states in the inputs, just copy them across without modifying + val upgradedState = upgradedContract.upgrade(input.state.data) + val inputConstraint = input.state.constraint + val outputConstraint = when (inputConstraint) { + is HashAttachmentConstraint -> HashAttachmentConstraint(upgradedContractAttachment.id) + WhitelistedByZoneAttachmentConstraint -> WhitelistedByZoneAttachmentConstraint + else -> throw IllegalArgumentException("Unsupported input contract constraint $inputConstraint") + } + // TODO: re-map encumbrance pointers + input.state.copy( + data = upgradedState, + contract = upgradedContractAttachment.contract, + constraint = outputConstraint + ) + } + + /** The required signers are the set of all input states' participants. */ + override val requiredSigningKeys: Set + get() = inputs.flatMap { it.state.data.participants }.map { it.owningKey }.toSet() + notary.owningKey + + override fun getKeyDescriptions(keys: Set): List { + return keys.map { it.toBase58String() } + } + + // TODO: load contract from the CorDapp classloader + private fun loadUpgradedContract(): UpgradedContract { + @Suppress("UNCHECKED_CAST") + return this::class.java.classLoader + .loadClass(upgradedContractAttachment.contract) + .asSubclass(Contract::class.java) + .getConstructor() + .newInstance() as UpgradedContract + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index dfda034eec..85fc1d0b81 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -4,13 +4,11 @@ import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.identity.Party import net.corda.core.internal.AttachmentWithContext -import net.corda.core.internal.UpgradeCommand import net.corda.core.internal.castIfPossible import net.corda.core.internal.uncheckedCast import net.corda.core.node.NetworkParameters import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.Try -import java.security.PublicKey import java.util.* import java.util.function.Predicate @@ -79,12 +77,7 @@ data class LedgerTransaction @JvmOverloads constructor( @Throws(TransactionVerificationException::class) fun verify() { verifyConstraints() - // TODO: make contract upgrade transactions have a separate type - if (commands.any { it.value is UpgradeCommand }) { - verifyContractUpgrade() - } else { - verifyContracts() - } + verifyContracts() } /** @@ -185,25 +178,6 @@ data class LedgerTransaction @JvmOverloads constructor( } } - private fun verifyContractUpgrade() { - // Contract Upgrade transaction should have 1 input, 1 output and 1 command. - val input = inputs.single().state - val output = outputs.single() - val commandData = commandsOfType().single() - - val command = commandData.value - val participantKeys: Set = input.data.participants.map { it.owningKey }.toSet() - val keysThatSigned: Set = commandData.signers.toSet() - @Suppress("UNCHECKED_CAST") - val upgradedContract = javaClass.classLoader.loadClass(command.upgradedContractClass).newInstance() as UpgradedContract - requireThat { - "The signing keys include all participant keys" using keysThatSigned.containsAll(participantKeys) - "Inputs state reference the legacy contract" using (input.contract == upgradedContract.legacyContract) - "Outputs state reference the upgraded contract" using (output.contract == command.upgradedContractClass) - "Output state must be an upgraded version of the input state" using (output.data == upgradedContract.upgrade(input.data)) - } - } - /** * Given a type and a function that returns a grouping key, associates inputs and outputs together so that they * can be processed as one. The grouping key is any arbitrary object that can act as a map key (so must implement @@ -429,3 +403,4 @@ data class LedgerTransaction @JvmOverloads constructor( privacySalt: PrivacySalt ) = copy(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, null) } + diff --git a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt index f0a6c7cc18..e230adbff8 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt @@ -4,11 +4,11 @@ import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.serializedHash -import net.corda.core.utilities.toBase58String import net.corda.core.identity.Party import net.corda.core.node.ServiceHub -import net.corda.core.node.StateLoader +import net.corda.core.node.ServicesForResolution import net.corda.core.serialization.CordaSerializable +import net.corda.core.utilities.toBase58String import java.security.PublicKey /** @@ -27,7 +27,8 @@ data class NotaryChangeWireTransaction( * [NotaryChangeLedgerTransaction] and applying the notary modification to inputs. */ override val outputs: List> - get() = emptyList() + get() = throw UnsupportedOperationException("NotaryChangeWireTransaction does not contain output states, " + + "outputs can only be obtained from a resolved NotaryChangeLedgerTransaction") init { check(inputs.isNotEmpty()) { "A notary change transaction must have inputs" } @@ -40,11 +41,14 @@ data class NotaryChangeWireTransaction( */ override val id: SecureHash by lazy { serializedHash(inputs + notary + newNotary) } - fun resolve(services: ServiceHub, sigs: List) = resolve(services as StateLoader, sigs) - fun resolve(stateLoader: StateLoader, sigs: List): NotaryChangeLedgerTransaction { - val resolvedInputs = stateLoader.loadStates(inputs.toSet()).toList() + /** Resolves input states and builds a [NotaryChangeLedgerTransaction]. */ + fun resolve(services: ServicesForResolution, sigs: List) : NotaryChangeLedgerTransaction { + val resolvedInputs = services.loadStates(inputs.toSet()).toList() return NotaryChangeLedgerTransaction(resolvedInputs, notary, newNotary, id, sigs) } + + /** Resolves input states and builds a [NotaryChangeLedgerTransaction]. */ + fun resolve(services: ServiceHub, sigs: List) = resolve(services as ServicesForResolution, sigs) } /** diff --git a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt index f6068c71f9..94a993e328 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt @@ -7,7 +7,7 @@ import net.corda.core.crypto.* import net.corda.core.identity.Party import net.corda.core.internal.VisibleForTesting import net.corda.core.node.ServiceHub -import net.corda.core.node.StateLoader +import net.corda.core.node.ServicesForResolution import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize @@ -48,19 +48,18 @@ data class SignedTransaction(val txBits: SerializedBytes, /** Cache the deserialized form of the transaction. This is useful when building a transaction or collecting signatures. */ @Volatile - @Transient private var cachedTransaction: CoreTransaction? = null - - /** Lazily calculated access to the deserialized/hashed transaction data. */ - private val transaction: CoreTransaction get() = cachedTransaction ?: txBits.deserialize().apply { cachedTransaction = this } + @Transient + private var cachedTransaction: CoreTransaction? = null /** The id of the contained [WireTransaction]. */ - override val id: SecureHash get() = transaction.id + override val id: SecureHash get() = coreTransaction.id - /** Returns the contained [WireTransaction], or throws if this is a notary change transaction. */ - val tx: WireTransaction get() = transaction as WireTransaction + /** Lazily calculated access to the deserialised/hashed transaction data. */ + val coreTransaction: CoreTransaction + get() = cachedTransaction ?: txBits.deserialize().apply { cachedTransaction = this } - /** Returns the contained [NotaryChangeWireTransaction], or throws if this is a normal transaction. */ - val notaryChangeTx: NotaryChangeWireTransaction get() = transaction as NotaryChangeWireTransaction + /** Returns the contained [WireTransaction], or throws if this is a notary change or contract upgrade transaction. */ + val tx: WireTransaction get() = coreTransaction as WireTransaction /** * Helper function to directly build a [FilteredTransaction] using provided filtering functions, @@ -69,9 +68,9 @@ data class SignedTransaction(val txBits: SerializedBytes, fun buildFilteredTransaction(filtering: Predicate) = tx.buildFilteredTransaction(filtering) /** Helper to access the inputs of the contained transaction. */ - val inputs: List get() = transaction.inputs + val inputs: List get() = coreTransaction.inputs /** Helper to access the notary of the contained transaction. */ - val notary: Party? get() = transaction.notary + val notary: Party? get() = coreTransaction.notary override val requiredSigningKeys: Set get() = tx.requiredSigningKeys @@ -162,13 +161,27 @@ data class SignedTransaction(val txBits: SerializedBytes, @JvmOverloads @Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class) fun verify(services: ServiceHub, checkSufficientSignatures: Boolean = true) { - if (isNotaryChangeTransaction()) { - verifyNotaryChangeTransaction(services, checkSufficientSignatures) - } else { - verifyRegularTransaction(services, checkSufficientSignatures) + when (coreTransaction) { + is NotaryChangeWireTransaction -> verifyNotaryChangeTransaction(services, checkSufficientSignatures) + is ContractUpgradeWireTransaction -> verifyContractUpgradeTransaction(services, checkSufficientSignatures) + else -> verifyRegularTransaction(services, checkSufficientSignatures) } } + /** No contract code is run when verifying notary change transactions, it is sufficient to check invariants during initialisation. */ + private fun verifyNotaryChangeTransaction(services: ServiceHub, checkSufficientSignatures: Boolean) { + val ntx = resolveNotaryChangeTransaction(services) + if (checkSufficientSignatures) ntx.verifyRequiredSignatures() + else checkSignaturesAreValid() + } + + /** No contract code is run when verifying contract upgrade transactions, it is sufficient to check invariants during initialisation. */ + private fun verifyContractUpgradeTransaction(services: ServicesForResolution, checkSufficientSignatures: Boolean) { + val ctx = resolveContractUpgradeTransaction(services) + if (checkSufficientSignatures) ctx.verifyRequiredSignatures() + else checkSignaturesAreValid() + } + // TODO: Verify contract constraints here as well as in LedgerTransaction to ensure that anything being deserialised // from the attachment is trusted. This will require some partial serialisation work to not load the ContractState // objects from the TransactionState. @@ -178,37 +191,32 @@ data class SignedTransaction(val txBits: SerializedBytes, services.transactionVerifierService.verify(ltx).getOrThrow() } - private fun verifyNotaryChangeTransaction(services: ServiceHub, checkSufficientSignatures: Boolean) { - val ntx = resolveNotaryChangeTransaction(services) - if (checkSufficientSignatures) ntx.verifyRequiredSignatures() - else checkSignaturesAreValid() - } - - fun isNotaryChangeTransaction() = transaction is NotaryChangeWireTransaction - /** * Resolves the underlying base transaction and then returns it, handling any special case transactions such as * [NotaryChangeWireTransaction]. */ - fun resolveBaseTransaction(services: StateLoader): BaseTransaction { - return when (transaction) { - is NotaryChangeWireTransaction -> resolveNotaryChangeTransaction(services) + fun resolveBaseTransaction(servicesForResolution: ServicesForResolution): BaseTransaction { + return when (coreTransaction) { + is NotaryChangeWireTransaction -> resolveNotaryChangeTransaction(servicesForResolution) + is ContractUpgradeWireTransaction -> resolveContractUpgradeTransaction(servicesForResolution) is WireTransaction -> this.tx is FilteredTransaction -> throw IllegalStateException("Persistence of filtered transactions is not supported.") - else -> throw IllegalStateException("Unknown transaction type ${transaction::class.qualifiedName}") + else -> throw IllegalStateException("Unknown transaction type ${coreTransaction::class.qualifiedName}") } } + /** * Resolves the underlying transaction with signatures and then returns it, handling any special case transactions * such as [NotaryChangeWireTransaction]. */ - fun resolveTransactionWithSignatures(services: ServiceHub): TransactionWithSignatures { - return when (transaction) { + fun resolveTransactionWithSignatures(services: ServicesForResolution): TransactionWithSignatures { + return when (coreTransaction) { is NotaryChangeWireTransaction -> resolveNotaryChangeTransaction(services) + is ContractUpgradeWireTransaction -> resolveContractUpgradeTransaction(services) is WireTransaction -> this is FilteredTransaction -> throw IllegalStateException("Persistence of filtered transactions is not supported.") - else -> throw IllegalStateException("Unknown transaction type ${transaction::class.qualifiedName}") + else -> throw IllegalStateException("Unknown transaction type ${coreTransaction::class.qualifiedName}") } } @@ -216,12 +224,26 @@ data class SignedTransaction(val txBits: SerializedBytes, * If [transaction] is a [NotaryChangeWireTransaction], loads the input states and resolves it to a * [NotaryChangeLedgerTransaction] so the signatures can be verified. */ - fun resolveNotaryChangeTransaction(services: ServiceHub) = resolveNotaryChangeTransaction(services as StateLoader) + fun resolveNotaryChangeTransaction(services: ServicesForResolution): NotaryChangeLedgerTransaction { + val ntx = coreTransaction as? NotaryChangeWireTransaction + ?: throw IllegalStateException("Expected a ${NotaryChangeWireTransaction::class.simpleName} but found ${coreTransaction::class.simpleName}") + return ntx.resolve(services, sigs) + } - fun resolveNotaryChangeTransaction(stateLoader: StateLoader): NotaryChangeLedgerTransaction { - val ntx = transaction as? NotaryChangeWireTransaction - ?: throw IllegalStateException("Expected a ${NotaryChangeWireTransaction::class.simpleName} but found ${transaction::class.simpleName}") - return ntx.resolve(stateLoader, sigs) + /** + * If [transaction] is a [NotaryChangeWireTransaction], loads the input states and resolves it to a + * [NotaryChangeLedgerTransaction] so the signatures can be verified. + */ + fun resolveNotaryChangeTransaction(services: ServiceHub) = resolveNotaryChangeTransaction(services as ServicesForResolution) + + /** + * If [coreTransaction] is a [ContractUpgradeWireTransaction], loads the input states and resolves it to a + * [ContractUpgradeLedgerTransaction] so the signatures can be verified. + */ + fun resolveContractUpgradeTransaction(services: ServicesForResolution): ContractUpgradeLedgerTransaction { + val ctx = coreTransaction as? ContractUpgradeWireTransaction + ?: throw IllegalStateException("Expected a ${ContractUpgradeWireTransaction::class.simpleName} but found ${coreTransaction::class.simpleName}") + return ctx.resolve(services, sigs) } override fun toString(): String = "${javaClass.simpleName}(id=$id)" @@ -234,4 +256,14 @@ data class SignedTransaction(val txBits: SerializedBytes, @CordaSerializable class SignaturesMissingException(val missing: Set, val descriptions: List, override val id: SecureHash) : NamedByHash, SignatureException(missingSignatureMsg(missing, descriptions, id)), CordaThrowable by CordaException(missingSignatureMsg(missing, descriptions, id)) + + //region Deprecated + /** Returns the contained [NotaryChangeWireTransaction], or throws if this is a normal transaction. */ + @Deprecated("No replacement, this should not be used outside of Corda core") + val notaryChangeTx: NotaryChangeWireTransaction + get() = coreTransaction as NotaryChangeWireTransaction + + @Deprecated("No replacement, this should not be used outside of Corda core") + fun isNotaryChangeTransaction() = this.coreTransaction is NotaryChangeWireTransaction + //endregion } diff --git a/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt b/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt index be99697d27..e69de29bb2 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt @@ -1,58 +0,0 @@ -package net.corda.core.contracts - -import com.nhaarman.mockito_kotlin.any -import com.nhaarman.mockito_kotlin.doReturn -import com.nhaarman.mockito_kotlin.whenever -import net.corda.core.cordapp.CordappProvider -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.SecureHash.Companion.allOnesHash -import net.corda.core.internal.UpgradeCommand -import net.corda.core.node.ServicesForResolution -import net.corda.testing.contracts.DummyContract -import net.corda.testing.contracts.DummyContractV2 -import net.corda.testing.core.ALICE_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.internal.rigorousMock -import org.junit.Rule -import org.junit.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -/** - * Tests for the version 2 dummy contract, to cover ensuring upgrade transactions are built correctly. - */ -class DummyContractV2Tests { - private companion object { - val ALICE = TestIdentity(ALICE_NAME, 70).party - val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party - } - - @Rule - @JvmField - val testSerialization = SerializationEnvironmentRule() - - @Test - fun `upgrade from v1`() { - val services = rigorousMock().also { - doReturn(rigorousMock().also { - doReturn(allOnesHash).whenever(it).getContractAttachmentID(any()) - }).whenever(it).cordappProvider - } - val contractUpgrade = DummyContractV2() - val v1State = TransactionState(DummyContract.SingleOwnerState(0, ALICE), DummyContract.PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint) - val v1Ref = StateRef(SecureHash.randomSHA256(), 0) - val v1StateAndRef = StateAndRef(v1State, v1Ref) - val (tx, _) = DummyContractV2().generateUpgradeFromV1(services, v1StateAndRef) - - assertEquals(v1Ref, tx.inputs.single()) - - val expectedOutput = TransactionState(contractUpgrade.upgrade(v1State.data), DummyContractV2.PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint) - val actualOutput = tx.outputs.single() - assertEquals(expectedOutput, actualOutput) - - val actualCommand = tx.commands.map { it.value }.single() - assertTrue((actualCommand as UpgradeCommand).upgradedContractClass == DummyContractV2::class.java.name) - } -} diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index efcc570ac4..3278c93e93 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -10,6 +10,7 @@ import net.corda.core.messaging.startFlow import net.corda.core.node.services.queryBy import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.finance.USD @@ -101,20 +102,12 @@ class ContractUpgradeFlowTest { val result = resultFuture.getOrThrow() fun check(node: StartedNode) { - val nodeStx = node.database.transaction { - node.services.validatedTransactions.getTransaction(result.ref.txhash) + val upgradeTx = node.database.transaction { + val wtx = node.services.validatedTransactions.getTransaction(result.ref.txhash) + wtx!!.resolveContractUpgradeTransaction(node.services) } - requireNotNull(nodeStx) - - // Verify inputs. - val input = node.database.transaction { - node.services.validatedTransactions.getTransaction(nodeStx!!.tx.inputs.single().txhash) - } - requireNotNull(input) - assertTrue(input!!.tx.outputs.single().data is DummyContract.State) - - // Verify outputs. - assertTrue(nodeStx!!.tx.outputs.single().data is DummyContractV2.State) + assertTrue(upgradeTx.inputs.single().state.data is DummyContract.State) + assertTrue(upgradeTx.outputs.single().data is DummyContractV2.State) } check(aliceNode) check(bobNode) @@ -192,16 +185,12 @@ class ContractUpgradeFlowTest { val result = resultFuture.getOrThrow() // Check results. listOf(aliceNode, bobNode).forEach { - val signedTX = aliceNode.database.transaction { aliceNode.services.validatedTransactions.getTransaction(result.ref.txhash) } - requireNotNull(signedTX) - - // Verify inputs. - val input = aliceNode.database.transaction { aliceNode.services.validatedTransactions.getTransaction(signedTX!!.tx.inputs.single().txhash) } - requireNotNull(input) - assertTrue(input!!.tx.outputs.single().data is DummyContract.State) - - // Verify outputs. - assertTrue(signedTX!!.tx.outputs.single().data is DummyContractV2.State) + val upgradeTx = aliceNode.database.transaction { + val wtx = aliceNode.services.validatedTransactions.getTransaction(result.ref.txhash) + wtx!!.resolveContractUpgradeTransaction(aliceNode.services) + } + assertTrue(upgradeTx.inputs.single().state.data is DummyContract.State) + assertTrue(upgradeTx.outputs.single().data is DummyContractV2.State) } } } @@ -222,14 +211,34 @@ class ContractUpgradeFlowTest { mockNet.runNetwork() upgradeResult.getOrThrow() // Get contract state from the vault. - val firstState = aliceNode.database.transaction { aliceNode.services.vaultService.queryBy().states.single() } - assertTrue(firstState.state.data is CashV2.State, "Contract state is upgraded to the new version.") - assertEquals(Amount(1000000, USD).`issued by`(chosenIdentity.ref(1)), (firstState.state.data as CashV2.State).amount, "Upgraded cash contain the correct amount.") - assertEquals>(listOf(anonymisedRecipient), (firstState.state.data as CashV2.State).owners, "Upgraded cash belongs to the right owner.") + val upgradedStateFromVault = aliceNode.database.transaction { aliceNode.services.vaultService.queryBy().states.single() } + assertEquals(Amount(1000000, USD).`issued by`(chosenIdentity.ref(1)), upgradedStateFromVault.state.data.amount, "Upgraded cash contain the correct amount.") + assertEquals>(listOf(anonymisedRecipient), upgradedStateFromVault.state.data.owners, "Upgraded cash belongs to the right owner.") + // Make sure the upgraded state can be spent + val movedState = upgradedStateFromVault.state.data.copy(amount = upgradedStateFromVault.state.data.amount.times(2)) + val spendUpgradedTx = aliceNode.services.signInitialTransaction( + TransactionBuilder(notary) + .addInputState(upgradedStateFromVault) + .addOutputState( + upgradedStateFromVault.state.copy(data = movedState) + ) + .addCommand(CashV2.Move(), alice.owningKey) + + ) + aliceNode.services.startFlow(FinalityFlow(spendUpgradedTx)).apply { + mockNet.runNetwork() + get() + } + val movedStateFromVault = aliceNode.database.transaction { aliceNode.services.vaultService.queryBy().states.single() } + assertEquals(movedState, movedStateFromVault.state.data) } - class CashV2 : UpgradedContract { + class CashV2 : UpgradedContractWithLegacyConstraint { override val legacyContract = Cash.PROGRAM_ID + override val legacyContractConstraint: AttachmentConstraint + get() = AlwaysAcceptAttachmentConstraint + + class Move : TypeOnlyCommandData() data class State(override val amount: Amount>, val owners: List) : FungibleAsset { override val owner: AbstractParty = owners.first() diff --git a/core/src/test/kotlin/net/corda/core/node/VaultUpdateTests.kt b/core/src/test/kotlin/net/corda/core/node/VaultUpdateTests.kt index 3805a703bd..7a97bc6e51 100644 --- a/core/src/test/kotlin/net/corda/core/node/VaultUpdateTests.kt +++ b/core/src/test/kotlin/net/corda/core/node/VaultUpdateTests.kt @@ -16,6 +16,7 @@ class VaultUpdateTests { private companion object { val DUMMY_PROGRAM_ID = "net.corda.core.node.VaultUpdateTests.DummyContract" val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party + val emptyUpdate = Vault.Update(emptySet(), emptySet(), type = Vault.UpdateType.GENERAL) } object DummyContract : Contract { @@ -42,21 +43,21 @@ class VaultUpdateTests { @Test fun `nothing plus nothing is nothing`() { - val before = Vault.NoUpdate - val after = before + Vault.NoUpdate + val before = emptyUpdate + val after = before + emptyUpdate assertEquals(before, after) } @Test fun `something plus nothing is something`() { val before = Vault.Update(setOf(stateAndRef0, stateAndRef1), setOf(stateAndRef2, stateAndRef3)) - val after = before + Vault.NoUpdate + val after = before + emptyUpdate assertEquals(before, after) } @Test fun `nothing plus something is something`() { - val before = Vault.NoUpdate + val before = emptyUpdate val after = before + Vault.Update(setOf(stateAndRef0, stateAndRef1), setOf(stateAndRef2, stateAndRef3)) val expected = Vault.Update(setOf(stateAndRef0, stateAndRef1), setOf(stateAndRef2, stateAndRef3)) assertEquals(expected, after) diff --git a/docs/source/upgrading-cordapps.rst b/docs/source/upgrading-cordapps.rst index 70c9a03dbe..8d0a0e3865 100644 --- a/docs/source/upgrading-cordapps.rst +++ b/docs/source/upgrading-cordapps.rst @@ -320,6 +320,23 @@ upgraded contracts must implement the ``UpgradedContract`` interface. This inter The ``upgrade`` method describes how the old state type is upgraded to the new state type. When the state isn't being upgraded, the same state type can be used for both the old and new state type parameters. +By default this new contract will only be able to upgrade legacy states which are constrained by the zone whitelist (see :doc:`api-contract-constraints`). +If hash or other constraint types are used, the new contract should implement ``UpgradedContractWithLegacyConstraint`` +instead, and specify the constraint explicitly: + +.. sourcecode:: kotlin + + interface UpgradedContractWithLegacyConstraint : UpgradedContract { + val legacyContractConstraint: AttachmentConstraint + } + +For example, in case of hash constraints the hash of the legacy JAR file should be provided: + +.. sourcecode:: kotlin + + override val legacyContractConstraint: AttachmentConstraint + get() = HashAttachmentConstraint(SecureHash.parse("E02BD2B9B010BBCE49C0D7C35BECEF2C79BEB2EE80D902B54CC9231418A4FA0C")) + Authorising the upgrade ^^^^^^^^^^^^^^^^^^^^^^^ Once the new states and contracts are on the classpath for all the relevant nodes, the next step is for all nodes to diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/DefaultKryoCustomizer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/DefaultKryoCustomizer.kt index 63a08fd3c4..5bf8e1eac4 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/DefaultKryoCustomizer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/DefaultKryoCustomizer.kt @@ -22,6 +22,7 @@ import net.corda.core.serialization.MissingAttachmentsException import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SerializedBytes +import net.corda.core.transactions.ContractUpgradeWireTransaction import net.corda.core.transactions.NotaryChangeWireTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction @@ -127,6 +128,7 @@ object DefaultKryoCustomizer { register(java.lang.invoke.SerializedLambda::class.java) register(ClosureSerializer.Closure::class.java, CordaClosureBlacklistSerializer) + register(ContractUpgradeWireTransaction::class.java, ContractUpgradeWireTransactionSerializer) for (whitelistProvider in serializationWhitelists) { val types = whitelistProvider.whitelist diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/Kryo.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/Kryo.kt index 2cc255438f..fc0be0c8eb 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/Kryo.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/Kryo.kt @@ -8,9 +8,12 @@ import com.esotericsoftware.kryo.serializers.CompatibleFieldSerializer import com.esotericsoftware.kryo.serializers.FieldSerializer import com.esotericsoftware.kryo.util.MapReferenceResolver import net.corda.core.concurrent.CordaFuture +import net.corda.core.contracts.ContractState import net.corda.core.contracts.PrivacySalt import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TransactionState import net.corda.core.crypto.Crypto +import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature import net.corda.core.identity.Party import net.corda.core.internal.uncheckedCast @@ -265,6 +268,29 @@ object NotaryChangeWireTransactionSerializer : Serializer() { + override fun write(kryo: Kryo, output: Output, obj: ContractUpgradeWireTransaction) { + kryo.writeClassAndObject(output, obj.inputs) + kryo.writeClassAndObject(output, obj.notary) + kryo.writeClassAndObject(output, obj.legacyContractAttachmentId) + kryo.writeClassAndObject(output, obj.upgradeContractClassName) + kryo.writeClassAndObject(output, obj.upgradedContractAttachmentId) + kryo.writeClassAndObject(output, obj.privacySalt) + } + + override fun read(kryo: Kryo, input: Input, type: Class): ContractUpgradeWireTransaction { + val inputs: List = uncheckedCast(kryo.readClassAndObject(input)) + val notary = kryo.readClassAndObject(input) as Party + val legacyContractAttachment = kryo.readClassAndObject(input) as SecureHash + val upgradeContractClassName = kryo.readClassAndObject(input) as String + val upgradedContractAttachment = kryo.readClassAndObject(input) as SecureHash + val privacySalt = kryo.readClassAndObject(input) as PrivacySalt + + return ContractUpgradeWireTransaction(inputs, notary, legacyContractAttachment, upgradeContractClassName, upgradedContractAttachment, privacySalt) + } +} + @ThreadSafe object SignedTransactionSerializer : Serializer() { override fun write(kryo: Kryo, output: Output, obj: SignedTransaction) { diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index ca39b2b660..1ad418052e 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -208,17 +208,24 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val networkMapCache = NetworkMapCacheImpl(PersistentNetworkMapCache(database, networkParameters.notaries).start(), identityService) val (keyPairs, nodeInfo) = initNodeInfo(networkMapCache, identity, identityKeyPair) identityService.loadIdentities(nodeInfo.legalIdentitiesAndCerts) + val metrics = MetricRegistry() val transactionStorage = makeTransactionStorage(database, configuration.transactionCacheSizeBytes) + attachments = NodeAttachmentService(metrics, configuration.attachmentContentCacheSizeBytes, configuration.attachmentCacheBound) + val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(), attachments, networkParameters.whitelistedContractImplementations) + val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParameters, transactionStorage) val nodeProperties = NodePropertiesPersistentStore(StubbedNodeUniqueIdProvider::value, database) val nodeServices = makeServices( keyPairs, schemaService, transactionStorage, + metrics, + servicesForResolution, database, nodeInfo, identityService, networkMapCache, nodeProperties, + cordappProvider, networkParameters) val notaryService = makeNotaryService(nodeServices, database) val smm = makeStateMachineManager(database) @@ -228,7 +235,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, platformClock, database, flowStarter, - transactionStorage, + servicesForResolution, unfinishedSchedules = busyNodeLatch, serverThread = serverThread, flowLogicRefFactory = flowLogicRefFactory, @@ -547,16 +554,17 @@ abstract class AbstractNode(val configuration: NodeConfiguration, private fun makeServices(keyPairs: Set, schemaService: SchemaService, transactionStorage: WritableTransactionStorage, + metrics: MetricRegistry, + servicesForResolution: ServicesForResolution, database: CordaPersistence, nodeInfo: NodeInfo, identityService: IdentityServiceInternal, networkMapCache: NetworkMapCacheInternal, nodeProperties: NodePropertiesStore, + cordappProvider: CordappProviderInternal, networkParameters: NetworkParameters): MutableList { checkpointStorage = DBCheckpointStorage() - val metrics = MetricRegistry() - attachments = NodeAttachmentService(metrics, configuration.attachmentContentCacheSizeBytes, configuration.attachmentCacheBound) - val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(), attachments, networkParameters.whitelistedContractImplementations) + val keyManagementService = makeKeyManagementService(identityService, keyPairs) _services = ServiceHubInternalImpl( identityService, @@ -569,7 +577,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, nodeInfo, networkMapCache, nodeProperties, - networkParameters) + networkParameters, + servicesForResolution) network = makeMessagingService(database, nodeInfo, nodeProperties, networkParameters) val tokenizableServices = mutableListOf(attachments, network, services.vaultService, services.keyManagementService, services.identityService, platformClock, @@ -760,8 +769,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } protected open fun generateKeyPair() = cryptoGenerateKeyPair() - protected open fun makeVaultService(keyManagementService: KeyManagementService, stateLoader: StateLoader, hibernateConfig: HibernateConfiguration): VaultServiceInternal { - return NodeVaultService(platformClock, keyManagementService, stateLoader, hibernateConfig) + protected open fun makeVaultService(keyManagementService: KeyManagementService, services: ServicesForResolution, hibernateConfig: HibernateConfiguration): VaultServiceInternal { + return NodeVaultService(platformClock, keyManagementService, services, hibernateConfig) } /** Load configured JVM agents */ @@ -794,13 +803,14 @@ abstract class AbstractNode(val configuration: NodeConfiguration, override val myInfo: NodeInfo, override val networkMapCache: NetworkMapCacheInternal, override val nodeProperties: NodePropertiesStore, - override val networkParameters: NetworkParameters - ) : SingletonSerializeAsToken(), ServiceHubInternal, StateLoader by validatedTransactions { + override val networkParameters: NetworkParameters, + private val servicesForResolution: ServicesForResolution + ) : SingletonSerializeAsToken(), ServiceHubInternal, ServicesForResolution by servicesForResolution { override val rpcFlows = ArrayList>>() override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage() override val auditService = DummyAuditService() override val transactionVerifierService by lazy { makeTransactionVerifierService() } - override val vaultService by lazy { makeVaultService(keyManagementService, validatedTransactions, database.hibernateConfig) } + override val vaultService by lazy { makeVaultService(keyManagementService, servicesForResolution, database.hibernateConfig) } override val contractUpgradeService by lazy { ContractUpgradeServiceImpl() } override val attachments: AttachmentStorage get() = this@AbstractNode.attachments override val networkService: MessagingService get() = network diff --git a/node/src/main/kotlin/net/corda/node/internal/ServiesForResolutionImpl.kt b/node/src/main/kotlin/net/corda/node/internal/ServiesForResolutionImpl.kt new file mode 100644 index 0000000000..c653a689b0 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/ServiesForResolutionImpl.kt @@ -0,0 +1,32 @@ +package net.corda.node.internal + +import net.corda.core.contracts.* +import net.corda.core.cordapp.CordappProvider +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.TransactionStorage + +data class ServicesForResolutionImpl( + override val identityService: IdentityService, + override val attachments: AttachmentStorage, + override val cordappProvider: CordappProvider, + override val networkParameters: NetworkParameters, + private val validatedTransactions: TransactionStorage +) : ServicesForResolution { + @Throws(TransactionResolutionException::class) + override fun loadState(stateRef: StateRef): TransactionState<*> { + val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash) + return stx.resolveBaseTransaction(this).outputs[stateRef.index] + } + + @Throws(TransactionResolutionException::class) + override fun loadStates(stateRefs: Set): Set> { + return stateRefs.groupBy { it.txhash }.map { + val stx = validatedTransactions.getTransaction(it.key) ?: throw TransactionResolutionException(it.key) + val baseTx = stx.resolveBaseTransaction(this) + it.value.map { StateAndRef(baseTx.outputs[it.index], it) } + }.flatMap { it }.toSet() + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt index 50f0140338..98947dbe78 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt @@ -4,6 +4,7 @@ import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult import net.corda.core.contracts.Contract import net.corda.core.contracts.UpgradedContract +import net.corda.core.contracts.UpgradedContractWithLegacyConstraint import net.corda.core.cordapp.Cordapp import net.corda.core.flows.* import net.corda.core.internal.* @@ -241,7 +242,13 @@ class CordappLoader private constructor(private val cordappJarPaths: List { - return (scanResult.getNamesOfClassesImplementing(Contract::class) + scanResult.getNamesOfClassesImplementing(UpgradedContract::class)).distinct() + return (scanResult.getNamesOfClassesImplementing(Contract::class) + + scanResult.getNamesOfClassesImplementing(UpgradedContract::class) + + // Even though UpgradedContractWithLegacyConstraint implements UpgradedContract + // we need to specify it separately. Otherwise, classes implementing UpgradedContractWithLegacyConstraint + // don't get picked up. + scanResult.getNamesOfClassesImplementing(UpgradedContractWithLegacyConstraint::class)) + .distinct() } private fun findPlugins(cordappJarPath: RestrictedURL): List { diff --git a/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt b/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt index 7ff620a96b..bb388eea30 100644 --- a/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt +++ b/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt @@ -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.transactions.ContractUpgradeWireTransaction import net.corda.core.node.StatesToRecord import net.corda.core.transactions.SignedTransaction @@ -56,13 +57,13 @@ class ContractUpgradeHandler(otherSide: FlowSession) : AbstractStateReplacementF val oldStateAndRef = ourSTX!!.tx.outRef(proposal.stateRef.index) val authorisedUpgrade = serviceHub.contractUpgradeService.getAuthorisedContractUpgrade(oldStateAndRef.ref) ?: throw IllegalStateException("Contract state upgrade is unauthorised. State hash : ${oldStateAndRef.ref}") - val proposedTx = stx.tx - val expectedTx = ContractUpgradeUtils.assembleBareTx(oldStateAndRef, proposal.modification, proposedTx.privacySalt).toWireTransaction(serviceHub) + val proposedTx = stx.coreTransaction as ContractUpgradeWireTransaction + val expectedTx = ContractUpgradeUtils.assembleUpgradeTx(oldStateAndRef, proposal.modification, proposedTx.privacySalt, serviceHub) requireThat { "The instigator is one of the participants" using (initiatingSession.counterparty in oldStateAndRef.state.data.participants) "The proposed upgrade ${proposal.modification.javaClass} is a trusted upgrade path" using (proposal.modification.name == authorisedUpgrade) "The proposed tx matches the expected tx for this upgrade" using (proposedTx == expectedTx) } - proposedTx.toLedgerTransaction(serviceHub).verify() + proposedTx.resolve(serviceHub, stx.sigs) } } 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 02a8645428..36374f80c9 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 @@ -81,7 +81,6 @@ interface ServiceHubInternal : ServiceHub { } if (statesToRecord != StatesToRecord.NONE) { - val toNotify = recordedTransactions.map { if (it.isNotaryChangeTransaction()) it.notaryChangeTx else it.tx } // When the user has requested StatesToRecord.ALL we may end up recording and relationally mapping states // that do not involve us and that we cannot sign for. This will break coin selection and thus a warning // is present in the documentation for this feature (see the "Observer nodes" tutorial on docs.corda.net). @@ -116,7 +115,7 @@ interface ServiceHubInternal : ServiceHub { // // 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, toNotify) + vaultService.notifyAll(statesToRecord, txs.map { it.coreTransaction }) } } diff --git a/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt b/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt index b79d7b3acd..21f07b6749 100644 --- a/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt +++ b/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt @@ -16,7 +16,7 @@ import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.concurrent.flatMap import net.corda.core.internal.join import net.corda.core.internal.until -import net.corda.core.node.StateLoader +import net.corda.core.node.ServicesForResolution import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.contextLogger @@ -58,7 +58,7 @@ import com.google.common.util.concurrent.SettableFuture as GuavaSettableFuture class NodeSchedulerService(private val clock: CordaClock, private val database: CordaPersistence, private val flowStarter: FlowStarter, - private val stateLoader: StateLoader, + private val servicesForResolution: ServicesForResolution, private val unfinishedSchedules: ReusableLatch = ReusableLatch(), private val serverThread: Executor, private val flowLogicRefFactory: FlowLogicRefFactory, @@ -311,7 +311,7 @@ class NodeSchedulerService(private val clock: CordaClock, } private fun getScheduledActivity(scheduledState: ScheduledStateRef): ScheduledActivity? { - val txState = stateLoader.loadState(scheduledState.ref) + val txState = servicesForResolution.loadState(scheduledState.ref) val state = txState.data as SchedulableState return try { // This can throw as running contract code. diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt b/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt index 5eaaa71c52..a9c9e2d21e 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt @@ -10,6 +10,7 @@ import net.corda.core.flows.NotarisationRequest import net.corda.core.internal.validateRequest import net.corda.core.node.services.TrustedAuthorityNotaryService import net.corda.core.transactions.CoreTransaction +import net.corda.core.transactions.ContractUpgradeFilteredTransaction import net.corda.core.transactions.FilteredTransaction import net.corda.core.transactions.NotaryChangeWireTransaction import net.corda.core.utilities.unwrap @@ -44,6 +45,7 @@ class NonValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAut val notary = tx.notary TransactionParts(tx.id, tx.inputs, tx.timeWindow, notary) } + is ContractUpgradeFilteredTransaction -> TransactionParts(tx.id, tx.inputs, null, tx.notary) is NotaryChangeWireTransaction -> TransactionParts(tx.id, tx.inputs, null, tx.notary) else -> { throw IllegalArgumentException("Received unexpected transaction type: ${tx::class.java.simpleName}," + diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt index 907428429c..27151c2ce7 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt @@ -4,13 +4,12 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.TimeWindow import net.corda.core.contracts.TransactionVerificationException import net.corda.core.flows.* -import net.corda.core.flows.NotarisationPayload -import net.corda.core.flows.NotarisationRequest import net.corda.core.internal.ResolveTransactionsFlow import net.corda.core.internal.validateRequest import net.corda.core.node.services.TrustedAuthorityNotaryService import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionWithSignatures +import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.unwrap import java.security.SignatureException @@ -31,12 +30,9 @@ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthor val stx = receiveTransaction() val notary = stx.notary checkNotary(notary) - val timeWindow: TimeWindow? = if (stx.isNotaryChangeTransaction()) - null - else - stx.tx.timeWindow resolveAndContractVerify(stx) verifySignatures(stx) + val timeWindow: TimeWindow? = if (stx.coreTransaction is WireTransaction) stx.tx.timeWindow else null return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!) } catch (e: Exception) { throw when (e) { 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 c9df780765..3134c57d1b 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 @@ -6,15 +6,13 @@ import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.internal.* import net.corda.core.messaging.DataFeed -import net.corda.core.node.StateLoader +import net.corda.core.node.ServicesForResolution import net.corda.core.node.StatesToRecord import net.corda.core.node.services.* import net.corda.core.node.services.vault.* import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.SingletonSerializeAsToken -import net.corda.core.transactions.CoreTransaction -import net.corda.core.transactions.NotaryChangeWireTransaction -import net.corda.core.transactions.WireTransaction +import net.corda.core.transactions.* import net.corda.core.utilities.* import net.corda.node.services.api.VaultServiceInternal import net.corda.node.services.statemachine.FlowStateMachineImpl @@ -49,7 +47,7 @@ private fun CriteriaBuilder.executeUpdate(session: Session, configure: Root<*>.( class NodeVaultService( private val clock: Clock, private val keyManagementService: KeyManagementService, - private val stateLoader: StateLoader, + private val servicesForResolution: ServicesForResolution, hibernateConfig: HibernateConfiguration ) : SingletonSerializeAsToken(), VaultServiceInternal { private companion object { @@ -108,41 +106,29 @@ class NodeVaultService( override val updates: Observable> get() = mutex.locked { _updatesInDbTx } + /** Groups adjacent transactions into batches to generate separate net updates per transaction type. */ override fun notifyAll(statesToRecord: StatesToRecord, txns: Iterable) { - if (statesToRecord == StatesToRecord.NONE) - return + if (statesToRecord == StatesToRecord.NONE || !txns.any()) return + val batch = mutableListOf() - // It'd be easier to just group by type, but then we'd lose ordering. - val regularTxns = mutableListOf() - val notaryChangeTxns = mutableListOf() - - for (tx in txns) { - when (tx) { - is WireTransaction -> { - regularTxns.add(tx) - if (notaryChangeTxns.isNotEmpty()) { - notifyNotaryChange(notaryChangeTxns.toList(), statesToRecord) - notaryChangeTxns.clear() - } - } - is NotaryChangeWireTransaction -> { - notaryChangeTxns.add(tx) - if (regularTxns.isNotEmpty()) { - notifyRegular(regularTxns.toList(), statesToRecord) - regularTxns.clear() - } - } - } + fun flushBatch() { + val updates = makeUpdates(batch, statesToRecord) + processAndNotify(updates) + batch.clear() } - if (regularTxns.isNotEmpty()) notifyRegular(regularTxns.toList(), statesToRecord) - if (notaryChangeTxns.isNotEmpty()) notifyNotaryChange(notaryChangeTxns.toList(), statesToRecord) + for (tx in txns) { + if (batch.isNotEmpty() && tx.javaClass != batch.last().javaClass) { + flushBatch() + } + batch.add(tx) + } + flushBatch() } - private fun notifyRegular(txns: Iterable, statesToRecord: StatesToRecord) { - fun makeUpdate(tx: WireTransaction): Vault.Update { + private fun makeUpdates(batch: Iterable, statesToRecord: StatesToRecord): List> { + fun makeUpdate(tx: WireTransaction): Vault.Update? { val myKeys = keyManagementService.filterMyKeys(tx.outputs.flatMap { it.data.participants.map { it.owningKey } }) - val ourNewStates = when (statesToRecord) { StatesToRecord.NONE -> throw AssertionError("Should not reach here") StatesToRecord.ONLY_RELEVANT -> tx.outputs.filter { isRelevant(it.data, myKeys.toSet()) } @@ -155,45 +141,48 @@ class NodeVaultService( // Is transaction irrelevant? if (consumedStates.isEmpty() && ourNewStates.isEmpty()) { log.trace { "tx ${tx.id} was irrelevant to this vault, ignoring" } - return Vault.NoUpdate + return null } return Vault.Update(consumedStates.toSet(), ourNewStates.toSet()) } - val netDelta = txns.fold(Vault.NoUpdate) { netDelta, txn -> netDelta + makeUpdate(txn) } - processAndNotify(netDelta) - } - - private fun notifyNotaryChange(txns: Iterable, statesToRecord: StatesToRecord) { - fun makeUpdate(tx: NotaryChangeWireTransaction): Vault.Update { + fun resolveAndMakeUpdate(tx: CoreTransaction): Vault.Update? { // We need to resolve the full transaction here because outputs are calculated from inputs - // We also can't do filtering beforehand, since output encumbrance pointers get recalculated based on - // input positions - val ltx = tx.resolve(stateLoader, emptyList()) + // We also can't do filtering beforehand, since for notary change transactions output encumbrance pointers + // get recalculated based on input positions. + val ltx: FullTransaction = when (tx) { + is NotaryChangeWireTransaction -> tx.resolve(servicesForResolution, emptyList()) + is ContractUpgradeWireTransaction -> tx.resolve(servicesForResolution, emptyList()) + else -> throw IllegalArgumentException("Unsupported transaction type: ${tx.javaClass.name}") + } val myKeys = keyManagementService.filterMyKeys(ltx.outputs.flatMap { it.data.participants.map { it.owningKey } }) val (consumedStateAndRefs, producedStates) = ltx.inputs. zip(ltx.outputs). filter { (_, output) -> - if (statesToRecord == StatesToRecord.ONLY_RELEVANT) - isRelevant(output.data, myKeys.toSet()) - else - true + if (statesToRecord == StatesToRecord.ONLY_RELEVANT) isRelevant(output.data, myKeys.toSet()) + else true }. unzip() val producedStateAndRefs = producedStates.map { ltx.outRef(it.data) } - if (consumedStateAndRefs.isEmpty() && producedStateAndRefs.isEmpty()) { log.trace { "tx ${tx.id} was irrelevant to this vault, ignoring" } - return Vault.NoNotaryUpdate + return null } - return Vault.Update(consumedStateAndRefs.toHashSet(), producedStateAndRefs.toHashSet(), null, Vault.UpdateType.NOTARY_CHANGE) + val updateType = if (tx is ContractUpgradeWireTransaction) { + Vault.UpdateType.CONTRACT_UPGRADE + } else { + Vault.UpdateType.NOTARY_CHANGE + } + return Vault.Update(consumedStateAndRefs.toSet(), producedStateAndRefs.toSet(), null, updateType) } - val netDelta = txns.fold(Vault.NoNotaryUpdate) { netDelta, txn -> netDelta + makeUpdate(txn) } - processAndNotify(netDelta) + + return batch.mapNotNull { + if (it is WireTransaction) makeUpdate(it) else resolveAndMakeUpdate(it) + } } private fun loadStates(refs: Collection): Collection> { @@ -202,13 +191,15 @@ class NodeVaultService( else emptySet() } - private fun processAndNotify(update: Vault.Update) { - if (!update.isEmpty()) { - recordUpdate(update) + private fun processAndNotify(updates: List>) { + if (updates.isEmpty()) return + val netUpdate = updates.reduce { update1, update2 -> update1 + update2 } + if (!netUpdate.isEmpty()) { + recordUpdate(netUpdate) mutex.locked { // flowId required by SoftLockManager to perform auto-registration of soft locks for new states val uuid = (Strand.currentStrand() as? FlowStateMachineImpl<*>)?.id?.uuid - val vaultUpdate = if (uuid != null) update.copy(flowId = uuid) else update + val vaultUpdate = if (uuid != null) netUpdate.copy(flowId = uuid) else netUpdate updatesPublisher.onNext(vaultUpdate) } } @@ -457,7 +448,7 @@ class NodeVaultService( } } if (stateRefs.isNotEmpty()) - statesAndRefs.addAll(stateLoader.loadStates(stateRefs) as Collection>) + statesAndRefs.addAll(servicesForResolution.loadStates(stateRefs) as Collection>) return Vault.Page(states = statesAndRefs, statesMetadata = statesMeta, stateTypes = criteriaParser.stateTypes, totalStatesAvailable = totalStates, otherResults = otherResults) } catch (e: java.lang.Exception) { diff --git a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt index 659eb1161f..3feebf74de 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt @@ -9,7 +9,7 @@ import net.corda.core.flows.FlowLogicRefFactory import net.corda.core.internal.FlowStateMachine import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.uncheckedCast -import net.corda.core.node.StateLoader +import net.corda.core.node.ServicesForResolution import net.corda.core.utilities.days import net.corda.node.services.api.FlowStarter import net.corda.node.services.api.NodePropertiesStore @@ -48,7 +48,7 @@ class NodeSchedulerServiceTest { doReturn(flowsDraingMode).whenever(it).flowsDrainingMode } private val transactionStates = mutableMapOf>() - private val stateLoader = rigorousMock().also { + private val servicesForResolution = rigorousMock().also { doLookup(transactionStates).whenever(it).loadState(any()) } private val flows = mutableMapOf>() @@ -63,7 +63,7 @@ class NodeSchedulerServiceTest { testClock, database, flowStarter, - stateLoader, + servicesForResolution, serverThread = MoreExecutors.directExecutor(), flowLogicRefFactory = flowLogicRefFactory, nodeProperties = nodeProperties, diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt index 6e996a8d94..ef7f1f76c7 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt @@ -118,7 +118,7 @@ class HibernateConfigurationTest { services = object : MockServices(cordappPackages, BOB_NAME, rigorousMock().also { doNothing().whenever(it).justVerifyAndRegisterIdentity(argThat { name == BOB_NAME }) }, generateKeyPair(), dummyNotary.keyPair) { - override val vaultService = NodeVaultService(Clock.systemUTC(), keyManagementService, validatedTransactions, hibernateConfig) + override val vaultService = NodeVaultService(Clock.systemUTC(), keyManagementService, servicesForResolution, hibernateConfig) override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable) { for (stx in txs) { validatedTransactions.addTransaction(stx) diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt index c3b423cfe1..1ec7772bea 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt @@ -11,7 +11,7 @@ import net.corda.core.identity.AbstractParty import net.corda.core.internal.FlowStateMachine import net.corda.core.internal.packageName import net.corda.core.internal.uncheckedCast -import net.corda.core.node.StateLoader +import net.corda.core.node.ServicesForResolution import net.corda.core.node.services.KeyManagementService import net.corda.core.node.services.queryBy import net.corda.core.node.services.vault.QueryCriteria.SoftLockingCondition @@ -83,8 +83,8 @@ class VaultSoftLockManagerTest { } private val mockNet = InternalMockNetwork(cordappPackages = listOf(ContractImpl::class.packageName), defaultFactory = { args -> object : InternalMockNetwork.MockNode(args) { - override fun makeVaultService(keyManagementService: KeyManagementService, stateLoader: StateLoader, hibernateConfig: HibernateConfiguration): VaultServiceInternal { - val realVault = super.makeVaultService(keyManagementService, stateLoader, hibernateConfig) + override fun makeVaultService(keyManagementService: KeyManagementService, services: ServicesForResolution, hibernateConfig: HibernateConfiguration): VaultServiceInternal { + val realVault = super.makeVaultService(keyManagementService, services, hibernateConfig) return object : VaultServiceInternal by realVault { override fun softLockRelease(lockId: UUID, stateRefs: NonEmptySet?) { mockVault.softLockRelease(lockId, stateRefs) // No need to also call the real one for these tests. diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt index e79c54e5bf..090ec3b0e8 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt @@ -11,8 +11,10 @@ import net.corda.core.internal.validateRequest import net.corda.core.node.AppServiceHub import net.corda.core.node.services.CordaService import net.corda.core.node.services.TrustedAuthorityNotaryService +import net.corda.core.transactions.CoreTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionWithSignatures +import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.unwrap import net.corda.node.services.transactions.PersistentUniquenessProvider import java.security.PublicKey @@ -49,12 +51,9 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating val stx = receiveTransaction() val notary = stx.notary checkNotary(notary) - val timeWindow: TimeWindow? = if (stx.isNotaryChangeTransaction()) - null - else - stx.tx.timeWindow - resolveAndContractVerify(stx) verifySignatures(stx) + resolveAndContractVerify(stx) + val timeWindow: TimeWindow? = if (stx.coreTransaction is WireTransaction) stx.tx.timeWindow else null return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!) } catch (e: Exception) { throw when (e) { diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index f553047f7c..9ea95e0880 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -2,6 +2,9 @@ package net.corda.testing.node import com.google.common.collect.MutableClassToInstanceMap import net.corda.core.contracts.ContractClassName +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.StateRef import net.corda.core.cordapp.CordappProvider import net.corda.core.crypto.* import net.corda.core.flows.FlowLogic @@ -17,6 +20,7 @@ import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.VersionInfo +import net.corda.node.internal.ServicesForResolutionImpl import net.corda.node.internal.configureDatabase import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.services.api.SchemaService @@ -65,8 +69,7 @@ open class MockServices private constructor( final override val networkParameters: NetworkParameters, private val initialIdentity: TestIdentity, private val moreKeys: Array -) : ServiceHub, StateLoader by validatedTransactions { - +) : ServiceHub { companion object { @JvmStatic val MOCK_VERSION_INFO = VersionInfo(1, "Mock release", "Mock revision", "Mock Vendor") @@ -113,7 +116,7 @@ open class MockServices private constructor( override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable) { super.recordTransactions(statesToRecord, txs) // Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions. - vaultService.notifyAll(statesToRecord, txs.map { it.tx }) + vaultService.notifyAll(statesToRecord, txs.map { it.coreTransaction }) } override fun jdbcSession(): Connection = database.createSession() @@ -172,7 +175,7 @@ open class MockServices private constructor( /** * Create a mock [ServiceHub] that can't load CorDapp code, and which uses a default service identity. */ - constructor(cordappPackages: List): this(cordappPackages, CordaX500Name("TestIdentity", "", "GB"), makeTestIdentityService()) + constructor(cordappPackages: List) : this(cordappPackages, CordaX500Name("TestIdentity", "", "GB"), makeTestIdentityService()) /** * Create a mock [ServiceHub] which uses the package of the caller to find CorDapp code. It uses the provided identity service @@ -207,7 +210,8 @@ open class MockServices private constructor( * Create a mock [ServiceHub] which uses the package of the caller to find CorDapp code. It uses a default service * identity. */ - constructor(): this(listOf(getCallerPackage()), CordaX500Name("TestIdentity", "", "GB"), makeTestIdentityService()) + constructor() : this(listOf(getCallerPackage()), CordaX500Name("TestIdentity", "", "GB"), makeTestIdentityService()) + override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable) { txs.forEach { @@ -229,8 +233,10 @@ open class MockServices private constructor( private val mockCordappProvider: MockCordappProvider = MockCordappProvider(cordappLoader, attachments, networkParameters.whitelistedContractImplementations) override val cordappProvider: CordappProvider get() = mockCordappProvider + protected val servicesForResolution: ServicesForResolution get() = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParameters, validatedTransactions) + internal fun makeVaultService(hibernateConfig: HibernateConfiguration, schemaService: SchemaService): VaultServiceInternal { - val vaultService = NodeVaultService(Clock.systemUTC(), keyManagementService, validatedTransactions, hibernateConfig) + val vaultService = NodeVaultService(Clock.systemUTC(), keyManagementService, servicesForResolution, hibernateConfig) HibernateObserver.install(vaultService.rawUpdates, hibernateConfig, schemaService) return vaultService } @@ -249,6 +255,9 @@ open class MockServices private constructor( fun addMockCordapp(contractClassName: ContractClassName) { mockCordappProvider.addMockCordapp(contractClassName, attachments) } + + override fun loadState(stateRef: StateRef) = servicesForResolution.loadState(stateRef) + override fun loadStates(stateRefs: Set) = servicesForResolution.loadStates(stateRefs) } class MockKeyManagementService(val identityService: IdentityService, diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV2.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV2.kt index 5b7f80044d..36472ac17d 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV2.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV2.kt @@ -1,12 +1,9 @@ package net.corda.testing.contracts import net.corda.core.contracts.* +import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty -import net.corda.core.internal.UpgradeCommand -import net.corda.core.node.ServicesForResolution import net.corda.core.transactions.LedgerTransaction -import net.corda.core.transactions.TransactionBuilder -import net.corda.core.transactions.WireTransaction // The dummy contract doesn't do anything useful. It exists for testing purposes. @@ -14,12 +11,13 @@ import net.corda.core.transactions.WireTransaction * Dummy contract state for testing of the upgrade process. */ // DOCSTART 1 -class DummyContractV2 : UpgradedContract { +class DummyContractV2 : UpgradedContractWithLegacyConstraint { companion object { const val PROGRAM_ID: ContractClassName = "net.corda.testing.contracts.DummyContractV2" } override val legacyContract: String = DummyContract::class.java.name + override val legacyContractConstraint: AttachmentConstraint = AlwaysAcceptAttachmentConstraint data class State(val magicNumber: Int = 0, val owners: List) : ContractState { override val participants: List = owners @@ -38,25 +36,4 @@ class DummyContractV2 : UpgradedContract): Pair> { - val notary = states.map { it.state.notary }.single() - require(states.isNotEmpty()) - - val signees: Set = states.flatMap { it.state.data.participants }.distinct().toSet() - return Pair(TransactionBuilder(notary).apply { - states.forEach { - addInputState(it) - addOutputState(upgrade(it.state.data), DummyContractV2.PROGRAM_ID, it.state.constraint) - addCommand(UpgradeCommand(DummyContractV2.PROGRAM_ID), signees.map { it.owningKey }.toList()) - } - }.toWireTransaction(services), signees) - } }