mirror of
https://github.com/corda/corda.git
synced 2025-06-13 12:48:18 +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:
@ -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.
|
||||
|
Reference in New Issue
Block a user