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 net.corda.core.contracts.ContractState upgrade(net.corda.core.contracts.ContractState)
##
@net.corda.core.serialization.CordaSerializable public interface net.corda.core.contracts.UpgradedContractWithLegacyConstraint extends net.corda.core.contracts.UpgradedContract
@org.jetbrains.annotations.NotNull public abstract net.corda.core.contracts.AttachmentConstraint getLegacyContractConstraint()
##
@net.corda.core.serialization.CordaSerializable @net.corda.core.DoNotImplement public final class net.corda.core.contracts.WhitelistedByZoneAttachmentConstraint extends java.lang.Object implements net.corda.core.contracts.AttachmentConstraint
public boolean isSatisfiedBy(net.corda.core.contracts.Attachment)
public static final net.corda.core.contracts.WhitelistedByZoneAttachmentConstraint INSTANCE
@ -1894,16 +1897,12 @@ public @interface net.corda.core.messaging.RPCReturnsObservables
@org.jetbrains.annotations.NotNull public abstract net.corda.core.transactions.SignedTransaction signInitialTransaction(net.corda.core.transactions.TransactionBuilder, java.security.PublicKey)
@org.jetbrains.annotations.NotNull public abstract net.corda.core.contracts.StateAndRef toStateAndRef(net.corda.core.contracts.StateRef)
##
@net.corda.core.DoNotImplement public interface net.corda.core.node.ServicesForResolution extends net.corda.core.node.StateLoader
@net.corda.core.DoNotImplement public interface net.corda.core.node.ServicesForResolution
@org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.AttachmentStorage getAttachments()
@org.jetbrains.annotations.NotNull public abstract net.corda.core.cordapp.CordappProvider getCordappProvider()
@org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.IdentityService getIdentityService()
@org.jetbrains.annotations.NotNull public abstract net.corda.core.node.NetworkParameters getNetworkParameters()
##
@net.corda.core.DoNotImplement public interface net.corda.core.node.StateLoader
@org.jetbrains.annotations.NotNull public abstract net.corda.core.contracts.TransactionState loadState(net.corda.core.contracts.StateRef)
@org.jetbrains.annotations.NotNull public abstract Set loadStates(Set)
##
public final class net.corda.core.node.StatesToRecord extends java.lang.Enum
protected <init>(String, int)
public static net.corda.core.node.StatesToRecord valueOf(String)
@ -2052,10 +2051,9 @@ public final class net.corda.core.node.services.TimeWindowChecker extends java.l
@org.jetbrains.annotations.NotNull public final java.time.Clock getClock()
public final boolean isValid(net.corda.core.contracts.TimeWindow)
##
@net.corda.core.DoNotImplement public interface net.corda.core.node.services.TransactionStorage extends net.corda.core.node.StateLoader
@net.corda.core.DoNotImplement public interface net.corda.core.node.services.TransactionStorage
@org.jetbrains.annotations.Nullable public abstract net.corda.core.transactions.SignedTransaction getTransaction(net.corda.core.crypto.SecureHash)
@org.jetbrains.annotations.NotNull public abstract rx.Observable getUpdates()
@org.jetbrains.annotations.NotNull public abstract net.corda.core.contracts.TransactionState loadState(net.corda.core.contracts.StateRef)
@org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed track()
##
@net.corda.core.DoNotImplement public interface net.corda.core.node.services.TransactionVerifierService
@ -3137,7 +3135,6 @@ public static final class net.corda.core.transactions.LedgerTransaction$InOutGro
@org.jetbrains.annotations.NotNull public List getOutputs()
public int hashCode()
@org.jetbrains.annotations.NotNull public final net.corda.core.transactions.NotaryChangeLedgerTransaction resolve(net.corda.core.node.ServiceHub, List)
@org.jetbrains.annotations.NotNull public final net.corda.core.transactions.NotaryChangeLedgerTransaction resolve(net.corda.core.node.StateLoader, List)
public String toString()
##
@net.corda.core.serialization.CordaSerializable @net.corda.core.DoNotImplement public final class net.corda.core.transactions.SignedTransaction extends java.lang.Object implements net.corda.core.transactions.TransactionWithSignatures
@ -3160,13 +3157,11 @@ public static final class net.corda.core.transactions.LedgerTransaction$InOutGro
@org.jetbrains.annotations.NotNull public final net.corda.core.transactions.WireTransaction getTx()
@org.jetbrains.annotations.NotNull public final net.corda.core.serialization.SerializedBytes getTxBits()
public int hashCode()
public final boolean isNotaryChangeTransaction()
@kotlin.Deprecated public final boolean isNotaryChangeTransaction()
@org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction plus(Collection)
@org.jetbrains.annotations.NotNull public final net.corda.core.transactions.SignedTransaction plus(net.corda.core.crypto.TransactionSignature)
@org.jetbrains.annotations.NotNull public final net.corda.core.transactions.BaseTransaction resolveBaseTransaction(net.corda.core.node.StateLoader)
@org.jetbrains.annotations.NotNull public final net.corda.core.transactions.NotaryChangeLedgerTransaction resolveNotaryChangeTransaction(net.corda.core.node.ServiceHub)
@org.jetbrains.annotations.NotNull public final net.corda.core.transactions.NotaryChangeLedgerTransaction resolveNotaryChangeTransaction(net.corda.core.node.StateLoader)
@org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionWithSignatures resolveTransactionWithSignatures(net.corda.core.node.ServiceHub)
@org.jetbrains.annotations.NotNull public final net.corda.core.transactions.TransactionWithSignatures resolveTransactionWithSignatures(net.corda.core.node.ServicesForResolution)
@org.jetbrains.annotations.NotNull public final net.corda.core.transactions.LedgerTransaction toLedgerTransaction(net.corda.core.node.ServiceHub)
@org.jetbrains.annotations.NotNull public final net.corda.core.transactions.LedgerTransaction toLedgerTransaction(net.corda.core.node.ServiceHub, boolean)
@org.jetbrains.annotations.NotNull public String toString()
@ -4075,7 +4070,7 @@ public final class net.corda.testing.node.MockNodeParameters extends java.lang.O
@org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNodeParameters withForcedID(Integer)
@org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNodeParameters withLegalName(net.corda.core.identity.CordaX500Name)
##
public class net.corda.testing.node.MockServices extends java.lang.Object implements net.corda.core.node.StateLoader, net.corda.core.node.ServiceHub
public class net.corda.testing.node.MockServices extends java.lang.Object implements net.corda.core.node.ServiceHub
public <init>()
public <init>(List)
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)
@org.jetbrains.annotations.Nullable public net.corda.core.transactions.SignedTransaction getTransaction(net.corda.core.crypto.SecureHash)
@org.jetbrains.annotations.NotNull public rx.Observable getUpdates()
@org.jetbrains.annotations.NotNull public net.corda.core.contracts.TransactionState loadState(net.corda.core.contracts.StateRef)
@org.jetbrains.annotations.NotNull public Set loadStates(Set)
@org.jetbrains.annotations.NotNull public net.corda.core.messaging.DataFeed track()
##
public final class net.corda.testing.node.NodeTestUtils extends java.lang.Object
@ -4351,9 +4344,10 @@ public static final class net.corda.testing.contracts.DummyContract$SingleOwnerS
@net.corda.core.DoNotImplement public static interface net.corda.testing.contracts.DummyContract$State extends net.corda.core.contracts.ContractState
public abstract int getMagicNumber()
##
public final class net.corda.testing.contracts.DummyContractV2 extends java.lang.Object implements net.corda.core.contracts.UpgradedContract
public final class net.corda.testing.contracts.DummyContractV2 extends java.lang.Object implements net.corda.core.contracts.UpgradedContractWithLegacyConstraint
public <init>()
@org.jetbrains.annotations.NotNull public String getLegacyContract()
@org.jetbrains.annotations.NotNull public net.corda.core.contracts.AttachmentConstraint getLegacyContractConstraint()
@org.jetbrains.annotations.NotNull public net.corda.testing.contracts.DummyContractV2$State upgrade(net.corda.testing.contracts.DummyContract$State)
public void verify(net.corda.core.transactions.LedgerTransaction)
public static final net.corda.testing.contracts.DummyContractV2$Companion Companion

