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:
Dominic Fox
2018-11-26 16:02:32 +00:00
committed by GitHub
parent 5d43f3139e
commit 88fbb47f67
13 changed files with 217 additions and 33 deletions

View File

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

View File

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

View File

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

View File

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

View File

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