mirror of
https://github.com/corda/corda.git
synced 2025-02-20 09:26:41 +00:00
CORDA-1332 Reference input states (#3525)
* * First commit for reference input states feature. * Added docs. * Added additional test. * Fixed whitespace. * Rebased to master. * Updated Raft and persistent notary implementations. * Updated changelog. * Updated topo sort to handle reference states. * Stubbed out with referenced states flow. * Added WithReferencedStatesFlow. * Added Tests for WithReferencedStatesFlow. * Added ReferenceState type. * Rebased to latest version of master. * Added better comments. * Updated unit test. * Added comment to explain a little hack. * Fixed broken contract upgrade RPC test. * Added minimum platform version check. * Updated mock network so that notary nodes inherit the platform version set by the network's minimum platform version. * References states can now only be used when minimum platform version >= 4. * Created a new file to hold async operations as "WaitForStatesToUpdate" is broadly reusable. * Refactored WithReferenceStatesFlow * Updated javadoc for WaitForStatesToUpdate aysnc operation. * Added network parameters property to InternalMockNetwork. * Added min platform version capability to mock services. * Removed erroneous chars from file. * Made async operation internal and now call it from FlowLogic. * Moved some transaction checking code around. * Removed serializable annotation from referenced state and ref. * Added reference states design doc. * Added missing KDocs. * Updated with reference states flow to handle consecutive update races. * Made platform version info an inheritable thread local. * Fixed various typos. * Updated docs. * Fixed race. * Removed min platform version checks as API needs more thought. * Added deprecation to method and supressed warnings. * Renamed WaitForStatesToUpdate to WaitForStateConsumption. * Fixed race in WaitForStateConsumption. * Addressed PR comments and updated comments / KDocs. * Reverse vault bugfixes. * * Vault bug fixes. * * Fixed broken test. * Moved WaitForStateConsumption to internal package.
This commit is contained in:
parent
f5b86d32f8
commit
8207ac3222
@ -11,5 +11,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.
|
||||
}
|
||||
|
@ -169,9 +169,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 }
|
||||
|
@ -92,7 +92,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)
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,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
|
||||
@ -20,6 +21,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
|
||||
@ -400,6 +402,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,8 +65,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
|
||||
|
@ -16,6 +16,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...")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -24,4 +24,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
|
@ -42,12 +42,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()
|
||||
@ -61,7 +60,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.
|
||||
@ -146,8 +145,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()
|
||||
}
|
||||
}
|
@ -34,7 +34,7 @@ abstract class NotaryServiceFlow(val otherSideSession: FlowSession, val service:
|
||||
val parts = validateRequest(requestPayload)
|
||||
txId = parts.id
|
||||
checkNotary(parts.notary)
|
||||
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) {
|
||||
throw NotaryException(e.error, txId)
|
||||
@ -82,7 +82,17 @@ 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]. */
|
||||
|
@ -25,12 +25,14 @@ 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 conflicts = inputs.filterIndexed { _, stateRef ->
|
||||
val allInputs = inputs + references
|
||||
val conflicts = allInputs.filterIndexed { _, stateRef ->
|
||||
val cause = e.error.consumedStates[stateRef]
|
||||
cause != null && cause.hashOfTransactionId != txId.sha256()
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ interface UniquenessProvider {
|
||||
txId: SecureHash,
|
||||
callerIdentity: Party,
|
||||
requestSignature: NotarisationRequestSignature,
|
||||
timeWindow: TimeWindow? = null
|
||||
timeWindow: TimeWindow? = null,
|
||||
references: List<StateRef> = emptyList()
|
||||
)
|
||||
}
|
@ -105,4 +105,4 @@ data class NetworkParameters(
|
||||
*/
|
||||
@KeepForDJVM
|
||||
@CordaSerializable
|
||||
data class NotaryInfo(val identity: Party, val validating: Boolean)
|
||||
data class NotaryInfo(val identity: Party, val validating: Boolean)
|
@ -9,6 +9,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.*
|
||||
@ -180,7 +181,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()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -15,6 +15,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. */
|
||||
@ -29,16 +31,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"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -12,23 +12,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" }
|
||||
}
|
||||
}
|
@ -55,8 +55,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)
|
||||
@ -145,6 +148,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
|
||||
@ -173,6 +177,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()
|
||||
|
@ -42,7 +42,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 {
|
||||
@ -66,10 +67,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
|
||||
@ -149,13 +152,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)
|
||||
@ -254,6 +257,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.
|
||||
@ -264,6 +274,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.
|
||||
@ -276,6 +296,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.
|
||||
@ -292,6 +324,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.
|
||||
@ -308,6 +356,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.
|
||||
@ -325,6 +389,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.
|
||||
@ -342,6 +423,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.
|
||||
@ -409,14 +507,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
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -23,6 +23,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. */
|
||||
@ -49,10 +52,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
|
||||
@ -181,6 +185,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
|
||||
|
@ -33,6 +33,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()
|
||||
@ -99,6 +100,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) ->
|
||||
|
@ -72,6 +72,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
|
||||
|
||||
|
@ -35,7 +35,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(),
|
||||
@ -43,11 +43,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.
|
||||
@ -60,9 +60,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
|
||||
}
|
||||
|
||||
@ -72,6 +74,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)
|
||||
@ -113,7 +116,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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -127,6 +130,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))
|
||||
@ -145,42 +149,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))
|
||||
|
||||
@ -209,11 +283,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)
|
||||
|
||||
/**
|
||||
|
@ -48,15 +48,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 {
|
||||
@ -76,7 +77,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
|
||||
@ -133,8 +134,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
|
||||
}
|
||||
@ -150,6 +154,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)
|
||||
@ -239,15 +244,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() }))
|
||||
@ -265,6 +273,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())
|
||||
}
|
||||
}
|
||||
}
|
@ -36,6 +36,7 @@ A transaction consists of six types of components:
|
||||
|
||||
* 0+ input states
|
||||
* 0+ output states
|
||||
* 0+ reference input states
|
||||
|
||||
* 1+ commands
|
||||
* 0+ attachments
|
||||
@ -90,6 +91,53 @@ The ``StateRef`` links an input state back to the transaction that created it. T
|
||||
"chains" linking each input back to an original issuance transaction. This allows nodes verifying the transaction
|
||||
to "walk the chain" and verify that each input was generated through a valid sequence of transactions.
|
||||
|
||||
Reference input states
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A reference input state is a ``ContractState`` which can be referred to in a transaction by the contracts of input and
|
||||
output states but whose contract is not executed as part of the transaction verification process. Furthermore,
|
||||
reference states are not consumed when the transaction is committed to the ledger but they are checked for
|
||||
"current-ness". In other words, the contract logic isn't run for the referencing transaction only. It's still a normal
|
||||
state when it occurs in an input or output position.
|
||||
|
||||
Reference data states enable many parties to "reuse" the same state in their transactions as reference data whilst
|
||||
still allowing the reference data state owner the capability to update the state.
|
||||
|
||||
A reference input state is added to a transaction as a ``ReferencedStateAndRef``. A ``ReferencedStateAndRef`` can be
|
||||
obtained from a ``StateAndRef`` by calling the ``StateAndRef.referenced()`` method which returns a
|
||||
``ReferencedStateAndRef``.
|
||||
|
||||
.. warning:: Reference states are only available on Corda networks with a minimum platform version >= 4.
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 55
|
||||
:end-before: DOCEND 55
|
||||
:dedent: 8
|
||||
|
||||
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java
|
||||
:language: java
|
||||
:start-after: DOCSTART 55
|
||||
:end-before: DOCEND 55
|
||||
:dedent: 12
|
||||
|
||||
**Known limitations:**
|
||||
|
||||
*Notary change:* 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.
|
||||
|
||||
Output states
|
||||
^^^^^^^^^^^^^
|
||||
Since a transaction's output states do not exist until the transaction is committed, they cannot be referenced as the
|
||||
@ -298,6 +346,7 @@ We can add components to the builder using the ``TransactionBuilder.withItems``
|
||||
``withItems`` takes a ``vararg`` of objects and adds them to the builder based on their type:
|
||||
|
||||
* ``StateAndRef`` objects are added as input states
|
||||
* ``ReferencedStateAndRef`` objects are added as reference input states
|
||||
* ``TransactionState`` and ``StateAndContract`` objects are added as output states
|
||||
|
||||
* Both ``TransactionState`` and ``StateAndContract`` are wrappers around a ``ContractState`` output that link the
|
||||
|
@ -182,6 +182,12 @@ Unreleased
|
||||
|
||||
* Upgraded Artemis to v2.6.2.
|
||||
|
||||
* Introduced the concept of "reference input states". A reference input state is a ``ContractState`` which can be referred
|
||||
to in a transaction by the contracts of input and output states but whose contract is not executed as part of the
|
||||
transaction verification process and is not consumed when the transaction is committed to the ledger but is checked
|
||||
for "current-ness". In other words, the contract logic isn't run for the referencing transaction only. It's still a
|
||||
normal state when it occurs in an input or output position.
|
||||
|
||||
.. _changelog_v3.1:
|
||||
|
||||
Version 3.1
|
||||
|
239
docs/source/design/reference-states/design.md
Normal file
239
docs/source/design/reference-states/design.md
Normal file
@ -0,0 +1,239 @@
|
||||

|
||||
|
||||
# Design
|
||||
|
||||
DOCUMENT MANAGEMENT
|
||||
---
|
||||
|
||||
Design documents should follow the standard GitHub version management and pull request (PR) review workflow mechanism.
|
||||
|
||||
## Document Control
|
||||
|
||||
| Title | |
|
||||
| -------------------- | ---------------------------------------- |
|
||||
| Date | 27 March 2018 |
|
||||
| Author | Roger Willis |
|
||||
| Distribution | Matthew Nesbit, Rick Parker |
|
||||
| Corda target version | open source and enterprise |
|
||||
| JIRA reference | No JIRA's raised. |
|
||||
|
||||
## Approvals
|
||||
|
||||
#### Document Sign-off
|
||||
|
||||
| Author | |
|
||||
| ----------------- | ---------------------------------------- |
|
||||
| Reviewer(s) | (GitHub PR reviewers) |
|
||||
| Final approver(s) | (GitHub PR approver(s) from Design Approval Board) |
|
||||
|
||||
#### Design Decisions
|
||||
|
||||
There's only really one way to do this that satisfies our requirements - add a new input `StateAndRef` component group to the transaction classes. Other possible solutions are discussed below and why they are inappropriate.
|
||||
|
||||
## Document History
|
||||
|
||||
* [Version 1](https://github.com/corda/enterprise/blob/779aaefa5c09a6a28191496dd45252b6e207b7f7/docs/source/design/reference-states/design.md) (Received comments from Richard Brown and Mark Oldfield).
|
||||
* [Version 2](https://github.com/corda/enterprise/blob/a87f1dcb22ba15081b0da92ba1501b6b81ae2baf/docs/source/design/reference-states/design.md) (Version presented to the DRB).
|
||||
|
||||
HIGH LEVEL DESIGN
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
See a prototype implementation here: https://github.com/corda/corda/pull/2889
|
||||
|
||||
There is an increasing need for Corda to support use-cases which require reference data which is issued and updated by specific parties, but available for use, by reference, in transactions built by other parties.
|
||||
|
||||
Why is this type of reference data required?
|
||||
|
||||
1. A key benefit of blockchain systems is that everybody is sure they see the same as their counterpart - and for this to work in situations where accurate processing depends on reference data requires everybody to be operating on the same reference data.
|
||||
2. This, in turn, requires any given piece of reference data to be uniquely identifiable and, requires that any given transaction must be certain to be operating on the most current version of that reference data.
|
||||
3. In cases where the latter condition applies, only the notary can attest to this fact and this, in turn, means the reference data must be in the form of an unconsumed state.
|
||||
|
||||
This document outlines the approach for adding support for this type of reference data to the Corda transaction model via a new approach called "reference input states".
|
||||
|
||||
## Background
|
||||
|
||||
Firstly, it is worth considering the types of reference data on Corda how it is distributed:
|
||||
|
||||
1. **Rarely changing universal reference data.** Such as currency codes and holiday calendars. This type of data can be added to transactions as attachments and referenced within contracts, if required. This data would only change based upon the decision of an International standards body, for example, therefore it is not critical to check the data is current each time it is used.
|
||||
2. **Constantly changing reference data.** Typically, this type of data must be collected and aggregated by a central party. Oracles can be used as a central source of truth for this type of constantly changing data. There are multiple examples of making transaction validity contingent on data provided by Oracles (IRS demo and SIMM demo). The Oracle asserts the data was valid at the time it was provided.
|
||||
3. **Periodically changing subjective reference data.** Reference data provided by entities such as bond issuers where the data changes frequently enough to warrant users of the data check it is current.
|
||||
|
||||
At present, periodically changing subjective data can only be provided via:
|
||||
|
||||
* Oracles,
|
||||
* Attachments,
|
||||
* Regular contract states, or alternatively,
|
||||
* kept off-ledger entirely
|
||||
|
||||
However, neither of these solutions are optimal for reasons discussed in later sections of this design document.
|
||||
|
||||
As such, this design document introduces the concept of a "reference input state" which is a better way to serve "periodically changing subjective reference data" on Corda.
|
||||
|
||||
*What is a "reference input state"?*
|
||||
|
||||
A reference input state is a `ContractState` which can be referred to in a transaction by the contracts of input and output states but whose contract is not executed as part of the transaction verification process and is not consumed when the transaction is committed to the ledger but _is_ checked for "current-ness". In other words, the contract logic isn't run for the referencing transaction only. It's still a normal state when it occurs in an input or output position.
|
||||
|
||||
*What will reference input states enable?*
|
||||
|
||||
Reference data states will enable many parties to "reuse" the same state in their transactions as reference data whilst still allowing the reference data state owner the capability to update the state. When data distribution groups are available then reference state owners will be able to distribute updates to subscribers more easily. Currently, distribution would have to be performed manually.
|
||||
|
||||
*Roughly, how are reference input states implemented?*
|
||||
|
||||
Reference input states can be added to Corda by adding a new transaction component group that allows developers to add reference data `ContractState`s that are not consumed when the transaction is committed to the ledger. This eliminates the problems created by long chains of provenance, contention, and allows developers to use any `ContractState` for reference data. The feature should allow developers to add _any_ `ContractState` available in their vault, even if they are not a `participant` whilst nevertheless providing a guarantee that the state being used is the most recent version of that piece of information.
|
||||
|
||||
## Scope
|
||||
|
||||
Goals
|
||||
|
||||
* Add the capability to Corda transactions to support reference states
|
||||
|
||||
Non-goals (eg. out of scope)
|
||||
|
||||
* Data distribution groups are required to realise the full potential of reference data states. This design document does not discuss data distribution groups.
|
||||
|
||||
## Timeline
|
||||
|
||||
This work should be ready by the release of Corda V4. There is a prototype which is currently good enough for one of the firm's most critical projects, but more work is required:
|
||||
|
||||
* to assess the impact of this change
|
||||
* write tests
|
||||
* write documentation
|
||||
|
||||
## Requirements
|
||||
|
||||
1. Reference states can be any `ContractState` created by one or more `Party`s and subsequently updated by those `Party`s. E.g. `Cash`, `CompanyData`, `InterestRateSwap`, `FxRate`. Reference states can be `OwnableState`s, but it's more likely they will be `LinearState`s.
|
||||
2. Any `Party` with a `StateRef` for a reference state should be able to add it to a transaction to be used as a reference, even if they are not a `participant` of the reference state.
|
||||
3. The contract code for reference states should not be executed. However, reference data states can be referred to by the contracts of `ContractState`s in the input and output lists.
|
||||
4. `ContractStates` should not be consumed when used as reference data.
|
||||
5. Reference data must be current, therefore when reference data states are used in a transaction, notaries should check that they have not been consumed before.
|
||||
6. To ensure determinism of the contract verification process, reference data states must be in scope for the purposes of transaction resolution. This is because whilst users of the reference data are not consuming the state, they must be sure that the series of transactions that created and evolved the state were executed validly.
|
||||
|
||||
**Use-cases:**
|
||||
|
||||
The canonical use-case for reference states: *KYC*
|
||||
|
||||
* KYC data can be distributed as reference data states.
|
||||
* KYC data states can only updatable by the data owner.
|
||||
* Usable by any party - transaction verification can be conditional on this KYC/reference data.
|
||||
* Notary ensures the data is current.
|
||||
|
||||
Collateral reporting:
|
||||
|
||||
* Imagine a bank needs to provide evidence to another party (like a regulator) that they hold certain states, such as cash and collateral, for liquidity reporting purposes
|
||||
* The regulator holds a liquidity reporting state that maintains a record of past collateral reports and automates the handling of current reports using some contract code
|
||||
* To update the liquidity reporting state, the regulator needs to include the bank’s cash/collateral states in a transaction – the contract code checks available collateral vs requirements. By doing this, the cash/collateral states would be consumed, which is not desirable
|
||||
* Instead, what if those cash/collateral states could be referenced in a transaction but not consumed? And at the same time, the notary still checks to see if the cash/collateral states are current, or not (i.e. does the bank still own them)
|
||||
|
||||
Other uses:
|
||||
|
||||
* Distributing reference data for financial instruments. E.g. Bond issuance details created, updated and distributed by the bond issuer rather than a third party.
|
||||
* Account level data included in cash payment transactions.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
There are various other ways to implement reference data on Corda, discussed below:
|
||||
|
||||
**Regular contract states**
|
||||
|
||||
Currently, the transaction model is too cumbersome to support reference data as unconsumed states for the following reasons:
|
||||
|
||||
* Contract verification is required for the `ContractState`s used as reference data. This limits the use of states, such as `Cash` as reference data (unless a special "reference" command is added which allows a "NOOP" state transaction to assert no that changes were made.)
|
||||
* As such, whenever an input state reference is added to a transaction as reference data, an output state must be added, otherwise the state will be extinguished. This results in long chains of unnecessarily duplicated data.
|
||||
* Long chains of provenance result in confidentiality breaches as down-stream users of the reference data state see all the prior uses of the reference data in the chain of provenance. This is an important point: it means that two parties, who have no business relationship and care little about each other's transactions nevertheless find themselves intimately bound: should one of them rely on a piece of common reference data in a transaction, the other one will not only need to be informed but will need to be furnished with a copy of the transaction.
|
||||
* Reference data states will likely be used by many parties so they will be come highly contended. Parties will "race" to use the reference data. The latest copy must be continually distributed to all that require it.
|
||||
|
||||
**Attachments**
|
||||
|
||||
Of course, attachments can be used to store and share reference data. This approach does solve the contention issue around reference data as regular contract states. However, attachments don't allow users to ascertain whether they are working on the most recent copy of the data. Given that it's crucial to know whether reference data is current, attachments cannot provide a workable solution here.
|
||||
|
||||
The other issue with attachments is that they do not give an intrinsic "format" to data, like state objects do. This makes working with attachments much harder as their contents are effectively bespoke. Whilst a data format tool could be written, it's more convenient to work with state objects.
|
||||
|
||||
**Oracles**
|
||||
|
||||
Whilst Oracles could provide a solution for periodically changing reference data, they introduce unnecessary centralisation and are onerous to implement for each class of reference data. Oracles don't feel like an optimal solution here.
|
||||
|
||||
**Keeping reference data off-ledger**
|
||||
|
||||
It makes sense to push as much verification as possible into the contract code, otherwise why bother having it? Performing verification inside flows is generally not a good idea as the flows can be re-written by malicious developers. In almost all cases, it is much more difficult to change the contract code. If transaction verification can be conditional on reference data included in a transaction, as a state, then the result is a more robust and secure ledger (and audit trail).
|
||||
|
||||
## Target Solution
|
||||
|
||||
Changes required:
|
||||
|
||||
1. Add a `references` property of type `List<StateRef>` and `List<StateAndRef>` (for `FullTransaction`s) to all the transaction types.
|
||||
2. Add a `REFERENCE_STATES` component group.
|
||||
3. Amend the notary flows to check that reference states are current (but do not consume them)
|
||||
4. Add a `ReferencedStateAndRef` class that encapsulates a `StateAndRef`, this is so `TransactionBuilder.withItems` can delineate between `StateAndRef`s and state references.
|
||||
5. Add a `StateAndRef.referenced` method which wraps a `StateAndRef` in a `ReferencedStateAndRef`.
|
||||
6. Add helper methods to `LedgerTransaction` to get `references` by type, etc.
|
||||
7. Add a check to the transaction classes that asserts all references and inputs are on the same notary.
|
||||
8. Add a method to `TransactionBuilder` to add a reference state.
|
||||
9. Update the transaction resolution flow to resolve references.
|
||||
10. Update the transaction and ledger DSLs to support references.
|
||||
11. No changes are required to be made to contract upgrade or notary change transactions.
|
||||
|
||||
Implications:
|
||||
|
||||
**Versioning**
|
||||
|
||||
This can be done in a backwards compatible way. However, a minimum platform version must be mandated. Nodes running on an older version of Corda will not be able to verify transactions which include references. Indeed, contracts which refer to `references` will fail at run-time for older nodes.
|
||||
|
||||
**Privacy**
|
||||
|
||||
Reference states will be visible to all that possess a chain of provenance including them. There are potential implications from a data protection perspective here. Creators of reference data must be careful **not** to include sensitive personal data.
|
||||
|
||||
Outstanding issues:
|
||||
|
||||
**Oracle choice**
|
||||
|
||||
If the party building a transaction is using a reference state which they are not the owner of, they must move their states to the reference state's notary. If two or more reference states with different notaries are used, then the transaction cannot be committed as there is no notary change solution that works absent asking the reference state owner to change the notary.
|
||||
|
||||
This can be mitigated by requesting that reference state owners distribute reference states for all notaries. This solution doesn't work for `OwnableState`s used as reference data as `OwnableState`s should be unique. However, in most cases it is anticipated that the users of `OwnableState`s as reference data will be the owners of those states.
|
||||
|
||||
This solution introduces a new issue where nodes may store the same piece of reference data under different linear IDs. `TransactionBuilder`s would also need to know the required notary before a reference state is added.
|
||||
|
||||
**Syndication of reference states**
|
||||
|
||||
In the absence of data distribution groups, reference data must be manually transmitted to those that require it. Pulling might have the effect of DoS attacking nodes that own reference data used by many frequent users. Pushing requires reference data owners to be aware of all current users of the reference data. A temporary solution is required before data distribution groups are implemented.
|
||||
|
||||
Initial thoughts are that pushing reference states is the better approach.
|
||||
|
||||
**Interaction with encumbrances**
|
||||
|
||||
It is likely not possible to reference encumbered states unless the encumbrance state is also referenced. For example, a cash state referenced for collateral reporting purposes may have been "seized" and thus encumbered by a regulator, thus cannot be counted for the collateral report.
|
||||
|
||||
**What happens if a state is added to a transaction as an input as well as an input reference state?**
|
||||
|
||||
An edge case where a developer might erroneously add the same StateRef as an input state _and_ input reference state. The effect is referring to reference data that immediately becomes out of date! This is an edge case that should be prevented as it is likely to confuse CorDapp developers.
|
||||
|
||||
**Handling of update races.**
|
||||
|
||||
Usage of a referenced state may race with an update to it. This would cause notarisation failure, however, the flow cannot simply loop and re-calculate the transaction because it has not necessarily seen the updated tx yet (it may be a slow broadcast).
|
||||
|
||||
Therefore, it would make sense to extend the flows API with a new flow - call it WithReferencedStatesFlow that is given a set of LinearIDs and a factory that instantiates a subflow given a set of resolved StateAndRefs.
|
||||
|
||||
It does the following:
|
||||
|
||||
1. Checks that those linear IDs are in the vault and throws if not.
|
||||
2. Resolves the linear IDs to the tip StateAndRefs.
|
||||
3. Creates the subflow, passing in the resolved StateAndRefs to the factory, and then invokes it.
|
||||
4. If the subflow throws a NotaryException because it tried to finalise and failed, that exception is caught and examined. If the failure was due to a conflict on a referenced state, the flow suspends until that state has been updated in the vault (there is an API to do wait for transaction already, but here the flow must wait for a state update).
|
||||
5. Then it re-does the initial calculation, re-creates the subflow with the new resolved tips using the factory, and re-runs it as a new subflow.
|
||||
|
||||
Care must be taken to handle progress tracking correctly in case of loops.
|
||||
|
||||
## Complementary solutions
|
||||
|
||||
See discussion of alternative approaches above in the "design decisions" section.
|
||||
|
||||
## Final recommendation
|
||||
|
||||
Proceed to Implementation
|
||||
|
||||
TECHNICAL DESIGN
|
||||
---
|
||||
|
||||
* Summary of changes to be included.
|
||||
* Summary of outstanding issues to be included.
|
@ -256,6 +256,10 @@ public class FlowCookbookJava {
|
||||
------------------------------------------*/
|
||||
progressTracker.setCurrentStep(OTHER_TX_COMPONENTS);
|
||||
|
||||
// Reference input states are constructed from StateAndRefs.
|
||||
// DOCSTART 55
|
||||
ReferencedStateAndRef referenceState = ourStateAndRef.referenced();
|
||||
// DOCEND 55
|
||||
// Output states are constructed from scratch.
|
||||
// DOCSTART 22
|
||||
DummyState ourOutputState = new DummyState();
|
||||
|
@ -257,6 +257,10 @@ class InitiatorFlow(val arg1: Boolean, val arg2: Int, private val counterparty:
|
||||
-----------------------------------------**/
|
||||
progressTracker.currentStep = OTHER_TX_COMPONENTS
|
||||
|
||||
// Reference input states are constructed from StateAndRefs.
|
||||
// DOCSTART 55
|
||||
val referenceState: ReferencedStateAndRef<DummyState> = ourStateAndRef.referenced()
|
||||
// DOCEND 55
|
||||
// Output states are constructed from scratch.
|
||||
// DOCSTART 22
|
||||
val ourOutputState: DummyState = DummyState()
|
||||
|
@ -4,6 +4,17 @@ Release notes
|
||||
Unreleased
|
||||
----------
|
||||
|
||||
Significant Changes in 4.0
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* **Reference states**:
|
||||
|
||||
Introduced the concept of "reference input states". A reference input state is a ``ContractState`` which can be referred
|
||||
to in a transaction by the contracts of input and output states but whose contract is not executed as part of the
|
||||
transaction verification process and is not consumed when the transaction is committed to the ledger but is checked
|
||||
for "current-ness". In other words, the contract logic isn't run for the referencing transaction only. It's still a
|
||||
normal state when it occurs in an input or output position.
|
||||
|
||||
Issues Fixed
|
||||
~~~~~~~~~~~~
|
||||
* Cordform Gradle task (`deployNodes`) doesn't work when `configFile` element was used.
|
||||
|
@ -39,8 +39,9 @@ class NonValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAut
|
||||
verify()
|
||||
checkAllComponentsVisible(ComponentGroupEnum.INPUTS_GROUP)
|
||||
checkAllComponentsVisible(ComponentGroupEnum.TIMEWINDOW_GROUP)
|
||||
checkAllComponentsVisible(ComponentGroupEnum.REFERENCES_GROUP)
|
||||
}
|
||||
TransactionParts(tx.id, tx.inputs, tx.timeWindow, tx.notary)
|
||||
TransactionParts(tx.id, tx.inputs, tx.timeWindow, tx.notary, tx.references)
|
||||
}
|
||||
is ContractUpgradeFilteredTransaction -> TransactionParts(tx.id, tx.inputs, null, tx.notary)
|
||||
is NotaryChangeWireTransaction -> TransactionParts(tx.id, tx.inputs, null, tx.notary)
|
||||
|
@ -103,10 +103,12 @@ class PersistentUniquenessProvider(val clock: Clock) : UniquenessProvider, Singl
|
||||
txId: SecureHash,
|
||||
callerIdentity: Party,
|
||||
requestSignature: NotarisationRequestSignature,
|
||||
timeWindow: TimeWindow?) {
|
||||
timeWindow: TimeWindow?,
|
||||
references: List<StateRef>
|
||||
) {
|
||||
mutex.locked {
|
||||
logRequest(txId, callerIdentity, requestSignature)
|
||||
val conflictingStates = findAlreadyCommitted(states, commitLog)
|
||||
val conflictingStates = findAlreadyCommitted(states, references, commitLog)
|
||||
if (conflictingStates.isNotEmpty()) {
|
||||
handleConflicts(txId, conflictingStates)
|
||||
} else {
|
||||
@ -126,12 +128,23 @@ class PersistentUniquenessProvider(val clock: Clock) : UniquenessProvider, Singl
|
||||
session.persist(request)
|
||||
}
|
||||
|
||||
private fun findAlreadyCommitted(states: List<StateRef>, commitLog: AppendOnlyPersistentMap<StateRef, SecureHash, CommittedState, PersistentStateRef>): LinkedHashMap<StateRef, StateConsumptionDetails> {
|
||||
private fun findAlreadyCommitted(
|
||||
states: List<StateRef>,
|
||||
references: List<StateRef>,
|
||||
commitLog: AppendOnlyPersistentMap<StateRef, SecureHash, CommittedState, PersistentStateRef>
|
||||
): LinkedHashMap<StateRef, StateConsumptionDetails> {
|
||||
val conflictingStates = LinkedHashMap<StateRef, StateConsumptionDetails>()
|
||||
for (inputState in states) {
|
||||
val consumingTx = commitLog[inputState]
|
||||
if (consumingTx != null) conflictingStates[inputState] = StateConsumptionDetails(consumingTx.sha256())
|
||||
|
||||
fun checkConflicts(toCheck: List<StateRef>, type: StateConsumptionDetails.ConsumedStateType) {
|
||||
return toCheck.forEach { stateRef ->
|
||||
val consumingTx = commitLog[stateRef]
|
||||
if (consumingTx != null) conflictingStates[stateRef] = StateConsumptionDetails(consumingTx.sha256(), type)
|
||||
}
|
||||
}
|
||||
|
||||
checkConflicts(states, StateConsumptionDetails.ConsumedStateType.INPUT_STATE)
|
||||
checkConflicts(references, StateConsumptionDetails.ConsumedStateType.REFERENCE_INPUT_STATE)
|
||||
|
||||
return conflictingStates
|
||||
}
|
||||
|
||||
|
@ -45,12 +45,13 @@ class RaftTransactionCommitLog<E, EK>(
|
||||
createMap: () -> AppendOnlyPersistentMap<StateRef, Pair<Long, SecureHash>, E, EK>
|
||||
) : StateMachine(), Snapshottable {
|
||||
object Commands {
|
||||
class CommitTransaction(
|
||||
class CommitTransaction @JvmOverloads constructor(
|
||||
val states: List<StateRef>,
|
||||
val txId: SecureHash,
|
||||
val requestingParty: String,
|
||||
val requestSignature: ByteArray,
|
||||
val timeWindow: TimeWindow? = null
|
||||
val timeWindow: TimeWindow? = null,
|
||||
val references: List<StateRef> = emptyList()
|
||||
) : Command<NotaryError?> {
|
||||
override fun compaction(): Command.CompactionMode {
|
||||
// The FULL compaction mode retains the command in the log until it has been stored and applied on all
|
||||
@ -73,18 +74,21 @@ class RaftTransactionCommitLog<E, EK>(
|
||||
|
||||
/** Commits the input states for the transaction as specified in the given [Commands.CommitTransaction]. */
|
||||
fun commitTransaction(raftCommit: Commit<Commands.CommitTransaction>): NotaryError? {
|
||||
val conflictingStates = LinkedHashMap<StateRef, StateConsumptionDetails>()
|
||||
|
||||
fun checkConflict(states: List<StateRef>, type: StateConsumptionDetails.ConsumedStateType) = states.forEach { stateRef ->
|
||||
map[stateRef]?.let { conflictingStates[stateRef] = StateConsumptionDetails(it.second.sha256(), type) }
|
||||
}
|
||||
|
||||
raftCommit.use {
|
||||
val index = it.index()
|
||||
return db.transaction {
|
||||
val commitCommand = raftCommit.command()
|
||||
logRequest(commitCommand)
|
||||
val states = commitCommand.states
|
||||
val txId = commitCommand.txId
|
||||
log.debug("State machine commit: storing entries with keys (${states.joinToString()})")
|
||||
val conflictingStates = LinkedHashMap<StateRef, StateConsumptionDetails>()
|
||||
for (state in states) {
|
||||
map[state]?.let { conflictingStates[state] = StateConsumptionDetails(it.second.sha256()) }
|
||||
}
|
||||
log.debug("State machine commit: attempting to store entries with keys (${commitCommand.states.joinToString()})")
|
||||
checkConflict(commitCommand.states, StateConsumptionDetails.ConsumedStateType.INPUT_STATE)
|
||||
checkConflict(commitCommand.references, StateConsumptionDetails.ConsumedStateType.REFERENCE_INPUT_STATE)
|
||||
if (conflictingStates.isNotEmpty()) {
|
||||
if (isConsumedByTheSameTx(commitCommand.txId.sha256(), conflictingStates)) {
|
||||
null
|
||||
@ -95,9 +99,9 @@ class RaftTransactionCommitLog<E, EK>(
|
||||
} else {
|
||||
val outsideTimeWindowError = validateTimeWindow(clock.instant(), commitCommand.timeWindow)
|
||||
if (outsideTimeWindowError == null) {
|
||||
val entries = states.map { it to Pair(index, txId) }.toMap()
|
||||
val entries = commitCommand.states.map { it to Pair(index, txId) }.toMap()
|
||||
map.putAll(entries)
|
||||
log.debug { "Successfully committed all input states: $states" }
|
||||
log.debug { "Successfully committed all input states: ${commitCommand.states}" }
|
||||
null
|
||||
} else {
|
||||
outsideTimeWindowError
|
||||
|
@ -190,14 +190,17 @@ class RaftUniquenessProvider(
|
||||
txId: SecureHash,
|
||||
callerIdentity: Party,
|
||||
requestSignature: NotarisationRequestSignature,
|
||||
timeWindow: TimeWindow?) {
|
||||
timeWindow: TimeWindow?,
|
||||
references: List<StateRef>
|
||||
) {
|
||||
log.debug { "Attempting to commit input states: ${states.joinToString()}" }
|
||||
val commitCommand = CommitTransaction(
|
||||
states,
|
||||
txId,
|
||||
callerIdentity.name.toString(),
|
||||
requestSignature.serialize().bytes,
|
||||
timeWindow
|
||||
timeWindow,
|
||||
references
|
||||
)
|
||||
val commitError = client.submit(commitCommand).get()
|
||||
if (commitError != null) throw NotaryInternalException(commitError)
|
||||
|
@ -38,7 +38,7 @@ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthor
|
||||
resolveAndContractVerify(stx)
|
||||
verifySignatures(stx)
|
||||
val timeWindow: TimeWindow? = if (stx.coreTransaction is WireTransaction) stx.tx.timeWindow else null
|
||||
return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!)
|
||||
return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!, stx.references)
|
||||
} catch (e: Exception) {
|
||||
throw when (e) {
|
||||
is TransactionVerificationException,
|
||||
|
@ -491,14 +491,12 @@ class NodeVaultService(
|
||||
|
||||
@Throws(VaultQueryException::class)
|
||||
override fun <T : ContractState> _trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractStateType: Class<out T>): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
||||
return database.transaction {
|
||||
mutex.locked {
|
||||
val snapshotResults = _queryBy(criteria, paging, sorting, contractStateType)
|
||||
val updates: Observable<Vault.Update<T>> = uncheckedCast(_updatesPublisher.bufferUntilSubscribed()
|
||||
.filter { it.containsType(contractStateType, snapshotResults.stateTypes) }
|
||||
.map { filterContractStates(it, contractStateType) })
|
||||
DataFeed(snapshotResults, updates)
|
||||
}
|
||||
return mutex.locked {
|
||||
val snapshotResults = _queryBy(criteria, paging, sorting, contractStateType)
|
||||
val updates: Observable<Vault.Update<T>> = uncheckedCast(_updatesPublisher.bufferUntilSubscribed()
|
||||
.filter { it.containsType(contractStateType, snapshotResults.stateTypes) }
|
||||
.map { filterContractStates(it, contractStateType) })
|
||||
DataFeed(snapshotResults, updates)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -52,7 +52,7 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating
|
||||
verifySignatures(stx)
|
||||
resolveAndContractVerify(stx)
|
||||
val timeWindow: TimeWindow? = if (stx.coreTransaction is WireTransaction) stx.tx.timeWindow else null
|
||||
return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!)
|
||||
return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!, stx.references)
|
||||
} catch (e: Exception) {
|
||||
throw when (e) {
|
||||
is TransactionVerificationException,
|
||||
|
@ -134,7 +134,7 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe
|
||||
servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = defaultParameters.servicePeerAllocationStrategy,
|
||||
val notarySpecs: List<MockNetworkNotarySpec> = defaultParameters.notarySpecs,
|
||||
val testDirectory: Path = Paths.get("build", getTimestampAsDirectoryName()),
|
||||
networkParameters: NetworkParameters = testNetworkParameters(),
|
||||
val networkParameters: NetworkParameters = testNetworkParameters(),
|
||||
val defaultFactory: (MockNodeArgs, CordappLoader?) -> MockNode = { args, cordappLoader -> cordappLoader?.let { MockNode(args, it) } ?: MockNode(args) },
|
||||
val cordappsForAllNodes: Set<TestCorDapp> = emptySet()) {
|
||||
init {
|
||||
@ -238,6 +238,7 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe
|
||||
// The network parameters must be serialised before starting any of the nodes
|
||||
networkParametersCopier = NetworkParametersCopier(networkParameters.copy(notaries = notaryInfos))
|
||||
@Suppress("LeakingThis")
|
||||
// Notary nodes need a platform version >= network min platform version.
|
||||
notaryNodes = createNotaries()
|
||||
} catch (t: Throwable) {
|
||||
stopNodes()
|
||||
@ -254,10 +255,13 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe
|
||||
|
||||
@VisibleForTesting
|
||||
internal open fun createNotaries(): List<TestStartedNode> {
|
||||
val version = VersionInfo(networkParameters.minimumPlatformVersion, "Mock release", "Mock revision", "Mock Vendor")
|
||||
return notarySpecs.map { (name, validating) ->
|
||||
createNode(InternalMockNodeParameters(legalName = name, configOverrides = {
|
||||
doReturn(NotaryConfig(validating)).whenever(it).notary
|
||||
}))
|
||||
createNode(InternalMockNodeParameters(
|
||||
legalName = name,
|
||||
configOverrides = { doReturn(NotaryConfig(validating)).whenever(it).notary },
|
||||
version = version
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,8 +3,8 @@ package net.corda.testing.dsl
|
||||
import net.corda.core.DoNotImplement
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.cordapp.CordappProvider
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.crypto.NullKeys.NULL_SIGNATURE
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.UNKNOWN_UPLOADER
|
||||
@ -14,9 +14,9 @@ import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.testing.services.MockAttachmentStorage
|
||||
import net.corda.testing.internal.MockCordappProvider
|
||||
import net.corda.testing.core.dummyCommand
|
||||
import net.corda.testing.internal.MockCordappProvider
|
||||
import net.corda.testing.services.MockAttachmentStorage
|
||||
import java.io.InputStream
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
@ -98,6 +98,12 @@ data class TestTransactionDSLInterpreter private constructor(
|
||||
transactionBuilder.addInputState(StateAndRef(state, stateRef))
|
||||
}
|
||||
|
||||
override fun reference(stateRef: StateRef) {
|
||||
val state = ledgerInterpreter.resolveStateRef<ContractState>(stateRef)
|
||||
@Suppress("DEPRECATION") // Will remove when feature finalised.
|
||||
transactionBuilder.addReferenceState(StateAndRef(state, stateRef).referenced())
|
||||
}
|
||||
|
||||
override fun output(contractClassName: ContractClassName,
|
||||
label: String?,
|
||||
notary: Party,
|
||||
@ -293,7 +299,8 @@ data class TestLedgerDSLInterpreter private constructor(
|
||||
val wtx = value.transaction
|
||||
val ltx = wtx.toLedgerTransaction(services)
|
||||
ltx.verify()
|
||||
val doubleSpend = wtx.inputs.intersect(usedInputs)
|
||||
val allInputs = wtx.inputs union wtx.references
|
||||
val doubleSpend = allInputs intersect usedInputs
|
||||
if (!doubleSpend.isEmpty()) {
|
||||
val txIds = mutableListOf(wtx.id)
|
||||
doubleSpend.mapTo(txIds) { it.txhash }
|
||||
|
@ -28,6 +28,12 @@ interface TransactionDSLInterpreter : Verifies, OutputStateLookup {
|
||||
*/
|
||||
fun input(stateRef: StateRef)
|
||||
|
||||
/**
|
||||
* Add a reference input state to the transaction. Note that [verifies] will resolve this reference.
|
||||
* @param stateRef The input [StateRef].
|
||||
*/
|
||||
fun reference(stateRef: StateRef)
|
||||
|
||||
/**
|
||||
* Adds an output to the transaction.
|
||||
* @param label An optional label that may be later used to retrieve the output probably in other transactions.
|
||||
@ -80,6 +86,24 @@ interface TransactionDSLInterpreter : Verifies, OutputStateLookup {
|
||||
* Underlying class for the transaction DSL. Do not instantiate directly, instead use the [transaction] function.
|
||||
* */
|
||||
class TransactionDSL<out T : TransactionDSLInterpreter>(interpreter: T, private val notary: Party) : TransactionDSLInterpreter by interpreter {
|
||||
/**
|
||||
* Looks up the output label and adds the found state as an reference input state.
|
||||
* @param stateLabel The label of the output state specified when calling [TransactionDSLInterpreter.output] and friends.
|
||||
*/
|
||||
fun reference(stateLabel: String) = reference(retrieveOutputStateAndRef(ContractState::class.java, stateLabel).ref)
|
||||
|
||||
/**
|
||||
* Creates an [LedgerDSLInterpreter._unverifiedTransaction] with a single reference input state and adds its
|
||||
* reference as in input to the current transaction.
|
||||
* @param state The state to be added.
|
||||
*/
|
||||
fun reference(contractClassName: ContractClassName, state: ContractState) {
|
||||
val transaction = ledgerInterpreter._unverifiedTransaction(null, TransactionBuilder(notary)) {
|
||||
output(contractClassName, null, notary, null, AlwaysAcceptAttachmentConstraint, state)
|
||||
}
|
||||
reference(transaction.outRef<ContractState>(0).ref)
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up the output label and adds the found state as an input.
|
||||
* @param stateLabel The label of the output state specified when calling [TransactionDSLInterpreter.output] and friends.
|
||||
@ -92,7 +116,7 @@ class TransactionDSL<out T : TransactionDSLInterpreter>(interpreter: T, private
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an [LedgerDSLInterpreter._unverifiedTransaction] with a single output state and adds it's reference as an
|
||||
* Creates an [LedgerDSLInterpreter._unverifiedTransaction] with a single output state and adds its reference as an
|
||||
* input to the current transaction.
|
||||
* @param state The state to be added.
|
||||
*/
|
||||
|
Loading…
x
Reference in New Issue
Block a user