Merge remote-tracking branch 'open/master' into kat-merge-27072018

Conflicts:
	core/src/main/kotlin/net/corda/core/internal/notary/NotaryServiceFlow.kt
	core/src/main/kotlin/net/corda/core/internal/notary/TrustedAuthorityNotaryService.kt
	docs/source/blob-inspector.rst
	docs/source/release-notes.rst
	docs/source/upgrade-notes.rst
	node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt
This commit is contained in:
Katelyn Baker
2018-07-27 16:09:26 +01:00
80 changed files with 1774 additions and 261 deletions

View File

@ -21,5 +21,6 @@ enum class ComponentGroupEnum {
ATTACHMENTS_GROUP, // ordinal = 3.
NOTARY_GROUP, // ordinal = 4.
TIMEWINDOW_GROUP, // ordinal = 5.
SIGNERS_GROUP // ordinal = 6.
SIGNERS_GROUP, // ordinal = 6.
REFERENCES_GROUP // ordinal = 7.
}

View File

@ -179,9 +179,15 @@ data class StateRef(val txhash: SecureHash, val index: Int) {
@KeepForDJVM
@CordaSerializable
// DOCSTART 7
data class StateAndRef<out T : ContractState>(val state: TransactionState<T>, val ref: StateRef)
data class StateAndRef<out T : ContractState>(val state: TransactionState<T>, val ref: StateRef) {
/** For adding [StateAndRef]s as references to a [TransactionBuilder]. */
fun referenced() = ReferencedStateAndRef(this)
}
// DOCEND 7
/** A wrapper for a [StateAndRef] indicating that it should be added to a transaction as a reference input state. */
data class ReferencedStateAndRef<out T : ContractState>(val stateAndRef: StateAndRef<T>)
/** Filters a list of [StateAndRef] objects according to the type of the states */
inline fun <reified T : ContractState> Iterable<StateAndRef<ContractState>>.filterStatesOfType(): List<StateAndRef<T>> {
return mapNotNull { if (it.state.data is T) StateAndRef(TransactionState(it.state.data, it.state.contract, it.state.notary), it.ref) else null }

View File

@ -102,7 +102,7 @@ class FinalityFlow(val transaction: SignedTransaction,
private fun needsNotarySignature(stx: SignedTransaction): Boolean {
val wtx = stx.tx
val needsNotarisation = wtx.inputs.isNotEmpty() || wtx.timeWindow != null
val needsNotarisation = wtx.inputs.isNotEmpty() || wtx.references.isNotEmpty() || wtx.timeWindow != null
return needsNotarisation && hasNoNotarySignature(stx)
}

View File

@ -13,6 +13,7 @@ package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable
import co.paralleluniverse.strands.Strand
import net.corda.core.CordaInternal
import net.corda.core.contracts.StateRef
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate
@ -30,6 +31,7 @@ import net.corda.core.utilities.debug
import net.corda.core.utilities.toNonEmptySet
import org.slf4j.Logger
import java.time.Duration
import java.util.*
/**
* A sub-class of [FlowLogic<T>] implements a flow using direct, straight line blocking code. Thus you
@ -410,6 +412,18 @@ abstract class FlowLogic<out T> {
return stateMachine.suspend(request, maySkipCheckpoint = maySkipCheckpoint)
}
/**
* Suspends the current flow until all the provided [StateRef]s have been consumed.
*
* WARNING! Remember that the flow which uses this async operation will _NOT_ wake-up until all the supplied StateRefs
* have been consumed. If the node isn't aware of the supplied StateRefs or if the StateRefs are never consumed, then
* the calling flow will remain suspended FOREVER!!
*
* @param stateRefs the StateRefs which will be consumed in the future.
*/
@Suspendable
fun waitForStateConsumption(stateRefs: Set<StateRef>) = executeAsync(WaitForStateConsumption(stateRefs, serviceHub))
/**
* Returns a shallow copy of the Quasar stack frames at the time of call to [flowStackSnapshot]. Use this to inspect
* what objects would be serialised at the time of call to a suspending action (e.g. send/receive).

View File

@ -65,11 +65,25 @@ sealed class NotaryError {
/** Contains information about the consuming transaction for a particular state. */
// TODO: include notary timestamp?
@CordaSerializable
data class StateConsumptionDetails(
data class StateConsumptionDetails @JvmOverloads constructor(
/**
* Hash of the consuming transaction id.
*
* Note that this is NOT the transaction id itself revealing it could lead to privacy leaks.
*/
val hashOfTransactionId: SecureHash
)
val hashOfTransactionId: SecureHash,
/**
* The type of consumed state: Either a reference input state or a regular input state.
*/
val type: ConsumedStateType = ConsumedStateType.INPUT_STATE
) {
@CordaSerializable
enum class ConsumedStateType { INPUT_STATE, REFERENCE_INPUT_STATE }
fun copy(hashOfTransactionId: SecureHash): StateConsumptionDetails {
return StateConsumptionDetails(hashOfTransactionId, type)
}
}

View File

@ -75,8 +75,8 @@ 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(serviceHub.loadStates(stx.inputs.toSet()).all { it.state.notary == notaryParty }) {
"Input states must have the same Notary"
check(serviceHub.loadStates(stx.inputs.toSet() + stx.references.toSet()).all { it.state.notary == notaryParty }) {
"Input states and reference input states must have the same Notary"
}
stx.resolveTransactionWithSignatures(serviceHub).verifySignaturesExcept(notaryParty.owningKey)
return notaryParty

View File

@ -26,6 +26,9 @@ import net.corda.core.transactions.SignedTransaction
* While the signature must be retained, the notarisation request does not need to be transferred or stored anywhere - it
* can be built from a [SignedTransaction] or a [CoreTransaction]. The notary can recompute it from the committed states index.
*
* Reference inputs states are not included as a separate property in the [NotarisationRequest] as they are not
* consumed.
*
* In case there is a need to prove that a party spent a particular state, the notary will:
* 1) Locate the consuming transaction id in the index, along with all other states consumed in the same transaction.
* 2) Build a [NotarisationRequest].

View File

@ -0,0 +1,115 @@
package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.StateRef
import net.corda.core.internal.uncheckedCast
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.contextLogger
/**
* Given a flow which uses reference states, the [WithReferencedStatesFlow] will execute the the flow as a subFlow.
* If the flow fails due to a [NotaryError.Conflict] for a reference state, then it will be suspended until the
* state refs for the reference states are consumed. In this case, a consumption means that:
*
* 1. the owner of the reference state has updated the state with a valid, notarised transaction
* 2. the owner of the reference state has shared the update with the node attempting to run the flow which uses the
* reference state
* 3. The node has successfully committed the transaction updating the reference state (and all the dependencies), and
* added the updated reference state to the vault.
*
* WARNING: Caution should be taken when using this flow as it facilitates automated re-running of flows which use
* reference states. The flow using reference states should include checks to ensure that the reference data is
* reasonable, especially if some economics transaction depends upon it.
*
* @param flowLogic a flow which uses reference states.
* @param progressTracker a progress tracker instance.
*/
class WithReferencedStatesFlow<T : Any>(
val flowLogic: FlowLogic<T>,
override val progressTracker: ProgressTracker = WithReferencedStatesFlow.tracker()
) : FlowLogic<T>() {
companion object {
val logger = contextLogger()
object ATTEMPT : ProgressTracker.Step("Attempting to run flow which uses reference states.")
object RETRYING : ProgressTracker.Step("Reference states are out of date! Waiting for updated states...")
object SUCCESS : ProgressTracker.Step("Flow ran successfully.")
@JvmStatic
fun tracker() = ProgressTracker(ATTEMPT, RETRYING, SUCCESS)
}
private sealed class FlowResult {
data class Success<T : Any>(val value: T) : FlowResult()
data class Conflict(val stateRefs: Set<StateRef>) : FlowResult()
}
/**
* Process the flow result. We don't care about anything other than NotaryExceptions. If it is a
* NotaryException but not a Conflict, then just rethrow. If it's a Conflict, then extract the reference
* input state refs. Otherwise, if the flow completes successfully then return the result.
*/
private fun processFlowResult(result: Any): FlowResult {
return when (result) {
is NotaryException -> {
val error = result.error
if (error is NotaryError.Conflict) {
val conflictingReferenceStateRefs = error.consumedStates.filter {
it.value.type == StateConsumptionDetails.ConsumedStateType.REFERENCE_INPUT_STATE
}.map { it.key }.toSet()
FlowResult.Conflict(conflictingReferenceStateRefs)
} else {
throw result
}
}
is FlowException -> throw result
else -> FlowResult.Success(result)
}
}
@Suspendable
override fun call(): T {
progressTracker.currentStep = ATTEMPT
// Loop until the flow successfully completes. We need to
// do this because there might be consecutive update races.
while (true) {
// Return a successful flow result or a FlowException.
logger.info("Attempting to run the supplied flow ${flowLogic.javaClass.canonicalName}.")
val result = try {
subFlow(flowLogic)
} catch (e: FlowException) {
e
}
val processedResult = processFlowResult(result)
// Return the flow result or wait for the StateRefs to be updated and try again.
// 1. If a success, we can always cast the return type to T.
// 2. If there is a conflict, then suspend this flow, only waking it up when the conflicting reference
// states have been updated.
@Suppress("UNCHECKED_CAST")
when (processedResult) {
is FlowResult.Success<*> -> {
logger.info("Flow ${flowLogic.javaClass.canonicalName} completed successfully.")
progressTracker.currentStep = SUCCESS
return uncheckedCast(processedResult.value)
}
is FlowResult.Conflict -> {
val conflicts = processedResult.stateRefs
logger.info("Flow ${flowLogic.javaClass.name} failed due to reference state conflicts: $conflicts.")
// Only set currentStep to FAILURE once.
if (progressTracker.currentStep != RETRYING) {
progressTracker.currentStep = RETRYING
}
// Suspend this flow.
waitForStateConsumption(conflicts)
logger.info("All referenced states have been updated. Retrying flow...")
}
}
}
}
}

View File

@ -34,4 +34,4 @@ fun <T, R : Any> FlowLogic<T>.executeAsync(operation: FlowAsyncOperation<R>, may
val request = FlowIORequest.ExecuteAsyncOperation(operation)
return stateMachine.suspend(request, maySkipCheckpoint)
}
// DOCEND executeAsync
// DOCEND executeAsync

View File

@ -52,12 +52,11 @@ class ResolveTransactionsFlow(txHashesArg: Set<SecureHash>,
@DeleteForDJVM
companion object {
private fun dependencyIDs(stx: SignedTransaction) = stx.inputs.map { it.txhash }.toSet()
private fun dependencyIDs(stx: SignedTransaction) = stx.inputs.map { it.txhash }.toSet() + stx.references.map { it.txhash }.toSet()
private const val RESOLUTION_PAGE_SIZE = 100
/**
* Topologically sorts the given transactions such that dependencies are listed before dependers. */
/** Topologically sorts the given transactions such that dependencies are listed before dependers. */
@JvmStatic
fun topologicalSort(transactions: Collection<SignedTransaction>): List<SignedTransaction> {
val sort = TopologicalSort()
@ -71,7 +70,7 @@ class ResolveTransactionsFlow(txHashesArg: Set<SecureHash>,
@CordaSerializable
class ExcessivelyLargeTransactionGraph : FlowException()
/** Transaction for fetch attachments for */
/** Transaction to fetch attachments for. */
private var signedTransaction: SignedTransaction? = null
// TODO: Figure out a more appropriate DOS limit here, 5000 is simply a very bad guess.
@ -156,8 +155,8 @@ class ResolveTransactionsFlow(txHashesArg: Set<SecureHash>,
for (stx in downloads)
check(resultQ.putIfAbsent(stx.id, stx) == null) // Assert checks the filter at the start.
// Add all input states to the work queue.
val inputHashes = downloads.flatMap { it.inputs }.map { it.txhash }
// Add all input states and reference input states to the work queue.
val inputHashes = downloads.flatMap { it.inputs + it.references }.map { it.txhash }
nextRequests.addAll(inputHashes)
limitCounter = limitCounter exactAdd nextRequests.size

View File

@ -17,9 +17,10 @@ class TopologicalSort {
* Add a transaction to the to-be-sorted set of transactions.
*/
fun add(stx: SignedTransaction) {
for (input in stx.inputs) {
// Note that we use a LinkedHashSet here to make the traversal deterministic (as long as the input list is)
forwardGraph.getOrPut(input.txhash) { LinkedHashSet() }.add(stx)
val stateRefs = stx.references + stx.inputs
stateRefs.forEach { (txhash) ->
// Note that we use a LinkedHashSet here to make the traversal deterministic (as long as the input list is).
forwardGraph.getOrPut(txhash) { LinkedHashSet() }.add(stx)
}
transactions.add(stx)
}

View File

@ -0,0 +1,43 @@
package net.corda.core.internal
import net.corda.core.concurrent.CordaFuture
import net.corda.core.contracts.StateRef
import net.corda.core.internal.concurrent.asCordaFuture
import net.corda.core.node.ServiceHub
import net.corda.core.utilities.contextLogger
import java.util.concurrent.CompletableFuture
/**
* An [FlowAsyncOperation] which suspends a flow until the provided [StateRef]s have been updated.
*
* WARNING! Remember that if the node isn't aware of the supplied StateRefs or if the StateRefs are never updated, then
* the calling flow will remain suspended.
*
* @property stateRefs the StateRefs which will be updated.
* @property services a ServiceHub instance
*/
class WaitForStateConsumption(val stateRefs: Set<StateRef>, val services: ServiceHub) : FlowAsyncOperation<Unit> {
companion object {
val logger = contextLogger()
}
override fun execute(): CordaFuture<Unit> {
val futures = stateRefs.map { services.vaultService.whenConsumed(it).toCompletableFuture() }
val completedFutures = futures.filter { it.isDone }
when {
completedFutures.isEmpty() ->
logger.info("All StateRefs $stateRefs have yet to be consumed. Suspending...")
futures == completedFutures ->
logger.info("All StateRefs $stateRefs have already been consumed. No need to suspend.")
else -> {
val updatedStateRefs = completedFutures.flatMap { it.get().consumed.map { it.ref } }
val notUpdatedStateRefs = stateRefs - updatedStateRefs
logger.info("StateRefs $notUpdatedStateRefs have yet to be consumed. Suspending...")
}
}
return CompletableFuture.allOf(*futures.toTypedArray()).thenApply { Unit }.asCordaFuture()
}
}

View File

@ -37,10 +37,10 @@ abstract class NotaryServiceFlow(val otherSideSession: FlowSession, val service:
txId = parts.id
checkNotary(parts.notary)
if (service is AsyncCFTNotaryService) {
val result = executeAsync(AsyncCFTNotaryService.CommitOperation(service, parts.inputs, txId, otherSideSession.counterparty, requestPayload.requestSignature, parts.timestamp))
val result = executeAsync(AsyncCFTNotaryService.CommitOperation(service, parts.inputs, txId, otherSideSession.counterparty, requestPayload.requestSignature, parts.timestamp, parts.references))
if (result is Result.Failure) throw NotaryInternalException(result.error)
} else {
service.commitInputStates(parts.inputs, txId, otherSideSession.counterparty, requestPayload.requestSignature, parts.timestamp)
service.commitInputStates(parts.inputs, txId, otherSideSession.counterparty, requestPayload.requestSignature, parts.timestamp, parts.references)
}
signTransactionAndSendResponse(txId)
} catch (e: NotaryInternalException) {
@ -89,8 +89,18 @@ abstract class NotaryServiceFlow(val otherSideSession: FlowSession, val service:
* The minimum amount of information needed to notarise a transaction. Note that this does not include
* any sensitive transaction details.
*/
protected data class TransactionParts(val id: SecureHash, val inputs: List<StateRef>, val timestamp: TimeWindow?, val notary: Party?)
protected data class TransactionParts @JvmOverloads constructor(
val id: SecureHash,
val inputs: List<StateRef>,
val timestamp: TimeWindow?,
val notary: Party?,
val references: List<StateRef> = emptyList()
) {
fun copy(id: SecureHash, inputs: List<StateRef>, timestamp: TimeWindow?, notary: Party?): TransactionParts {
return TransactionParts(id, inputs, timestamp, notary, references)
}
}
}
/** Exception internal to the notary service. Does not get exposed to CorDapps and flows calling [NotaryFlow.Client]. */
class NotaryInternalException(val error: NotaryError) : FlowException("Unable to notarise: $error")
class NotaryInternalException(val error: NotaryError) : FlowException("Unable to notarise: $error")

View File

@ -25,9 +25,23 @@ abstract class TrustedAuthorityNotaryService : NotaryService() {
* A NotaryException is thrown if any of the states have been consumed by a different transaction. Note that
* this method does not throw an exception when input states are present multiple times within the transaction.
*/
fun commitInputStates(inputs: List<StateRef>, txId: SecureHash, caller: Party, requestSignature: NotarisationRequestSignature, timeWindow: TimeWindow?) {
@JvmOverloads
fun commitInputStates(inputs: List<StateRef>, txId: SecureHash, caller: Party, requestSignature: NotarisationRequestSignature, timeWindow: TimeWindow?, references: List<StateRef> = emptyList()) {
try {
uniquenessProvider.commit(inputs, txId, caller, requestSignature, timeWindow)
uniquenessProvider.commit(inputs, txId, caller, requestSignature, timeWindow, references)
} catch (e: NotaryInternalException) {
if (e.error is NotaryError.Conflict) {
val allInputs = inputs + references
val conflicts = allInputs.filterIndexed { _, stateRef ->
val cause = e.error.consumedStates[stateRef]
cause != null && cause.hashOfTransactionId != txId.sha256()
}
if (conflicts.isNotEmpty()) {
// TODO: Create a new UniquenessException that only contains the conflicts filtered above.
log.info("Notary conflicts for $txId: $conflicts")
throw e
}
} else throw e
} catch (e: Exception) {
if (e is NotaryInternalException) throw e
log.error("Internal error", e)
@ -47,4 +61,4 @@ abstract class TrustedAuthorityNotaryService : NotaryService() {
}
// TODO: Sign multiple transactions at once by building their Merkle tree and then signing over its root.
}
}

View File

@ -19,6 +19,7 @@ interface UniquenessProvider {
txId: SecureHash,
callerIdentity: Party,
requestSignature: NotarisationRequestSignature,
timeWindow: TimeWindow? = null
timeWindow: TimeWindow? = null,
references: List<StateRef> = emptyList()
)
}

View File

@ -115,4 +115,4 @@ data class NetworkParameters(
*/
@KeepForDJVM
@CordaSerializable
data class NotaryInfo(val identity: Party, val validating: Boolean)
data class NotaryInfo(val identity: Party, val validating: Boolean)

View File

@ -19,6 +19,7 @@ import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowException
import net.corda.core.flows.FlowLogic
import net.corda.core.identity.AbstractParty
import net.corda.core.internal.concurrent.doneFuture
import net.corda.core.messaging.DataFeed
import net.corda.core.node.services.Vault.StateStatus
import net.corda.core.node.services.vault.*
@ -190,7 +191,14 @@ interface VaultService {
*/
@DeleteForDJVM
fun whenConsumed(ref: StateRef): CordaFuture<Vault.Update<ContractState>> {
return updates.filter { it.consumed.any { it.ref == ref } }.toFuture()
val query = QueryCriteria.VaultQueryCriteria(stateRefs = listOf(ref), status = Vault.StateStatus.CONSUMED)
val result = trackBy<ContractState>(query)
val snapshot = result.snapshot.states
return if (snapshot.isNotEmpty()) {
doneFuture(Vault.Update(consumed = setOf(snapshot.single()), produced = emptySet()))
} else {
result.updates.toFuture()
}
}
/**

View File

@ -25,6 +25,8 @@ import java.util.function.Predicate
@KeepForDJVM
@DoNotImplement
abstract class BaseTransaction : NamedByHash {
/** A list of reusable reference data states which can be referred to by other contracts in this transaction. */
abstract val references: List<*>
/** The inputs of this transaction. Note that in BaseTransaction subclasses the type of this list may change! */
abstract val inputs: List<*>
/** Ordered list of states defined by this transaction, along with the associated notaries. */
@ -39,16 +41,26 @@ abstract class BaseTransaction : NamedByHash {
protected open fun checkBaseInvariants() {
checkNotarySetIfInputsPresent()
checkNoDuplicateInputs()
checkForInputsAndReferencesOverlap()
}
private fun checkNotarySetIfInputsPresent() {
if (inputs.isNotEmpty()) {
if (inputs.isNotEmpty() || references.isNotEmpty()) {
check(notary != null) { "The notary must be specified explicitly for any transaction that has inputs" }
}
}
private fun checkNoDuplicateInputs() {
check(inputs.size == inputs.toSet().size) { "Duplicate input states detected" }
check(references.size == references.toSet().size) { "Duplicate reference states detected" }
}
private fun checkForInputsAndReferencesOverlap() {
val intersection = inputs intersect references
require(intersection.isEmpty()) {
"A StateRef cannot be both an input and a reference input in the same transaction. Offending " +
"StateRefs: $intersection"
}
}
/**

View File

@ -22,23 +22,26 @@ import net.corda.core.serialization.CordaSerializable
*/
@CordaSerializable
abstract class CoreTransaction : BaseTransaction() {
/** The inputs of this transaction, containing state references only **/
/** The inputs of this transaction, containing state references only. **/
abstract override val inputs: List<StateRef>
/** The reference inputs of this transaction, containing the state references only. **/
abstract override val references: List<StateRef>
}
/** A transaction with fully resolved components, such as input states. */
abstract class FullTransaction : BaseTransaction() {
abstract override val inputs: List<StateAndRef<ContractState>>
abstract override val references: List<StateAndRef<ContractState>>
override fun checkBaseInvariants() {
super.checkBaseInvariants()
checkInputsHaveSameNotary()
checkInputsAndReferencesHaveSameNotary()
}
private fun checkInputsHaveSameNotary() {
if (inputs.isEmpty()) return
val inputNotaries = inputs.map { it.state.notary }.toHashSet()
check(inputNotaries.size == 1) { "All inputs must point to the same notary" }
check(inputNotaries.single() == notary) { "The specified notary must be the one specified by all inputs" }
private fun checkInputsAndReferencesHaveSameNotary() {
if (inputs.isEmpty() && references.isEmpty()) return
val notaries = (inputs + references).map { it.state.notary }.toHashSet()
check(notaries.size == 1) { "All inputs and reference inputs must point to the same notary" }
check(notaries.single() == notary) { "The specified notary must be the one specified by all inputs and input references" }
}
}

View File

@ -65,8 +65,11 @@ data class ContractUpgradeWireTransaction(
get() = throw UnsupportedOperationException("ContractUpgradeWireTransaction does not contain output states, " +
"outputs can only be obtained from a resolved ContractUpgradeLedgerTransaction")
/** ContractUpgradeWireTransactions should not contain reference input states. */
override val references: List<StateRef> get() = emptyList()
override val id: SecureHash by lazy {
val componentHashes =serializedComponents.mapIndexed { index, component ->
val componentHashes = serializedComponents.mapIndexed { index, component ->
componentHash(nonces[index], component)
}
combinedHash(componentHashes)
@ -155,6 +158,7 @@ data class ContractUpgradeFilteredTransaction(
combinedHash(hashList)
}
override val outputs: List<TransactionState<ContractState>> get() = emptyList()
override val references: List<StateRef> get() = emptyList()
/** Contains the serialized component and the associated nonce for computing the transaction id. */
@CordaSerializable
@ -183,6 +187,8 @@ data class ContractUpgradeLedgerTransaction(
override val sigs: List<TransactionSignature>,
private val networkParameters: NetworkParameters
) : FullTransaction(), TransactionWithSignatures {
/** ContractUpgradeLEdgerTransactions do not contain reference input states. */
override val references: List<StateAndRef<ContractState>> = emptyList()
/** The legacy contract class name is determined by the first input state. */
private val legacyContractClassName = inputs.first().state.contract
private val upgradedContract: UpgradedContract<ContractState, *> = loadUpgradedContract()

View File

@ -52,7 +52,8 @@ data class LedgerTransaction @JvmOverloads constructor(
override val notary: Party?,
val timeWindow: TimeWindow?,
val privacySalt: PrivacySalt,
private val networkParameters: NetworkParameters? = null
private val networkParameters: NetworkParameters? = null,
override val references: List<StateAndRef<ContractState>> = emptyList()
) : FullTransaction() {
//DOCEND 1
init {
@ -76,10 +77,12 @@ data class LedgerTransaction @JvmOverloads constructor(
}
}
// Input reference state contracts are not required for verification.
private val contracts: Map<ContractClassName, Try<Class<out Contract>>> = (inputs.map { it.state } + outputs)
.map { it.contract to stateToContractClass(it) }.toMap()
val inputStates: List<ContractState> get() = inputs.map { it.state.data }
val referenceStates: List<ContractState> get() = references.map { it.state.data }
/**
* Returns the typed input StateAndRef at the specified index
@ -159,13 +162,13 @@ data class LedgerTransaction @JvmOverloads constructor(
/**
* Make sure the notary has stayed the same. As we can't tell how inputs and outputs connect, if there
* are any inputs, all outputs must have the same notary.
* are any inputs or reference inputs, all outputs must have the same notary.
*
* TODO: Is that the correct set of restrictions? May need to come back to this, see if we can be more
* flexible on output notaries.
*/
private fun checkNoNotaryChange() {
if (notary != null && inputs.isNotEmpty()) {
if (notary != null && (inputs.isNotEmpty() || references.isNotEmpty())) {
outputs.forEach {
if (it.notary != notary) {
throw TransactionVerificationException.NotaryChangeInWrongTransactionType(id, notary, it.notary)
@ -264,6 +267,13 @@ data class LedgerTransaction @JvmOverloads constructor(
*/
fun getInput(index: Int): ContractState = inputs[index].state.data
/**
* Helper to simplify getting an indexed reference input [ContractState].
* @param index the position of the item in the references.
* @return The [StateAndRef] at the requested index.
*/
fun getReferenceInput(index: Int): ContractState = references[index].state.data
/**
* Helper to simplify getting all inputs states of a particular class, interface, or base class.
* @param clazz The class type used for filtering via an [Class.isInstance] check.
@ -274,6 +284,16 @@ data class LedgerTransaction @JvmOverloads constructor(
inline fun <reified T : ContractState> inputsOfType(): List<T> = inputsOfType(T::class.java)
/**
* Helper to simplify getting all reference input states of a particular class, interface, or base class.
* @param clazz The class type used for filtering via an [Class.isInstance] check.
* [clazz] must be an extension of [ContractState].
* @return the possibly empty list of inputs matching the clazz restriction.
*/
fun <T : ContractState> referenceInputsOfType(clazz: Class<T>): List<T> = references.mapNotNull { clazz.castIfPossible(it.state.data) }
inline fun <reified T : ContractState> referenceInputsOfType(): List<T> = referenceInputsOfType(T::class.java)
/**
* Helper to simplify getting all inputs states of a particular class, interface, or base class.
* @param clazz The class type used for filtering via an [Class.isInstance] check.
@ -286,6 +306,18 @@ data class LedgerTransaction @JvmOverloads constructor(
inline fun <reified T : ContractState> inRefsOfType(): List<StateAndRef<T>> = inRefsOfType(T::class.java)
/**
* Helper to simplify getting all reference input states of a particular class, interface, or base class.
* @param clazz The class type used for filtering via an [Class.isInstance] check.
* [clazz] must be an extension of [ContractState].
* @return the possibly empty list of reference inputs [StateAndRef] matching the clazz restriction.
*/
fun <T : ContractState> referenceInputRefsOfType(clazz: Class<T>): List<StateAndRef<T>> {
return references.mapNotNull { if (clazz.isInstance(it.state.data)) uncheckedCast<StateAndRef<ContractState>, StateAndRef<T>>(it) else null }
}
inline fun <reified T : ContractState> referenceInputRefsOfType(): List<StateAndRef<T>> = referenceInputRefsOfType(T::class.java)
/**
* Helper to simplify filtering inputs according to a [Predicate].
* @param clazz The class type used for filtering via an [Class.isInstance] check.
@ -302,6 +334,22 @@ data class LedgerTransaction @JvmOverloads constructor(
return filterInputs(T::class.java, Predicate { predicate(it) })
}
/**
* Helper to simplify filtering reference inputs according to a [Predicate].
* @param clazz The class type used for filtering via an [Class.isInstance] check.
* [clazz] must be an extension of [ContractState].
* @param predicate A filtering function taking a state of type T and returning true if it should be included in the list.
* The class filtering is applied before the predicate.
* @return the possibly empty list of reference states matching the predicate and clazz restrictions.
*/
fun <T : ContractState> filterReferenceInputs(clazz: Class<T>, predicate: Predicate<T>): List<T> {
return referenceInputsOfType(clazz).filter { predicate.test(it) }
}
inline fun <reified T : ContractState> filterReferenceInputs(crossinline predicate: (T) -> Boolean): List<T> {
return filterReferenceInputs(T::class.java, Predicate { predicate(it) })
}
/**
* Helper to simplify filtering inputs according to a [Predicate].
* @param predicate A filtering function taking a state of type T and returning true if it should be included in the list.
@ -318,6 +366,22 @@ data class LedgerTransaction @JvmOverloads constructor(
return filterInRefs(T::class.java, Predicate { predicate(it) })
}
/**
* Helper to simplify filtering reference inputs according to a [Predicate].
* @param predicate A filtering function taking a state of type T and returning true if it should be included in the list.
* The class filtering is applied before the predicate.
* @param clazz The class type used for filtering via an [Class.isInstance] check.
* [clazz] must be an extension of [ContractState].
* @return the possibly empty list of references [StateAndRef] matching the predicate and clazz restrictions.
*/
fun <T : ContractState> filterReferenceInputRefs(clazz: Class<T>, predicate: Predicate<T>): List<StateAndRef<T>> {
return referenceInputRefsOfType(clazz).filter { predicate.test(it.state.data) }
}
inline fun <reified T : ContractState> filterReferenceInputRefs(crossinline predicate: (T) -> Boolean): List<StateAndRef<T>> {
return filterReferenceInputRefs(T::class.java, Predicate { predicate(it) })
}
/**
* Helper to simplify finding a single input [ContractState] matching a [Predicate].
* @param clazz The class type used for filtering via an [Class.isInstance] check.
@ -335,6 +399,23 @@ data class LedgerTransaction @JvmOverloads constructor(
return findInput(T::class.java, Predicate { predicate(it) })
}
/**
* Helper to simplify finding a single reference inputs [ContractState] matching a [Predicate].
* @param clazz The class type used for filtering via an [Class.isInstance] check.
* [clazz] must be an extension of ContractState.
* @param predicate A filtering function taking a state of type T and returning true if this is the desired item.
* The class filtering is applied before the predicate.
* @return the single item matching the predicate.
* @throws IllegalArgumentException if no item, or multiple items are found matching the requirements.
*/
fun <T : ContractState> findReference(clazz: Class<T>, predicate: Predicate<T>): T {
return referenceInputsOfType(clazz).single { predicate.test(it) }
}
inline fun <reified T : ContractState> findReference(crossinline predicate: (T) -> Boolean): T {
return findReference(T::class.java, Predicate { predicate(it) })
}
/**
* Helper to simplify finding a single input matching a [Predicate].
* @param clazz The class type used for filtering via an [Class.isInstance] check.
@ -352,6 +433,23 @@ data class LedgerTransaction @JvmOverloads constructor(
return findInRef(T::class.java, Predicate { predicate(it) })
}
/**
* Helper to simplify finding a single reference input matching a [Predicate].
* @param clazz The class type used for filtering via an [Class.isInstance] check.
* [clazz] must be an extension of ContractState.
* @param predicate A filtering function taking a state of type T and returning true if this is the desired item.
* The class filtering is applied before the predicate.
* @return the single item matching the predicate.
* @throws IllegalArgumentException if no item, or multiple items are found matching the requirements.
*/
fun <T : ContractState> findReferenceInputRef(clazz: Class<T>, predicate: Predicate<T>): StateAndRef<T> {
return referenceInputRefsOfType(clazz).single { predicate.test(it.state.data) }
}
inline fun <reified T : ContractState> findReferenceInputRef(crossinline predicate: (T) -> Boolean): StateAndRef<T> {
return findReferenceInputRef(T::class.java, Predicate { predicate(it) })
}
/**
* Helper to simplify getting an indexed command.
* @param index the position of the item in the commands.
@ -419,14 +517,26 @@ data class LedgerTransaction @JvmOverloads constructor(
*/
fun getAttachment(id: SecureHash): Attachment = attachments.first { it.id == id }
fun copy(inputs: List<StateAndRef<ContractState>>,
outputs: List<TransactionState<ContractState>>,
commands: List<CommandWithParties<CommandData>>,
attachments: List<Attachment>,
id: SecureHash,
notary: Party?,
timeWindow: TimeWindow?,
privacySalt: PrivacySalt
) = copy(inputs = inputs, outputs = outputs, commands = commands, attachments = attachments, id = id, notary = notary, timeWindow = timeWindow, privacySalt = privacySalt, networkParameters = null)
@JvmOverloads
fun copy(inputs: List<StateAndRef<ContractState>> = this.inputs,
outputs: List<TransactionState<ContractState>> = this.outputs,
commands: List<CommandWithParties<CommandData>> = this.commands,
attachments: List<Attachment> = this.attachments,
id: SecureHash = this.id,
notary: Party? = this.notary,
timeWindow: TimeWindow? = this.timeWindow,
privacySalt: PrivacySalt = this.privacySalt,
networkParameters: NetworkParameters? = this.networkParameters
) = copy(inputs = inputs,
outputs = outputs,
commands = commands,
attachments = attachments,
id = id,
notary = notary,
timeWindow = timeWindow,
privacySalt = privacySalt,
networkParameters = networkParameters,
references = references
)
}

View File

@ -33,6 +33,9 @@ abstract class TraversableTransaction(open val componentGroups: List<ComponentGr
/** Pointers to the input states on the ledger, identified by (tx identity hash, output index). */
override val inputs: List<StateRef> = deserialiseComponentGroup(ComponentGroupEnum.INPUTS_GROUP, { SerializedBytes<StateRef>(it).deserialize() })
/** Pointers to reference states, identified by (tx identity hash, output index). */
override val references: List<StateRef> = deserialiseComponentGroup(ComponentGroupEnum.REFERENCES_GROUP, { SerializedBytes<StateRef>(it).deserialize() })
override val outputs: List<TransactionState<ContractState>> = deserialiseComponentGroup(ComponentGroupEnum.OUTPUTS_GROUP, { SerializedBytes<TransactionState<ContractState>>(it).deserialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentsClassLoader(attachments)) })
/** Ordered list of ([CommandData], [PublicKey]) pairs that instruct the contracts what to do. */
@ -59,10 +62,11 @@ abstract class TraversableTransaction(open val componentGroups: List<ComponentGr
* - list of each attachment that is present
* - The notary [Party], if present (list with one element)
* - The time-window of the transaction, if present (list with one element)
* - list of each reference input that is present
*/
val availableComponentGroups: List<List<Any>>
get() {
val result = mutableListOf(inputs, outputs, commands, attachments)
val result = mutableListOf(inputs, outputs, commands, attachments, references)
notary?.let { result += listOf(it) }
timeWindow?.let { result += listOf(it) }
return result
@ -191,6 +195,7 @@ class FilteredTransaction internal constructor(
wtx.attachments.forEachIndexed { internalIndex, it -> filter(it, ComponentGroupEnum.ATTACHMENTS_GROUP.ordinal, internalIndex) }
if (wtx.notary != null) filter(wtx.notary, ComponentGroupEnum.NOTARY_GROUP.ordinal, 0)
if (wtx.timeWindow != null) filter(wtx.timeWindow, ComponentGroupEnum.TIMEWINDOW_GROUP.ordinal, 0)
wtx.references.forEachIndexed { internalIndex, it -> filter(it, ComponentGroupEnum.REFERENCES_GROUP.ordinal, internalIndex) }
// It is highlighted that because there is no a signers property in TraversableTransaction,
// one cannot specifically filter them in or out.
// The above is very important to ensure someone won't filter out the signers component group if at least one

View File

@ -43,6 +43,7 @@ data class NotaryChangeWireTransaction(
val serializedComponents: List<OpaqueBytes>
) : CoreTransaction() {
override val inputs: List<StateRef> = serializedComponents[INPUTS.ordinal].deserialize()
override val references: List<StateRef> = emptyList()
override val notary: Party = serializedComponents[NOTARY.ordinal].deserialize()
/** Identity of the notary service to reassign the states to.*/
val newNotary: Party = serializedComponents[NEW_NOTARY.ordinal].deserialize()
@ -109,6 +110,8 @@ data class NotaryChangeLedgerTransaction(
checkEncumbrances()
}
override val references: List<StateAndRef<ContractState>> = emptyList()
/** We compute the outputs on demand by applying the notary field modification to the inputs */
override val outputs: List<TransactionState<ContractState>>
get() = inputs.mapIndexed { pos, (state) ->

View File

@ -82,6 +82,8 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
/** Helper to access the inputs of the contained transaction. */
val inputs: List<StateRef> get() = coreTransaction.inputs
/** Helper to access the unspendable inputs of the contained transaction. */
val references: List<StateRef> get() = coreTransaction.references
/** Helper to access the notary of the contained transaction. */
val notary: Party? get() = coreTransaction.notary

View File

@ -45,7 +45,7 @@ import kotlin.collections.ArrayList
* [TransactionState] with this notary specified will be generated automatically.
*/
@DeleteForDJVM
open class TransactionBuilder(
open class TransactionBuilder @JvmOverloads constructor(
var notary: Party? = null,
var lockId: UUID = (Strand.currentStrand() as? FlowStateMachine<*>)?.id?.uuid ?: UUID.randomUUID(),
protected val inputs: MutableList<StateRef> = arrayListOf(),
@ -53,11 +53,11 @@ open class TransactionBuilder(
protected val outputs: MutableList<TransactionState<ContractState>> = arrayListOf(),
protected val commands: MutableList<Command<*>> = arrayListOf(),
protected var window: TimeWindow? = null,
protected var privacySalt: PrivacySalt = PrivacySalt()
protected var privacySalt: PrivacySalt = PrivacySalt(),
protected val references: MutableList<StateRef> = arrayListOf()
) {
constructor(notary: Party) : this(notary, (Strand.currentStrand() as? FlowStateMachine<*>)?.id?.uuid ?: UUID.randomUUID())
private val inputsWithTransactionState = arrayListOf<TransactionState<ContractState>>()
private val referencesWithTransactionState = arrayListOf<TransactionState<ContractState>>()
/**
* Creates a copy of the builder.
@ -70,9 +70,11 @@ open class TransactionBuilder(
outputs = ArrayList(outputs),
commands = ArrayList(commands),
window = window,
privacySalt = privacySalt
privacySalt = privacySalt,
references = references
)
t.inputsWithTransactionState.addAll(this.inputsWithTransactionState)
t.referencesWithTransactionState.addAll(this.referencesWithTransactionState)
return t
}
@ -82,6 +84,7 @@ open class TransactionBuilder(
for (t in items) {
when (t) {
is StateAndRef<*> -> addInputState(t)
is ReferencedStateAndRef<*> -> @Suppress("DEPRECATION") addReferenceState(t) // Will remove when feature finalised.
is SecureHash -> addAttachment(t)
is TransactionState<*> -> addOutputState(t)
is StateAndContract -> addOutputState(t.state, t.contract)
@ -123,7 +126,7 @@ open class TransactionBuilder(
}
return SerializationFactory.defaultFactory.withCurrentContext(serializationContext) {
WireTransaction(WireTransaction.createComponentGroups(inputStates(), resolvedOutputs, commands, attachments + makeContractAttachments(services.cordappProvider), notary, window), privacySalt)
WireTransaction(WireTransaction.createComponentGroups(inputStates(), resolvedOutputs, commands, attachments + makeContractAttachments(services.cordappProvider), notary, window, referenceStates()), privacySalt)
}
}
@ -137,6 +140,7 @@ open class TransactionBuilder(
* TODO - review this logic
*/
private fun makeContractAttachments(cordappProvider: CordappProvider): List<AttachmentId> {
// Reference inputs not included as it is not necessary to verify them.
return (inputsWithTransactionState + outputs).map { state ->
cordappProvider.getContractAttachmentID(state.contract)
?: throw MissingContractAttachments(listOf(state))
@ -155,42 +159,112 @@ open class TransactionBuilder(
toLedgerTransaction(services).verify()
}
open fun addInputState(stateAndRef: StateAndRef<*>): TransactionBuilder {
private fun checkNotary(stateAndRef: StateAndRef<*>) {
val notary = stateAndRef.state.notary
require(notary == this.notary) { "Input state requires notary \"$notary\" which does not match the transaction notary \"${this.notary}\"." }
require(notary == this.notary) {
"Input state requires notary \"$notary\" which does not match the transaction notary \"${this.notary}\"."
}
}
// This check is performed here as well as in BaseTransaction.
private fun checkForInputsAndReferencesOverlap() {
val intersection = inputs intersect references
require(intersection.isEmpty()) {
"A StateRef cannot be both an input and a reference input in the same transaction."
}
}
private fun checkReferencesUseSameNotary() = referencesWithTransactionState.map { it.notary }.toSet().size == 1
/**
* Adds a reference input [StateRef] to the transaction.
*
* This feature was added in version 4 of Corda, so will throw an exception for any Corda networks with a minimum
* platform version less than 4.
*
* @throws UncheckedVersionException
*/
@Deprecated(message = "Feature not yet released. Pending stabilisation.")
open fun addReferenceState(referencedStateAndRef: ReferencedStateAndRef<*>): TransactionBuilder {
val stateAndRef = referencedStateAndRef.stateAndRef
referencesWithTransactionState.add(stateAndRef.state)
// It is likely the case that users of reference states do not have permission to change the notary assigned
// to a reference state. Even if users _did_ have this permission the result would likely be a bunch of
// notary change races. As such, if a reference state is added to a transaction which is assigned to a
// different notary to the input and output states then all those inputs and outputs must be moved to the
// notary which the reference state uses.
//
// If two or more reference states assigned to different notaries are added to a transaction then it follows
// that this transaction likely _cannot_ be committed to the ledger as it unlikely that the party using the
// reference state can change the assigned notary for one of the reference states.
//
// As such, if reference states assigned to multiple different notaries are added to a transaction builder
// then the check below will fail.
check(checkReferencesUseSameNotary()) {
"Transactions with reference states using multiple different notaries are currently unsupported."
}
checkNotary(stateAndRef)
references.add(stateAndRef.ref)
checkForInputsAndReferencesOverlap()
return this
}
/** Adds an input [StateRef] to the transaction. */
open fun addInputState(stateAndRef: StateAndRef<*>): TransactionBuilder {
checkNotary(stateAndRef)
inputs.add(stateAndRef.ref)
inputsWithTransactionState.add(stateAndRef.state)
return this
}
/** Adds an attachment with the specified hash to the TransactionBuilder. */
fun addAttachment(attachmentId: SecureHash): TransactionBuilder {
attachments.add(attachmentId)
return this
}
/** Adds an output state to the transaction. */
fun addOutputState(state: TransactionState<*>): TransactionBuilder {
outputs.add(state)
return this
}
/** Adds an output state, with associated contract code (and constraints), and notary, to the transaction. */
@JvmOverloads
fun addOutputState(state: ContractState, contract: ContractClassName, notary: Party, encumbrance: Int? = null, constraint: AttachmentConstraint = AutomaticHashConstraint): TransactionBuilder {
fun addOutputState(
state: ContractState,
contract: ContractClassName,
notary: Party, encumbrance: Int? = null,
constraint: AttachmentConstraint = AutomaticHashConstraint
): TransactionBuilder {
return addOutputState(TransactionState(state, contract, notary, encumbrance, constraint))
}
/** A default notary must be specified during builder construction to use this method */
@JvmOverloads
fun addOutputState(state: ContractState, contract: ContractClassName, constraint: AttachmentConstraint = AutomaticHashConstraint): TransactionBuilder {
checkNotNull(notary) { "Need to specify a notary for the state, or set a default one on TransactionBuilder initialisation" }
fun addOutputState(
state: ContractState, contract: ContractClassName,
constraint: AttachmentConstraint = AutomaticHashConstraint
): TransactionBuilder {
checkNotNull(notary) {
"Need to specify a notary for the state, or set a default one on TransactionBuilder initialisation"
}
addOutputState(state, contract, notary!!, constraint = constraint)
return this
}
/** Adds a [Command] to the transaction. */
fun addCommand(arg: Command<*>): TransactionBuilder {
commands.add(arg)
return this
}
/**
* Adds a [Command] to the transaction, specified by the encapsulated [CommandData] object and required list of
* signing [PublicKey]s.
*/
fun addCommand(data: CommandData, vararg keys: PublicKey) = addCommand(Command(data, listOf(*keys)))
fun addCommand(data: CommandData, keys: List<PublicKey>) = addCommand(Command(data, keys))
@ -219,11 +293,19 @@ open class TransactionBuilder(
return this
}
// Accessors that yield immutable snapshots.
/** Returns an immutable list of input [StateRefs]. */
fun inputStates(): List<StateRef> = ArrayList(inputs)
/** Returns an immutable list of reference input [StateRefs]. */
fun referenceStates(): List<StateRef> = ArrayList(references)
/** Returns an immutable list of attachment hashes. */
fun attachments(): List<SecureHash> = ArrayList(attachments)
/** Returns an immutable list of output [TransactionState]s. */
fun outputStates(): List<TransactionState<*>> = ArrayList(outputs)
/** Returns an immutable list of [Command]s. */
fun commands(): List<Command<*>> = ArrayList(commands)
/**

View File

@ -58,15 +58,16 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
@DeleteForDJVM
constructor(componentGroups: List<ComponentGroup>) : this(componentGroups, PrivacySalt())
@Deprecated("Required only for backwards compatibility purposes.", ReplaceWith("WireTransaction(val componentGroups: List<ComponentGroup>, override val privacySalt: PrivacySalt)"), DeprecationLevel.WARNING)
@Deprecated("Required only in some unit-tests and for backwards compatibility purposes.", ReplaceWith("WireTransaction(val componentGroups: List<ComponentGroup>, override val privacySalt: PrivacySalt)"), DeprecationLevel.WARNING)
@DeleteForDJVM
constructor(inputs: List<StateRef>,
attachments: List<SecureHash>,
outputs: List<TransactionState<ContractState>>,
commands: List<Command<*>>,
notary: Party?,
timeWindow: TimeWindow?,
privacySalt: PrivacySalt = PrivacySalt()
@JvmOverloads constructor(
inputs: List<StateRef>,
attachments: List<SecureHash>,
outputs: List<TransactionState<ContractState>>,
commands: List<Command<*>>,
notary: Party?,
timeWindow: TimeWindow?,
privacySalt: PrivacySalt = PrivacySalt()
) : this(createComponentGroups(inputs, outputs, commands, attachments, notary, timeWindow), privacySalt)
init {
@ -86,7 +87,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
get() {
val commandKeys = commands.flatMap { it.signers }.toSet()
// TODO: prevent notary field from being set if there are no inputs and no time-window.
return if (notary != null && (inputs.isNotEmpty() || timeWindow != null)) {
return if (notary != null && (inputs.isNotEmpty() || references.isNotEmpty() || timeWindow != null)) {
commandKeys + notary.owningKey
} else {
commandKeys
@ -143,8 +144,11 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
val resolvedInputs = inputs.map { ref ->
resolveStateRef(ref)?.let { StateAndRef(it, ref) } ?: throw TransactionResolutionException(ref.txhash)
}
val resolvedReferences = references.map { ref ->
resolveStateRef(ref)?.let { StateAndRef(it, ref) } ?: throw TransactionResolutionException(ref.txhash)
}
val attachments = attachments.map { resolveAttachment(it) ?: throw AttachmentResolutionException(it) }
val ltx = LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, timeWindow, privacySalt, networkParameters)
val ltx = LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, timeWindow, privacySalt, networkParameters, resolvedReferences)
checkTransactionSize(ltx, networkParameters?.maxTransactionSize ?: 10485760)
return ltx
}
@ -160,6 +164,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
// Check attachments size first as they are most likely to go over the limit. With ContractAttachment instances
// it's likely that the same underlying Attachment CorDapp will occur more than once so we dedup on the attachment id.
ltx.attachments.distinctBy { it.id }.forEach { minus(it.size) }
minus(ltx.references.serialize().size)
minus(ltx.inputs.serialize().size)
minus(ltx.commands.serialize().size)
minus(ltx.outputs.serialize().size)
@ -249,15 +254,18 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
* Creating list of [ComponentGroup] used in one of the constructors of [WireTransaction] required
* for backwards compatibility purposes.
*/
@JvmOverloads
@CordaInternal
fun createComponentGroups(inputs: List<StateRef>,
outputs: List<TransactionState<ContractState>>,
commands: List<Command<*>>,
attachments: List<SecureHash>,
notary: Party?,
timeWindow: TimeWindow?): List<ComponentGroup> {
timeWindow: TimeWindow?,
references: List<StateRef> = emptyList()): List<ComponentGroup> {
val componentGroupMap: MutableList<ComponentGroup> = mutableListOf()
if (inputs.isNotEmpty()) componentGroupMap.add(ComponentGroup(INPUTS_GROUP.ordinal, inputs.map { it.serialize() }))
if (references.isNotEmpty()) componentGroupMap.add(ComponentGroup(REFERENCES_GROUP.ordinal, references.map { it.serialize() }))
if (outputs.isNotEmpty()) componentGroupMap.add(ComponentGroup(OUTPUTS_GROUP.ordinal, outputs.map { it.serialize() }))
// Adding commandData only to the commands group. Signers are added in their own group.
if (commands.isNotEmpty()) componentGroupMap.add(ComponentGroup(COMMANDS_GROUP.ordinal, commands.map { it.value.serialize() }))
@ -275,6 +283,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
override fun toString(): String {
val buf = StringBuilder()
buf.appendln("Transaction:")
for (reference in references) buf.appendln("${Emoji.rightArrow}REFS: $reference")
for (input in inputs) buf.appendln("${Emoji.rightArrow}INPUT: $input")
for ((data) in outputs) buf.appendln("${Emoji.leftArrow}OUTPUT: $data")
for (command in commands) buf.appendln("${Emoji.diamond}COMMAND: $command")

View File

@ -0,0 +1,164 @@
package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.*
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.node.StatesToRecord
import net.corda.core.node.services.queryBy
import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.getOrThrow
import net.corda.node.VersionInfo
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.contracts.DummyContract
import net.corda.testing.contracts.DummyState
import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.InternalMockNodeParameters
import net.corda.testing.node.internal.cordappsForPackages
import net.corda.testing.node.internal.startFlow
import org.junit.After
import org.junit.Test
import kotlin.test.assertEquals
// A dummy reference state contract.
internal class RefState : Contract {
companion object {
val CONTRACT_ID = "net.corda.core.flows.RefState"
}
override fun verify(tx: LedgerTransaction) = Unit
data class State(val owner: Party, val version: Int = 0, override val linearId: UniqueIdentifier = UniqueIdentifier()) : LinearState {
override val participants: List<AbstractParty> get() = listOf(owner)
fun update() = copy(version = version + 1)
}
class Create : CommandData
class Update : CommandData
}
// A flow to create a reference state.
internal class CreateRefState : FlowLogic<SignedTransaction>() {
@Suspendable
override fun call(): SignedTransaction {
val notary = serviceHub.networkMapCache.notaryIdentities.first()
return subFlow(FinalityFlow(
transaction = serviceHub.signInitialTransaction(TransactionBuilder(notary = notary).apply {
addOutputState(RefState.State(ourIdentity), RefState.CONTRACT_ID)
addCommand(RefState.Create(), listOf(ourIdentity.owningKey))
})
))
}
}
// A flow to update a specific reference state.
internal class UpdateRefState(val stateAndRef: StateAndRef<ContractState>) : FlowLogic<SignedTransaction>() {
@Suspendable
override fun call(): SignedTransaction {
val notary = serviceHub.networkMapCache.notaryIdentities.first()
return subFlow(FinalityFlow(
transaction = serviceHub.signInitialTransaction(TransactionBuilder(notary = notary).apply {
addInputState(stateAndRef)
addOutputState((stateAndRef.state.data as RefState.State).update(), RefState.CONTRACT_ID)
addCommand(RefState.Update(), listOf(ourIdentity.owningKey))
})
))
}
}
// A set of flows to share a stateref with all other nodes in the mock network.
internal object ShareRefState {
@InitiatingFlow
class Initiator(val stateAndRef: StateAndRef<ContractState>) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
val sessions = serviceHub.networkMapCache.allNodes.flatMap { it.legalIdentities }.map { initiateFlow(it) }
val transactionId = stateAndRef.ref.txhash
val transaction = serviceHub.validatedTransactions.getTransaction(transactionId)
?: throw FlowException("Cannot find $transactionId.")
sessions.forEach { subFlow(SendTransactionFlow(it, transaction)) }
}
}
@InitiatedBy(ShareRefState.Initiator::class)
class Responder(val otherSession: FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
logger.info("Receiving dependencies.")
subFlow(ReceiveTransactionFlow(
otherSideSession = otherSession,
checkSufficientSignatures = true,
statesToRecord = StatesToRecord.ALL_VISIBLE
))
}
}
}
// A flow to use a reference state in another transaction.
internal class UseRefState(val linearId: UniqueIdentifier) : FlowLogic<SignedTransaction>() {
@Suspendable
override fun call(): SignedTransaction {
val notary = serviceHub.networkMapCache.notaryIdentities.first()
val query = QueryCriteria.LinearStateQueryCriteria(linearId = listOf(linearId))
val referenceState = serviceHub.vaultService.queryBy<ContractState>(query).states.single()
return subFlow(FinalityFlow(
transaction = serviceHub.signInitialTransaction(TransactionBuilder(notary = notary).apply {
@Suppress("DEPRECATION") // To be removed when feature is finalised.
addReferenceState(referenceState.referenced())
addOutputState(DummyState(), DummyContract.PROGRAM_ID)
addCommand(DummyContract.Commands.Create(), listOf(ourIdentity.owningKey))
})
))
}
}
class WithReferencedStatesFlowTests {
companion object {
@JvmStatic
private val mockNet = InternalMockNetwork(
cordappsForAllNodes = cordappsForPackages("net.corda.core.flows", "net.corda.testing.contracts"),
threadPerNode = true,
networkParameters = testNetworkParameters(minimumPlatformVersion = 4)
)
}
private val nodes = (0..1).map {
mockNet.createNode(
parameters = InternalMockNodeParameters(version = VersionInfo(4, "Blah", "Blah", "Blah"))
)
}
@After
fun stop() {
mockNet.stopNodes()
}
@Test
fun test() {
// 1. Create reference state.
val newRefTx = nodes[0].services.startFlow(CreateRefState()).resultFuture.getOrThrow()
val newRefState = newRefTx.tx.outRefsOfType<RefState.State>().single()
// 2. Share it with others.
nodes[0].services.startFlow(ShareRefState.Initiator(newRefState)).resultFuture.getOrThrow()
// 3. Update the reference state but don't share the update.
val updatedRefTx = nodes[0].services.startFlow(UpdateRefState(newRefState)).resultFuture.getOrThrow()
val updatedRefState = updatedRefTx.tx.outRefsOfType<RefState.State>().single()
// 4. Try to use the old reference state. This will throw a NotaryException.
val useRefTx = nodes[1].services.startFlow(WithReferencedStatesFlow(UseRefState(newRefState.state.data.linearId))).resultFuture
// 5. Share the update reference state.
nodes[0].services.startFlow(ShareRefState.Initiator(updatedRefState)).resultFuture.getOrThrow()
// 6. Check that we have a valid signed transaction with the updated reference state.
val result = useRefTx.getOrThrow()
assertEquals(updatedRefState.ref, result.tx.references.single())
}
}

View File

@ -24,8 +24,9 @@ class TopologicalSortTest {
class DummyTransaction constructor(
override val id: SecureHash,
override val inputs: List<StateRef>,
@Suppress("CanBeParameter") private val numberOfOutputs: Int,
override val notary: Party
@Suppress("CanBeParameter") val numberOfOutputs: Int,
override val notary: Party,
override val references: List<StateRef> = emptyList()
) : CoreTransaction() {
override val outputs: List<TransactionState<ContractState>> = (1..numberOfOutputs).map {
TransactionState(DummyState(), "", notary)

View File

@ -0,0 +1,196 @@
package net.corda.core.transactions
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.contracts.*
import net.corda.core.contracts.Requirements.using
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.finance.DOLLARS
import net.corda.finance.`issued by`
import net.corda.finance.contracts.asset.Cash
import net.corda.node.services.api.IdentityServiceInternal
import net.corda.testing.common.internal.testNetworkParameters
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 net.corda.testing.node.MockServices
import net.corda.testing.node.ledger
import org.junit.Rule
import org.junit.Test
import kotlin.test.assertFailsWith
val CONTRACT_ID = "net.corda.core.transactions.ReferenceStateTests\$ExampleContract"
class ReferenceStateTests {
private companion object {
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
val ISSUER = TestIdentity(CordaX500Name("ISSUER", "London", "GB"))
val ALICE = TestIdentity(CordaX500Name("ALICE", "London", "GB"))
val ALICE_PARTY get() = ALICE.party
val ALICE_PUBKEY get() = ALICE.publicKey
val BOB = TestIdentity(CordaX500Name("BOB", "London", "GB"))
val BOB_PARTY get() = BOB.party
val BOB_PUBKEY get() = BOB.publicKey
}
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
val defaultIssuer = ISSUER.ref(1)
val bobCash = Cash.State(amount = 1000.DOLLARS `issued by` defaultIssuer, owner = BOB_PARTY)
private val ledgerServices = MockServices(
cordappPackages = listOf("net.corda.core.transactions", "net.corda.finance.contracts.asset"),
initialIdentity = ALICE,
identityService = rigorousMock<IdentityServiceInternal>().also {
doReturn(ALICE_PARTY).whenever(it).partyFromKey(ALICE_PUBKEY)
doReturn(BOB_PARTY).whenever(it).partyFromKey(BOB_PUBKEY)
},
networkParameters = testNetworkParameters(minimumPlatformVersion = 4)
)
// This state has only been created to serve reference data so it cannot ever be used as an input or
// output when it is being referred to. However, we might want all states to be referable, so this
// check might not be present in other contracts, like Cash, for example. Cash might have a command
// called "Share" that allows a party to prove to another that they own over a certain amount of cash.
// As such, cash can be added to the references list with a "Share" command.
data class ExampleState(val creator: Party, val data: String) : ContractState {
override val participants: List<AbstractParty> get() = listOf(creator)
}
class ExampleContract : Contract {
interface Commands : CommandData
class Create : Commands
class Update : Commands
override fun verify(tx: LedgerTransaction) {
val command = tx.commands.requireSingleCommand<Commands>()
when (command.value) {
is Create -> requireThat {
"Must have no inputs" using (tx.inputs.isEmpty())
"Must have only one output" using (tx.outputs.size == 1)
val output = tx.outputsOfType<ExampleState>().single()
val signedByCreator = command.signers.single() == output.participants.single().owningKey
"Must be signed by creator" using signedByCreator
}
is Update -> {
"Must have no inputs" using (tx.inputs.size == 1)
"Must have only one output" using (tx.outputs.size == 1)
val input = tx.inputsOfType<ExampleState>().single()
val output = tx.outputsOfType<ExampleState>().single()
"Must update the data" using (input.data != output.data)
val signedByCreator = command.signers.single() == output.participants.single().owningKey
"Must be signed by creator" using signedByCreator
}
}
}
}
@Test
fun `create a reference state then refer to it multiple times`() {
ledgerServices.ledger(DUMMY_NOTARY) {
// Create a reference state. The reference state is created in the normal way. A transaction with one
// or more outputs. It makes sense to create them one at a time, so the creator can have fine grained
// control over who sees what.
transaction {
output(CONTRACT_ID, "REF DATA", ExampleState(ALICE_PARTY, "HELLO CORDA"))
command(ALICE_PUBKEY, ExampleContract.Create())
verifies()
}
// Somewhere down the line, Bob obtains the ExampleState and now refers to it as a reference state. As such,
// it is added to the references list.
transaction {
reference("REF DATA")
input(Cash.PROGRAM_ID, bobCash)
output(Cash.PROGRAM_ID, "ALICE CASH", bobCash.withNewOwner(ALICE_PARTY).ownableState)
command(BOB_PUBKEY, Cash.Commands.Move())
verifies()
}
// Alice can use it too.
transaction {
reference("REF DATA")
input("ALICE CASH")
output(Cash.PROGRAM_ID, "BOB CASH 2", bobCash.withNewOwner(BOB_PARTY).ownableState)
command(ALICE_PUBKEY, Cash.Commands.Move())
verifies()
}
// Bob can use it again.
transaction {
reference("REF DATA")
input("BOB CASH 2")
output(Cash.PROGRAM_ID, bobCash.withNewOwner(ALICE_PARTY).ownableState)
command(BOB_PUBKEY, Cash.Commands.Move())
verifies()
}
}
}
@Test
fun `Non-creator node cannot spend spend a reference state`() {
ledgerServices.ledger(DUMMY_NOTARY) {
transaction {
output(CONTRACT_ID, "REF DATA", ExampleState(ALICE_PARTY, "HELLO CORDA"))
command(ALICE_PUBKEY, ExampleContract.Create())
verifies()
}
// Try to spend an unspendable input by accident. Opps! This should fail as per the contract above.
transaction {
input("REF DATA")
input(Cash.PROGRAM_ID, bobCash)
output(Cash.PROGRAM_ID, bobCash.withNewOwner(ALICE_PARTY).ownableState)
command(BOB_PUBKEY, Cash.Commands.Move())
fails()
}
}
}
@Test
fun `Can't use old reference states`() {
val refData = ExampleState(ALICE_PARTY, "HELLO CORDA")
ledgerServices.ledger(DUMMY_NOTARY) {
transaction {
output(CONTRACT_ID, "REF DATA", refData)
command(ALICE_PUBKEY, ExampleContract.Create())
verifies()
}
// Refer to it. All OK.
transaction {
reference("REF DATA")
input(Cash.PROGRAM_ID, bobCash)
output(Cash.PROGRAM_ID, "ALICE CASH", bobCash.withNewOwner(ALICE_PARTY).ownableState)
command(BOB_PUBKEY, Cash.Commands.Move())
verifies()
}
// Update it.
transaction {
input("REF DATA")
command(ALICE_PUBKEY, ExampleContract.Update())
output(Cash.PROGRAM_ID, "UPDATED REF DATA", "REF DATA".output<ExampleState>().copy(data = "NEW STUFF!"))
verifies()
}
// Try to use the old one.
transaction {
reference("REF DATA")
input("ALICE CASH")
output(Cash.PROGRAM_ID, bobCash.withNewOwner(BOB_PARTY).ownableState)
command(ALICE_PUBKEY, Cash.Commands.Move())
verifies()
}
fails() // "double spend" of ExampleState!! Alice updated it in the 3rd transaction.
}
}
@Test
fun `state ref cannot be a reference input and regular input in the same transaction`() {
val state = ExampleState(ALICE_PARTY, "HELLO CORDA")
val stateAndRef = StateAndRef(TransactionState(state, CONTRACT_ID, DUMMY_NOTARY), StateRef(SecureHash.zeroHash, 0))
assertFailsWith(IllegalArgumentException::class, "A StateRef cannot be both an input and a reference input in the same transaction.") {
@Suppress("DEPRECATION") // To be removed when feature is finalised.
TransactionBuilder(notary = DUMMY_NOTARY).addInputState(stateAndRef).addReferenceState(stateAndRef.referenced())
}
}
}