mirror of
https://github.com/corda/corda.git
synced 2024-12-19 21:17:58 +00:00
ENT-2320 state contract identification (#4285)
* Enforce state/contract agreement validation * Fix some broken tests * Ascertain targetVersion by inspecting the jar source of the ContractState * Docs added and rebased against master * contextLogger doesn't work here * Java examples in docs * Label IRSState with owning contract * Fix rst formatting * Add @BelongsToContract annotation to PortfolioState
This commit is contained in:
parent
5d43f3139e
commit
88fbb47f67
@ -51,6 +51,7 @@ task patchCore(type: Zip, dependsOn: coreJarTask) {
|
||||
exclude 'net/corda/core/internal/*ToggleField*.class'
|
||||
exclude 'net/corda/core/serialization/*SerializationFactory*.class'
|
||||
exclude 'net/corda/core/serialization/internal/CheckpointSerializationFactory*.class'
|
||||
exclude 'net/corda/core/internal/rules/*.class'
|
||||
}
|
||||
|
||||
reproducibleFileOrder = true
|
||||
|
@ -0,0 +1,12 @@
|
||||
package net.corda.core.internal.rules
|
||||
|
||||
import net.corda.core.contracts.ContractState
|
||||
|
||||
// This file provides rules that depend on the targetVersion of the current Contract or Flow.
|
||||
// In core, this is determined by means which are unavailable in the DJVM,
|
||||
// so we must provide deterministic alternatives here.
|
||||
|
||||
@Suppress("unused")
|
||||
object StateContractValidationEnforcementRule {
|
||||
fun shouldEnforce(state: ContractState): Boolean = true
|
||||
}
|
@ -149,7 +149,7 @@ abstract class TransactionVerificationException(val txId: SecureHash, message: S
|
||||
"is not satisfied. Encumbered states should also be referenced as an encumbrance of another state to form " +
|
||||
"a full cycle. Offending indices $nonMatching", null)
|
||||
|
||||
/**
|
||||
/**HEAD
|
||||
* All encumbered states should be assigned to the same notary. This is due to the fact that multi-notary
|
||||
* transactions are not supported and thus two encumbered states with different notaries cannot be consumed
|
||||
* in the same transaction.
|
||||
@ -159,6 +159,36 @@ abstract class TransactionVerificationException(val txId: SecureHash, message: S
|
||||
: TransactionVerificationException(txId, "Encumbered output states assigned to different notaries found. " +
|
||||
"Output state with index $encumberedIndex is assigned to notary [$encumberedNotary], while its encumbrance with index $encumbranceIndex is assigned to notary [$encumbranceNotary]", null)
|
||||
|
||||
/**
|
||||
* If a state is identified as belonging to a contract, either because the state class is defined as an inner class
|
||||
* of the contract class or because the state class is annotated with [BelongsToContract], then it must not be
|
||||
* bundled in a [TransactionState] with a different contract.
|
||||
*
|
||||
* @param state The [TransactionState] whose bundled state and contract are in conflict.
|
||||
* @param requiredContractClassName The class name of the contract to which the state belongs.
|
||||
*/
|
||||
@KeepForDJVM
|
||||
class TransactionContractConflictException(txId: SecureHash, state: TransactionState<ContractState>, requiredContractClassName: String)
|
||||
: TransactionVerificationException(txId,
|
||||
"""
|
||||
State of class ${state.data::class.java.typeName} belongs to contract $requiredContractClassName, but
|
||||
is bundled in TransactionState with ${state.contract}.
|
||||
|
||||
For details see: https://docs.corda.net/api-contract-constraints.html#contract-state-agreement
|
||||
""".trimIndent().replace('\n', ' '), null)
|
||||
|
||||
// TODO: add reference to documentation
|
||||
@KeepForDJVM
|
||||
class TransactionRequiredContractUnspecifiedException(txId: SecureHash, state: TransactionState<ContractState>)
|
||||
: TransactionVerificationException(txId,
|
||||
"""
|
||||
State of class ${state.data::class.java.typeName} does not have a specified owning contract.
|
||||
Add the @BelongsToContract annotation to this class to ensure that it can only be bundled in a TransactionState
|
||||
with the correct contract.
|
||||
|
||||
For details see: https://docs.corda.net/api-contract-constraints.html#contract-state-agreement
|
||||
""".trimIndent(), null)
|
||||
|
||||
/** Whether the inputs or outputs list contains an encumbrance issue, see [TransactionMissingEncumbranceException]. */
|
||||
@CordaSerializable
|
||||
@KeepForDJVM
|
||||
|
@ -0,0 +1,55 @@
|
||||
package net.corda.core.internal.rules
|
||||
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.warnOnce
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.net.URL
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.jar.JarInputStream
|
||||
import java.util.jar.Manifest
|
||||
|
||||
// This file provides rules that depend on the targetVersion of the current Contract or Flow.
|
||||
// Rules defined in this package are automatically removed from the DJVM in core-deterministic,
|
||||
// and must be replaced by a deterministic alternative defined within that module.
|
||||
|
||||
/**
|
||||
* Rule which determines whether [ContractState]s must declare the [Contract] to which they belong (e.g. via the
|
||||
* [BelongsToContract] annotation), and must be bundled together with that contract in any [TransactionState].
|
||||
*
|
||||
* This rule is consulted during validation by [LedgerTransaction].
|
||||
*/
|
||||
object StateContractValidationEnforcementRule {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(StateContractValidationEnforcementRule::class.java)
|
||||
|
||||
private val targetVersionCache = ConcurrentHashMap<URL, Int>()
|
||||
|
||||
fun shouldEnforce(state: ContractState): Boolean {
|
||||
val jarLocation = state::class.java.protectionDomain.codeSource.location
|
||||
|
||||
if (jarLocation == null) {
|
||||
logger.warnOnce("""
|
||||
Unable to determine JAR location for contract state class ${state::class.java.name},
|
||||
and consequently unable to determine target platform version.
|
||||
Enforcing state/contract agreement validation by default.
|
||||
|
||||
For details see: https://docs.corda.net/api-contract-constraints.html#contract-state-agreement
|
||||
""".trimIndent().replace("\n", " "))
|
||||
return true
|
||||
}
|
||||
|
||||
val targetVersion = targetVersionCache.computeIfAbsent(jarLocation) {
|
||||
jarLocation.openStream().use { inputStream ->
|
||||
JarInputStream(inputStream).manifest?.targetPlatformVersion ?: 1
|
||||
}
|
||||
}
|
||||
|
||||
return targetVersion >= 4
|
||||
}
|
||||
}
|
||||
|
||||
private val Manifest.targetPlatformVersion: Int get() {
|
||||
val minPlatformVersion = mainAttributes.getValue("Min-Platform-Version")?.toInt() ?: 1
|
||||
return mainAttributes.getValue("Target-Platform-Version")?.toInt() ?: minPlatformVersion
|
||||
}
|
@ -3,10 +3,14 @@ package net.corda.core.transactions
|
||||
import net.corda.core.CordaInternal
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.contracts.TransactionVerificationException.TransactionContractConflictException
|
||||
import net.corda.core.contracts.TransactionVerificationException.TransactionRequiredContractUnspecifiedException
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.internal.rules.StateContractValidationEnforcementRule
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.serialization.ConstructorForDeserialization
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
@ -133,7 +137,37 @@ private constructor(
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that package ownership is respected.
|
||||
* For all input and output [TransactionState]s, validates that the wrapped [ContractState] matches up with the
|
||||
* wrapped [Contract], as declared by the [BelongsToContract] annotation on the [ContractState]'s class.
|
||||
*
|
||||
* If the target platform version of the current CorDapp is lower than 4.0, a warning will be written to the log
|
||||
* if any mismatch is detected. If it is 4.0 or later, then [TransactionContractConflictException] will be thrown.
|
||||
*/
|
||||
private fun validateStatesAgainstContract(internalTx: LedgerTransaction) =
|
||||
internalTx.allStates.forEach(::validateStateAgainstContract)
|
||||
|
||||
private fun validateStateAgainstContract(state: TransactionState<ContractState>) {
|
||||
val shouldEnforce = StateContractValidationEnforcementRule.shouldEnforce(state.data)
|
||||
|
||||
val requiredContractClassName = state.data.requiredContractClassName ?:
|
||||
if (shouldEnforce) throw TransactionRequiredContractUnspecifiedException(id, state)
|
||||
else return
|
||||
|
||||
if (state.contract != requiredContractClassName)
|
||||
if (shouldEnforce) {
|
||||
throw TransactionContractConflictException(id, state, requiredContractClassName)
|
||||
} else {
|
||||
logger.warnOnce("""
|
||||
State of class ${state.data::class.java.typeName} belongs to contract $requiredContractClassName, but
|
||||
is bundled in TransactionState with ${state.contract}.
|
||||
|
||||
For details see: https://docs.corda.net/api-contract-constraints.html#contract-state-agreement
|
||||
""".trimIndent().replace('\n', ' '))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that for each contract the network wide package owner is respected.
|
||||
*
|
||||
* TODO - revisit once transaction contains network parameters.
|
||||
*/
|
||||
@ -156,24 +190,6 @@ private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For all input and output [TransactionState]s, validates that the wrapped [ContractState] matches up with the
|
||||
* wrapped [Contract], as declared by the [BelongsToContract] annotation on the [ContractState]'s class.
|
||||
*
|
||||
* A warning will be written to the log if any mismatch is detected.
|
||||
*/
|
||||
private fun validateStatesAgainstContract(internalTx: LedgerTransaction) = internalTx.allStates.forEach { validateStateAgainstContract(it) }
|
||||
|
||||
private fun validateStateAgainstContract(state: TransactionState<ContractState>) {
|
||||
state.data.requiredContractClassName?.let { requiredContractClassName ->
|
||||
if (state.contract != requiredContractClassName)
|
||||
logger.warnOnce("""
|
||||
State of class ${state.data::class.java.typeName} belongs to contract $requiredContractClassName, but
|
||||
is bundled in TransactionState with ${state.contract}.
|
||||
""".trimIndent().replace('\n', ' '))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforces the validity of the actual constraints.
|
||||
* * Constraints should be one of the valid supported ones.
|
||||
|
@ -14,8 +14,6 @@ import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.VersionInfo
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.contracts.DummyState
|
||||
import net.corda.testing.node.internal.InternalMockNetwork
|
||||
import net.corda.testing.node.internal.InternalMockNodeParameters
|
||||
import net.corda.testing.node.internal.cordappsForPackages
|
||||
@ -106,16 +104,16 @@ internal class UseRefState(private val linearId: UniqueIdentifier) : FlowLogic<S
|
||||
relevancyStatus = Vault.RelevancyStatus.ALL
|
||||
)
|
||||
val referenceState = serviceHub.vaultService.queryBy<ContractState>(query).states.single()
|
||||
|
||||
val stx = serviceHub.signInitialTransaction(TransactionBuilder(notary = notary).apply {
|
||||
addReferenceState(referenceState.referenced())
|
||||
addOutputState(DummyState(), DummyContract.PROGRAM_ID)
|
||||
addCommand(DummyContract.Commands.Create(), listOf(ourIdentity.owningKey))
|
||||
addOutputState(RefState.State(ourIdentity), RefState.CONTRACT_ID)
|
||||
addCommand(RefState.Create(), listOf(ourIdentity.owningKey))
|
||||
})
|
||||
return subFlow(FinalityFlow(stx, emptyList()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class WithReferencedStatesFlowTests {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
|
@ -58,10 +58,21 @@ class ReferenceStateTests {
|
||||
// check might not be present in other contracts, like Cash, for example. Cash might have a command
|
||||
// called "Share" that allows a party to prove to another that they own over a certain amount of cash.
|
||||
// As such, cash can be added to the references list with a "Share" command.
|
||||
@BelongsToContract(ExampleContract::class)
|
||||
data class ExampleState(val creator: Party, val data: String) : ContractState {
|
||||
override val participants: List<AbstractParty> get() = listOf(creator)
|
||||
}
|
||||
|
||||
// This state has only been created to serve reference data so it cannot ever be used as an input or
|
||||
// output when it is being referred to. However, we might want all states to be referable, so this
|
||||
// check might not be present in other contracts, like Cash, for example. Cash might have a command
|
||||
// called "Share" that allows a party to prove to another that they own over a certain amount of cash.
|
||||
// As such, cash can be added to the references list with a "Share" command.
|
||||
@BelongsToContract(Cash::class)
|
||||
data class ExampleCashState(val creator: Party, val data: String) : ContractState {
|
||||
override val participants: List<AbstractParty> get() = listOf(creator)
|
||||
}
|
||||
|
||||
class ExampleContract : Contract {
|
||||
interface Commands : CommandData
|
||||
class Create : Commands
|
||||
@ -169,7 +180,7 @@ class ReferenceStateTests {
|
||||
transaction {
|
||||
input("REF DATA")
|
||||
command(ALICE_PUBKEY, ExampleContract.Update())
|
||||
output(Cash.PROGRAM_ID, "UPDATED REF DATA", "REF DATA".output<ExampleState>().copy(data = "NEW STUFF!"))
|
||||
output(ExampleContract::class.java.typeName, "UPDATED REF DATA", "REF DATA".output<ExampleState>().copy(data = "NEW STUFF!"))
|
||||
verifies()
|
||||
}
|
||||
// Try to use the old one.
|
||||
|
@ -62,6 +62,58 @@ consumes notary and ledger resources, and is just in general more complex.
|
||||
|
||||
.. _implicit_constraint_types:
|
||||
|
||||
Contract/State Agreement
|
||||
------------------------
|
||||
|
||||
Starting with Corda 4, ``ContractState``s must explicitly indicate which ``Contract`` they belong to. When a transaction is
|
||||
verified, the contract bundled with each state in the transaction must be its "owning" contract, otherwise we cannot guarantee that
|
||||
the transition of the ``ContractState`` will be verified against the business rules that should apply to it.
|
||||
|
||||
There are two mechanisms for indicating ownership. One is to annotate the ``ContractState`` with the ``BelongsToContract`` annotation,
|
||||
indicating the ``Contract`` class to which it is tied:
|
||||
|
||||
.. sourcecode:: java
|
||||
|
||||
@BelongToContract(MyContract.class)
|
||||
public class MyState implements ContractState {
|
||||
// implementation goes here
|
||||
}
|
||||
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
@BelongsToContract(MyContract::class)
|
||||
data class MyState(val value: Int) : ContractState {
|
||||
// implementation goes here
|
||||
}
|
||||
|
||||
|
||||
The other is to define the ``ContractState`` class as an inner class of the ``Contract`` class
|
||||
|
||||
.. sourcecode:: java
|
||||
|
||||
public class MyContract implements Contract {
|
||||
|
||||
public static class MyState implements ContractState {
|
||||
// state implementation goes here
|
||||
}
|
||||
|
||||
// contract implementation goes here
|
||||
}
|
||||
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
class MyContract : Contract {
|
||||
data class MyState(val value: Int) : ContractState
|
||||
}
|
||||
|
||||
|
||||
If a ``ContractState``'s owning ``Contract`` cannot be identified by either of these mechanisms, and the ``targetVersion`` of the
|
||||
CorDapp is 4 or greater, then transaction verification will fail with a ``TransactionRequiredContractUnspecifiedException``. If
|
||||
the owning ``Contract`` _can_ be identified, but the ``ContractState`` has been bundled with a different contract, then
|
||||
transaction verification will fail with a ``TransactionContractConflictException``.
|
||||
|
||||
How constraints work
|
||||
--------------------
|
||||
|
||||
|
@ -160,12 +160,17 @@ class CashTests {
|
||||
}
|
||||
}
|
||||
|
||||
@BelongsToContract(Cash::class)
|
||||
object DummyState: ContractState {
|
||||
override val participants: List<AbstractParty> = emptyList()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `issue by move`() {
|
||||
// Check we can't "move" money into existence.
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
input(Cash.PROGRAM_ID, DummyState())
|
||||
input(Cash.PROGRAM_ID, DummyState)
|
||||
output(Cash.PROGRAM_ID, outState)
|
||||
command(miniCorp.publicKey, Cash.Commands.Move())
|
||||
this `fails with` "there is at least one cash input for this group"
|
||||
|
@ -21,7 +21,6 @@ import net.corda.finance.contracts.NetType
|
||||
import net.corda.finance.contracts.asset.Obligation.Lifecycle
|
||||
import net.corda.node.services.api.IdentityServiceInternal
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.contracts.DummyState
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.dsl.*
|
||||
import net.corda.testing.internal.TEST_TX_TIME
|
||||
@ -145,12 +144,17 @@ class ObligationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@BelongsToContract(DummyContract::class)
|
||||
object DummyState: ContractState {
|
||||
override val participants: List<AbstractParty> = emptyList()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `issue debt`() {
|
||||
// Check we can't "move" debt into existence.
|
||||
transaction {
|
||||
attachments(DummyContract.PROGRAM_ID, Obligation.PROGRAM_ID)
|
||||
input(DummyContract.PROGRAM_ID, DummyState())
|
||||
input(DummyContract.PROGRAM_ID, DummyState)
|
||||
output(Obligation.PROGRAM_ID, outState)
|
||||
command(MINI_CORP_PUBKEY, Obligation.Commands.Move())
|
||||
this `fails with` "at least one obligation input"
|
||||
|
@ -1,9 +1,6 @@
|
||||
package net.corda.vega.contracts
|
||||
|
||||
import net.corda.core.contracts.Command
|
||||
import net.corda.core.contracts.ContractClassName
|
||||
import net.corda.core.contracts.StateAndContract
|
||||
import net.corda.core.contracts.UniqueIdentifier
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
@ -16,6 +13,7 @@ const val IRS_PROGRAM_ID: ContractClassName = "net.corda.vega.contracts.OGTrade"
|
||||
*
|
||||
* TODO: Merge with the existing demo IRS code.
|
||||
*/
|
||||
@BelongsToContract(OGTrade::class)
|
||||
data class IRSState(val swap: SwapData,
|
||||
val buyer: AbstractParty,
|
||||
val seller: AbstractParty,
|
||||
|
@ -17,6 +17,7 @@ const val PORTFOLIO_SWAP_PROGRAM_ID = "net.corda.vega.contracts.PortfolioSwap"
|
||||
* Represents an aggregate set of trades agreed between two parties and a possible valuation of that portfolio at a
|
||||
* given point in time. This state can be consumed to create a new state with a mutated valuation or portfolio.
|
||||
*/
|
||||
@BelongsToContract(PortfolioSwap::class)
|
||||
data class PortfolioState(val portfolio: List<StateRef>,
|
||||
val _parties: Pair<AbstractParty, AbstractParty>,
|
||||
val valuationDate: LocalDate,
|
||||
|
@ -310,6 +310,7 @@ class VaultFiller @JvmOverloads constructor(
|
||||
|
||||
|
||||
/** A state representing a commodity claim against some party */
|
||||
@BelongsToContract(Obligation::class)
|
||||
data class CommodityState(
|
||||
override val amount: Amount<Issued<Commodity>>,
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user