Merge remote-tracking branch 'open/master' into andrius-merge-02-26

This commit is contained in:
Andrius Dagys
2018-02-26 11:55:17 +00:00
46 changed files with 666 additions and 403 deletions

View File

@ -48,6 +48,7 @@ data class Issued<out P : Any>(val issuer: PartyAndReference, val product: P) {
init {
require(issuer.reference.size <= MAX_ISSUER_REF_SIZE) { "Maximum issuer reference size is $MAX_ISSUER_REF_SIZE." }
}
override fun toString() = "$product issued by $issuer"
}
@ -248,12 +249,21 @@ annotation class LegalProseReference(val uri: String)
/**
* Interface which can upgrade state objects issued by a contract to a new state object issued by a different contract.
* The upgraded contract should specify the legacy contract class name, and provide an upgrade function that will convert
* legacy contract states into states defined by this contract.
*
* In addition to the legacy contract class name, you can also specify the legacy contract constraint by implementing
* [UpgradedContractWithLegacyConstraint] instead. Otherwise, the default [WhitelistedByZoneAttachmentConstraint] will
* be used for verifying the validity of an upgrade transaction.
*
* @param OldState the old contract state (can be [ContractState] or other common supertype if this supports upgrading
* more than one state).
* @param NewState the upgraded contract state.
*/
interface UpgradedContract<in OldState : ContractState, out NewState : ContractState> : Contract {
/**
* Name of the contract this is an upgraded version of, used as part of verification of upgrade transactions.
*/
val legacyContract: ContractClassName
/**
* Upgrade contract's state object to a new state object.
@ -264,6 +274,17 @@ interface UpgradedContract<in OldState : ContractState, out NewState : ContractS
fun upgrade(state: OldState): NewState
}
/**
* This interface allows specifying a custom legacy contract constraint for upgraded contracts. The default for [UpgradedContract]
* is [WhitelistedByZoneAttachmentConstraint].
*/
interface UpgradedContractWithLegacyConstraint<in OldState : ContractState, out NewState : ContractState> : UpgradedContract<OldState, NewState> {
/**
* A validator for the legacy (pre-upgrade) contract attachments on the transaction.
*/
val legacyContractConstraint: AttachmentConstraint
}
/**
* A privacy salt is required to compute nonces per transaction component in order to ensure that an adversary cannot
* use brute force techniques and reveal the content of a Merkle-leaf hashed value.

View File

@ -2,7 +2,11 @@ package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.*
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SignableData
import net.corda.core.crypto.SignatureMetadata
import net.corda.core.internal.ContractUpgradeUtils
import net.corda.core.transactions.SignedTransaction
/**
* A flow to be used for authorising and upgrading state objects of an old contract to a new contract.
@ -38,7 +42,6 @@ object ContractUpgradeFlow {
serviceHub.contractUpgradeService.storeAuthorisedContractUpgrade(stateAndRef.ref, upgradedContractClass)
return null
}
}
/**
@ -68,11 +71,13 @@ object ContractUpgradeFlow {
@Suspendable
override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx {
val baseTx = ContractUpgradeUtils.assembleBareTx(originalState, modification, PrivacySalt())
val tx = ContractUpgradeUtils.assembleUpgradeTx(originalState, modification, PrivacySalt(), serviceHub)
val participantKeys = originalState.state.data.participants.map { it.owningKey }.toSet()
// TODO: We need a much faster way of finding our key in the transaction
val myKey = serviceHub.keyManagementService.filterMyKeys(participantKeys).single()
val stx = serviceHub.signInitialTransaction(baseTx, myKey)
val signableData = SignableData(tx.id, SignatureMetadata(serviceHub.myInfo.platformVersion, Crypto.findSignatureScheme(myKey).schemeNumberID))
val mySignature = serviceHub.keyManagementService.sign(signableData, myKey)
val stx = SignedTransaction(tx, listOf(mySignature))
return AbstractStateReplacementFlow.UpgradeTx(stx)
}
}

View File

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

View File

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

View File

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

View File

@ -1,7 +0,0 @@
package net.corda.core.internal
import net.corda.core.contracts.CommandData
import net.corda.core.contracts.ContractClassName
/** Indicates that this transaction replaces the inputs contract state to another contract state */
data class UpgradeCommand(val upgradedContractClass: ContractClassName) : CommandData

View File

@ -18,37 +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].
*
* @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>> {
return stateRefs.map { StateAndRef(loadState(it), it) }.toSet()
}
}
/**
* 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
@ -64,6 +39,27 @@ interface ServicesForResolution : StateLoader {
/** Returns the network parameters the node is operating under. */
val networkParameters: NetworkParameters
/**
* Given a [StateRef] loads the referenced transaction and looks up the specified output [ContractState].
*
* *WARNING* Do not use this method unless you really only want a single state - any batch loading should
* go through [loadStates] as repeatedly calling [loadState] can lead to repeat deserialsiation work and
* severe performance degradation.
*
* @throws TransactionResolutionException if [stateRef] points to a non-existent transaction.
*/
@Throws(TransactionResolutionException::class)
fun loadState(stateRef: StateRef): TransactionState<*>
/**
* Given a [Set] of [StateRef]'s loads the referenced transaction and looks up the specified output [ContractState].
*
* @throws TransactionResolutionException if [stateRef] points to a non-existent transaction.
*/
// TODO: future implementation to use a Vault state ref -> contract state BLOB table and perform single query bulk load
// as the existing transaction store will become encrypted at some point
@Throws(TransactionResolutionException::class)
fun loadStates(stateRefs: Set<StateRef>): Set<StateAndRef<ContractState>>
}
/**

View File

@ -1,13 +1,9 @@
package net.corda.core.node.services
import net.corda.core.DoNotImplement
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.TransactionResolutionException
import net.corda.core.contracts.TransactionState
import net.corda.core.concurrent.CordaFuture
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
@ -15,18 +11,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]
}
/**
* Get a synchronous Observable of updates. When observations are pushed to the Observer, the vault will already
* incorporate the update.

View File

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

View File

@ -0,0 +1,183 @@
package net.corda.core.transactions
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.TransactionSignature
import net.corda.core.crypto.serializedHash
import net.corda.core.identity.Party
import net.corda.core.internal.AttachmentWithContext
import net.corda.core.node.NetworkParameters
import net.corda.core.node.ServicesForResolution
import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.toBase58String
import java.security.PublicKey
// TODO: copy across encumbrances when performing contract upgrades
// TODO: check transaction size is within limits
/** A special transaction for upgrading the contract of a state. */
@CordaSerializable
data class ContractUpgradeWireTransaction(
override val inputs: List<StateRef>,
override val notary: Party,
val legacyContractAttachmentId: SecureHash,
val upgradeContractClassName: ContractClassName,
val upgradedContractAttachmentId: SecureHash,
val privacySalt: PrivacySalt = PrivacySalt()
) : CoreTransaction() {
init {
check(inputs.isNotEmpty()) { "A contract upgrade transaction must have inputs" }
}
/**
* This transaction does not contain any output states, outputs can be obtained by resolving a
* [ContractUpgradeLedgerTransaction] outputs will be calculated on demand by applying the contract
* upgrade operation to inputs.
*/
override val outputs: List<TransactionState<ContractState>>
get() = throw UnsupportedOperationException("ContractUpgradeWireTransaction does not contain output states, " +
"outputs can only be obtained from a resolved ContractUpgradeLedgerTransaction")
/** Hash of the list of components that are hidden in the [ContractUpgradeFilteredTransaction]. */
private val hiddenComponentHash: SecureHash
get() = serializedHash(listOf(legacyContractAttachmentId, upgradeContractClassName, privacySalt))
override val id: SecureHash by lazy { serializedHash(inputs + notary).hashConcat(hiddenComponentHash) }
/** Resolves input states and contract attachments, and builds a ContractUpgradeLedgerTransaction. */
fun resolve(services: ServicesForResolution, sigs: List<TransactionSignature>): ContractUpgradeLedgerTransaction {
val resolvedInputs = services.loadStates(inputs.toSet()).toList()
val legacyContractClassName = resolvedInputs.first().state.contract
val legacyContractAttachment = services.attachments.openAttachment(legacyContractAttachmentId)
?: throw AttachmentResolutionException(legacyContractAttachmentId)
val upgradedContractAttachment = services.attachments.openAttachment(upgradedContractAttachmentId)
?: throw AttachmentResolutionException(upgradedContractAttachmentId)
return ContractUpgradeLedgerTransaction(
resolvedInputs,
notary,
ContractAttachment(legacyContractAttachment, legacyContractClassName),
ContractAttachment(upgradedContractAttachment, upgradeContractClassName),
id,
privacySalt,
sigs,
services.networkParameters
)
}
fun buildFilteredTransaction(): ContractUpgradeFilteredTransaction {
return ContractUpgradeFilteredTransaction(inputs, notary, hiddenComponentHash)
}
}
/**
* A filtered version of the [ContractUpgradeWireTransaction]. In comparison with a regular [FilteredTransaction], there
* is no flexibility on what parts of the transaction to reveal the inputs and notary field are always visible and the
* rest of the transaction is always hidden. Its only purpose is to hide transaction data when using a non-validating notary.
*
* @property inputs The inputs of this transaction.
* @property notary The notary for this transaction.
* @property rest Hash of the hidden components of the [ContractUpgradeWireTransaction].
*/
@CordaSerializable
data class ContractUpgradeFilteredTransaction(
override val inputs: List<StateRef>,
override val notary: Party,
val rest: SecureHash
) : CoreTransaction() {
override val id: SecureHash get() = serializedHash(inputs + notary).hashConcat(rest)
override val outputs: List<TransactionState<ContractState>> get() = emptyList()
}
/**
* A contract upgrade transaction with fully resolved inputs and signatures. Contract upgrade transactions are separate
* to regular transactions because their validation logic is specialised; the original contract by definition cannot be
* aware of the upgraded contract (it was written after the original contract was developed), so its validation logic
* cannot succeed. Instead alternative verification logic is used which verifies that the outputs correspond to the
* inputs after upgrading.
*
* In contrast with a regular transaction, signatures are checked against the signers specified by input states'
* *participants* fields, so full resolution is needed for signature verification.
*/
data class ContractUpgradeLedgerTransaction(
override val inputs: List<StateAndRef<ContractState>>,
override val notary: Party,
val legacyContractAttachment: ContractAttachment,
val upgradedContractAttachment: ContractAttachment,
override val id: SecureHash,
val privacySalt: PrivacySalt,
override val sigs: List<TransactionSignature>,
private val networkParameters: NetworkParameters
) : FullTransaction(), TransactionWithSignatures {
private val upgradedContract: UpgradedContract<ContractState, *> = loadUpgradedContract()
init {
// TODO: relax this constraint once upgrading encumbered states is supported
check(inputs.all { it.state.contract == legacyContractAttachment.contract }) {
"All input states must point to the legacy contract"
}
check(inputs.all { it.state.constraint.isSatisfiedBy(legacyContractAttachment) }) {
"Legacy contract constraint does not satisfy the constraint of the input states"
}
verifyLegacyContractConstraint()
}
private fun verifyLegacyContractConstraint() {
check(upgradedContract.legacyContract == legacyContractAttachment.contract) {
"Outputs' contract must be an upgraded version of the inputs' contract"
}
val attachmentWithContext = AttachmentWithContext(
legacyContractAttachment,
upgradedContract.legacyContract,
networkParameters.whitelistedContractImplementations
)
val constraintCheck = if (upgradedContract is UpgradedContractWithLegacyConstraint) {
upgradedContract.legacyContractConstraint.isSatisfiedBy(attachmentWithContext)
} else {
// If legacy constraint not specified, defaulting to WhitelistedByZoneAttachmentConstraint
WhitelistedByZoneAttachmentConstraint.isSatisfiedBy(attachmentWithContext)
}
check(constraintCheck) {
"Legacy contract does not satisfy the upgraded contract's constraint"
}
}
/**
* Outputs are computed by running the contract upgrade logic on input states. This is done eagerly so that the
* transaction is verified during construction.
*/
override val outputs: List<TransactionState<ContractState>> = inputs.map { input ->
// TODO: if there are encumbrance states in the inputs, just copy them across without modifying
val upgradedState = upgradedContract.upgrade(input.state.data)
val inputConstraint = input.state.constraint
val outputConstraint = when (inputConstraint) {
is HashAttachmentConstraint -> HashAttachmentConstraint(upgradedContractAttachment.id)
WhitelistedByZoneAttachmentConstraint -> WhitelistedByZoneAttachmentConstraint
else -> throw IllegalArgumentException("Unsupported input contract constraint $inputConstraint")
}
// TODO: re-map encumbrance pointers
input.state.copy(
data = upgradedState,
contract = upgradedContractAttachment.contract,
constraint = outputConstraint
)
}
/** The required signers are the set of all input states' participants. */
override val requiredSigningKeys: Set<PublicKey>
get() = inputs.flatMap { it.state.data.participants }.map { it.owningKey }.toSet() + notary.owningKey
override fun getKeyDescriptions(keys: Set<PublicKey>): List<String> {
return keys.map { it.toBase58String() }
}
// TODO: load contract from the CorDapp classloader
private fun loadUpgradedContract(): UpgradedContract<ContractState, *> {
@Suppress("UNCHECKED_CAST")
return this::class.java.classLoader
.loadClass(upgradedContractAttachment.contract)
.asSubclass(Contract::class.java)
.getConstructor()
.newInstance() as UpgradedContract<ContractState, *>
}
}

