mirror of
https://github.com/corda/corda.git
synced 2024-12-20 21:43:14 +00:00
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:
parent
a483e7e8ce
commit
0edfef2409
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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
|
@ -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>>
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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, *>
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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> {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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}," +
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user