CORDA-696 - Create separate transaction types for contract upgrade transactions (#2589)

* CORDA-986 and CORDA-985 CompositeKey and Signature verification performance fixes (#2467)

* CORDA-696: Create separate transaction types for contract upgrade transactions.

Add rationale around upgrade transactions

Move contract upgrade transaction resolution logic into internal until it's stabilised.

Throw a better exception when contract attachment not found

Default legacy contract constraint to always accepting - needs to be changed to whitelist constraint before merging

Introduce a new upgraded contract interface that allows specifying the legacy constraint.

Remove StateLoader, make all tx resolution functions take in ServicesForResolution

Contract upgrade transactions can handle whitelist by zone constraints

When creating a contract upgrade transaction, make sure the attachment of the old cordapp gets attached when using hash constraints.
Attachment lookup for a given contract class name only scans currently loaded cordapps, and we don't load old versions of cordapps.

CORDA-696: Update upgrade docs
This commit is contained in:
Andrius Dagys 2018-02-22 17:51:41 +00:00
parent a483e7e8ce
commit 0edfef2409
36 changed files with 622 additions and 395 deletions

View File

@ -627,6 +627,9 @@ public static final class net.corda.core.contracts.UniqueIdentifier$Companion ex
@org.jetbrains.annotations.NotNull public abstract String getLegacyContract() @org.jetbrains.annotations.NotNull public abstract String getLegacyContract()
@org.jetbrains.annotations.NotNull public abstract net.corda.core.contracts.ContractState upgrade(net.corda.core.contracts.ContractState) @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 @net.corda.core.serialization.CordaSerializable @net.corda.core.DoNotImplement public final class net.corda.core.contracts.WhitelistedByZoneAttachmentConstraint extends java.lang.Object implements net.corda.core.contracts.AttachmentConstraint
public boolean isSatisfiedBy(net.corda.core.contracts.Attachment) public boolean isSatisfiedBy(net.corda.core.contracts.Attachment)
public static final net.corda.core.contracts.WhitelistedByZoneAttachmentConstraint INSTANCE public static final net.corda.core.contracts.WhitelistedByZoneAttachmentConstraint INSTANCE
@ -1894,16 +1897,12 @@ public @interface net.corda.core.messaging.RPCReturnsObservables
@org.jetbrains.annotations.NotNull public abstract net.corda.core.transactions.SignedTransaction signInitialTransaction(net.corda.core.transactions.TransactionBuilder, java.security.PublicKey) @org.jetbrains.annotations.NotNull public abstract net.corda.core.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) @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.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.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.services.IdentityService getIdentityService()
@org.jetbrains.annotations.NotNull public abstract net.corda.core.node.NetworkParameters getNetworkParameters() @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 public final class net.corda.core.node.StatesToRecord extends java.lang.Enum
protected <init>(String, int) protected <init>(String, int)
public static net.corda.core.node.StatesToRecord valueOf(String) public static net.corda.core.node.StatesToRecord valueOf(String)
@ -2052,10 +2051,9 @@ public final class net.corda.core.node.services.TimeWindowChecker extends java.l
@org.jetbrains.annotations.NotNull public final java.time.Clock getClock() @org.jetbrains.annotations.NotNull public final java.time.Clock getClock()
public final boolean isValid(net.corda.core.contracts.TimeWindow) 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.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 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() @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed track()
## ##
@net.corda.core.DoNotImplement public interface net.corda.core.node.services.TransactionVerifierService @net.corda.core.DoNotImplement public interface net.corda.core.node.services.TransactionVerifierService
@ -3137,7 +3135,6 @@ public static final class net.corda.core.transactions.LedgerTransaction$InOutGro
@org.jetbrains.annotations.NotNull public List getOutputs() @org.jetbrains.annotations.NotNull public List getOutputs()
public int hashCode() 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.ServiceHub, List)
@org.jetbrains.annotations.NotNull public final net.corda.core.transactions.NotaryChangeLedgerTransaction resolve(net.corda.core.node.StateLoader, List)
public String toString() 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 @net.corda.core.serialization.CordaSerializable @net.corda.core.DoNotImplement public final class net.corda.core.transactions.SignedTransaction extends java.lang.Object implements net.corda.core.transactions.TransactionWithSignatures
@ -3160,13 +3157,11 @@ public static final class net.corda.core.transactions.LedgerTransaction$InOutGro
@org.jetbrains.annotations.NotNull public final net.corda.core.transactions.WireTransaction getTx() @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.WireTransaction getTx()
@org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializedBytes getTxBits() @org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializedBytes getTxBits()
public int hashCode() 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(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.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.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.ServicesForResolution)
@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.LedgerTransaction toLedgerTransaction(net.corda.core.node.ServiceHub) @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 final net.corda.core.transactions.LedgerTransaction toLedgerTransaction(net.corda.core.node.ServiceHub, boolean)
@org.jetbrains.annotations.NotNull public String toString() @org.jetbrains.annotations.NotNull public String toString()
@ -4075,7 +4070,7 @@ public final class net.corda.testing.node.MockNodeParameters extends java.lang.O
@org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNodeParameters withForcedID(Integer) @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNodeParameters withForcedID(Integer)
@org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNodeParameters withLegalName(net.corda.core.identity.CordaX500Name) @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 <init>() public <init>()
public <init>(List) public <init>(List)
public <init>(List, net.corda.core.identity.CordaX500Name) public <init>(List, net.corda.core.identity.CordaX500Name)
@ -4174,8 +4169,6 @@ public class net.corda.testing.node.MockTransactionStorage extends net.corda.cor
public boolean addTransaction(net.corda.core.transactions.SignedTransaction) 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.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 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() @org.jetbrains.annotations.NotNull public net.corda.core.messaging.DataFeed track()
## ##
public final class net.corda.testing.node.NodeTestUtils extends java.lang.Object public final class net.corda.testing.node.NodeTestUtils extends java.lang.Object
@ -4351,9 +4344,10 @@ public static final class net.corda.testing.contracts.DummyContract$SingleOwnerS
@net.corda.core.DoNotImplement public static interface net.corda.testing.contracts.DummyContract$State extends net.corda.core.contracts.ContractState @net.corda.core.DoNotImplement public static interface net.corda.testing.contracts.DummyContract$State extends net.corda.core.contracts.ContractState
public abstract int getMagicNumber() 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 <init>() public <init>()
@org.jetbrains.annotations.NotNull public String getLegacyContract() @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) @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 void verify(net.corda.core.transactions.LedgerTransaction)
public static final net.corda.testing.contracts.DummyContractV2$Companion Companion public static final net.corda.testing.contracts.DummyContractV2$Companion Companion

View File

@ -24,10 +24,7 @@ import net.corda.core.node.services.IdentityService
import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.deserialize import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.core.transactions.CoreTransaction import net.corda.core.transactions.*
import net.corda.core.transactions.NotaryChangeWireTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.base58ToByteArray import net.corda.core.utilities.base58ToByteArray
import net.corda.core.utilities.base64ToByteArray import net.corda.core.utilities.base64ToByteArray

View File