View File

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

View File

@ -48,6 +48,7 @@ data class Issued<out P : Any>(val issuer: PartyAndReference, val product: P) {
init {
require(issuer.reference.size <= MAX_ISSUER_REF_SIZE) { "Maximum issuer reference size is $MAX_ISSUER_REF_SIZE." }
}
override fun toString() = "$product issued by $issuer"
}
@ -248,12 +249,21 @@ annotation class LegalProseReference(val uri: String)
/**
* Interface which can upgrade state objects issued by a contract to a new state object issued by a different contract.
* The upgraded contract should specify the legacy contract class name, and provide an upgrade function that will convert
* legacy contract states into states defined by this contract.
*
* In addition to the legacy contract class name, you can also specify the legacy contract constraint by implementing
* [UpgradedContractWithLegacyConstraint] instead. Otherwise, the default [WhitelistedByZoneAttachmentConstraint] will
* be used for verifying the validity of an upgrade transaction.
*
* @param OldState the old contract state (can be [ContractState] or other common supertype if this supports upgrading
* more than one state).
* @param NewState the upgraded contract state.
*/
interface UpgradedContract<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
/**
* 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
}
/**
* 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
* 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 net.corda.core.contracts.*
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SignableData
import net.corda.core.crypto.SignatureMetadata
import net.corda.core.internal.ContractUpgradeUtils
import net.corda.core.transactions.SignedTransaction
/**
* A flow to be used for authorising and upgrading state objects of an old contract to a new contract.
@ -38,7 +42,6 @@ object ContractUpgradeFlow {
serviceHub.contractUpgradeService.storeAuthorisedContractUpgrade(stateAndRef.ref, upgradedContractClass)
return null
}
}
/**
@ -68,11 +71,13 @@ object ContractUpgradeFlow {
@Suspendable
override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx {
val baseTx = ContractUpgradeUtils.assembleBareTx(originalState, modification, PrivacySalt())
val tx = ContractUpgradeUtils.assembleUpgradeTx(originalState, modification, PrivacySalt(), serviceHub)
val participantKeys = originalState.state.data.participants.map { it.owningKey }.toSet()
// TODO: We need a much faster way of finding our key in the transaction
val myKey = serviceHub.keyManagementService.filterMyKeys(participantKeys).single()
val stx = serviceHub.signInitialTransaction(baseTx, myKey)
val signableData = SignableData(tx.id, SignatureMetadata(serviceHub.myInfo.platformVersion, Crypto.findSignatureScheme(myKey).schemeNumberID))
val mySignature = serviceHub.keyManagementService.sign(signableData, myKey)
val stx = SignedTransaction(tx, listOf(mySignature))
return AbstractStateReplacementFlow.UpgradeTx(stx)
}
}

View File

@ -15,7 +15,9 @@ import net.corda.core.node.services.TrustedAuthorityNotaryService
import net.corda.core.node.services.UniquenessProvider
import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.CoreTransaction
import net.corda.core.transactions.ContractUpgradeWireTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.UntrustworthyData
import net.corda.core.utilities.unwrap
@ -104,10 +106,11 @@ class NotaryFlow {
@Suspendable
private fun sendAndReceiveNonValidating(notaryParty: Party, session: FlowSession, signature: NotarisationRequestSignature): UntrustworthyData<List<TransactionSignature>> {
val tx: CoreTransaction = if (stx.isNotaryChangeTransaction()) {
stx.notaryChangeTx // Notary change transactions do not support filtering
} else {
stx.buildFilteredTransaction(Predicate { it is StateRef || it is TimeWindow || it == notaryParty })
val ctx = stx.coreTransaction
val tx = when (ctx) {
is ContractUpgradeWireTransaction -> ctx.buildFilteredTransaction()
is WireTransaction -> ctx.buildFilteredTransaction(Predicate { it is StateRef || it is TimeWindow || it == notaryParty })
else -> ctx
}
return session.sendAndReceiveWithRetry(NotarisationPayload(tx, signature))
}
@ -168,12 +171,10 @@ class NotaryFlow {
@Suspendable
abstract fun receiveAndVerifyTx(): TransactionParts
// Check if transaction is intended to be signed by this notary.
/** Check if transaction is intended to be signed by this notary. */
@Suspendable
protected fun checkNotary(notary: Party?) {
// TODO This check implies that it's OK to use the node's main identity. Shouldn't it be just limited to the
// notary identities?
if (notary == null || !serviceHub.myInfo.isLegalIdentity(notary)) {
if (notary?.owningKey != service.notaryIdentityKey) {
throw NotaryException(NotaryError.WrongNotary)
}
}

View File

@ -1,21 +1,38 @@
package net.corda.core.internal
import net.corda.core.contracts.*
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.node.ServicesForResolution
import net.corda.core.node.services.AttachmentId
import net.corda.core.transactions.ContractUpgradeWireTransaction
object ContractUpgradeUtils {
fun <OldState : ContractState, NewState : ContractState> assembleBareTx(
stateRef: StateAndRef<OldState>,
fun <OldState : ContractState, NewState : ContractState> assembleUpgradeTx(
stateAndRef: StateAndRef<OldState>,
upgradedContractClass: Class<out UpgradedContract<OldState, NewState>>,
privacySalt: PrivacySalt
): TransactionBuilder {
val contractUpgrade = upgradedContractClass.newInstance()
return TransactionBuilder(stateRef.state.notary)
.withItems(
stateRef,
StateAndContract(contractUpgrade.upgrade(stateRef.state.data), upgradedContractClass.name),
Command(UpgradeCommand(upgradedContractClass.name), stateRef.state.data.participants.map { it.owningKey }),
privacySalt: PrivacySalt,
services: ServicesForResolution
): ContractUpgradeWireTransaction {
require(stateAndRef.state.encumbrance == null) { "Upgrading an encumbered state is not yet supported" }
val legacyConstraint = stateAndRef.state.constraint
val legacyContractAttachmentId = when (legacyConstraint) {
is HashAttachmentConstraint -> legacyConstraint.attachmentId
else -> getContractAttachmentId(stateAndRef.state.contract, services)
}
val upgradedContractAttachmentId = getContractAttachmentId(upgradedContractClass.name, services)
val inputs = listOf(stateAndRef.ref)
return ContractUpgradeWireTransaction(
inputs,
stateAndRef.state.notary,
legacyContractAttachmentId,
upgradedContractClass.name,
upgradedContractAttachmentId,
privacySalt
)
}
private fun getContractAttachmentId(name: ContractClassName, services: ServicesForResolution): AttachmentId {
return services.cordappProvider.getContractAttachmentID(name)
?: throw IllegalStateException("Attachment not found for contract: $name")
}
}

View File

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

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.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
* forms ready for verification.
*/
interface ServicesForResolution : StateLoader {
@DoNotImplement
interface ServicesForResolution {
/**
* An identity service maintains a directory of parties by their associated distinguished name/public keys and thus
* supports lookup of a party given its key, or name. The service also manages the certificates linking confidential
@ -66,6 +39,27 @@ interface ServicesForResolution : StateLoader {
/** Returns the network parameters the node is operating under. */
val networkParameters: NetworkParameters
/**
* Given a [StateRef] loads the referenced transaction and looks up the specified output [ContractState].
*
* *WARNING* Do not use this method unless you really only want a single state - any batch loading should
* go through [loadStates] as repeatedly calling [loadState] can lead to repeat deserialsiation work and
* severe performance degradation.
*
* @throws TransactionResolutionException if [stateRef] points to a non-existent transaction.
*/
@Throws(TransactionResolutionException::class)
fun loadState(stateRef: StateRef): TransactionState<*>
/**
* Given a [Set] of [StateRef]'s loads the referenced transaction and looks up the specified output [ContractState].
*
* @throws TransactionResolutionException if [stateRef] points to a non-existent transaction.
*/
// TODO: future implementation to use a Vault state ref -> contract state BLOB table and perform single query bulk load
// as the existing transaction store will become encrypted at some point
@Throws(TransactionResolutionException::class)
fun loadStates(stateRefs: Set<StateRef>): Set<StateAndRef<ContractState>>
}
/**

View File

@ -1,10 +1,8 @@
package net.corda.core.node.services
import net.corda.core.DoNotImplement
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.messaging.DataFeed
import net.corda.core.node.StateLoader
import net.corda.core.transactions.SignedTransaction
import rx.Observable
@ -12,27 +10,12 @@ import rx.Observable
* Thread-safe storage of transactions.
*/
@DoNotImplement
interface TransactionStorage : StateLoader {
interface TransactionStorage {
/**
* Return the transaction with the given [id], or null if no such transaction exists.
*/
fun getTransaction(id: SecureHash): SignedTransaction?
@Throws(TransactionResolutionException::class)
override fun loadState(stateRef: StateRef): TransactionState<*> {
val stx = getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash)
return stx.resolveBaseTransaction(this).outputs[stateRef.index]
}
@Throws(TransactionResolutionException::class)
override fun loadStates(stateRefs: Set<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
* 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 flowId: UUID? = null,
/**
* Specifies the type of update, currently supported types are general and notary change. Notary
* change transactions only modify the notary field on states, and potentially need to be handled
* Specifies the type of update, currently supported types are general and, contract upgrade and notary change.
* Notary change transactions only modify the notary field on states, and potentially need to be handled
* differently.
*/
val type: UpdateType = UpdateType.GENERAL
@ -97,11 +97,6 @@ class Vault<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
enum class StateStatus {
UNCONSUMED, CONSUMED, ALL
@ -109,7 +104,7 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
@CordaSerializable
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 lockId: String?,
val lockUpdateTime: Instant?)
companion object {
@Deprecated("No longer used. The vault does not emit empty updates")
val NoUpdate = Update(emptySet(), emptySet(), type = Vault.UpdateType.GENERAL)
@Deprecated("No longer used. The vault does not emit empty updates")
val NoNotaryUpdate = Vault.Update(emptySet(), emptySet(), type = Vault.UpdateType.NOTARY_CHANGE)
}
}
/**

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.identity.Party
import net.corda.core.internal.AttachmentWithContext
import net.corda.core.internal.UpgradeCommand
import net.corda.core.internal.castIfPossible
import net.corda.core.internal.uncheckedCast
import net.corda.core.node.NetworkParameters
import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.Try
import java.security.PublicKey
import java.util.*
import java.util.function.Predicate
@ -79,13 +77,8 @@ data class LedgerTransaction @JvmOverloads constructor(
@Throws(TransactionVerificationException::class)
fun verify() {
verifyConstraints()
// TODO: make contract upgrade transactions have a separate type
if (commands.any { it.value is UpgradeCommand }) {
verifyContractUpgrade()
} else {
verifyContracts()
}
}
/**
* Verify that all contract constraints are valid for each state before running any contract code
@ -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
* can be processed as one. The grouping key is any arbitrary object that can act as a map key (so must implement
@ -429,3 +403,4 @@ data class LedgerTransaction @JvmOverloads constructor(
privacySalt: PrivacySalt
) = copy(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, null)
}

View File

@ -4,11 +4,11 @@ import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.TransactionSignature
import net.corda.core.crypto.serializedHash
import net.corda.core.utilities.toBase58String
import net.corda.core.identity.Party
import net.corda.core.node.ServiceHub
import net.corda.core.node.StateLoader
import net.corda.core.node.ServicesForResolution
import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.toBase58String
import java.security.PublicKey
/**
@ -27,7 +27,8 @@ data class NotaryChangeWireTransaction(
* [NotaryChangeLedgerTransaction] and applying the notary modification to inputs.
*/
override val outputs: List<TransactionState<ContractState>>
get() = emptyList()
get() = throw UnsupportedOperationException("NotaryChangeWireTransaction does not contain output states, " +
"outputs can only be obtained from a resolved NotaryChangeLedgerTransaction")
init {
check(inputs.isNotEmpty()) { "A notary change transaction must have inputs" }
@ -40,11 +41,14 @@ data class NotaryChangeWireTransaction(
*/
override val id: SecureHash by lazy { serializedHash(inputs + notary + newNotary) }
fun resolve(services: ServiceHub, sigs: List<TransactionSignature>) = resolve(services as StateLoader, sigs)
fun resolve(stateLoader: StateLoader, sigs: List<TransactionSignature>): NotaryChangeLedgerTransaction {
val resolvedInputs = stateLoader.loadStates(inputs.toSet()).toList()
/** Resolves input states and builds a [NotaryChangeLedgerTransaction]. */
fun resolve(services: ServicesForResolution, sigs: List<TransactionSignature>) : NotaryChangeLedgerTransaction {
val resolvedInputs = services.loadStates(inputs.toSet()).toList()
return NotaryChangeLedgerTransaction(resolvedInputs, notary, newNotary, id, sigs)
}
/** Resolves input states and builds a [NotaryChangeLedgerTransaction]. */
fun resolve(services: ServiceHub, sigs: List<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.internal.VisibleForTesting
import net.corda.core.node.ServiceHub
import net.corda.core.node.StateLoader
import net.corda.core.node.ServicesForResolution
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.deserialize
@ -48,19 +48,18 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
/** Cache the deserialized form of the transaction. This is useful when building a transaction or collecting signatures. */
@Volatile
@Transient private var cachedTransaction: CoreTransaction? = null
/** Lazily calculated access to the deserialized/hashed transaction data. */
private val transaction: CoreTransaction get() = cachedTransaction ?: txBits.deserialize().apply { cachedTransaction = this }
@Transient
private var cachedTransaction: CoreTransaction? = null
/** The id of the contained [WireTransaction]. */
override val id: SecureHash get() = transaction.id
override val id: SecureHash get() = coreTransaction.id
/** Returns the contained [WireTransaction], or throws if this is a notary change transaction. */
val tx: WireTransaction get() = transaction as WireTransaction
/** Lazily calculated access to the deserialised/hashed transaction data. */
val coreTransaction: CoreTransaction
get() = cachedTransaction ?: txBits.deserialize().apply { cachedTransaction = this }
/** Returns the contained [NotaryChangeWireTransaction], or throws if this is a normal transaction. */
val notaryChangeTx: NotaryChangeWireTransaction get() = transaction as NotaryChangeWireTransaction
/** Returns the contained [WireTransaction], or throws if this is a notary change or contract upgrade transaction. */
val tx: WireTransaction get() = coreTransaction as WireTransaction
/**
* Helper function to directly build a [FilteredTransaction] using provided filtering functions,
@ -69,9 +68,9 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
fun buildFilteredTransaction(filtering: Predicate<Any>) = tx.buildFilteredTransaction(filtering)
/** 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. */
val notary: Party? get() = transaction.notary
val notary: Party? get() = coreTransaction.notary
override val requiredSigningKeys: Set<PublicKey> get() = tx.requiredSigningKeys
@ -162,13 +161,27 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
@JvmOverloads
@Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class)
fun verify(services: ServiceHub, checkSufficientSignatures: Boolean = true) {
if (isNotaryChangeTransaction()) {
verifyNotaryChangeTransaction(services, checkSufficientSignatures)
} else {
verifyRegularTransaction(services, checkSufficientSignatures)
when (coreTransaction) {
is NotaryChangeWireTransaction -> verifyNotaryChangeTransaction(services, checkSufficientSignatures)
is ContractUpgradeWireTransaction -> verifyContractUpgradeTransaction(services, checkSufficientSignatures)
else -> verifyRegularTransaction(services, checkSufficientSignatures)
}
}
/** No contract code is run when verifying notary change transactions, it is sufficient to check invariants during initialisation. */
private fun verifyNotaryChangeTransaction(services: ServiceHub, checkSufficientSignatures: Boolean) {
val ntx = resolveNotaryChangeTransaction(services)
if (checkSufficientSignatures) ntx.verifyRequiredSignatures()
else checkSignaturesAreValid()
}
/** No contract code is run when verifying contract upgrade transactions, it is sufficient to check invariants during initialisation. */
private fun verifyContractUpgradeTransaction(services: ServicesForResolution, checkSufficientSignatures: Boolean) {
val ctx = resolveContractUpgradeTransaction(services)
if (checkSufficientSignatures) ctx.verifyRequiredSignatures()
else checkSignaturesAreValid()
}
// TODO: Verify contract constraints here as well as in LedgerTransaction to ensure that anything being deserialised
// from the attachment is trusted. This will require some partial serialisation work to not load the ContractState
// objects from the TransactionState.
@ -178,37 +191,32 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
services.transactionVerifierService.verify(ltx).getOrThrow()
}
private fun verifyNotaryChangeTransaction(services: ServiceHub, checkSufficientSignatures: Boolean) {
val ntx = resolveNotaryChangeTransaction(services)
if (checkSufficientSignatures) ntx.verifyRequiredSignatures()
else checkSignaturesAreValid()
}
fun isNotaryChangeTransaction() = transaction is NotaryChangeWireTransaction
/**
* Resolves the underlying base transaction and then returns it, handling any special case transactions such as
* [NotaryChangeWireTransaction].
*/
fun resolveBaseTransaction(services: StateLoader): BaseTransaction {
return when (transaction) {
is NotaryChangeWireTransaction -> resolveNotaryChangeTransaction(services)
fun resolveBaseTransaction(servicesForResolution: ServicesForResolution): BaseTransaction {
return when (coreTransaction) {
is NotaryChangeWireTransaction -> resolveNotaryChangeTransaction(servicesForResolution)
is ContractUpgradeWireTransaction -> resolveContractUpgradeTransaction(servicesForResolution)
is WireTransaction -> this.tx
is FilteredTransaction -> throw IllegalStateException("Persistence of filtered transactions is not supported.")
else -> throw IllegalStateException("Unknown transaction type ${transaction::class.qualifiedName}")
else -> throw IllegalStateException("Unknown transaction type ${coreTransaction::class.qualifiedName}")
}
}
/**
* Resolves the underlying transaction with signatures and then returns it, handling any special case transactions
* such as [NotaryChangeWireTransaction].
*/
fun resolveTransactionWithSignatures(services: ServiceHub): TransactionWithSignatures {
return when (transaction) {
fun resolveTransactionWithSignatures(services: ServicesForResolution): TransactionWithSignatures {
return when (coreTransaction) {
is NotaryChangeWireTransaction -> resolveNotaryChangeTransaction(services)
is ContractUpgradeWireTransaction -> resolveContractUpgradeTransaction(services)
is WireTransaction -> this
is FilteredTransaction -> throw IllegalStateException("Persistence of filtered transactions is not supported.")
else -> throw IllegalStateException("Unknown transaction type ${transaction::class.qualifiedName}")
else -> throw IllegalStateException("Unknown transaction type ${coreTransaction::class.qualifiedName}")
}
}
@ -216,12 +224,26 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
* If [transaction] is a [NotaryChangeWireTransaction], loads the input states and resolves it to a
* [NotaryChangeLedgerTransaction] so the signatures can be verified.
*/
fun resolveNotaryChangeTransaction(services: ServiceHub) = resolveNotaryChangeTransaction(services as StateLoader)
fun resolveNotaryChangeTransaction(services: ServicesForResolution): NotaryChangeLedgerTransaction {
val ntx = coreTransaction as? NotaryChangeWireTransaction
?: throw IllegalStateException("Expected a ${NotaryChangeWireTransaction::class.simpleName} but found ${coreTransaction::class.simpleName}")
return ntx.resolve(services, sigs)
}
fun resolveNotaryChangeTransaction(stateLoader: StateLoader): NotaryChangeLedgerTransaction {
val ntx = transaction as? NotaryChangeWireTransaction
?: throw IllegalStateException("Expected a ${NotaryChangeWireTransaction::class.simpleName} but found ${transaction::class.simpleName}")
return ntx.resolve(stateLoader, sigs)
/**
* If [transaction] is a [NotaryChangeWireTransaction], loads the input states and resolves it to a
* [NotaryChangeLedgerTransaction] so the signatures can be verified.
*/
fun resolveNotaryChangeTransaction(services: ServiceHub) = resolveNotaryChangeTransaction(services as ServicesForResolution)
/**
* If [coreTransaction] is a [ContractUpgradeWireTransaction], loads the input states and resolves it to a
* [ContractUpgradeLedgerTransaction] so the signatures can be verified.
*/
fun resolveContractUpgradeTransaction(services: ServicesForResolution): ContractUpgradeLedgerTransaction {
val ctx = coreTransaction as? ContractUpgradeWireTransaction
?: throw IllegalStateException("Expected a ${ContractUpgradeWireTransaction::class.simpleName} but found ${coreTransaction::class.simpleName}")
return ctx.resolve(services, sigs)
}
override fun toString(): String = "${javaClass.simpleName}(id=$id)"
@ -234,4 +256,14 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
@CordaSerializable
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))
//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.transactions.LedgerTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.finance.USD
@ -101,20 +102,12 @@ class ContractUpgradeFlowTest {
val result = resultFuture.getOrThrow()
fun check(node: StartedNode<MockNode>) {
val nodeStx = node.database.transaction {
node.services.validatedTransactions.getTransaction(result.ref.txhash)
val upgradeTx = node.database.transaction {
val wtx = node.services.validatedTransactions.getTransaction(result.ref.txhash)
wtx!!.resolveContractUpgradeTransaction(node.services)
}
requireNotNull(nodeStx)
// Verify inputs.
val input = node.database.transaction {
node.services.validatedTransactions.getTransaction(nodeStx!!.tx.inputs.single().txhash)
}
requireNotNull(input)
assertTrue(input!!.tx.outputs.single().data is DummyContract.State)
// Verify outputs.
assertTrue(nodeStx!!.tx.outputs.single().data is DummyContractV2.State)
assertTrue(upgradeTx.inputs.single().state.data is DummyContract.State)
assertTrue(upgradeTx.outputs.single().data is DummyContractV2.State)
}
check(aliceNode)
check(bobNode)
@ -192,16 +185,12 @@ class ContractUpgradeFlowTest {
val result = resultFuture.getOrThrow()
// Check results.
listOf(aliceNode, bobNode).forEach {
val signedTX = aliceNode.database.transaction { aliceNode.services.validatedTransactions.getTransaction(result.ref.txhash) }
requireNotNull(signedTX)
// Verify inputs.
val input = aliceNode.database.transaction { aliceNode.services.validatedTransactions.getTransaction(signedTX!!.tx.inputs.single().txhash) }
requireNotNull(input)
assertTrue(input!!.tx.outputs.single().data is DummyContract.State)
// Verify outputs.
assertTrue(signedTX!!.tx.outputs.single().data is DummyContractV2.State)
val upgradeTx = aliceNode.database.transaction {
val wtx = aliceNode.services.validatedTransactions.getTransaction(result.ref.txhash)
wtx!!.resolveContractUpgradeTransaction(aliceNode.services)
}
assertTrue(upgradeTx.inputs.single().state.data is DummyContract.State)
assertTrue(upgradeTx.outputs.single().data is DummyContractV2.State)
}
}
}
@ -222,14 +211,34 @@ class ContractUpgradeFlowTest {
mockNet.runNetwork()
upgradeResult.getOrThrow()
// Get contract state from the vault.
val firstState = aliceNode.database.transaction { aliceNode.services.vaultService.queryBy<ContractState>().states.single() }
assertTrue(firstState.state.data is CashV2.State, "Contract state is upgraded to the new version.")
assertEquals(Amount(1000000, USD).`issued by`(chosenIdentity.ref(1)), (firstState.state.data as CashV2.State).amount, "Upgraded cash contain the correct amount.")
assertEquals<Collection<AbstractParty>>(listOf(anonymisedRecipient), (firstState.state.data as CashV2.State).owners, "Upgraded cash belongs to the right owner.")
val upgradedStateFromVault = aliceNode.database.transaction { aliceNode.services.vaultService.queryBy<CashV2.State>().states.single() }
assertEquals(Amount(1000000, USD).`issued by`(chosenIdentity.ref(1)), upgradedStateFromVault.state.data.amount, "Upgraded cash contain the correct amount.")
assertEquals<Collection<AbstractParty>>(listOf(anonymisedRecipient), upgradedStateFromVault.state.data.owners, "Upgraded cash belongs to the right owner.")
// Make sure the upgraded state can be spent
val movedState = upgradedStateFromVault.state.data.copy(amount = upgradedStateFromVault.state.data.amount.times(2))
val spendUpgradedTx = aliceNode.services.signInitialTransaction(
TransactionBuilder(notary)
.addInputState(upgradedStateFromVault)
.addOutputState(
upgradedStateFromVault.state.copy(data = movedState)
)
.addCommand(CashV2.Move(), alice.owningKey)
)
aliceNode.services.startFlow(FinalityFlow(spendUpgradedTx)).apply {
mockNet.runNetwork()
get()
}
val movedStateFromVault = aliceNode.database.transaction { aliceNode.services.vaultService.queryBy<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 legacyContractConstraint: AttachmentConstraint
get() = AlwaysAcceptAttachmentConstraint
class Move : TypeOnlyCommandData()
data class State(override val amount: Amount<Issued<Currency>>, val owners: List<AbstractParty>) : FungibleAsset<Currency> {
override val owner: AbstractParty = owners.first()

View File

@ -16,6 +16,7 @@ class VaultUpdateTests {
private companion object {
val DUMMY_PROGRAM_ID = "net.corda.core.node.VaultUpdateTests.DummyContract"
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
val emptyUpdate = Vault.Update(emptySet(), emptySet(), type = Vault.UpdateType.GENERAL)
}
object DummyContract : Contract {
@ -42,21 +43,21 @@ class VaultUpdateTests {
@Test
fun `nothing plus nothing is nothing`() {
val before = Vault.NoUpdate
val after = before + Vault.NoUpdate
val before = emptyUpdate
val after = before + emptyUpdate
assertEquals(before, after)
}
@Test
fun `something plus nothing is something`() {
val before = Vault.Update<ContractState>(setOf(stateAndRef0, stateAndRef1), setOf(stateAndRef2, stateAndRef3))
val after = before + Vault.NoUpdate
val after = before + emptyUpdate
assertEquals(before, after)
}
@Test
fun `nothing plus something is something`() {
val before = Vault.NoUpdate
val before = emptyUpdate
val after = before + Vault.Update<ContractState>(setOf(stateAndRef0, stateAndRef1), setOf(stateAndRef2, stateAndRef3))
val expected = Vault.Update<ContractState>(setOf(stateAndRef0, stateAndRef1), setOf(stateAndRef2, stateAndRef3))
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
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
^^^^^^^^^^^^^^^^^^^^^^^
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.SerializeAsToken
import net.corda.core.serialization.SerializedBytes
import net.corda.core.transactions.ContractUpgradeWireTransaction
import net.corda.core.transactions.NotaryChangeWireTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction
@ -127,6 +128,7 @@ object DefaultKryoCustomizer {
register(java.lang.invoke.SerializedLambda::class.java)
register(ClosureSerializer.Closure::class.java, CordaClosureBlacklistSerializer)
register(ContractUpgradeWireTransaction::class.java, ContractUpgradeWireTransactionSerializer)
for (whitelistProvider in serializationWhitelists) {
val types = whitelistProvider.whitelist

View File

@ -8,9 +8,12 @@ import com.esotericsoftware.kryo.serializers.CompatibleFieldSerializer
import com.esotericsoftware.kryo.serializers.FieldSerializer
import com.esotericsoftware.kryo.util.MapReferenceResolver
import net.corda.core.concurrent.CordaFuture
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.PrivacySalt
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.TransactionState
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.TransactionSignature
import net.corda.core.identity.Party
import net.corda.core.internal.uncheckedCast
@ -265,6 +268,29 @@ object NotaryChangeWireTransactionSerializer : Serializer<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
object SignedTransactionSerializer : Serializer<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 (keyPairs, nodeInfo) = initNodeInfo(networkMapCache, identity, identityKeyPair)
identityService.loadIdentities(nodeInfo.legalIdentitiesAndCerts)
val metrics = MetricRegistry()
val transactionStorage = makeTransactionStorage(database, configuration.transactionCacheSizeBytes)
attachments = NodeAttachmentService(metrics, configuration.attachmentContentCacheSizeBytes, configuration.attachmentCacheBound)
val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(), attachments, networkParameters.whitelistedContractImplementations)
val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParameters, transactionStorage)
val nodeProperties = NodePropertiesPersistentStore(StubbedNodeUniqueIdProvider::value, database)
val nodeServices = makeServices(
keyPairs,
schemaService,
transactionStorage,
metrics,
servicesForResolution,
database,
nodeInfo,
identityService,
networkMapCache,
nodeProperties,
cordappProvider,
networkParameters)
val notaryService = makeNotaryService(nodeServices, database)
val smm = makeStateMachineManager(database)
@ -228,7 +235,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
platformClock,
database,
flowStarter,
transactionStorage,
servicesForResolution,
unfinishedSchedules = busyNodeLatch,
serverThread = serverThread,
flowLogicRefFactory = flowLogicRefFactory,
@ -547,16 +554,17 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
private fun makeServices(keyPairs: Set<KeyPair>,
schemaService: SchemaService,
transactionStorage: WritableTransactionStorage,
metrics: MetricRegistry,
servicesForResolution: ServicesForResolution,
database: CordaPersistence,
nodeInfo: NodeInfo,
identityService: IdentityServiceInternal,
networkMapCache: NetworkMapCacheInternal,
nodeProperties: NodePropertiesStore,
cordappProvider: CordappProviderInternal,
networkParameters: NetworkParameters): MutableList<Any> {
checkpointStorage = DBCheckpointStorage()
val metrics = MetricRegistry()
attachments = NodeAttachmentService(metrics, configuration.attachmentContentCacheSizeBytes, configuration.attachmentCacheBound)
val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(), attachments, networkParameters.whitelistedContractImplementations)
val keyManagementService = makeKeyManagementService(identityService, keyPairs)
_services = ServiceHubInternalImpl(
identityService,
@ -569,7 +577,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
nodeInfo,
networkMapCache,
nodeProperties,
networkParameters)
networkParameters,
servicesForResolution)
network = makeMessagingService(database, nodeInfo, nodeProperties, networkParameters)
val tokenizableServices = mutableListOf(attachments, network, services.vaultService,
services.keyManagementService, services.identityService, platformClock,
@ -760,8 +769,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
}
protected open fun generateKeyPair() = cryptoGenerateKeyPair()
protected open fun makeVaultService(keyManagementService: KeyManagementService, stateLoader: StateLoader, hibernateConfig: HibernateConfiguration): VaultServiceInternal {
return NodeVaultService(platformClock, keyManagementService, stateLoader, hibernateConfig)
protected open fun makeVaultService(keyManagementService: KeyManagementService, services: ServicesForResolution, hibernateConfig: HibernateConfiguration): VaultServiceInternal {
return NodeVaultService(platformClock, keyManagementService, services, hibernateConfig)
}
/** Load configured JVM agents */
@ -794,13 +803,14 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
override val myInfo: NodeInfo,
override val networkMapCache: NetworkMapCacheInternal,
override val nodeProperties: NodePropertiesStore,
override val networkParameters: NetworkParameters
) : SingletonSerializeAsToken(), ServiceHubInternal, StateLoader by validatedTransactions {
override val networkParameters: NetworkParameters,
private val servicesForResolution: ServicesForResolution
) : SingletonSerializeAsToken(), ServiceHubInternal, ServicesForResolution by servicesForResolution {
override val rpcFlows = ArrayList<Class<out FlowLogic<*>>>()
override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage()
override val auditService = DummyAuditService()
override val transactionVerifierService by lazy { makeTransactionVerifierService() }
override val vaultService by lazy { makeVaultService(keyManagementService, validatedTransactions, database.hibernateConfig) }
override val vaultService by lazy { makeVaultService(keyManagementService, servicesForResolution, database.hibernateConfig) }
override val contractUpgradeService by lazy { ContractUpgradeServiceImpl() }
override val attachments: AttachmentStorage get() = this@AbstractNode.attachments
override val networkService: MessagingService get() = network

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 net.corda.core.contracts.Contract
import net.corda.core.contracts.UpgradedContract
import net.corda.core.contracts.UpgradedContractWithLegacyConstraint
import net.corda.core.cordapp.Cordapp
import net.corda.core.flows.*
import net.corda.core.internal.*
@ -241,7 +242,13 @@ class CordappLoader private constructor(private val cordappJarPaths: List<Restri
}
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> {

View File

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

View File

@ -81,7 +81,6 @@ interface ServiceHubInternal : ServiceHub {
}
if (statesToRecord != StatesToRecord.NONE) {
val toNotify = recordedTransactions.map { if (it.isNotaryChangeTransaction()) it.notaryChangeTx else it.tx }
// When the user has requested StatesToRecord.ALL we may end up recording and relationally mapping states
// that do not involve us and that we cannot sign for. This will break coin selection and thus a warning
// is present in the documentation for this feature (see the "Observer nodes" tutorial on docs.corda.net).
@ -116,7 +115,7 @@ interface ServiceHubInternal : ServiceHub {
//
// Because the primary use case for recording irrelevant states is observer/regulator nodes, who are unlikely
// to make writes to the ledger very often or at all, we choose to punt this issue for the time being.
vaultService.notifyAll(statesToRecord, toNotify)
vaultService.notifyAll(statesToRecord, txs.map { it.coreTransaction })
}
}

View File

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

View File

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

View File

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

View File

@ -6,15 +6,13 @@ import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.*
import net.corda.core.messaging.DataFeed
import net.corda.core.node.StateLoader
import net.corda.core.node.ServicesForResolution
import net.corda.core.node.StatesToRecord
import net.corda.core.node.services.*
import net.corda.core.node.services.vault.*
import net.corda.core.schemas.PersistentStateRef
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.transactions.CoreTransaction
import net.corda.core.transactions.NotaryChangeWireTransaction
import net.corda.core.transactions.WireTransaction
import net.corda.core.transactions.*
import net.corda.core.utilities.*
import net.corda.node.services.api.VaultServiceInternal
import net.corda.node.services.statemachine.FlowStateMachineImpl
@ -49,7 +47,7 @@ private fun CriteriaBuilder.executeUpdate(session: Session, configure: Root<*>.(
class NodeVaultService(
private val clock: Clock,
private val keyManagementService: KeyManagementService,
private val stateLoader: StateLoader,
private val servicesForResolution: ServicesForResolution,
hibernateConfig: HibernateConfiguration
) : SingletonSerializeAsToken(), VaultServiceInternal {
private companion object {
@ -108,41 +106,29 @@ class NodeVaultService(
override val updates: Observable<Vault.Update<ContractState>>
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>) {
if (statesToRecord == StatesToRecord.NONE)
return
if (statesToRecord == StatesToRecord.NONE || !txns.any()) return
val batch = mutableListOf<CoreTransaction>()
// It'd be easier to just group by type, but then we'd lose ordering.
val regularTxns = mutableListOf<WireTransaction>()
val notaryChangeTxns = mutableListOf<NotaryChangeWireTransaction>()
fun flushBatch() {
val updates = makeUpdates(batch, statesToRecord)
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 (batch.isNotEmpty() && tx.javaClass != batch.last().javaClass) {
flushBatch()
}
batch.add(tx)
}
flushBatch()
}
if (regularTxns.isNotEmpty()) notifyRegular(regularTxns.toList(), statesToRecord)
if (notaryChangeTxns.isNotEmpty()) notifyNotaryChange(notaryChangeTxns.toList(), statesToRecord)
}
private fun notifyRegular(txns: Iterable<WireTransaction>, statesToRecord: StatesToRecord) {
fun makeUpdate(tx: WireTransaction): Vault.Update<ContractState> {
private fun makeUpdates(batch: Iterable<CoreTransaction>, statesToRecord: StatesToRecord): List<Vault.Update<ContractState>> {
fun makeUpdate(tx: WireTransaction): Vault.Update<ContractState>? {
val myKeys = keyManagementService.filterMyKeys(tx.outputs.flatMap { it.data.participants.map { it.owningKey } })
val ourNewStates = when (statesToRecord) {
StatesToRecord.NONE -> throw AssertionError("Should not reach here")
StatesToRecord.ONLY_RELEVANT -> tx.outputs.filter { isRelevant(it.data, myKeys.toSet()) }
@ -155,45 +141,48 @@ class NodeVaultService(
// Is transaction irrelevant?
if (consumedStates.isEmpty() && ourNewStates.isEmpty()) {
log.trace { "tx ${tx.id} was irrelevant to this vault, ignoring" }
return Vault.NoUpdate
return null
}
return Vault.Update(consumedStates.toSet(), ourNewStates.toSet())
}
val netDelta = txns.fold(Vault.NoUpdate) { netDelta, txn -> netDelta + makeUpdate(txn) }
processAndNotify(netDelta)
}
private fun notifyNotaryChange(txns: Iterable<NotaryChangeWireTransaction>, statesToRecord: StatesToRecord) {
fun makeUpdate(tx: NotaryChangeWireTransaction): Vault.Update<ContractState> {
fun resolveAndMakeUpdate(tx: CoreTransaction): Vault.Update<ContractState>? {
// We need to resolve the full transaction here because outputs are calculated from inputs
// We also can't do filtering beforehand, since output encumbrance pointers get recalculated based on
// input positions
val ltx = tx.resolve(stateLoader, emptyList())
// We also can't do filtering beforehand, since for notary change transactions output encumbrance pointers
// get recalculated based on input positions.
val ltx: FullTransaction = when (tx) {
is NotaryChangeWireTransaction -> tx.resolve(servicesForResolution, emptyList())
is ContractUpgradeWireTransaction -> tx.resolve(servicesForResolution, emptyList())
else -> throw IllegalArgumentException("Unsupported transaction type: ${tx.javaClass.name}")
}
val myKeys = keyManagementService.filterMyKeys(ltx.outputs.flatMap { it.data.participants.map { it.owningKey } })
val (consumedStateAndRefs, producedStates) = ltx.inputs.
zip(ltx.outputs).
filter { (_, output) ->
if (statesToRecord == StatesToRecord.ONLY_RELEVANT)
isRelevant(output.data, myKeys.toSet())
else
true
if (statesToRecord == StatesToRecord.ONLY_RELEVANT) isRelevant(output.data, myKeys.toSet())
else true
}.
unzip()
val producedStateAndRefs = producedStates.map { ltx.outRef<ContractState>(it.data) }
if (consumedStateAndRefs.isEmpty() && producedStateAndRefs.isEmpty()) {
log.trace { "tx ${tx.id} was irrelevant to this vault, ignoring" }
return Vault.NoNotaryUpdate
return null
}
return Vault.Update(consumedStateAndRefs.toHashSet(), producedStateAndRefs.toHashSet(), null, Vault.UpdateType.NOTARY_CHANGE)
val updateType = if (tx is ContractUpgradeWireTransaction) {
Vault.UpdateType.CONTRACT_UPGRADE
} else {
Vault.UpdateType.NOTARY_CHANGE
}
return Vault.Update(consumedStateAndRefs.toSet(), producedStateAndRefs.toSet(), null, updateType)
}
val netDelta = txns.fold(Vault.NoNotaryUpdate) { netDelta, txn -> netDelta + makeUpdate(txn) }
processAndNotify(netDelta)
return batch.mapNotNull {
if (it is WireTransaction) makeUpdate(it) else resolveAndMakeUpdate(it)
}
}
private fun loadStates(refs: Collection<StateRef>): Collection<StateAndRef<ContractState>> {
@ -202,13 +191,15 @@ class NodeVaultService(
else emptySet()
}
private fun processAndNotify(update: Vault.Update<ContractState>) {
if (!update.isEmpty()) {
recordUpdate(update)
private fun processAndNotify(updates: List<Vault.Update<ContractState>>) {
if (updates.isEmpty()) return
val netUpdate = updates.reduce { update1, update2 -> update1 + update2 }
if (!netUpdate.isEmpty()) {
recordUpdate(netUpdate)
mutex.locked {
// flowId required by SoftLockManager to perform auto-registration of soft locks for new states
val uuid = (Strand.currentStrand() as? FlowStateMachineImpl<*>)?.id?.uuid
val vaultUpdate = if (uuid != null) update.copy(flowId = uuid) else update
val vaultUpdate = if (uuid != null) netUpdate.copy(flowId = uuid) else netUpdate
updatesPublisher.onNext(vaultUpdate)
}
}
@ -457,7 +448,7 @@ class NodeVaultService(
}
}
if (stateRefs.isNotEmpty())
statesAndRefs.addAll(stateLoader.loadStates(stateRefs) as Collection<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)
} 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.concurrent.openFuture
import net.corda.core.internal.uncheckedCast
import net.corda.core.node.StateLoader
import net.corda.core.node.ServicesForResolution
import net.corda.core.utilities.days
import net.corda.node.services.api.FlowStarter
import net.corda.node.services.api.NodePropertiesStore
@ -48,7 +48,7 @@ class NodeSchedulerServiceTest {
doReturn(flowsDraingMode).whenever(it).flowsDrainingMode
}
private val transactionStates = mutableMapOf<StateRef, TransactionState<*>>()
private val stateLoader = rigorousMock<StateLoader>().also {
private val servicesForResolution = rigorousMock<ServicesForResolution>().also {
doLookup(transactionStates).whenever(it).loadState(any())
}
private val flows = mutableMapOf<FlowLogicRef, FlowLogic<*>>()
@ -63,7 +63,7 @@ class NodeSchedulerServiceTest {
testClock,
database,
flowStarter,
stateLoader,
servicesForResolution,
serverThread = MoreExecutors.directExecutor(),
flowLogicRefFactory = flowLogicRefFactory,
nodeProperties = nodeProperties,

View File

@ -118,7 +118,7 @@ class HibernateConfigurationTest {
services = object : MockServices(cordappPackages, BOB_NAME, rigorousMock<IdentityServiceInternal>().also {
doNothing().whenever(it).justVerifyAndRegisterIdentity(argThat { name == BOB_NAME })
}, generateKeyPair(), dummyNotary.keyPair) {
override val vaultService = NodeVaultService(Clock.systemUTC(), keyManagementService, validatedTransactions, hibernateConfig)
override val vaultService = NodeVaultService(Clock.systemUTC(), keyManagementService, servicesForResolution, hibernateConfig)
override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable<SignedTransaction>) {
for (stx in txs) {
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.packageName
import net.corda.core.internal.uncheckedCast
import net.corda.core.node.StateLoader
import net.corda.core.node.ServicesForResolution
import net.corda.core.node.services.KeyManagementService
import net.corda.core.node.services.queryBy
import net.corda.core.node.services.vault.QueryCriteria.SoftLockingCondition
@ -83,8 +83,8 @@ class VaultSoftLockManagerTest {
}
private val mockNet = InternalMockNetwork(cordappPackages = listOf(ContractImpl::class.packageName), defaultFactory = { args ->
object : InternalMockNetwork.MockNode(args) {
override fun makeVaultService(keyManagementService: KeyManagementService, stateLoader: StateLoader, hibernateConfig: HibernateConfiguration): VaultServiceInternal {
val realVault = super.makeVaultService(keyManagementService, stateLoader, hibernateConfig)
override fun makeVaultService(keyManagementService: KeyManagementService, services: ServicesForResolution, hibernateConfig: HibernateConfiguration): VaultServiceInternal {
val realVault = super.makeVaultService(keyManagementService, services, hibernateConfig)
return object : VaultServiceInternal by realVault {
override fun softLockRelease(lockId: UUID, stateRefs: NonEmptySet<StateRef>?) {
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.services.CordaService
import net.corda.core.node.services.TrustedAuthorityNotaryService
import net.corda.core.transactions.CoreTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionWithSignatures
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.unwrap
import net.corda.node.services.transactions.PersistentUniquenessProvider
import java.security.PublicKey
@ -49,12 +51,9 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating
val stx = receiveTransaction()
val notary = stx.notary
checkNotary(notary)
val timeWindow: TimeWindow? = if (stx.isNotaryChangeTransaction())
null
else
stx.tx.timeWindow
resolveAndContractVerify(stx)
verifySignatures(stx)
resolveAndContractVerify(stx)
val timeWindow: TimeWindow? = if (stx.coreTransaction is WireTransaction) stx.tx.timeWindow else null
return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!)
} catch (e: Exception) {
throw when (e) {

View File

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

View File

@ -1,12 +1,9 @@
package net.corda.testing.contracts
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.AbstractParty
import net.corda.core.internal.UpgradeCommand
import net.corda.core.node.ServicesForResolution
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction
// The dummy contract doesn't do anything useful. It exists for testing purposes.
@ -14,12 +11,13 @@ import net.corda.core.transactions.WireTransaction
* Dummy contract state for testing of the upgrade process.
*/
// DOCSTART 1
class DummyContractV2 : UpgradedContract<DummyContract.State, DummyContractV2.State> {
class DummyContractV2 : UpgradedContractWithLegacyConstraint<DummyContract.State, DummyContractV2.State> {
companion object {
const val PROGRAM_ID: ContractClassName = "net.corda.testing.contracts.DummyContractV2"
}
override val legacyContract: String = DummyContract::class.java.name
override val legacyContractConstraint: AttachmentConstraint = AlwaysAcceptAttachmentConstraint
data class State(val magicNumber: Int = 0, val owners: List<AbstractParty>) : ContractState {
override val participants: List<AbstractParty> = owners
@ -38,25 +36,4 @@ class DummyContractV2 : UpgradedContract<DummyContract.State, DummyContractV2.St
// Other verifications.
}
// 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)
}
}