View File

@ -4,13 +4,11 @@ import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.Party
import net.corda.core.internal.AttachmentWithContext
import net.corda.core.internal.UpgradeCommand
import net.corda.core.internal.castIfPossible
import net.corda.core.internal.uncheckedCast
import net.corda.core.node.NetworkParameters
import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.Try
import java.security.PublicKey
import java.util.*
import java.util.function.Predicate
@ -79,12 +77,7 @@ data class LedgerTransaction @JvmOverloads constructor(
@Throws(TransactionVerificationException::class)
fun verify() {
verifyConstraints()
// TODO: make contract upgrade transactions have a separate type
if (commands.any { it.value is UpgradeCommand }) {
verifyContractUpgrade()
} else {
verifyContracts()
}
verifyContracts()
}
/**
@ -185,25 +178,6 @@ data class LedgerTransaction @JvmOverloads constructor(
}
}
private fun verifyContractUpgrade() {
// Contract Upgrade transaction should have 1 input, 1 output and 1 command.
val input = inputs.single().state
val output = outputs.single()
val commandData = commandsOfType<UpgradeCommand>().single()
val command = commandData.value
val participantKeys: Set<PublicKey> = input.data.participants.map { it.owningKey }.toSet()
val keysThatSigned: Set<PublicKey> = commandData.signers.toSet()
@Suppress("UNCHECKED_CAST")
val upgradedContract = javaClass.classLoader.loadClass(command.upgradedContractClass).newInstance() as UpgradedContract<ContractState, *>
requireThat {
"The signing keys include all participant keys" using keysThatSigned.containsAll(participantKeys)
"Inputs state reference the legacy contract" using (input.contract == upgradedContract.legacyContract)
"Outputs state reference the upgraded contract" using (output.contract == command.upgradedContractClass)
"Output state must be an upgraded version of the input state" using (output.data == upgradedContract.upgrade(input.data))
}
}
/**
* Given a type and a function that returns a grouping key, associates inputs and outputs together so that they
* can be processed as one. The grouping key is any arbitrary object that can act as a map key (so must implement
@ -429,3 +403,4 @@ data class LedgerTransaction @JvmOverloads constructor(
privacySalt: PrivacySalt
) = copy(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, null)
}

View File

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

View File

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

View File

@ -1,58 +0,0 @@
package net.corda.core.contracts
import com.nhaarman.mockito_kotlin.any
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.cordapp.CordappProvider
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SecureHash.Companion.allOnesHash
import net.corda.core.internal.UpgradeCommand
import net.corda.core.node.ServicesForResolution
import net.corda.testing.contracts.DummyContract
import net.corda.testing.contracts.DummyContractV2
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.core.TestIdentity
import net.corda.testing.internal.rigorousMock
import org.junit.Rule
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
/**
* Tests for the version 2 dummy contract, to cover ensuring upgrade transactions are built correctly.
*/
class DummyContractV2Tests {
private companion object {
val ALICE = TestIdentity(ALICE_NAME, 70).party
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
}
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
@Test
fun `upgrade from v1`() {
val services = rigorousMock<ServicesForResolution>().also {
doReturn(rigorousMock<CordappProvider>().also {
doReturn(allOnesHash).whenever(it).getContractAttachmentID(any())
}).whenever(it).cordappProvider
}
val contractUpgrade = DummyContractV2()
val v1State = TransactionState(DummyContract.SingleOwnerState(0, ALICE), DummyContract.PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint)
val v1Ref = StateRef(SecureHash.randomSHA256(), 0)
val v1StateAndRef = StateAndRef(v1State, v1Ref)
val (tx, _) = DummyContractV2().generateUpgradeFromV1(services, v1StateAndRef)
assertEquals(v1Ref, tx.inputs.single())
val expectedOutput = TransactionState(contractUpgrade.upgrade(v1State.data), DummyContractV2.PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint)
val actualOutput = tx.outputs.single()
assertEquals(expectedOutput, actualOutput)
val actualCommand = tx.commands.map { it.value }.single()
assertTrue((actualCommand as UpgradeCommand).upgradedContractClass == DummyContractV2::class.java.name)
}
}

View File

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

View File

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