@ -48,6 +48,7 @@ data class Issued<out P : Any>(val issuer: PartyAndReference, val product: P) {
init { init {
require(issuer.reference.size <= MAX_ISSUER_REF_SIZE) { "Maximum issuer reference size is $MAX_ISSUER_REF_SIZE." } require(issuer.reference.size <= MAX_ISSUER_REF_SIZE) { "Maximum issuer reference size is $MAX_ISSUER_REF_SIZE." }
} }
override fun toString() = "$product issued by $issuer" 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. * 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 * @param OldState the old contract state (can be [ContractState] or other common supertype if this supports upgrading
* more than one state). * more than one state).
* @param NewState the upgraded contract state. * @param NewState the upgraded contract state.
*/ */
interface UpgradedContract<in OldState : ContractState, out NewState : ContractState> : Contract { interface UpgradedContract<in OldState : ContractState, out NewState : ContractState> : Contract {
/**
* Name of the contract this is an upgraded version of, used as part of verification of upgrade transactions.
*/
val legacyContract: ContractClassName val legacyContract: ContractClassName
/** /**
* Upgrade contract's state object to a new state object. * Upgrade contract's state object to a new state object.
@ -264,6 +274,17 @@ interface UpgradedContract<in OldState : ContractState, out NewState : ContractS
fun upgrade(state: OldState): NewState fun upgrade(state: OldState): NewState
} }
/**
* This interface allows specifying a custom legacy contract constraint for upgraded contracts. The default for [UpgradedContract]
* is [WhitelistedByZoneAttachmentConstraint].
*/
interface UpgradedContractWithLegacyConstraint<in OldState : ContractState, out NewState : ContractState> : UpgradedContract<OldState, NewState> {
/**
* 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 * 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. * use brute force techniques and reveal the content of a Merkle-leaf hashed value.

View File

@ -2,7 +2,11 @@ package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.* 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.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. * 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) serviceHub.contractUpgradeService.storeAuthorisedContractUpgrade(stateAndRef.ref, upgradedContractClass)
return null return null
} }
} }
/** /**
@ -68,11 +71,13 @@ object ContractUpgradeFlow {
@Suspendable @Suspendable
override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx { 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() val participantKeys = originalState.state.data.participants.map { it.owningKey }.toSet()
// TODO: We need a much faster way of finding our key in the transaction // TODO: We need a much faster way of finding our key in the transaction
val myKey = serviceHub.keyManagementService.filterMyKeys(participantKeys).single() 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) return AbstractStateReplacementFlow.UpgradeTx(stx)
} }
} }

View File

@ -15,7 +15,9 @@ import net.corda.core.node.services.TrustedAuthorityNotaryService
import net.corda.core.node.services.UniquenessProvider import net.corda.core.node.services.UniquenessProvider
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.CoreTransaction import net.corda.core.transactions.CoreTransaction
import net.corda.core.transactions.ContractUpgradeWireTransaction
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.UntrustworthyData import net.corda.core.utilities.UntrustworthyData
import net.corda.core.utilities.unwrap import net.corda.core.utilities.unwrap
@ -104,10 +106,11 @@ class NotaryFlow {
@Suspendable @Suspendable
private fun sendAndReceiveNonValidating(notaryParty: Party, session: FlowSession, signature: NotarisationRequestSignature): UntrustworthyData<List<TransactionSignature>> { private fun sendAndReceiveNonValidating(notaryParty: Party, session: FlowSession, signature: NotarisationRequestSignature): UntrustworthyData<List<TransactionSignature>> {
val tx: CoreTransaction = if (stx.isNotaryChangeTransaction()) { val ctx = stx.coreTransaction
stx.notaryChangeTx // Notary change transactions do not support filtering val tx = when (ctx) {
} else { is ContractUpgradeWireTransaction -> ctx.buildFilteredTransaction()
stx.buildFilteredTransaction(Predicate { it is StateRef || it is TimeWindow || it == notaryParty }) is WireTransaction -> ctx.buildFilteredTransaction(Predicate { it is StateRef || it is TimeWindow || it == notaryParty })
else -> ctx
} }
return session.sendAndReceiveWithRetry(NotarisationPayload(tx, signature)) return session.sendAndReceiveWithRetry(NotarisationPayload(tx, signature))
} }
@ -168,12 +171,10 @@ class NotaryFlow {
@Suspendable @Suspendable
abstract fun receiveAndVerifyTx(): TransactionParts 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 @Suspendable
protected fun checkNotary(notary: Party?) { 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 if (notary?.owningKey != service.notaryIdentityKey) {
// notary identities?
if (notary == null || !serviceHub.myInfo.isLegalIdentity(notary)) {
throw NotaryException(NotaryError.WrongNotary) throw NotaryException(NotaryError.WrongNotary)
} }
} }

View File

@ -1,21 +1,38 @@
package net.corda.core.internal package net.corda.core.internal
import net.corda.core.contracts.* 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 { object ContractUpgradeUtils {
fun <OldState : ContractState, NewState : ContractState> assembleBareTx( fun <OldState : ContractState, NewState : ContractState> assembleUpgradeTx(
stateRef: StateAndRef<OldState>, stateAndRef: StateAndRef<OldState>,
upgradedContractClass: Class<out UpgradedContract<OldState, NewState>>, upgradedContractClass: Class<out UpgradedContract<OldState, NewState>>,
privacySalt: PrivacySalt privacySalt: PrivacySalt,
): TransactionBuilder { services: ServicesForResolution
val contractUpgrade = upgradedContractClass.newInstance() ): ContractUpgradeWireTransaction {
return TransactionBuilder(stateRef.state.notary) require(stateAndRef.state.encumbrance == null) { "Upgrading an encumbered state is not yet supported" }
.withItems( val legacyConstraint = stateAndRef.state.constraint
stateRef, val legacyContractAttachmentId = when (legacyConstraint) {
StateAndContract(contractUpgrade.upgrade(stateRef.state.data), upgradedContractClass.name), is HashAttachmentConstraint -> legacyConstraint.attachmentId
Command(UpgradeCommand(upgradedContractClass.name), stateRef.state.data.participants.map { it.owningKey }), else -> getContractAttachmentId(stateAndRef.state.contract, services)
privacySalt }
) 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")
} }
} }

View File

@ -7,7 +7,9 @@ import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSession import net.corda.core.flows.FlowSession
import net.corda.core.node.StatesToRecord import net.corda.core.node.StatesToRecord
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.ContractUpgradeWireTransaction
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.exactAdd import net.corda.core.utilities.exactAdd
import java.util.* import java.util.*
@ -157,13 +159,17 @@ class ResolveTransactionsFlow(private val txHashes: Set<SecureHash>,
* Returns a list of all the dependencies of the given transactions, deepest first i.e. the last downloaded comes * 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. * 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 @Suspendable
private fun fetchMissingAttachments(downloads: List<SignedTransaction>) { private fun fetchMissingAttachments(downloads: List<SignedTransaction>) {
// TODO: This could be done in parallel with other fetches for extra speed. val attachments = downloads.map(SignedTransaction::coreTransaction).flatMap { tx ->
val wireTransactions = downloads.filterNot { it.isNotaryChangeTransaction() }.map { it.tx } when (tx) {
val missingAttachments = wireTransactions.flatMap { wtx -> is WireTransaction -> tx.attachments
wtx.attachments.filter { serviceHub.attachments.openAttachment(it) == null } is ContractUpgradeWireTransaction -> listOf(tx.legacyContractAttachmentId, tx.upgradedContractAttachmentId)
else -> emptyList()
}
} }
val missingAttachments = attachments.filter { serviceHub.attachments.openAttachment(it) == null }
if (missingAttachments.isNotEmpty()) if (missingAttachments.isNotEmpty())
subFlow(FetchAttachmentsFlow(missingAttachments.toSet(), otherSide)) subFlow(FetchAttachmentsFlow(missingAttachments.toSet(), otherSide))
} }

View File

@ -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

View File

@ -18,39 +18,12 @@ import java.security.PublicKey
import java.sql.Connection import java.sql.Connection
import java.time.Clock 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<StateRef>): Set<StateAndRef<ContractState>>
}
/** /**
* Subset of node services that are used for loading transactions from the wire into fully resolved, looked up * Subset of node services that are used for loading transactions from the wire into fully resolved, looked up
* forms ready for verification. * 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 * 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 * supports lookup of a party given its key, or name. The service also manages the certificates linking confidential
@ -66,6 +39,27 @@ interface ServicesForResolution : StateLoader {
/** Returns the network parameters the node is operating under. */ /** Returns the network parameters the node is operating under. */
val networkParameters: NetworkParameters 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<StateRef>): Set<StateAndRef<ContractState>>
} }
/** /**

View File

@ -1,10 +1,8 @@
package net.corda.core.node.services package net.corda.core.node.services
import net.corda.core.DoNotImplement import net.corda.core.DoNotImplement
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.messaging.DataFeed import net.corda.core.messaging.DataFeed
import net.corda.core.node.StateLoader
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import rx.Observable import rx.Observable
@ -12,27 +10,12 @@ import rx.Observable
* Thread-safe storage of transactions. * Thread-safe storage of transactions.
*/ */
@DoNotImplement @DoNotImplement
interface TransactionStorage : StateLoader { interface TransactionStorage {
/** /**
* Return the transaction with the given [id], or null if no such transaction exists. * Return the transaction with the given [id], or null if no such transaction exists.
*/ */
fun getTransaction(id: SecureHash): SignedTransaction? 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<StateRef>): Set<StateAndRef<ContractState>> {
return stateRefs.groupBy { it.txhash }.flatMap {
val stx = getTransaction(it.key) ?: throw TransactionResolutionException(it.key)
val baseTx = stx.resolveBaseTransaction(this)
it.value.map { StateAndRef(baseTx.outputs[it.index], it) }
}.toSet()
}
/** /**
* Get a synchronous Observable of updates. When observations are pushed to the Observer, the vault will already * Get a synchronous Observable of updates. When observations are pushed to the Observer, the vault will already
* incorporate the update. * incorporate the update.

View File

@ -46,8 +46,8 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
val produced: Set<StateAndRef<U>>, val produced: Set<StateAndRef<U>>,
val flowId: UUID? = null, val flowId: UUID? = null,
/** /**
* Specifies the type of update, currently supported types are general and notary change. Notary * Specifies the type of update, currently supported types are general and, contract upgrade and notary change.
* change transactions only modify the notary field on states, and potentially need to be handled * Notary change transactions only modify the notary field on states, and potentially need to be handled
* differently. * differently.
*/ */
val type: UpdateType = UpdateType.GENERAL val type: UpdateType = UpdateType.GENERAL
@ -97,11 +97,6 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
} }
} }
companion object {
val NoUpdate = Update(emptySet(), emptySet(), type = Vault.UpdateType.GENERAL)
val NoNotaryUpdate = Vault.Update(emptySet(), emptySet(), type = Vault.UpdateType.NOTARY_CHANGE)
}
@CordaSerializable @CordaSerializable
enum class StateStatus { enum class StateStatus {
UNCONSUMED, CONSUMED, ALL UNCONSUMED, CONSUMED, ALL
@ -109,7 +104,7 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
@CordaSerializable @CordaSerializable
enum class UpdateType { enum class UpdateType {
GENERAL, NOTARY_CHANGE GENERAL, NOTARY_CHANGE, CONTRACT_UPGRADE
} }
/** /**
@ -141,6 +136,13 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
val notary: AbstractParty?, val notary: AbstractParty?,
val lockId: String?, val lockId: String?,
val lockUpdateTime: Instant?) 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)
}
} }
/** /**

View File

@ -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<StateRef>,
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<TransactionState<ContractState>>
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<TransactionSignature>): 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<StateRef>,
override val notary: Party,
val rest: SecureHash
) : CoreTransaction() {
override val id: SecureHash get() = serializedHash(inputs + notary).hashConcat(rest)
override val outputs: List<TransactionState<ContractState>> 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<StateAndRef<ContractState>>,
override val notary: Party,
val legacyContractAttachment: ContractAttachment,
val upgradedContractAttachment: ContractAttachment,
override val id: SecureHash,
val privacySalt: PrivacySalt,
override val sigs: List<TransactionSignature>,
private val networkParameters: NetworkParameters
) : FullTransaction(), TransactionWithSignatures {
private val upgradedContract: UpgradedContract<ContractState, *> = 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<TransactionState<ContractState>> = 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<PublicKey>
get() = inputs.flatMap { it.state.data.participants }.map { it.owningKey }.toSet() + notary.owningKey
override fun getKeyDescriptions(keys: Set<PublicKey>): List<String> {
return keys.map { it.toBase58String() }
}
// TODO: load contract from the CorDapp classloader
private fun loadUpgradedContract(): UpgradedContract<ContractState, *> {
@Suppress("UNCHECKED_CAST")
return this::class.java.classLoader
.loadClass(upgradedContractAttachment.contract)
.asSubclass(Contract::class.java)
.getConstructor()
.newInstance() as UpgradedContract<ContractState, *>
}
}

View File

@ -4,13 +4,11 @@ import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.AttachmentWithContext import net.corda.core.internal.AttachmentWithContext
import net.corda.core.internal.UpgradeCommand
import net.corda.core.internal.castIfPossible import net.corda.core.internal.castIfPossible
import net.corda.core.internal.uncheckedCast import net.corda.core.internal.uncheckedCast
import net.corda.core.node.NetworkParameters import net.corda.core.node.NetworkParameters
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.Try import net.corda.core.utilities.Try
import java.security.PublicKey
import java.util.* import java.util.*
import java.util.function.Predicate import java.util.function.Predicate
@ -79,12 +77,7 @@ data class LedgerTransaction @JvmOverloads constructor(
@Throws(TransactionVerificationException::class) @Throws(TransactionVerificationException::class)
fun verify() { fun verify() {
verifyConstraints() verifyConstraints()
// TODO: make contract upgrade transactions have a separate type verifyContracts()
if (commands.any { it.value is UpgradeCommand }) {
verifyContractUpgrade()
} else {
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<UpgradeCommand>().single()
val command = commandData.value
val participantKeys: Set<PublicKey> = input.data.participants.map { it.owningKey }.toSet()
val keysThatSigned: Set<PublicKey> = commandData.signers.toSet()
@Suppress("UNCHECKED_CAST")
val upgradedContract = javaClass.classLoader.loadClass(command.upgradedContractClass).newInstance() as UpgradedContract<ContractState, *>
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 * 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 * can be processed as one. The grouping key is any arbitrary object that can act as a map key (so must implement
@ -429,3 +403,4 @@ data class LedgerTransaction @JvmOverloads constructor(
privacySalt: PrivacySalt privacySalt: PrivacySalt
) = copy(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, null) ) = copy(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, null)
} }

View File

@ -4,11 +4,11 @@ import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.TransactionSignature
import net.corda.core.crypto.serializedHash import net.corda.core.crypto.serializedHash
import net.corda.core.utilities.toBase58String
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.ServiceHub 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.CordaSerializable
import net.corda.core.utilities.toBase58String
import java.security.PublicKey import java.security.PublicKey
/** /**
@ -27,7 +27,8 @@ data class NotaryChangeWireTransaction(
* [NotaryChangeLedgerTransaction] and applying the notary modification to inputs. * [NotaryChangeLedgerTransaction] and applying the notary modification to inputs.
*/ */
override val outputs: List<TransactionState<ContractState>> override val outputs: List<TransactionState<ContractState>>
get() = emptyList() get() = throw UnsupportedOperationException("NotaryChangeWireTransaction does not contain output states, " +
"outputs can only be obtained from a resolved NotaryChangeLedgerTransaction")
init { init {
check(inputs.isNotEmpty()) { "A notary change transaction must have inputs" } 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) } override val id: SecureHash by lazy { serializedHash(inputs + notary + newNotary) }
fun resolve(services: ServiceHub, sigs: List<TransactionSignature>) = resolve(services as StateLoader, sigs) /** Resolves input states and builds a [NotaryChangeLedgerTransaction]. */
fun resolve(stateLoader: StateLoader, sigs: List<TransactionSignature>): NotaryChangeLedgerTransaction { fun resolve(services: ServicesForResolution, sigs: List<TransactionSignature>) : NotaryChangeLedgerTransaction {
val resolvedInputs = stateLoader.loadStates(inputs.toSet()).toList() val resolvedInputs = services.loadStates(inputs.toSet()).toList()
return NotaryChangeLedgerTransaction(resolvedInputs, notary, newNotary, id, sigs) return NotaryChangeLedgerTransaction(resolvedInputs, notary, newNotary, id, sigs)
} }
/** Resolves input states and builds a [NotaryChangeLedgerTransaction]. */
fun resolve(services: ServiceHub, sigs: List<TransactionSignature>) = resolve(services as ServicesForResolution, sigs)
} }
/** /**

View File

@ -7,7 +7,7 @@ import net.corda.core.crypto.*
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.VisibleForTesting
import net.corda.core.node.ServiceHub 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.CordaSerializable
import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.deserialize import net.corda.core.serialization.deserialize
@ -48,19 +48,18 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
/** Cache the deserialized form of the transaction. This is useful when building a transaction or collecting signatures. */ /** Cache the deserialized form of the transaction. This is useful when building a transaction or collecting signatures. */
@Volatile @Volatile
@Transient private var cachedTransaction: CoreTransaction? = null @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 }
/** The id of the contained [WireTransaction]. */ /** 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. */ /** Lazily calculated access to the deserialised/hashed transaction data. */
val tx: WireTransaction get() = transaction as WireTransaction val coreTransaction: CoreTransaction
get() = cachedTransaction ?: txBits.deserialize().apply { cachedTransaction = this }
/** Returns the contained [NotaryChangeWireTransaction], or throws if this is a normal transaction. */ /** Returns the contained [WireTransaction], or throws if this is a notary change or contract upgrade transaction. */
val notaryChangeTx: NotaryChangeWireTransaction get() = transaction as NotaryChangeWireTransaction val tx: WireTransaction get() = coreTransaction as WireTransaction
/** /**
* Helper function to directly build a [FilteredTransaction] using provided filtering functions, * Helper function to directly build a [FilteredTransaction] using provided filtering functions,
@ -69,9 +68,9 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
fun buildFilteredTransaction(filtering: Predicate<Any>) = tx.buildFilteredTransaction(filtering) fun buildFilteredTransaction(filtering: Predicate<Any>) = tx.buildFilteredTransaction(filtering)
/** Helper to access the inputs of the contained transaction. */ /** Helper to access the inputs of the contained transaction. */
val inputs: List<StateRef> get() = transaction.inputs val inputs: List<StateRef> get() = coreTransaction.inputs
/** Helper to access the notary of the contained transaction. */ /** Helper to access the notary of the contained transaction. */
val notary: Party? get() = transaction.notary val notary: Party? get() = coreTransaction.notary
override val requiredSigningKeys: Set<PublicKey> get() = tx.requiredSigningKeys override val requiredSigningKeys: Set<PublicKey> get() = tx.requiredSigningKeys
@ -162,13 +161,27 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
@JvmOverloads @JvmOverloads
@Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class) @Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class)
fun verify(services: ServiceHub, checkSufficientSignatures: Boolean = true) { fun verify(services: ServiceHub, checkSufficientSignatures: Boolean = true) {
if (isNotaryChangeTransaction()) { when (coreTransaction) {
verifyNotaryChangeTransaction(services, checkSufficientSignatures) is NotaryChangeWireTransaction -> verifyNotaryChangeTransaction(services, checkSufficientSignatures)
} else { is ContractUpgradeWireTransaction -> verifyContractUpgradeTransaction(services, checkSufficientSignatures)
verifyRegularTransaction(services, checkSufficientSignatures) else -> verifyRegularTransaction(services, checkSufficientSignatures)
} }
} }
/** No contract code is run when verifying notary change transactions, it is sufficient to check invariants during initialisation. */
private fun verifyNotaryChangeTransaction(services: ServiceHub, checkSufficientSignatures: Boolean) {
val ntx = resolveNotaryChangeTransaction(services)
if (checkSufficientSignatures) ntx.verifyRequiredSignatures()
else checkSignaturesAreValid()
}
/** No contract code is run when verifying contract upgrade transactions, it is sufficient to check invariants during initialisation. */
private fun verifyContractUpgradeTransaction(services: ServicesForResolution, checkSufficientSignatures: Boolean) {
val ctx = resolveContractUpgradeTransaction(services)
if (checkSufficientSignatures) ctx.verifyRequiredSignatures()
else checkSignaturesAreValid()
}
// TODO: Verify contract constraints here as well as in LedgerTransaction to ensure that anything being deserialised // 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 // from the attachment is trusted. This will require some partial serialisation work to not load the ContractState
// objects from the TransactionState. // objects from the TransactionState.
@ -178,37 +191,32 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
services.transactionVerifierService.verify(ltx).getOrThrow() services.transactionVerifierService.verify(ltx).getOrThrow()
} }
private fun verifyNotaryChangeTransaction(services: ServiceHub, checkSufficientSignatures: Boolean) {
val ntx = resolveNotaryChangeTransaction(services)
if (checkSufficientSignatures) ntx.verifyRequiredSignatures()
else checkSignaturesAreValid()
}
fun isNotaryChangeTransaction() = transaction is NotaryChangeWireTransaction
/** /**
* Resolves the underlying base transaction and then returns it, handling any special case transactions such as * Resolves the underlying base transaction and then returns it, handling any special case transactions such as
* [NotaryChangeWireTransaction]. * [NotaryChangeWireTransaction].
*/ */
fun resolveBaseTransaction(services: StateLoader): BaseTransaction { fun resolveBaseTransaction(servicesForResolution: ServicesForResolution): BaseTransaction {
return when (transaction) { return when (coreTransaction) {
is NotaryChangeWireTransaction -> resolveNotaryChangeTransaction(services) is NotaryChangeWireTransaction -> resolveNotaryChangeTransaction(servicesForResolution)
is ContractUpgradeWireTransaction -> resolveContractUpgradeTransaction(servicesForResolution)
is WireTransaction -> this.tx is WireTransaction -> this.tx
is FilteredTransaction -> throw IllegalStateException("Persistence of filtered transactions is not supported.") 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 * Resolves the underlying transaction with signatures and then returns it, handling any special case transactions
* such as [NotaryChangeWireTransaction]. * such as [NotaryChangeWireTransaction].
*/ */
fun resolveTransactionWithSignatures(services: ServiceHub): TransactionWithSignatures { fun resolveTransactionWithSignatures(services: ServicesForResolution): TransactionWithSignatures {
return when (transaction) { return when (coreTransaction) {
is NotaryChangeWireTransaction -> resolveNotaryChangeTransaction(services) is NotaryChangeWireTransaction -> resolveNotaryChangeTransaction(services)
is ContractUpgradeWireTransaction -> resolveContractUpgradeTransaction(services)
is WireTransaction -> this is WireTransaction -> this
is FilteredTransaction -> throw IllegalStateException("Persistence of filtered transactions is not supported.") is FilteredTransaction -> throw IllegalStateException("Persistence of filtered transactions is not supported.")
else -> throw IllegalStateException("Unknown transaction type ${transaction::class.qualifiedName}") else -> throw IllegalStateException("Unknown transaction type ${coreTransaction::class.qualifiedName}")
} }
} }
@ -216,12 +224,26 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
* If [transaction] is a [NotaryChangeWireTransaction], loads the input states and resolves it to a * If [transaction] is a [NotaryChangeWireTransaction], loads the input states and resolves it to a
* [NotaryChangeLedgerTransaction] so the signatures can be verified. * [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 * If [transaction] is a [NotaryChangeWireTransaction], loads the input states and resolves it to a
?: throw IllegalStateException("Expected a ${NotaryChangeWireTransaction::class.simpleName} but found ${transaction::class.simpleName}") * [NotaryChangeLedgerTransaction] so the signatures can be verified.
return ntx.resolve(stateLoader, sigs) */
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)" override fun toString(): String = "${javaClass.simpleName}(id=$id)"
@ -234,4 +256,14 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
@CordaSerializable @CordaSerializable
class SignaturesMissingException(val missing: Set<PublicKey>, val descriptions: List<String>, override val id: SecureHash) class SignaturesMissingException(val missing: Set<PublicKey>, val descriptions: List<String>, override val id: SecureHash)
: NamedByHash, SignatureException(missingSignatureMsg(missing, descriptions, id)), CordaThrowable by CordaException(missingSignatureMsg(missing, descriptions, id)) : 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
} }

View File

@ -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<ServicesForResolution>().also {
doReturn(rigorousMock<CordappProvider>().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)
}
}

View File

@ -10,6 +10,7 @@ import net.corda.core.messaging.startFlow
import net.corda.core.node.services.queryBy import net.corda.core.node.services.queryBy
import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.finance.USD import net.corda.finance.USD
@ -101,20 +102,12 @@ class ContractUpgradeFlowTest {
val result = resultFuture.getOrThrow() val result = resultFuture.getOrThrow()
fun check(node: StartedNode<MockNode>) { fun check(node: StartedNode<MockNode>) {
val nodeStx = node.database.transaction { val upgradeTx = node.database.transaction {
node.services.validatedTransactions.getTransaction(result.ref.txhash) val wtx = node.services.validatedTransactions.getTransaction(result.ref.txhash)
wtx!!.resolveContractUpgradeTransaction(node.services)
} }
requireNotNull(nodeStx) assertTrue(upgradeTx.inputs.single().state.data is DummyContract.State)
assertTrue(upgradeTx.outputs.single().data is DummyContractV2.State)
// 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)
} }
check(aliceNode) check(aliceNode)
check(bobNode) check(bobNode)
@ -192,16 +185,12 @@ class ContractUpgradeFlowTest {
val result = resultFuture.getOrThrow() val result = resultFuture.getOrThrow()
// Check results. // Check results.
listOf(aliceNode, bobNode).forEach { listOf(aliceNode, bobNode).forEach {
val signedTX = aliceNode.database.transaction { aliceNode.services.validatedTransactions.getTransaction(result.ref.txhash) } val upgradeTx = aliceNode.database.transaction {
requireNotNull(signedTX) val wtx = aliceNode.services.validatedTransactions.getTransaction(result.ref.txhash)
wtx!!.resolveContractUpgradeTransaction(aliceNode.services)
// Verify inputs. }
val input = aliceNode.database.transaction { aliceNode.services.validatedTransactions.getTransaction(signedTX!!.tx.inputs.single().txhash) } assertTrue(upgradeTx.inputs.single().state.data is DummyContract.State)
requireNotNull(input) assertTrue(upgradeTx.outputs.single().data is DummyContractV2.State)
assertTrue(input!!.tx.outputs.single().data is DummyContract.State)
// Verify outputs.
assertTrue(signedTX!!.tx.outputs.single().data is DummyContractV2.State)
} }
} }
} }
@ -222,14 +211,34 @@ class ContractUpgradeFlowTest {
mockNet.runNetwork() mockNet.runNetwork()
upgradeResult.getOrThrow() upgradeResult.getOrThrow()
// Get contract state from the vault. // Get contract state from the vault.
val firstState = aliceNode.database.transaction { aliceNode.services.vaultService.queryBy<ContractState>().states.single() } val upgradedStateFromVault = aliceNode.database.transaction { aliceNode.services.vaultService.queryBy<CashV2.State>().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)), upgradedStateFromVault.state.data.amount, "Upgraded cash contain the correct amount.")
assertEquals(Amount(1000000, USD).`issued by`(chosenIdentity.ref(1)), (firstState.state.data as CashV2.State).amount, "Upgraded cash contain the correct amount.") assertEquals<Collection<AbstractParty>>(listOf(anonymisedRecipient), upgradedStateFromVault.state.data.owners, "Upgraded cash belongs to the right owner.")
assertEquals<Collection<AbstractParty>>(listOf(anonymisedRecipient), (firstState.state.data as CashV2.State).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<CashV2.State>().states.single() }
assertEquals(movedState, movedStateFromVault.state.data)
} }
class CashV2 : UpgradedContract<Cash.State, CashV2.State> { class CashV2 : UpgradedContractWithLegacyConstraint<Cash.State, CashV2.State> {
override val legacyContract = Cash.PROGRAM_ID override val legacyContract = Cash.PROGRAM_ID
override val legacyContractConstraint: AttachmentConstraint
get() = AlwaysAcceptAttachmentConstraint
class Move : TypeOnlyCommandData()
data class State(override val amount: Amount<Issued<Currency>>, val owners: List<AbstractParty>) : FungibleAsset<Currency> { data class State(override val amount: Amount<Issued<Currency>>, val owners: List<AbstractParty>) : FungibleAsset<Currency> {
override val owner: AbstractParty = owners.first() override val owner: AbstractParty = owners.first()

View File

@ -16,6 +16,7 @@ class VaultUpdateTests {
private companion object { private companion object {
val DUMMY_PROGRAM_ID = "net.corda.core.node.VaultUpdateTests.DummyContract" val DUMMY_PROGRAM_ID = "net.corda.core.node.VaultUpdateTests.DummyContract"
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
val emptyUpdate = Vault.Update(emptySet(), emptySet(), type = Vault.UpdateType.GENERAL)
} }
object DummyContract : Contract { object DummyContract : Contract {
@ -42,21 +43,21 @@ class VaultUpdateTests {
@Test @Test
fun `nothing plus nothing is nothing`() { fun `nothing plus nothing is nothing`() {
val before = Vault.NoUpdate val before = emptyUpdate
val after = before + Vault.NoUpdate val after = before + emptyUpdate
assertEquals(before, after) assertEquals(before, after)
} }
@Test @Test
fun `something plus nothing is something`() { fun `something plus nothing is something`() {
val before = Vault.Update<ContractState>(setOf(stateAndRef0, stateAndRef1), setOf(stateAndRef2, stateAndRef3)) val before = Vault.Update<ContractState>(setOf(stateAndRef0, stateAndRef1), setOf(stateAndRef2, stateAndRef3))
val after = before + Vault.NoUpdate val after = before + emptyUpdate
assertEquals(before, after) assertEquals(before, after)
} }
@Test @Test
fun `nothing plus something is something`() { fun `nothing plus something is something`() {
val before = Vault.NoUpdate val before = emptyUpdate
val after = before + Vault.Update<ContractState>(setOf(stateAndRef0, stateAndRef1), setOf(stateAndRef2, stateAndRef3)) val after = before + Vault.Update<ContractState>(setOf(stateAndRef0, stateAndRef1), setOf(stateAndRef2, stateAndRef3))
val expected = Vault.Update<ContractState>(setOf(stateAndRef0, stateAndRef1), setOf(stateAndRef2, stateAndRef3)) val expected = Vault.Update<ContractState>(setOf(stateAndRef0, stateAndRef1), setOf(stateAndRef2, stateAndRef3))
assertEquals(expected, after) assertEquals(expected, after)

View File

@ -320,6 +320,23 @@ upgraded contracts must implement the ``UpgradedContract`` interface. This inter
The ``upgrade`` method describes how the old state type is upgraded to the new state type. When the state isn't being The ``upgrade`` method describes how the old state type is upgraded to the new state type. When the state isn't being
upgraded, the same state type can be used for both the old and new state type parameters. upgraded, the same state type can be used for both the old and new state type parameters.
By default this new contract will only be able to upgrade legacy states which are constrained by the zone whitelist (see :doc:`api-contract-constraints`).
If hash or other constraint types are used, the new contract should implement ``UpgradedContractWithLegacyConstraint``
instead, and specify the constraint explicitly:
.. sourcecode:: kotlin
interface UpgradedContractWithLegacyConstraint<in OldState : ContractState, out NewState : ContractState> : UpgradedContract<OldState, NewState> {
val legacyContractConstraint: AttachmentConstraint
}
For example, in case of hash constraints the hash of the legacy JAR file should be provided:
.. sourcecode:: kotlin
override val legacyContractConstraint: AttachmentConstraint
get() = HashAttachmentConstraint(SecureHash.parse("E02BD2B9B010BBCE49C0D7C35BECEF2C79BEB2EE80D902B54CC9231418A4FA0C"))
Authorising the upgrade Authorising the upgrade
^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^
Once the new states and contracts are on the classpath for all the relevant nodes, the next step is for all nodes to Once the new states and contracts are on the classpath for all the relevant nodes, the next step is for all nodes to

View File

@ -22,6 +22,7 @@ import net.corda.core.serialization.MissingAttachmentsException
import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.SerializationWhitelist
import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SerializeAsToken
import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.SerializedBytes
import net.corda.core.transactions.ContractUpgradeWireTransaction
import net.corda.core.transactions.NotaryChangeWireTransaction import net.corda.core.transactions.NotaryChangeWireTransaction
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction import net.corda.core.transactions.WireTransaction
@ -127,6 +128,7 @@ object DefaultKryoCustomizer {
register(java.lang.invoke.SerializedLambda::class.java) register(java.lang.invoke.SerializedLambda::class.java)
register(ClosureSerializer.Closure::class.java, CordaClosureBlacklistSerializer) register(ClosureSerializer.Closure::class.java, CordaClosureBlacklistSerializer)
register(ContractUpgradeWireTransaction::class.java, ContractUpgradeWireTransactionSerializer)
for (whitelistProvider in serializationWhitelists) { for (whitelistProvider in serializationWhitelists) {
val types = whitelistProvider.whitelist val types = whitelistProvider.whitelist

View File

@ -8,9 +8,12 @@ import com.esotericsoftware.kryo.serializers.CompatibleFieldSerializer
import com.esotericsoftware.kryo.serializers.FieldSerializer import com.esotericsoftware.kryo.serializers.FieldSerializer
import com.esotericsoftware.kryo.util.MapReferenceResolver import com.esotericsoftware.kryo.util.MapReferenceResolver
import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.CordaFuture
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.PrivacySalt import net.corda.core.contracts.PrivacySalt
import net.corda.core.contracts.StateRef import net.corda.core.contracts.StateRef
import net.corda.core.contracts.TransactionState
import net.corda.core.crypto.Crypto import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.TransactionSignature
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.uncheckedCast import net.corda.core.internal.uncheckedCast
@ -265,6 +268,29 @@ object NotaryChangeWireTransactionSerializer : Serializer<NotaryChangeWireTransa
} }
} }
@ThreadSafe
object ContractUpgradeWireTransactionSerializer : Serializer<ContractUpgradeWireTransaction>() {
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>): ContractUpgradeWireTransaction {
val inputs: List<StateRef> = 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 @ThreadSafe
object SignedTransactionSerializer : Serializer<SignedTransaction>() { object SignedTransactionSerializer : Serializer<SignedTransaction>() {
override fun write(kryo: Kryo, output: Output, obj: SignedTransaction) { override fun write(kryo: Kryo, output: Output, obj: SignedTransaction) {

View File

@ -208,17 +208,24 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
val networkMapCache = NetworkMapCacheImpl(PersistentNetworkMapCache(database, networkParameters.notaries).start(), identityService) val networkMapCache = NetworkMapCacheImpl(PersistentNetworkMapCache(database, networkParameters.notaries).start(), identityService)
val (keyPairs, nodeInfo) = initNodeInfo(networkMapCache, identity, identityKeyPair) val (keyPairs, nodeInfo) = initNodeInfo(networkMapCache, identity, identityKeyPair)
identityService.loadIdentities(nodeInfo.legalIdentitiesAndCerts) identityService.loadIdentities(nodeInfo.legalIdentitiesAndCerts)
val metrics = MetricRegistry()
val transactionStorage = makeTransactionStorage(database, configuration.transactionCacheSizeBytes) val transactionStorage = makeTransactionStorage(database, configuration.transactionCacheSizeBytes)
attachments = NodeAttachmentService(metrics, configuration.attachmentContentCacheSizeBytes, configuration.attachmentCacheBound)
val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(), attachments, networkParameters.whitelistedContractImplementations)
val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParameters, transactionStorage)
val nodeProperties = NodePropertiesPersistentStore(StubbedNodeUniqueIdProvider::value, database) val nodeProperties = NodePropertiesPersistentStore(StubbedNodeUniqueIdProvider::value, database)
val nodeServices = makeServices( val nodeServices = makeServices(
keyPairs, keyPairs,
schemaService, schemaService,
transactionStorage, transactionStorage,
metrics,
servicesForResolution,
database, database,
nodeInfo, nodeInfo,
identityService, identityService,
networkMapCache, networkMapCache,
nodeProperties, nodeProperties,
cordappProvider,
networkParameters) networkParameters)
val notaryService = makeNotaryService(nodeServices, database) val notaryService = makeNotaryService(nodeServices, database)
val smm = makeStateMachineManager(database) val smm = makeStateMachineManager(database)
@ -228,7 +235,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
platformClock, platformClock,
database, database,
flowStarter, flowStarter,
transactionStorage, servicesForResolution,
unfinishedSchedules = busyNodeLatch, unfinishedSchedules = busyNodeLatch,
serverThread = serverThread, serverThread = serverThread,
flowLogicRefFactory = flowLogicRefFactory, flowLogicRefFactory = flowLogicRefFactory,
@ -547,16 +554,17 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
private fun makeServices(keyPairs: Set<KeyPair>, private fun makeServices(keyPairs: Set<KeyPair>,
schemaService: SchemaService, schemaService: SchemaService,
transactionStorage: WritableTransactionStorage, transactionStorage: WritableTransactionStorage,
metrics: MetricRegistry,
servicesForResolution: ServicesForResolution,
database: CordaPersistence, database: CordaPersistence,
nodeInfo: NodeInfo, nodeInfo: NodeInfo,
identityService: IdentityServiceInternal, identityService: IdentityServiceInternal,
networkMapCache: NetworkMapCacheInternal, networkMapCache: NetworkMapCacheInternal,
nodeProperties: NodePropertiesStore, nodeProperties: NodePropertiesStore,
cordappProvider: CordappProviderInternal,
networkParameters: NetworkParameters): MutableList<Any> { networkParameters: NetworkParameters): MutableList<Any> {
checkpointStorage = DBCheckpointStorage() checkpointStorage = DBCheckpointStorage()
val metrics = MetricRegistry()
attachments = NodeAttachmentService(metrics, configuration.attachmentContentCacheSizeBytes, configuration.attachmentCacheBound)
val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(), attachments, networkParameters.whitelistedContractImplementations)
val keyManagementService = makeKeyManagementService(identityService, keyPairs) val keyManagementService = makeKeyManagementService(identityService, keyPairs)
_services = ServiceHubInternalImpl( _services = ServiceHubInternalImpl(
identityService, identityService,
@ -569,7 +577,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
nodeInfo, nodeInfo,
networkMapCache, networkMapCache,
nodeProperties, nodeProperties,
networkParameters) networkParameters,
servicesForResolution)
network = makeMessagingService(database, nodeInfo, nodeProperties, networkParameters) network = makeMessagingService(database, nodeInfo, nodeProperties, networkParameters)
val tokenizableServices = mutableListOf(attachments, network, services.vaultService, val tokenizableServices = mutableListOf(attachments, network, services.vaultService,
services.keyManagementService, services.identityService, platformClock, services.keyManagementService, services.identityService, platformClock,
@ -760,8 +769,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
} }
protected open fun generateKeyPair() = cryptoGenerateKeyPair() protected open fun generateKeyPair() = cryptoGenerateKeyPair()
protected open fun makeVaultService(keyManagementService: KeyManagementService, stateLoader: StateLoader, hibernateConfig: HibernateConfiguration): VaultServiceInternal { protected open fun makeVaultService(keyManagementService: KeyManagementService, services: ServicesForResolution, hibernateConfig: HibernateConfiguration): VaultServiceInternal {
return NodeVaultService(platformClock, keyManagementService, stateLoader, hibernateConfig) return NodeVaultService(platformClock, keyManagementService, services, hibernateConfig)
} }
/** Load configured JVM agents */ /** Load configured JVM agents */
@ -794,13 +803,14 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
override val myInfo: NodeInfo, override val myInfo: NodeInfo,
override val networkMapCache: NetworkMapCacheInternal, override val networkMapCache: NetworkMapCacheInternal,
override val nodeProperties: NodePropertiesStore, override val nodeProperties: NodePropertiesStore,
override val networkParameters: NetworkParameters override val networkParameters: NetworkParameters,
) : SingletonSerializeAsToken(), ServiceHubInternal, StateLoader by validatedTransactions { private val servicesForResolution: ServicesForResolution
) : SingletonSerializeAsToken(), ServiceHubInternal, ServicesForResolution by servicesForResolution {
override val rpcFlows = ArrayList<Class<out FlowLogic<*>>>() override val rpcFlows = ArrayList<Class<out FlowLogic<*>>>()
override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage() override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage()
override val auditService = DummyAuditService() override val auditService = DummyAuditService()
override val transactionVerifierService by lazy { makeTransactionVerifierService() } 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 contractUpgradeService by lazy { ContractUpgradeServiceImpl() }
override val attachments: AttachmentStorage get() = this@AbstractNode.attachments override val attachments: AttachmentStorage get() = this@AbstractNode.attachments
override val networkService: MessagingService get() = network override val networkService: MessagingService get() = network

View File

@ -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<StateRef>): Set<StateAndRef<ContractState>> {
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()
}
}

View File

@ -4,6 +4,7 @@ import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner
import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult
import net.corda.core.contracts.Contract import net.corda.core.contracts.Contract
import net.corda.core.contracts.UpgradedContract import net.corda.core.contracts.UpgradedContract
import net.corda.core.contracts.UpgradedContractWithLegacyConstraint
import net.corda.core.cordapp.Cordapp import net.corda.core.cordapp.Cordapp
import net.corda.core.flows.* import net.corda.core.flows.*
import net.corda.core.internal.* import net.corda.core.internal.*
@ -241,7 +242,13 @@ class CordappLoader private constructor(private val cordappJarPaths: List<Restri
} }
private fun findContractClassNames(scanResult: RestrictedScanResult): List<String> { private fun findContractClassNames(scanResult: RestrictedScanResult): List<String> {
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<SerializationWhitelist> { private fun findPlugins(cordappJarPath: RestrictedURL): List<SerializationWhitelist> {

View File

@ -7,6 +7,7 @@ import net.corda.core.contracts.requireThat
import net.corda.core.flows.* import net.corda.core.flows.*
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.ContractUpgradeUtils import net.corda.core.internal.ContractUpgradeUtils
import net.corda.core.transactions.ContractUpgradeWireTransaction
import net.corda.core.node.StatesToRecord import net.corda.core.node.StatesToRecord
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
@ -56,13 +57,13 @@ class ContractUpgradeHandler(otherSide: FlowSession) : AbstractStateReplacementF
val oldStateAndRef = ourSTX!!.tx.outRef<ContractState>(proposal.stateRef.index) val oldStateAndRef = ourSTX!!.tx.outRef<ContractState>(proposal.stateRef.index)
val authorisedUpgrade = serviceHub.contractUpgradeService.getAuthorisedContractUpgrade(oldStateAndRef.ref) ?: val authorisedUpgrade = serviceHub.contractUpgradeService.getAuthorisedContractUpgrade(oldStateAndRef.ref) ?:
throw IllegalStateException("Contract state upgrade is unauthorised. State hash : ${oldStateAndRef.ref}") throw IllegalStateException("Contract state upgrade is unauthorised. State hash : ${oldStateAndRef.ref}")
val proposedTx = stx.tx val proposedTx = stx.coreTransaction as ContractUpgradeWireTransaction
val expectedTx = ContractUpgradeUtils.assembleBareTx(oldStateAndRef, proposal.modification, proposedTx.privacySalt).toWireTransaction(serviceHub) val expectedTx = ContractUpgradeUtils.assembleUpgradeTx(oldStateAndRef, proposal.modification, proposedTx.privacySalt, serviceHub)
requireThat { requireThat {
"The instigator is one of the participants" using (initiatingSession.counterparty in oldStateAndRef.state.data.participants) "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 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) "The proposed tx matches the expected tx for this upgrade" using (proposedTx == expectedTx)
} }
proposedTx.toLedgerTransaction(serviceHub).verify() proposedTx.resolve(serviceHub, stx.sigs)
} }
} }

View File

@ -81,7 +81,6 @@ interface ServiceHubInternal : ServiceHub {
} }
if (statesToRecord != StatesToRecord.NONE) { 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 // 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 // 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). // 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 // 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. // 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 })
} }
} }

View File

@ -16,7 +16,7 @@ import net.corda.core.internal.VisibleForTesting
import net.corda.core.internal.concurrent.flatMap import net.corda.core.internal.concurrent.flatMap
import net.corda.core.internal.join import net.corda.core.internal.join
import net.corda.core.internal.until 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.schemas.PersistentStateRef
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.utilities.contextLogger 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, class NodeSchedulerService(private val clock: CordaClock,
private val database: CordaPersistence, private val database: CordaPersistence,
private val flowStarter: FlowStarter, private val flowStarter: FlowStarter,
private val stateLoader: StateLoader, private val servicesForResolution: ServicesForResolution,
private val unfinishedSchedules: ReusableLatch = ReusableLatch(), private val unfinishedSchedules: ReusableLatch = ReusableLatch(),
private val serverThread: Executor, private val serverThread: Executor,
private val flowLogicRefFactory: FlowLogicRefFactory, private val flowLogicRefFactory: FlowLogicRefFactory,
@ -311,7 +311,7 @@ class NodeSchedulerService(private val clock: CordaClock,
} }
private fun getScheduledActivity(scheduledState: ScheduledStateRef): ScheduledActivity? { private fun getScheduledActivity(scheduledState: ScheduledStateRef): ScheduledActivity? {
val txState = stateLoader.loadState(scheduledState.ref) val txState = servicesForResolution.loadState(scheduledState.ref)
val state = txState.data as SchedulableState val state = txState.data as SchedulableState
return try { return try {
// This can throw as running contract code. // This can throw as running contract code.

View File

@ -10,6 +10,7 @@ import net.corda.core.flows.NotarisationRequest
import net.corda.core.internal.validateRequest import net.corda.core.internal.validateRequest
import net.corda.core.node.services.TrustedAuthorityNotaryService import net.corda.core.node.services.TrustedAuthorityNotaryService
import net.corda.core.transactions.CoreTransaction import net.corda.core.transactions.CoreTransaction
import net.corda.core.transactions.ContractUpgradeFilteredTransaction
import net.corda.core.transactions.FilteredTransaction import net.corda.core.transactions.FilteredTransaction
import net.corda.core.transactions.NotaryChangeWireTransaction import net.corda.core.transactions.NotaryChangeWireTransaction
import net.corda.core.utilities.unwrap import net.corda.core.utilities.unwrap
@ -44,6 +45,7 @@ class NonValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAut
val notary = tx.notary val notary = tx.notary
TransactionParts(tx.id, tx.inputs, tx.timeWindow, 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) is NotaryChangeWireTransaction -> TransactionParts(tx.id, tx.inputs, null, tx.notary)
else -> { else -> {
throw IllegalArgumentException("Received unexpected transaction type: ${tx::class.java.simpleName}," + throw IllegalArgumentException("Received unexpected transaction type: ${tx::class.java.simpleName}," +

View File

@ -4,13 +4,12 @@ import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.TimeWindow import net.corda.core.contracts.TimeWindow
import net.corda.core.contracts.TransactionVerificationException import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.flows.* 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.ResolveTransactionsFlow
import net.corda.core.internal.validateRequest import net.corda.core.internal.validateRequest
import net.corda.core.node.services.TrustedAuthorityNotaryService import net.corda.core.node.services.TrustedAuthorityNotaryService
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionWithSignatures import net.corda.core.transactions.TransactionWithSignatures
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.unwrap import net.corda.core.utilities.unwrap
import java.security.SignatureException import java.security.SignatureException
@ -31,12 +30,9 @@ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthor
val stx = receiveTransaction() val stx = receiveTransaction()
val notary = stx.notary val notary = stx.notary
checkNotary(notary) checkNotary(notary)
val timeWindow: TimeWindow? = if (stx.isNotaryChangeTransaction())
null
else
stx.tx.timeWindow
resolveAndContractVerify(stx) resolveAndContractVerify(stx)
verifySignatures(stx) verifySignatures(stx)
val timeWindow: TimeWindow? = if (stx.coreTransaction is WireTransaction) stx.tx.timeWindow else null
return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!) return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!)
} catch (e: Exception) { } catch (e: Exception) {
throw when (e) { throw when (e) {

View File

@ -6,15 +6,13 @@ import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.internal.* import net.corda.core.internal.*
import net.corda.core.messaging.DataFeed 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.StatesToRecord
import net.corda.core.node.services.* import net.corda.core.node.services.*
import net.corda.core.node.services.vault.* import net.corda.core.node.services.vault.*
import net.corda.core.schemas.PersistentStateRef import net.corda.core.schemas.PersistentStateRef
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.transactions.CoreTransaction import net.corda.core.transactions.*
import net.corda.core.transactions.NotaryChangeWireTransaction
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.* import net.corda.core.utilities.*
import net.corda.node.services.api.VaultServiceInternal import net.corda.node.services.api.VaultServiceInternal
import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.statemachine.FlowStateMachineImpl
@ -49,7 +47,7 @@ private fun CriteriaBuilder.executeUpdate(session: Session, configure: Root<*>.(
class NodeVaultService( class NodeVaultService(
private val clock: Clock, private val clock: Clock,
private val keyManagementService: KeyManagementService, private val keyManagementService: KeyManagementService,
private val stateLoader: StateLoader, private val servicesForResolution: ServicesForResolution,
hibernateConfig: HibernateConfiguration hibernateConfig: HibernateConfiguration
) : SingletonSerializeAsToken(), VaultServiceInternal { ) : SingletonSerializeAsToken(), VaultServiceInternal {
private companion object { private companion object {
@ -108,41 +106,29 @@ class NodeVaultService(
override val updates: Observable<Vault.Update<ContractState>> override val updates: Observable<Vault.Update<ContractState>>
get() = mutex.locked { _updatesInDbTx } get() = mutex.locked { _updatesInDbTx }
/** Groups adjacent transactions into batches to generate separate net updates per transaction type. */
override fun notifyAll(statesToRecord: StatesToRecord, txns: Iterable<CoreTransaction>) { override fun notifyAll(statesToRecord: StatesToRecord, txns: Iterable<CoreTransaction>) {
if (statesToRecord == StatesToRecord.NONE) if (statesToRecord == StatesToRecord.NONE || !txns.any()) return
return val batch = mutableListOf<CoreTransaction>()
// It'd be easier to just group by type, but then we'd lose ordering. fun flushBatch() {
val regularTxns = mutableListOf<WireTransaction>() val updates = makeUpdates(batch, statesToRecord)
val notaryChangeTxns = mutableListOf<NotaryChangeWireTransaction>() processAndNotify(updates)
batch.clear()
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()
}
}
}
} }
if (regularTxns.isNotEmpty()) notifyRegular(regularTxns.toList(), statesToRecord) for (tx in txns) {
if (notaryChangeTxns.isNotEmpty()) notifyNotaryChange(notaryChangeTxns.toList(), statesToRecord) if (batch.isNotEmpty() && tx.javaClass != batch.last().javaClass) {
flushBatch()
}
batch.add(tx)
}
flushBatch()
} }
private fun notifyRegular(txns: Iterable<WireTransaction>, statesToRecord: StatesToRecord) { private fun makeUpdates(batch: Iterable<CoreTransaction>, statesToRecord: StatesToRecord): List<Vault.Update<ContractState>> {
fun makeUpdate(tx: WireTransaction): Vault.Update<ContractState> { fun makeUpdate(tx: WireTransaction): Vault.Update<ContractState>? {
val myKeys = keyManagementService.filterMyKeys(tx.outputs.flatMap { it.data.participants.map { it.owningKey } }) val myKeys = keyManagementService.filterMyKeys(tx.outputs.flatMap { it.data.participants.map { it.owningKey } })
val ourNewStates = when (statesToRecord) { val ourNewStates = when (statesToRecord) {
StatesToRecord.NONE -> throw AssertionError("Should not reach here") StatesToRecord.NONE -> throw AssertionError("Should not reach here")
StatesToRecord.ONLY_RELEVANT -> tx.outputs.filter { isRelevant(it.data, myKeys.toSet()) } StatesToRecord.ONLY_RELEVANT -> tx.outputs.filter { isRelevant(it.data, myKeys.toSet()) }
@ -155,45 +141,48 @@ class NodeVaultService(
// Is transaction irrelevant? // Is transaction irrelevant?
if (consumedStates.isEmpty() && ourNewStates.isEmpty()) { if (consumedStates.isEmpty() && ourNewStates.isEmpty()) {
log.trace { "tx ${tx.id} was irrelevant to this vault, ignoring" } log.trace { "tx ${tx.id} was irrelevant to this vault, ignoring" }
return Vault.NoUpdate return null
} }
return Vault.Update(consumedStates.toSet(), ourNewStates.toSet()) return Vault.Update(consumedStates.toSet(), ourNewStates.toSet())
} }
val netDelta = txns.fold(Vault.NoUpdate) { netDelta, txn -> netDelta + makeUpdate(txn) } fun resolveAndMakeUpdate(tx: CoreTransaction): Vault.Update<ContractState>? {
processAndNotify(netDelta)
}
private fun notifyNotaryChange(txns: Iterable<NotaryChangeWireTransaction>, statesToRecord: StatesToRecord) {
fun makeUpdate(tx: NotaryChangeWireTransaction): Vault.Update<ContractState> {
// We need to resolve the full transaction here because outputs are calculated from inputs // 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 // We also can't do filtering beforehand, since for notary change transactions output encumbrance pointers
// input positions // get recalculated based on input positions.
val ltx = tx.resolve(stateLoader, emptyList()) 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 myKeys = keyManagementService.filterMyKeys(ltx.outputs.flatMap { it.data.participants.map { it.owningKey } })
val (consumedStateAndRefs, producedStates) = ltx.inputs. val (consumedStateAndRefs, producedStates) = ltx.inputs.
zip(ltx.outputs). zip(ltx.outputs).
filter { (_, output) -> filter { (_, output) ->
if (statesToRecord == StatesToRecord.ONLY_RELEVANT) if (statesToRecord == StatesToRecord.ONLY_RELEVANT) isRelevant(output.data, myKeys.toSet())
isRelevant(output.data, myKeys.toSet()) else true
else
true
}. }.
unzip() unzip()
val producedStateAndRefs = producedStates.map { ltx.outRef<ContractState>(it.data) } val producedStateAndRefs = producedStates.map { ltx.outRef<ContractState>(it.data) }
if (consumedStateAndRefs.isEmpty() && producedStateAndRefs.isEmpty()) { if (consumedStateAndRefs.isEmpty() && producedStateAndRefs.isEmpty()) {
log.trace { "tx ${tx.id} was irrelevant to this vault, ignoring" } 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<StateRef>): Collection<StateAndRef<ContractState>> { private fun loadStates(refs: Collection<StateRef>): Collection<StateAndRef<ContractState>> {
@ -202,13 +191,15 @@ class NodeVaultService(
else emptySet() else emptySet()
} }
private fun processAndNotify(update: Vault.Update<ContractState>) { private fun processAndNotify(updates: List<Vault.Update<ContractState>>) {
if (!update.isEmpty()) { if (updates.isEmpty()) return
recordUpdate(update) val netUpdate = updates.reduce { update1, update2 -> update1 + update2 }
if (!netUpdate.isEmpty()) {
recordUpdate(netUpdate)
mutex.locked { mutex.locked {
// flowId required by SoftLockManager to perform auto-registration of soft locks for new states // flowId required by SoftLockManager to perform auto-registration of soft locks for new states
val uuid = (Strand.currentStrand() as? FlowStateMachineImpl<*>)?.id?.uuid 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) updatesPublisher.onNext(vaultUpdate)
} }
} }
@ -457,7 +448,7 @@ class NodeVaultService(
} }
} }
if (stateRefs.isNotEmpty()) if (stateRefs.isNotEmpty())
statesAndRefs.addAll(stateLoader.loadStates(stateRefs) as Collection<StateAndRef<T>>) statesAndRefs.addAll(servicesForResolution.loadStates(stateRefs) as Collection<StateAndRef<T>>)
return Vault.Page(states = statesAndRefs, statesMetadata = statesMeta, stateTypes = criteriaParser.stateTypes, totalStatesAvailable = totalStates, otherResults = otherResults) return Vault.Page(states = statesAndRefs, statesMetadata = statesMeta, stateTypes = criteriaParser.stateTypes, totalStatesAvailable = totalStates, otherResults = otherResults)
} catch (e: java.lang.Exception) { } catch (e: java.lang.Exception) {

View File

@ -9,7 +9,7 @@ import net.corda.core.flows.FlowLogicRefFactory
import net.corda.core.internal.FlowStateMachine import net.corda.core.internal.FlowStateMachine
import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.concurrent.openFuture
import net.corda.core.internal.uncheckedCast 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.core.utilities.days
import net.corda.node.services.api.FlowStarter import net.corda.node.services.api.FlowStarter
import net.corda.node.services.api.NodePropertiesStore import net.corda.node.services.api.NodePropertiesStore
@ -48,7 +48,7 @@ class NodeSchedulerServiceTest {
doReturn(flowsDraingMode).whenever(it).flowsDrainingMode doReturn(flowsDraingMode).whenever(it).flowsDrainingMode
} }
private val transactionStates = mutableMapOf<StateRef, TransactionState<*>>() private val transactionStates = mutableMapOf<StateRef, TransactionState<*>>()
private val stateLoader = rigorousMock<StateLoader>().also { private val servicesForResolution = rigorousMock<ServicesForResolution>().also {
doLookup(transactionStates).whenever(it).loadState(any()) doLookup(transactionStates).whenever(it).loadState(any())
} }
private val flows = mutableMapOf<FlowLogicRef, FlowLogic<*>>() private val flows = mutableMapOf<FlowLogicRef, FlowLogic<*>>()
@ -63,7 +63,7 @@ class NodeSchedulerServiceTest {
testClock, testClock,
database, database,
flowStarter, flowStarter,
stateLoader, servicesForResolution,
serverThread = MoreExecutors.directExecutor(), serverThread = MoreExecutors.directExecutor(),
flowLogicRefFactory = flowLogicRefFactory, flowLogicRefFactory = flowLogicRefFactory,
nodeProperties = nodeProperties, nodeProperties = nodeProperties,

View File

@ -118,7 +118,7 @@ class HibernateConfigurationTest {
services = object : MockServices(cordappPackages, BOB_NAME, rigorousMock<IdentityServiceInternal>().also { services = object : MockServices(cordappPackages, BOB_NAME, rigorousMock<IdentityServiceInternal>().also {
doNothing().whenever(it).justVerifyAndRegisterIdentity(argThat { name == BOB_NAME }) doNothing().whenever(it).justVerifyAndRegisterIdentity(argThat { name == BOB_NAME })
}, generateKeyPair(), dummyNotary.keyPair) { }, 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<SignedTransaction>) { override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable<SignedTransaction>) {
for (stx in txs) { for (stx in txs) {
validatedTransactions.addTransaction(stx) validatedTransactions.addTransaction(stx)

View File

@ -11,7 +11,7 @@ import net.corda.core.identity.AbstractParty
import net.corda.core.internal.FlowStateMachine import net.corda.core.internal.FlowStateMachine
import net.corda.core.internal.packageName import net.corda.core.internal.packageName
import net.corda.core.internal.uncheckedCast 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.KeyManagementService
import net.corda.core.node.services.queryBy import net.corda.core.node.services.queryBy
import net.corda.core.node.services.vault.QueryCriteria.SoftLockingCondition 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 -> private val mockNet = InternalMockNetwork(cordappPackages = listOf(ContractImpl::class.packageName), defaultFactory = { args ->
object : InternalMockNetwork.MockNode(args) { object : InternalMockNetwork.MockNode(args) {
override fun makeVaultService(keyManagementService: KeyManagementService, stateLoader: StateLoader, hibernateConfig: HibernateConfiguration): VaultServiceInternal { override fun makeVaultService(keyManagementService: KeyManagementService, services: ServicesForResolution, hibernateConfig: HibernateConfiguration): VaultServiceInternal {
val realVault = super.makeVaultService(keyManagementService, stateLoader, hibernateConfig) val realVault = super.makeVaultService(keyManagementService, services, hibernateConfig)
return object : VaultServiceInternal by realVault { return object : VaultServiceInternal by realVault {
override fun softLockRelease(lockId: UUID, stateRefs: NonEmptySet<StateRef>?) { override fun softLockRelease(lockId: UUID, stateRefs: NonEmptySet<StateRef>?) {
mockVault.softLockRelease(lockId, stateRefs) // No need to also call the real one for these tests. mockVault.softLockRelease(lockId, stateRefs) // No need to also call the real one for these tests.

View File

@ -11,8 +11,10 @@ import net.corda.core.internal.validateRequest
import net.corda.core.node.AppServiceHub import net.corda.core.node.AppServiceHub
import net.corda.core.node.services.CordaService import net.corda.core.node.services.CordaService
import net.corda.core.node.services.TrustedAuthorityNotaryService import net.corda.core.node.services.TrustedAuthorityNotaryService
import net.corda.core.transactions.CoreTransaction
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionWithSignatures import net.corda.core.transactions.TransactionWithSignatures
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.unwrap import net.corda.core.utilities.unwrap
import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.node.services.transactions.PersistentUniquenessProvider
import java.security.PublicKey import java.security.PublicKey
@ -49,12 +51,9 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating
val stx = receiveTransaction() val stx = receiveTransaction()
val notary = stx.notary val notary = stx.notary
checkNotary(notary) checkNotary(notary)
val timeWindow: TimeWindow? = if (stx.isNotaryChangeTransaction())
null
else
stx.tx.timeWindow
resolveAndContractVerify(stx)
verifySignatures(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!!) return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!)
} catch (e: Exception) { } catch (e: Exception) {
throw when (e) { throw when (e) {

View File

@ -2,6 +2,9 @@ package net.corda.testing.node
import com.google.common.collect.MutableClassToInstanceMap import com.google.common.collect.MutableClassToInstanceMap
import net.corda.core.contracts.ContractClassName 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.cordapp.CordappProvider
import net.corda.core.crypto.* import net.corda.core.crypto.*
import net.corda.core.flows.FlowLogic 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.transactions.SignedTransaction
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.node.VersionInfo import net.corda.node.VersionInfo
import net.corda.node.internal.ServicesForResolutionImpl
import net.corda.node.internal.configureDatabase import net.corda.node.internal.configureDatabase
import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappLoader
import net.corda.node.services.api.SchemaService import net.corda.node.services.api.SchemaService
@ -65,8 +69,7 @@ open class MockServices private constructor(
final override val networkParameters: NetworkParameters, final override val networkParameters: NetworkParameters,
private val initialIdentity: TestIdentity, private val initialIdentity: TestIdentity,
private val moreKeys: Array<out KeyPair> private val moreKeys: Array<out KeyPair>
) : ServiceHub, StateLoader by validatedTransactions { ) : ServiceHub {
companion object { companion object {
@JvmStatic @JvmStatic
val MOCK_VERSION_INFO = VersionInfo(1, "Mock release", "Mock revision", "Mock Vendor") 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<SignedTransaction>) { override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable<SignedTransaction>) {
super.recordTransactions(statesToRecord, txs) super.recordTransactions(statesToRecord, txs)
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions. // 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() 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. * Create a mock [ServiceHub] that can't load CorDapp code, and which uses a default service identity.
*/ */
constructor(cordappPackages: List<String>): this(cordappPackages, CordaX500Name("TestIdentity", "", "GB"), makeTestIdentityService()) constructor(cordappPackages: List<String>) : 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 * 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 * Create a mock [ServiceHub] which uses the package of the caller to find CorDapp code. It uses a default service
* identity. * identity.
*/ */
constructor(): this(listOf(getCallerPackage()), CordaX500Name("TestIdentity", "", "GB"), makeTestIdentityService()) constructor() : this(listOf(getCallerPackage()), CordaX500Name("TestIdentity", "", "GB"), makeTestIdentityService())
override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable<SignedTransaction>) { override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable<SignedTransaction>) {
txs.forEach { txs.forEach {
@ -229,8 +233,10 @@ open class MockServices private constructor(
private val mockCordappProvider: MockCordappProvider = MockCordappProvider(cordappLoader, attachments, networkParameters.whitelistedContractImplementations) private val mockCordappProvider: MockCordappProvider = MockCordappProvider(cordappLoader, attachments, networkParameters.whitelistedContractImplementations)
override val cordappProvider: CordappProvider get() = mockCordappProvider 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 { 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) HibernateObserver.install(vaultService.rawUpdates, hibernateConfig, schemaService)
return vaultService return vaultService
} }
@ -249,6 +255,9 @@ open class MockServices private constructor(
fun addMockCordapp(contractClassName: ContractClassName) { fun addMockCordapp(contractClassName: ContractClassName) {
mockCordappProvider.addMockCordapp(contractClassName, attachments) mockCordappProvider.addMockCordapp(contractClassName, attachments)
} }
override fun loadState(stateRef: StateRef) = servicesForResolution.loadState(stateRef)
override fun loadStates(stateRefs: Set<StateRef>) = servicesForResolution.loadStates(stateRefs)
} }
class MockKeyManagementService(val identityService: IdentityService, class MockKeyManagementService(val identityService: IdentityService,

View File

@ -1,12 +1,9 @@
package net.corda.testing.contracts package net.corda.testing.contracts
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.AbstractParty 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.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. // 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. * Dummy contract state for testing of the upgrade process.
*/ */
// DOCSTART 1 // DOCSTART 1
class DummyContractV2 : UpgradedContract<DummyContract.State, DummyContractV2.State> { class DummyContractV2 : UpgradedContractWithLegacyConstraint<DummyContract.State, DummyContractV2.State> {
companion object { companion object {
const val PROGRAM_ID: ContractClassName = "net.corda.testing.contracts.DummyContractV2" const val PROGRAM_ID: ContractClassName = "net.corda.testing.contracts.DummyContractV2"
} }
override val legacyContract: String = DummyContract::class.java.name override val legacyContract: String = DummyContract::class.java.name
override val legacyContractConstraint: AttachmentConstraint = AlwaysAcceptAttachmentConstraint
data class State(val magicNumber: Int = 0, val owners: List<AbstractParty>) : ContractState { data class State(val magicNumber: Int = 0, val owners: List<AbstractParty>) : ContractState {
override val participants: List<AbstractParty> = owners override val participants: List<AbstractParty> = owners
@ -38,25 +36,4 @@ class DummyContractV2 : UpgradedContract<DummyContract.State, DummyContractV2.St
// Other verifications. // Other verifications.
} }
// DOCEND 1 // DOCEND 1
/**
* Generate an upgrade transaction from [DummyContract].
*
* Note: This is a convenience helper method used for testing only.
*
* @param services Services required to resolve the wire transaction
* @return a pair of wire transaction, and a set of those who should sign the transaction for it to be valid.
*/
fun generateUpgradeFromV1(services: ServicesForResolution, vararg states: StateAndRef<DummyContract.State>): Pair<WireTransaction, Set<AbstractParty>> {
val notary = states.map { it.state.notary }.single()
require(states.isNotEmpty())
val signees: Set<AbstractParty> = 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)
}
} }