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:
Roger Willis 2018-11-05 10:33:26 +00:00 committed by GitHub
parent 3260d9f2c4
commit 80591bc6fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 622 additions and 2 deletions

View File

@ -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)

View 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 }
}
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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.

View 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".

View File

@ -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))
}
}

View File

@ -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