mirror of
https://github.com/corda/corda.git
synced 2025-01-18 02:39:51 +00:00
StatePointer (#4074)
* Introducing linear pointer. * Added design document. Added StatePointer interface. Updated design document. Updated StatePointer implementation. Added doc section for state pointer. * Updated design document. Added API for StatePointer. * Update core/src/main/kotlin/net/corda/core/contracts/Structures.kt Co-Authored-By: roger3cev <roger.willis@r3cev.com> * Update core/src/main/kotlin/net/corda/core/contracts/Structures.kt Co-Authored-By: roger3cev <roger.willis@r3cev.com> * Update core/src/main/kotlin/net/corda/core/contracts/Structures.kt Co-Authored-By: roger3cev <roger.willis@r3cev.com> * Update docs/source/design/linear-pointer/design.md Co-Authored-By: roger3cev <roger.willis@r3cev.com> * Update docs/source/design/linear-pointer/design.md Co-Authored-By: roger3cev <roger.willis@r3cev.com> * Resolve pointers Added test to check pointers are resolved. Updated docs and kdocs. Reverted changes to api-current.txt Revert "Reverted changes to api-current.txt" This reverts commit dc1cece91a595a4e772f63917b830c7e1fd0586d. Fix CI bug. Made StatePointers type safe. Resolving StatePointers is now optionally recursive Addressed review comments. Fixed compile error. Addressed review comments. Fixed bug in state pointer search. Improved efficiency of state pointer search. Removed whitespace. TxBuilder logs warning when no service hub is supplied for resolving pointers as opposed to throwing an exception. * Addressed review comments.
This commit is contained in:
parent
3260d9f2c4
commit
80591bc6fd
@ -617,6 +617,18 @@ public @interface net.corda.core.contracts.LegalProseReference
|
||||
public abstract String uri()
|
||||
##
|
||||
@CordaSerializable
|
||||
public final class net.corda.core.contracts.LinearPointer extends net.corda.core.contracts.StatePointer
|
||||
public <init>(net.corda.core.contracts.UniqueIdentifier, Class<T>)
|
||||
@NotNull
|
||||
public net.corda.core.contracts.UniqueIdentifier getPointer()
|
||||
@NotNull
|
||||
public Class<T> getType()
|
||||
@NotNull
|
||||
public net.corda.core.contracts.StateAndRef<T> resolve(net.corda.core.node.ServiceHub)
|
||||
@NotNull
|
||||
public net.corda.core.contracts.StateAndRef<T> resolve(net.corda.core.transactions.LedgerTransaction)
|
||||
##
|
||||
@CordaSerializable
|
||||
public interface net.corda.core.contracts.LinearState extends net.corda.core.contracts.ContractState
|
||||
@NotNull
|
||||
public abstract net.corda.core.contracts.UniqueIdentifier getLinearId()
|
||||
@ -760,6 +772,17 @@ public final class net.corda.core.contracts.StateAndRef extends java.lang.Object
|
||||
public String toString()
|
||||
##
|
||||
@CordaSerializable
|
||||
public abstract class net.corda.core.contracts.StatePointer extends java.lang.Object
|
||||
@NotNull
|
||||
public abstract Object getPointer()
|
||||
@NotNull
|
||||
public abstract Class<T> getType()
|
||||
@NotNull
|
||||
public abstract net.corda.core.contracts.StateAndRef<T> resolve(net.corda.core.node.ServiceHub)
|
||||
@NotNull
|
||||
public abstract net.corda.core.contracts.StateAndRef<T> resolve(net.corda.core.transactions.LedgerTransaction)
|
||||
##
|
||||
@CordaSerializable
|
||||
public final class net.corda.core.contracts.StateRef extends java.lang.Object
|
||||
public <init>(net.corda.core.crypto.SecureHash, int)
|
||||
@NotNull
|
||||
@ -775,6 +798,18 @@ public final class net.corda.core.contracts.StateRef extends java.lang.Object
|
||||
@NotNull
|
||||
public String toString()
|
||||
##
|
||||
@CordaSerializable
|
||||
public final class net.corda.core.contracts.StaticPointer extends net.corda.core.contracts.StatePointer
|
||||
public <init>(net.corda.core.contracts.StateRef, Class<T>)
|
||||
@NotNull
|
||||
public net.corda.core.contracts.StateRef getPointer()
|
||||
@NotNull
|
||||
public Class<T> getType()
|
||||
@NotNull
|
||||
public net.corda.core.contracts.StateAndRef<T> resolve(net.corda.core.node.ServiceHub)
|
||||
@NotNull
|
||||
public net.corda.core.contracts.StateAndRef<T> resolve(net.corda.core.transactions.LedgerTransaction)
|
||||
##
|
||||
public final class net.corda.core.contracts.Structures extends java.lang.Object
|
||||
@NotNull
|
||||
public static final net.corda.core.crypto.SecureHash hash(net.corda.core.contracts.ContractState)
|
||||
|
121
core/src/main/kotlin/net/corda/core/contracts/StatePointer.kt
Normal file
121
core/src/main/kotlin/net/corda/core/contracts/StatePointer.kt
Normal file
@ -0,0 +1,121 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.node.services.queryBy
|
||||
import net.corda.core.node.services.vault.QueryCriteria
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
|
||||
/**
|
||||
* A [StatePointer] contains a [pointer] to a [ContractState]. The [StatePointer] can be included in a [ContractState]
|
||||
* or included in an off-ledger data structure. [StatePointer]s can be resolved to a [StateAndRef] by performing a
|
||||
* vault query. There are two types of pointers; linear and static. [LinearPointer]s are for use with [LinearState]s.
|
||||
* [StaticPointer]s are for use with any type of [ContractState].
|
||||
*/
|
||||
@CordaSerializable
|
||||
sealed class StatePointer<T : ContractState> {
|
||||
/**
|
||||
* An identifier for the [ContractState] that this [StatePointer] points to.
|
||||
*/
|
||||
abstract val pointer: Any
|
||||
|
||||
/**
|
||||
* Type of the state which is being pointed to.
|
||||
*/
|
||||
abstract val type: Class<T>
|
||||
|
||||
/**
|
||||
* Resolves a [StatePointer] to a [StateAndRef] via a vault query. This method will either return a [StateAndRef]
|
||||
* or return an exception.
|
||||
*
|
||||
* @param services a [ServiceHub] implementation is required to resolve the pointer.
|
||||
*/
|
||||
abstract fun resolve(services: ServiceHub): StateAndRef<T>
|
||||
|
||||
/**
|
||||
* Resolves a [StatePointer] to a [StateAndRef] from inside a [LedgerTransaction]. The intuition here is that all
|
||||
* of the pointed-to states will be included in the transaction as reference states.
|
||||
*
|
||||
* @param ltx the [LedgerTransaction] containing the [pointer] and pointed-to states.
|
||||
*/
|
||||
abstract fun resolve(ltx: LedgerTransaction): StateAndRef<T>
|
||||
}
|
||||
|
||||
/**
|
||||
* A [StaticPointer] contains a [pointer] to a specific [StateRef] and can be resolved by looking up the [StateRef] via
|
||||
* [ServiceHub]. There are a number of things to keep in mind when using [StaticPointer]s:
|
||||
* - The [ContractState] being pointed to may be spent or unspent when the [pointer] is resolved
|
||||
* - The [ContractState] may not be known by the node performing the look-up in which case the [resolve] method will
|
||||
* throw a [TransactionResolutionException]
|
||||
*/
|
||||
class StaticPointer<T : ContractState>(override val pointer: StateRef, override val type: Class<T>) : StatePointer<T>() {
|
||||
/**
|
||||
* Resolves a [StaticPointer] to a [StateAndRef] via a [StateRef] look-up.
|
||||
*/
|
||||
@Throws(TransactionResolutionException::class)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun resolve(services: ServiceHub): StateAndRef<T> {
|
||||
val transactionState = services.loadState(pointer) as TransactionState<T>
|
||||
val castState: T = type.cast(transactionState.data)
|
||||
val castTransactionState: TransactionState<T> = transactionState.copy(data = castState)
|
||||
return StateAndRef(castTransactionState, pointer)
|
||||
}
|
||||
|
||||
override fun resolve(ltx: LedgerTransaction): StateAndRef<T> {
|
||||
return ltx.referenceInputRefsOfType(type).single { pointer == it.ref }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [LinearPointer] allows a [ContractState] to "point" to another [LinearState] creating a "many-to-one" relationship
|
||||
* between all the states containing the pointer to a particular [LinearState] and the [LinearState] being pointed to.
|
||||
* Using the [LinearPointer] is useful when one state depends on the data contained within another state that evolves
|
||||
* independently. When using [LinearPointer] it is worth noting:
|
||||
* - The node performing the resolution may not have seen any [LinearState]s with the specified [linearId], as such the
|
||||
* vault query in [resolve] will return null and [resolve] will throw an exception
|
||||
* - The node performing the resolution may not have the latest version of the [LinearState] and therefore will return
|
||||
* an older version of the [LinearState]. As the pointed-to state will be added as a reference state to the transaction
|
||||
* then the transaction with such a reference state cannot be committed to the ledger until the most up-to-date version
|
||||
* of the [LinearState] is available. See reference states documentation on docs.corda.net for more info.
|
||||
*/
|
||||
class LinearPointer<T : LinearState>(override val pointer: UniqueIdentifier, override val type: Class<T>) : StatePointer<T>() {
|
||||
/**
|
||||
* Resolves a [LinearPointer] using the [UniqueIdentifier] contained in the [pointer] property. Returns a
|
||||
* [StateAndRef] containing the latest version of the [LinearState] that the node calling [resolve] is aware of.
|
||||
*
|
||||
* @param services a [ServiceHub] implementation is required to perform a vault query.
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun resolve(services: ServiceHub): StateAndRef<T> {
|
||||
// Return the latest version of the linear state.
|
||||
// This query will only ever return one or zero states.
|
||||
val query = QueryCriteria.LinearStateQueryCriteria(
|
||||
linearId = listOf(pointer),
|
||||
status = Vault.StateStatus.UNCONSUMED,
|
||||
relevancyStatus = Vault.RelevancyStatus.ALL
|
||||
)
|
||||
val result: List<StateAndRef<LinearState>> = services.vaultService.queryBy<LinearState>(query).states
|
||||
|
||||
check(result.isNotEmpty()) {
|
||||
// Here either one of two things has happened:
|
||||
// 1. The pointed-to state has not been seen by the resolver node. It is unlikely that this is the case.
|
||||
// The state can probably be obtained via subscribing to the data distribution group which created and
|
||||
// and maintains this data.
|
||||
// 2. Uh oh... The pointed-to state has been exited from the ledger!
|
||||
// It is unlikely this would ever happen as most reference data states will be created such that they cannot
|
||||
// be exited from the ledger. At this point there are two options; use an old consumed version of the state,
|
||||
// or don't use it at all.
|
||||
"The LinearState with ID ${pointer.id} is unknown to this node or it has been exited from the ledger."
|
||||
}
|
||||
|
||||
val stateAndRef = result.single()
|
||||
val castState: T = type.cast(stateAndRef.state.data)
|
||||
val castTransactionState = stateAndRef.state.copy(data = castState) as TransactionState<T>
|
||||
return StateAndRef(castTransactionState, stateAndRef.ref)
|
||||
}
|
||||
|
||||
override fun resolve(ltx: LedgerTransaction): StateAndRef<T> {
|
||||
return ltx.referenceInputRefsOfType(type).single { pointer == it.state.data.linearId }
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.LinearPointer
|
||||
import net.corda.core.contracts.StatePointer
|
||||
import net.corda.core.contracts.StaticPointer
|
||||
import java.lang.reflect.Field
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Uses reflection to search for instances of [StatePointer] within a [ContractState].
|
||||
*/
|
||||
class StatePointerSearch(val state: ContractState) {
|
||||
// Classes in these packages should not be part of a search.
|
||||
private val blackListedPackages = setOf("java.", "javax.")
|
||||
|
||||
// Type required for traversal.
|
||||
private data class FieldWithObject(val obj: Any, val field: Field)
|
||||
|
||||
// List containing all discovered state pointers.
|
||||
private val statePointers = mutableSetOf<StatePointer<*>>()
|
||||
|
||||
// Record seen objects to avoid getting stuck in loops.
|
||||
private val seenObjects = mutableSetOf<Any>().apply { add(state) }
|
||||
|
||||
// Queue of fields to search.
|
||||
private val fieldQueue = ArrayDeque<FieldWithObject>().apply { addAllFields(state) }
|
||||
|
||||
// Helper for adding all fields to the queue.
|
||||
private fun ArrayDeque<FieldWithObject>.addAllFields(obj: Any) {
|
||||
val fields = obj::class.java.declaredFields
|
||||
val fieldsWithObjects = fields.mapNotNull { field ->
|
||||
// Ignore classes which have not been loaded.
|
||||
// Assumption: all required state classes are already loaded.
|
||||
val packageName = field.type.`package`?.name
|
||||
if (packageName == null) {
|
||||
null
|
||||
} else {
|
||||
// Ignore JDK classes.
|
||||
val isBlacklistedPackage = blackListedPackages.any { packageName.startsWith(it) }
|
||||
if (isBlacklistedPackage) {
|
||||
null
|
||||
} else {
|
||||
FieldWithObject(obj, field)
|
||||
}
|
||||
}
|
||||
}
|
||||
addAll(fieldsWithObjects)
|
||||
}
|
||||
|
||||
private fun handleField(obj: Any, field: Field) {
|
||||
when {
|
||||
// StatePointer. Handles nullable StatePointers too.
|
||||
field.type == LinearPointer::class.java -> statePointers.add(field.get(obj) as? LinearPointer<*> ?: return)
|
||||
field.type == StaticPointer::class.java -> statePointers.add(field.get(obj) as? StaticPointer<*> ?: return)
|
||||
// Not StatePointer.
|
||||
else -> {
|
||||
val newObj = field.get(obj) ?: return
|
||||
|
||||
// Ignore nulls.
|
||||
if (newObj in seenObjects) {
|
||||
return
|
||||
}
|
||||
|
||||
// Recurse.
|
||||
fieldQueue.addAllFields(newObj)
|
||||
seenObjects.add(obj)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun search(): Set<StatePointer<*>> {
|
||||
while (fieldQueue.isNotEmpty()) {
|
||||
val (obj, field) = fieldQueue.pop()
|
||||
field.isAccessible = true
|
||||
handleField(obj, field)
|
||||
}
|
||||
return statePointers
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ import net.corda.core.crypto.SignableData
|
||||
import net.corda.core.crypto.SignatureMetadata
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.FlowStateMachine
|
||||
import net.corda.core.internal.StatePointerSearch
|
||||
import net.corda.core.internal.ensureMinimumPlatformVersion
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.ServiceHub
|
||||
@ -20,6 +21,7 @@ import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.node.services.KeyManagementService
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationFactory
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import java.security.PublicKey
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
@ -48,7 +50,8 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
protected val commands: MutableList<Command<*>> = arrayListOf(),
|
||||
protected var window: TimeWindow? = null,
|
||||
protected var privacySalt: PrivacySalt = PrivacySalt(),
|
||||
protected val references: MutableList<StateRef> = arrayListOf()
|
||||
protected val references: MutableList<StateRef> = arrayListOf(),
|
||||
protected val serviceHub: ServiceHub? = (Strand.currentStrand() as? FlowStateMachine<*>)?.serviceHub
|
||||
) {
|
||||
private val inputsWithTransactionState = arrayListOf<TransactionState<ContractState>>()
|
||||
private val referencesWithTransactionState = arrayListOf<TransactionState<ContractState>>()
|
||||
@ -65,13 +68,18 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
commands = ArrayList(commands),
|
||||
window = window,
|
||||
privacySalt = privacySalt,
|
||||
references = references
|
||||
references = references,
|
||||
serviceHub = serviceHub
|
||||
)
|
||||
t.inputsWithTransactionState.addAll(this.inputsWithTransactionState)
|
||||
t.referencesWithTransactionState.addAll(this.referencesWithTransactionState)
|
||||
return t
|
||||
}
|
||||
|
||||
companion object {
|
||||
val logger = contextLogger()
|
||||
}
|
||||
|
||||
// DOCSTART 1
|
||||
/** A more convenient way to add items to this transaction that calls the add* methods for you based on type */
|
||||
fun withItems(vararg items: Any): TransactionBuilder {
|
||||
@ -209,6 +217,44 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
|
||||
private fun checkReferencesUseSameNotary() = referencesWithTransactionState.map { it.notary }.toSet().size == 1
|
||||
|
||||
/**
|
||||
* If any inputs or outputs added to the [TransactionBuilder] contain [StatePointer]s, then this method can be
|
||||
* optionally called to resolve those [StatePointer]s to [StateAndRef]s. The [StateAndRef]s are then added as
|
||||
* reference states to the transaction. The effect is that the referenced data is carried along with the
|
||||
* transaction. This may or may not be appropriate in all circumstances, which is why calling this method is
|
||||
* optional.
|
||||
*
|
||||
* If this method is called outside the context of a flow, a [ServiceHub] instance must be passed to this method
|
||||
* for it to be able to resolve [StatePointer]s. Usually for a unit test, this will be an instance of mock services.
|
||||
*
|
||||
* @param serviceHub a [ServiceHub] instance needed for performing vault queries.
|
||||
*
|
||||
* @throws IllegalStateException if no [ServiceHub] is provided and no flow context is available.
|
||||
*/
|
||||
private fun resolveStatePointers(transactionState: TransactionState<*>) {
|
||||
val contractState = transactionState.data
|
||||
// Find pointers in all inputs and outputs.
|
||||
val inputAndOutputPointers = StatePointerSearch(contractState).search()
|
||||
// Queue up the pointers to resolve.
|
||||
val statePointerQueue = ArrayDeque<StatePointer<*>>().apply { addAll(inputAndOutputPointers) }
|
||||
// Recursively resolve all pointers.
|
||||
while (statePointerQueue.isNotEmpty()) {
|
||||
val nextStatePointer = statePointerQueue.pop()
|
||||
if (serviceHub != null) {
|
||||
val resolvedStateAndRef = nextStatePointer.resolve(serviceHub)
|
||||
// Don't add dupe reference states because CoreTransaction doesn't allow it.
|
||||
if (resolvedStateAndRef.ref !in referenceStates()) {
|
||||
addReferenceState(resolvedStateAndRef.referenced())
|
||||
}
|
||||
} else {
|
||||
logger.warn("WARNING: You must pass in a ServiceHub reference to TransactionBuilder to resolve " +
|
||||
"state pointers outside of flows. If you are writing a unit test then pass in a " +
|
||||
"MockServices instance.")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a reference input [StateRef] to the transaction.
|
||||
*
|
||||
@ -235,6 +281,10 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
"Transactions with reference states using multiple different notaries are currently unsupported."
|
||||
}
|
||||
|
||||
// State Pointers are recursively resolved. NOTE: That this might be expensive.
|
||||
// TODO: Add support for making recursive resolution optional if it becomes an issue.
|
||||
resolveStatePointers(stateAndRef.state)
|
||||
|
||||
checkNotary(stateAndRef)
|
||||
references.add(stateAndRef.ref)
|
||||
checkForInputsAndReferencesOverlap()
|
||||
@ -246,6 +296,7 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
checkNotary(stateAndRef)
|
||||
inputs.add(stateAndRef.ref)
|
||||
inputsWithTransactionState.add(stateAndRef.state)
|
||||
resolveStatePointers(stateAndRef.state)
|
||||
return this
|
||||
}
|
||||
|
||||
@ -258,6 +309,7 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
/** Adds an output state to the transaction. */
|
||||
fun addOutputState(state: TransactionState<*>): TransactionBuilder {
|
||||
outputs.add(state)
|
||||
resolveStatePointers(state)
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -225,3 +225,32 @@ then the check below will fail.
|
||||
transaction that references the encumbered state. This is because the data contained within the
|
||||
encumbered state may take on a different meaning, and likely would do, once the encumbrance state
|
||||
is taken into account.
|
||||
|
||||
State Pointers
|
||||
--------------
|
||||
|
||||
A ``StatePointer`` contains a pointer to a ``ContractState``. The ``StatePointer`` can be included in a ``ContractState`` as a
|
||||
property, or included in an off-ledger data structure. ``StatePointer`` s can be resolved to a ``StateAndRef`` by performing
|
||||
a look-up. There are two types of pointers; linear and static.
|
||||
|
||||
1. ``StaticPointer`` s are for use with any type of ``ContractState``. The ``StaticPointer`` does as it suggests, it always
|
||||
points to the same ``ContractState``.
|
||||
2. The ``LinearPointer`` is for use with ``LinearState`` s. They are particularly useful because due to the way ``LinearState`` s
|
||||
work, the pointer will automatically point you to the latest version of a ``LinearState`` that the node performing ``resolve``
|
||||
is aware of. In effect, the pointer "moves" as the ``LinearState`` is updated.
|
||||
|
||||
``StatePointer`` s do not enable a feature in Corda which was unavailable before. Rather, they help to formalise a pattern
|
||||
which was already possible. In that light it is worth nothing some issues which you may encounter with `StatePointer` s:
|
||||
|
||||
* If the node calling ``resolve`` has not seen any transactions containing a ``ContractState`` which the ``StatePointer``
|
||||
points to, then ``resolve`` will throw an exception. Here, the node calling ``resolve`` might be missing some crucial data.
|
||||
* The node calling ``resolve`` for a ``LinearPointer`` may have seen and stored transactions containing a ``LinearState`` with
|
||||
the specified ``linearId``. However, there is no guarantee the ``StateAndRef<T>`` returned by ``resolve`` is the most recent
|
||||
version of the ``LinearState``. The node only returns the most recent version that _it_ is aware of.
|
||||
|
||||
**Resolving state pointers in `TransactionBuilder`**
|
||||
|
||||
When building transactions, any ``StatePointer`` s contained within inputs or outputs added to a ``TransactionBuilder`` can
|
||||
be optionally resolved to reference states using the ``resolveStatePointers`` method. The effect is that the pointed to
|
||||
data is carried along with the transaction. This may or may not be appropriate in all circumstances, which is why
|
||||
calling the method is optional.
|
144
docs/source/design/linear-pointer/design.md
Normal file
144
docs/source/design/linear-pointer/design.md
Normal file
@ -0,0 +1,144 @@
|
||||
# StatePointer
|
||||
|
||||
## Background
|
||||
|
||||
Occasionally there is a need to create a link from one `ContractState` to another. This has the effect of creating a uni-directional "one-to-one" relationship between a pair of `ContractState`s.
|
||||
|
||||
There are two ways to do this.
|
||||
|
||||
### By `StateRef`
|
||||
|
||||
Link one `ContractState` to another by including a `StateRef` or a `StateAndRef<T>` as a property inside another `ContractState`:
|
||||
|
||||
```kotlin
|
||||
// StateRef.
|
||||
data class FooState(val ref: StateRef) : ContractState
|
||||
// StateAndRef.
|
||||
data class FooState(val ref: StateAndRef<BarState>) : ContractState
|
||||
```
|
||||
|
||||
Linking to a `StateRef` or `StateAndRef<T>` is only recommended if a specific version of a state is required in perpetuity. Clearly, adding a `StateAndRef` embeds the data directly. This type of pointer is compatible with any `ContractState` type.
|
||||
|
||||
But what if the linked state is updated? The `StateRef` will be pointing to an older version of the data and this could be a problem for the `ContractState` which contains the pointer.
|
||||
|
||||
### By `linearId`
|
||||
|
||||
To create a link to the most up-to-date version of a state, instead of linking to a specific `StateRef`, a `linearId` which references a `LinearState` can be used. This is because all `LinearState`s contain a `linearId` which refers to a particular lineage of `LinearState`. The vault can be used to look-up the most recent state with the specified `linearId`.
|
||||
|
||||
```kotlin
|
||||
// Link by LinearId.
|
||||
data class FooState(val ref: UniqueIdentifier) : ContractState
|
||||
```
|
||||
|
||||
This type of pointer only works with `LinearState`s.
|
||||
|
||||
### Resolving pointers
|
||||
|
||||
The trade-off with pointing to data in another state is that the data being pointed to cannot be immediately seen. To see the data contained within the pointed-to state, it must be "resolved".
|
||||
|
||||
## Design
|
||||
|
||||
Introduce a `StatePointer` interface and two implementations of it; the `StaticPointer` and the `LinearPointer`. The `StatePointer` is defined as follows:
|
||||
|
||||
```kotlin
|
||||
interface StatePointer {
|
||||
val pointer: Any
|
||||
fun resolve(services: ServiceHub): StateAndRef<ContractState>
|
||||
}
|
||||
```
|
||||
|
||||
The `resolve` method facilitates the resolution of the `pointer` to a `StateAndRef`.
|
||||
|
||||
The `StaticPointer` type requires developers to provide a `StateRef` which points to a specific state.
|
||||
|
||||
```kotlin
|
||||
class StaticPointer(override val pointer: StateRef) : StatePointer {
|
||||
override fun resolve(services: ServiceHub): StateAndRef<ContractState> {
|
||||
val transactionState = services.loadState(pointer)
|
||||
return StateAndRef(transactionState, pointer)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `LinearPointer` type contains the `linearId` of the `LinearState` being pointed to and a `resolve` method. Resolving a `LinearPointer` returns a `StateAndRef<T>` containing the latest version of the `LinearState` that the node calling `resolve` is aware of.
|
||||
|
||||
```kotlin
|
||||
class LinearPointer(override val pointer: UniqueIdentifier) : StatePointer {
|
||||
override fun resolve(services: ServiceHub): StateAndRef<LinearState> {
|
||||
val query = QueryCriteria.LinearStateQueryCriteria(linearId = listOf(pointer))
|
||||
val result = services.vaultService.queryBy<LinearState>(query).states
|
||||
check(result.isNotEmpty()) { "LinearPointer $pointer cannot be resolved." }
|
||||
return result.single()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Bi-directional link
|
||||
|
||||
Symmetrical relationships can be modelled by embedding a `LinearPointer` in the pointed-to `LinearState` which points in the "opposite" direction. **Note:** this can only work if both states are `LinearState`s.
|
||||
|
||||
## Use-cases
|
||||
|
||||
It is important to note that this design only standardises a pattern which is currently possible with the platform. In other words, this design does not enable anything new.
|
||||
|
||||
#### Tokens
|
||||
|
||||
Uncoupling token type definitions from the notion of ownership. Using the `LinearPointer`, `Token` states can include an `Amount` of some pointed-to type. The pointed-to type can evolve independently from the `Token` state which should just be concerned with the question of ownership.
|
||||
|
||||
## Issues and resolutions
|
||||
|
||||
Some issue to be aware of and their resolutions:
|
||||
|
||||
| Problem | Resolution |
|
||||
| :----------------------------------------------------------- | ------------------------------------------------------------ |
|
||||
| If the node calling `resolve` has not seen the specified `StateRef`, then `resolve` will return `null`. Here, the node calling `resolve` might be missing some crucial data. | Use data distribution groups. Assuming the creator of the `ContractState` publishes it to a data distribution group, subscribing to that group ensures that the node calling resolve will eventually have the required data. |
|
||||
| The node calling `resolve` has seen and stored transactions containing a `LinearState` with the specified `linearId`. However, there is no guarantee the `StateAndRef<T>` returned by `resolve` is the most recent version of the `LinearState`. | Embed the pointed-to `LinearState` in transactions containing the `LinearPointer` as a reference state. The reference states feature will ensure the pointed-to state is the latest version. |
|
||||
| The creator of the pointed-to `ContractState` exits the state from the ledger. If the pointed-to state is included a reference state then notaries will reject transactions containing it. | Contract code can be used to make a state un-exitable. |
|
||||
|
||||
All of the noted resolutions rely on additional paltform features:
|
||||
|
||||
* Reference states which will be available in V4
|
||||
* Data distribution groups which are not currently available. However, there is an early prototype
|
||||
* Additional state interface
|
||||
|
||||
### Additional concerns and responses
|
||||
|
||||
#### Embedding reference states in transactions
|
||||
|
||||
**Concern:** Embedding reference states for pointed-to states in transactions could cause transactions to increase by some unbounded size.
|
||||
|
||||
**Response:** The introduction of this feature doesn't create a new platform capability. It merely formalises a pattern which is currently possible. Futhermore, there is a possibility that _any_ type of state can cause a transaction to increase by some un-bounded size. It is also worth remembering that the maximum transaction size is 10MB.
|
||||
|
||||
#### `StatePointer`s are not human readable
|
||||
|
||||
**Concern:** Users won't know what sits behind the pointer.
|
||||
|
||||
**Response:** When the state containing the pointer is used in a flow, the pointer can be easily resolved. When the state needs to be displayed on a UI, the pointer can be resolved via vault query.
|
||||
|
||||
#### This feature adds complexity to the platform
|
||||
|
||||
**Concern:** This all seems quite complicated.
|
||||
|
||||
**Response:** It's possible anyway. Use of this feature is optional.
|
||||
|
||||
#### Coinselection will be slow.
|
||||
|
||||
**Concern:** We'll need to join on other tables to perform coinselection, making it slower. This is when a `StatePointer` is used as a `FungibleState` or `FungibleAsset` type.
|
||||
|
||||
**Response:** This is probably not true in most cases. Take the existing coinselection code from `CashSelectionH2Impl.kt`:
|
||||
|
||||
```sql
|
||||
SELECT vs.transaction_id, vs.output_index, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id
|
||||
FROM vault_states AS vs, contract_cash_states AS ccs
|
||||
WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index
|
||||
AND vs.state_status = 0
|
||||
AND vs.relevancy_status = 0
|
||||
AND ccs.ccy_code = ? and @t < ?
|
||||
AND (vs.lock_id = ? OR vs.lock_id is null)
|
||||
```
|
||||
|
||||
Notice that the only property required which is not accessible from the `StatePointer` is the `ccy_code`. This is not necessarily a problem though, as the `pointer` specified in the pointer can be used as a proxy for the `ccy_code` or "token type".
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,158 @@
|
||||
package net.corda.node.services.transactions
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import net.corda.testing.core.TestIdentity
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.makeTestIdentityService
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class ResolveStatePointersTest {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
|
||||
private val myself = TestIdentity(CordaX500Name("Me", "London", "GB"))
|
||||
private val notary = TestIdentity(DUMMY_NOTARY_NAME, 20)
|
||||
private val cordapps = listOf("net.corda.testing.contracts")
|
||||
private val databaseAndServices = MockServices.makeTestDatabaseAndMockServices(
|
||||
cordappPackages = cordapps,
|
||||
identityService = makeTestIdentityService(notary.identity, myself.identity),
|
||||
initialIdentity = myself,
|
||||
networkParameters = testNetworkParameters(minimumPlatformVersion = 4)
|
||||
)
|
||||
|
||||
private val services = databaseAndServices.second
|
||||
|
||||
private data class Bar(
|
||||
override val participants: List<AbstractParty> = listOf(),
|
||||
val bar: Int = 0,
|
||||
val nestedPointer: LinearPointer<*>? = null,
|
||||
override val linearId: UniqueIdentifier = UniqueIdentifier()
|
||||
) : LinearState
|
||||
|
||||
private data class Foo<T : LinearState>(val baz: LinearPointer<T>, override val participants: List<AbstractParty>) : ContractState
|
||||
|
||||
private val barOne = Bar(listOf(myself.party), 1)
|
||||
private val barTwo = Bar(listOf(myself.party), 2, LinearPointer(barOne.linearId, barOne::class.java))
|
||||
|
||||
private fun createPointedToState(contractState: ContractState): StateAndRef<Bar> {
|
||||
// Create the pointed to state.
|
||||
return services.run {
|
||||
val tx = signInitialTransaction(TransactionBuilder(notary = notary.party, serviceHub = services).apply {
|
||||
addOutputState(contractState, DummyContract.PROGRAM_ID)
|
||||
addCommand(Command(DummyContract.Commands.Create(), myself.party.owningKey))
|
||||
})
|
||||
recordTransactions(listOf(tx))
|
||||
tx.tx.outRefsOfType<Bar>().single()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve state pointers and check reference state is added to transaction`() {
|
||||
val stateAndRef = createPointedToState(barOne)
|
||||
val linearId = stateAndRef.state.data.linearId
|
||||
|
||||
// Add a new state containing a linear pointer.
|
||||
val tx = TransactionBuilder(notary = notary.party, serviceHub = services).apply {
|
||||
val pointer = LinearPointer(linearId, barOne::class.java)
|
||||
addOutputState(Foo(pointer, listOf(myself.party)), DummyContract.PROGRAM_ID)
|
||||
addCommand(Command(DummyContract.Commands.Create(), myself.party.owningKey))
|
||||
}
|
||||
|
||||
// Check the StateRef for the pointed-to state is added as a reference.
|
||||
assertEquals(stateAndRef.ref, tx.referenceStates().single())
|
||||
|
||||
// Resolve the StateRef to the actual state.
|
||||
val ltx = tx.toLedgerTransaction(services)
|
||||
assertEquals(barOne, ltx.referenceStates.single())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolving nested pointers is possible`() {
|
||||
// Create barOne.
|
||||
createPointedToState(barOne)
|
||||
|
||||
// Create another Bar - barTwo - which points to barOne.
|
||||
val barTwoStateAndRef = createPointedToState(barTwo)
|
||||
val barTwoLinearId = barTwoStateAndRef.state.data.linearId
|
||||
|
||||
// Add a new state containing a linear pointer.
|
||||
val tx = TransactionBuilder(notary = notary.party, serviceHub = services).apply {
|
||||
val pointer = LinearPointer(barTwoLinearId, barTwo::class.java)
|
||||
addOutputState(Foo(pointer, listOf(myself.party)), DummyContract.PROGRAM_ID)
|
||||
addOutputState(Foo(pointer, listOf()), DummyContract.PROGRAM_ID)
|
||||
addCommand(Command(DummyContract.Commands.Create(), myself.party.owningKey))
|
||||
}
|
||||
|
||||
tx.toLedgerTransaction(services).referenceStates.forEach { println(it) }
|
||||
|
||||
// Check both Bar StateRefs have been added to the transaction.
|
||||
assertEquals(2, tx.referenceStates().size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Resolving to an unknown state throws an exception`() {
|
||||
// Don't create the pointed to state.
|
||||
// Resolve the pointer for barTwo.
|
||||
assertFailsWith(IllegalStateException::class) {
|
||||
barTwo.nestedPointer?.resolve(services)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolving an exited state throws an exception`() {
|
||||
// Create barOne.
|
||||
val stateAndRef = createPointedToState(barOne)
|
||||
|
||||
// Exit barOne from the ledger.
|
||||
services.run {
|
||||
val tx = signInitialTransaction(TransactionBuilder(notary = notary.party, serviceHub = services).apply {
|
||||
addInputState(stateAndRef)
|
||||
addCommand(Command(DummyContract.Commands.Move(), myself.party.owningKey))
|
||||
})
|
||||
recordTransactions(listOf(tx))
|
||||
}
|
||||
|
||||
assertFailsWith(IllegalStateException::class) {
|
||||
barTwo.nestedPointer?.resolve(services)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve linear pointer with correct type`() {
|
||||
val stateAndRef = createPointedToState(barOne)
|
||||
val linearPointer = LinearPointer(stateAndRef.state.data.linearId, barOne::class.java)
|
||||
val resolvedPointer = linearPointer.resolve(services)
|
||||
assertEquals(stateAndRef::class.java, resolvedPointer::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve state pointer in ledger transaction`() {
|
||||
val stateAndRef = createPointedToState(barOne)
|
||||
val linearId = stateAndRef.state.data.linearId
|
||||
|
||||
// Add a new state containing a linear pointer.
|
||||
val tx = TransactionBuilder(notary = notary.party, serviceHub = services).apply {
|
||||
val pointer = LinearPointer(linearId, barOne::class.java)
|
||||
addOutputState(Foo(pointer, listOf(myself.party)), DummyContract.PROGRAM_ID)
|
||||
addCommand(Command(DummyContract.Commands.Create(), myself.party.owningKey))
|
||||
}
|
||||
|
||||
val ltx = tx.toLedgerTransaction(services)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val foo = ltx.outputs.single().data as Foo<Bar>
|
||||
assertEquals(stateAndRef, foo.baz.resolve(ltx))
|
||||
}
|
||||
|
||||
}
|
@ -40,6 +40,7 @@ class TraderDemoTest {
|
||||
startNode(providedName = DUMMY_BANK_B_NAME, rpcUsers = listOf(demoUser)),
|
||||
startNode(providedName = BOC_NAME, rpcUsers = listOf(bankUser))
|
||||
).map { (it.getOrThrow() as InProcess) }
|
||||
|
||||
val (nodeARpc, nodeBRpc) = listOf(nodeA, nodeB).map {
|
||||
val client = CordaRPCClient(it.rpcAddress)
|
||||
client.start(demoUser.username, demoUser.password).proxy
|
||||
|
Loading…
Reference in New Issue
Block a user