diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 298c9ec549..8e264fd60b 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -629,6 +629,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) ## @@ -1836,16 +1839,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) @@ -1994,10 +1993,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 @@ -3077,7 +3075,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 @@ -3100,13 +3097,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() @@ -4015,7 +4010,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) @@ -4112,8 +4107,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 @@ -4289,9 +4282,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 cc1abb0d56..bf01867057 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.bytes.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/crypto/CompositeKey.kt b/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt index 1fe4103d4f..98bac0dae0 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt @@ -1,6 +1,5 @@ package net.corda.core.crypto -import net.corda.core.crypto.CompositeKey.NodeAndWeight import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.exactAdd import net.corda.core.utilities.sequence @@ -12,7 +11,7 @@ import java.util.* /** * A tree data structure that enables the representation of composite public keys, which are used to represent - * the signing requirements for multisignature scenarios such as RAFT notary services. A composite key is a list + * the signing requirements for multi-signature scenarios such as RAFT notary services. A composite key is a list * of leaf keys and their contributing weight, and each leaf can be a conventional single key or a composite key. * Keys contribute their weight to the total if they are matched by the signature. * @@ -53,9 +52,19 @@ class CompositeKey private constructor(val threshold: Int, children: List({ -it.weight }, { it.node.encoded.sequence() }) } - val children: List = children.sorted() + /** + * Τhe order of the children may not be the same to what was provided in the builder. + */ + val children: List = children.sortedWith(descWeightComparator) init { // TODO: replace with the more extensive, but slower, checkValidity() test. @@ -103,9 +112,9 @@ class CompositeKey private constructor(val threshold: Int, children: List() visitedMap.put(this, true) cycleDetection(visitedMap) // Graph cycle testing on the root node. @@ -143,6 +152,7 @@ class CompositeKey private constructor(val threshold: Int, children: List): Boolean { - if (keysToCheck.any { it is CompositeKey }) return false - val totalWeight = children.map { (node, weight) -> + var totalWeight = 0 + children.forEach { (node, weight) -> if (node is CompositeKey) { - if (node.checkFulfilledBy(keysToCheck)) weight else 0 + if (node.checkFulfilledBy(keysToCheck)) totalWeight += weight } else { - if (keysToCheck.contains(node)) weight else 0 + if (node in keysToCheck) totalWeight += weight } - }.sum() - return totalWeight >= threshold + if (totalWeight >= threshold) return true + } + return false } /** @@ -201,8 +212,8 @@ class CompositeKey private constructor(val threshold: Int, children: List): Boolean { // We validate keys only when checking if they're matched, as this checks subkeys as a result. // Doing these checks at deserialization/construction time would result in duplicate checks. - if (!validated) - checkValidity() // TODO: remove when checkValidity() will be eventually invoked during/after deserialization. + checkValidity() + if (keysToCheck.any { it is CompositeKey }) return false return checkFulfilledBy(keysToCheck) } 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 088bd4c5ab..2acd874524 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -17,39 +17,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 @@ -65,6 +38,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 f9c59e200d..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 }.map { - val stx = getTransaction(it.key) ?: throw TransactionResolutionException(it.key) - val baseTx = stx.resolveBaseTransaction(this) - it.value.map { StateAndRef(baseTx.outputs[it.index], it) } - }.flatMap { 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 632a725dd1..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 @@ -430,5 +404,3 @@ data class LedgerTransaction @JvmOverloads constructor( ) = 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 015e745987..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, @@ -68,15 +67,15 @@ 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 - /** Helper to access the notary of the contained transaction */ - val notary: Party? get() = transaction.notary + /** Helper to access the inputs of the contained transaction. */ + val inputs: List get() = coreTransaction.inputs + /** Helper to access the notary of the contained transaction. */ + val notary: Party? get() = coreTransaction.notary override val requiredSigningKeys: Set get() = tx.requiredSigningKeys override fun getKeyDescriptions(keys: Set): ArrayList { - // TODO: We need a much better way of structuring this data + // TODO: We need a much better way of structuring this data. val descriptions = ArrayList() this.tx.commands.forEach { command -> if (command.signers.any { it in keys }) @@ -134,8 +133,18 @@ data class SignedTransaction(val txBits: SerializedBytes, @JvmOverloads @Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class) fun toLedgerTransaction(services: ServiceHub, checkSufficientSignatures: Boolean = true): LedgerTransaction { - checkSignaturesAreValid() - if (checkSufficientSignatures) verifyRequiredSignatures() + // TODO: We could probably optimise the below by + // a) not throwing if threshold is eventually satisfied, but some of the rest of the signatures are failing. + // b) omit verifying signatures when threshold requirement is met. + // c) omit verifying signatures from keys not included in [requiredSigningKeys]. + // For the above to work, [checkSignaturesAreValid] should take the [requiredSigningKeys] as input + // and probably combine logic from signature validation and key-fulfilment + // in [TransactionWithSignatures.verifySignaturesExcept]. + if (checkSufficientSignatures) { + verifyRequiredSignatures() // It internally invokes checkSignaturesAreValid(). + } else { + checkSignaturesAreValid() + } return tx.toLedgerTransaction(services) } @@ -152,56 +161,62 @@ 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(checkSufficientSignatures, services) - } else { - verifyRegularTransaction(checkSufficientSignatures, services) + when (coreTransaction) { + is NotaryChangeWireTransaction -> verifyNotaryChangeTransaction(services, checkSufficientSignatures) + is ContractUpgradeWireTransaction -> verifyContractUpgradeTransaction(services, checkSufficientSignatures) + else -> verifyRegularTransaction(services, checkSufficientSignatures) } } - /** - * 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. - */ - private fun verifyRegularTransaction(checkSufficientSignatures: Boolean, services: ServiceHub) { - checkSignaturesAreValid() - if (checkSufficientSignatures) verifyRequiredSignatures() - val ltx = tx.toLedgerTransaction(services) - // TODO: allow non-blocking verification - services.transactionVerifierService.verify(ltx).getOrThrow() - } - - private fun verifyNotaryChangeTransaction(checkSufficientSignatures: Boolean, services: ServiceHub) { + /** 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() } - fun isNotaryChangeTransaction() = transaction is NotaryChangeWireTransaction + /** 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. + private fun verifyRegularTransaction(services: ServiceHub, checkSufficientSignatures: Boolean) { + val ltx = toLedgerTransaction(services, checkSufficientSignatures) + // TODO: allow non-blocking verification. + services.transactionVerifierService.verify(ltx).getOrThrow() + } /** * 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}") } } @@ -209,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)" @@ -227,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/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt index 8999a62056..77ceb3840b 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt @@ -11,7 +11,7 @@ import java.security.PublicKey import java.security.SignatureException import java.util.* -/** An interface for transactions containing signatures, with logic for signature verification */ +/** An interface for transactions containing signatures, with logic for signature verification. */ @DoNotImplement interface TransactionWithSignatures : NamedByHash { /** @@ -21,7 +21,7 @@ interface TransactionWithSignatures : NamedByHash { */ val sigs: List - /** Specifies all the public keys that require signatures for the transaction to be valid */ + /** Specifies all the public keys that require signatures for the transaction to be valid. */ val requiredSigningKeys: Set /** @@ -65,11 +65,10 @@ interface TransactionWithSignatures : NamedByHash { */ @Throws(SignatureException::class) fun verifySignaturesExcept(allowedToBeMissing: Collection) { - checkSignaturesAreValid() - val needed = getMissingSigners() - allowedToBeMissing if (needed.isNotEmpty()) throw SignaturesMissingException(needed.toNonEmptySet(), getKeyDescriptions(needed), id) + checkSignaturesAreValid() } /** 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/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 c798b36c5b..c43a894b14 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 @@ -128,6 +129,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 18b598919c..b5bef7ac52 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 47d7af4149..1d352e827a 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -206,17 +206,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, 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) @@ -226,7 +233,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, platformClock, database, flowStarter, - transactionStorage, + servicesForResolution, unfinishedSchedules = busyNodeLatch, serverThread = serverThread, flowLogicRefFactory = flowLogicRefFactory, @@ -543,16 +550,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: IdentityService, 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, attachments, networkParameters.whitelistedContractImplementations) + val keyManagementService = makeKeyManagementService(identityService, keyPairs) _services = ServiceHubInternalImpl( identityService, @@ -565,7 +573,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, @@ -755,8 +764,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) } private inner class ServiceHubInternalImpl( @@ -773,13 +782,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 481a73bfda..fee41e219c 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 2eeb318043..61099fff21 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( 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) - } }