diff --git a/.ci/api-current.txt b/.ci/api-current.txt index c7f3599aa7..3c04117395 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -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 (net.corda.core.contracts.UniqueIdentifier, Class) + @NotNull + public net.corda.core.contracts.UniqueIdentifier getPointer() + @NotNull + public Class getType() + @NotNull + public net.corda.core.contracts.StateAndRef resolve(net.corda.core.node.ServiceHub) + @NotNull + public net.corda.core.contracts.StateAndRef 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 getType() + @NotNull + public abstract net.corda.core.contracts.StateAndRef resolve(net.corda.core.node.ServiceHub) + @NotNull + public abstract net.corda.core.contracts.StateAndRef resolve(net.corda.core.transactions.LedgerTransaction) +## +@CordaSerializable public final class net.corda.core.contracts.StateRef extends java.lang.Object public (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 (net.corda.core.contracts.StateRef, Class) + @NotNull + public net.corda.core.contracts.StateRef getPointer() + @NotNull + public Class getType() + @NotNull + public net.corda.core.contracts.StateAndRef resolve(net.corda.core.node.ServiceHub) + @NotNull + public net.corda.core.contracts.StateAndRef 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) diff --git a/core/src/main/kotlin/net/corda/core/contracts/StatePointer.kt b/core/src/main/kotlin/net/corda/core/contracts/StatePointer.kt new file mode 100644 index 0000000000..5f1cc39401 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/contracts/StatePointer.kt @@ -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 { + /** + * 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 + + /** + * 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 + + /** + * 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 +} + +/** + * 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(override val pointer: StateRef, override val type: Class) : StatePointer() { + /** + * Resolves a [StaticPointer] to a [StateAndRef] via a [StateRef] look-up. + */ + @Throws(TransactionResolutionException::class) + @Suppress("UNCHECKED_CAST") + override fun resolve(services: ServiceHub): StateAndRef { + val transactionState = services.loadState(pointer) as TransactionState + val castState: T = type.cast(transactionState.data) + val castTransactionState: TransactionState = transactionState.copy(data = castState) + return StateAndRef(castTransactionState, pointer) + } + + override fun resolve(ltx: LedgerTransaction): StateAndRef { + 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(override val pointer: UniqueIdentifier, override val type: Class) : StatePointer() { + /** + * 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 { + // 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> = services.vaultService.queryBy(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 + return StateAndRef(castTransactionState, stateAndRef.ref) + } + + override fun resolve(ltx: LedgerTransaction): StateAndRef { + return ltx.referenceInputRefsOfType(type).single { pointer == it.state.data.linearId } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/internal/StatePointerSearch.kt b/core/src/main/kotlin/net/corda/core/internal/StatePointerSearch.kt new file mode 100644 index 0000000000..60e8b93482 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/StatePointerSearch.kt @@ -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>() + + // Record seen objects to avoid getting stuck in loops. + private val seenObjects = mutableSetOf().apply { add(state) } + + // Queue of fields to search. + private val fieldQueue = ArrayDeque().apply { addAllFields(state) } + + // Helper for adding all fields to the queue. + private fun ArrayDeque.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> { + while (fieldQueue.isNotEmpty()) { + val (obj, field) = fieldQueue.pop() + field.isAccessible = true + handleField(obj, field) + } + return statePointers + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index e52bda5456..e246128e0f 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -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> = arrayListOf(), protected var window: TimeWindow? = null, protected var privacySalt: PrivacySalt = PrivacySalt(), - protected val references: MutableList = arrayListOf() + protected val references: MutableList = arrayListOf(), + protected val serviceHub: ServiceHub? = (Strand.currentStrand() as? FlowStateMachine<*>)?.serviceHub ) { private val inputsWithTransactionState = arrayListOf>() private val referencesWithTransactionState = arrayListOf>() @@ -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>().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 } diff --git a/docs/source/api-states.rst b/docs/source/api-states.rst index 156a38fcd7..6b9e6ed0ef 100644 --- a/docs/source/api-states.rst +++ b/docs/source/api-states.rst @@ -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`` 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. \ No newline at end of file diff --git a/docs/source/design/linear-pointer/design.md b/docs/source/design/linear-pointer/design.md new file mode 100644 index 0000000000..d19f695e99 --- /dev/null +++ b/docs/source/design/linear-pointer/design.md @@ -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` as a property inside another `ContractState`: + +```kotlin +// StateRef. +data class FooState(val ref: StateRef) : ContractState +// StateAndRef. +data class FooState(val ref: StateAndRef) : ContractState +``` + +Linking to a `StateRef` or `StateAndRef` 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 +} +``` + +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 { + 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` 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 { + val query = QueryCriteria.LinearStateQueryCriteria(linearId = listOf(pointer)) + val result = services.vaultService.queryBy(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` 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". + + + + diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/ResolveStatePointersTest.kt b/node/src/test/kotlin/net/corda/node/services/transactions/ResolveStatePointersTest.kt new file mode 100644 index 0000000000..847d6daccd --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/transactions/ResolveStatePointersTest.kt @@ -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 = listOf(), + val bar: Int = 0, + val nestedPointer: LinearPointer<*>? = null, + override val linearId: UniqueIdentifier = UniqueIdentifier() + ) : LinearState + + private data class Foo(val baz: LinearPointer, override val participants: List) : 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 { + // 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().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 + assertEquals(stateAndRef, foo.baz.resolve(ltx)) + } + +} \ No newline at end of file diff --git a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt index aa074cdab2..e4a093ad61 100644 --- a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt +++ b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt @@ -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