mirror of
https://github.com/corda/corda.git
synced 2025-06-22 17:09:00 +00:00
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:
@ -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.
|
||||
}
|
||||
|
@ -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 }
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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).
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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].
|
||||
|
@ -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...")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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")
|
||||
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ interface UniquenessProvider {
|
||||
txId: SecureHash,
|
||||
callerIdentity: Party,
|
||||
requestSignature: NotarisationRequestSignature,
|
||||
timeWindow: TimeWindow? = null
|
||||
timeWindow: TimeWindow? = null,
|
||||
references: List<StateRef> = emptyList()
|
||||
)
|
||||
}
|
@ -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)
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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" }
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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) ->
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
||||
/**
|
||||
|
@ -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")
|
||||
|
@ -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())
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user