-
Notifications
You must be signed in to change notification settings - Fork 0
Chained and Unique States
In Corda, LinearState
represents an evolvable contract state that may be identified by its linear ID, sort of like a primary key in a database. Unlike a traditional database, each linear state must be consumed and a new state created (as is the nature of DLT), which results in a historic record of each state transition. Whilst linear states are intended to be unique, there's no guarantee of uniqueness across the ledger, or even on an individual node.
The ChainState
interface is similar in nature to LinearState
, however it provides a stricter state evolution. Each chain state instance implements a previousStateRef
property which should point to the state reference of the previous state (or null
if it's the first state in the chain), akin to blocks in a blockchain.
The following example demonstrates a ChainState
implementation. Note that in order to amend the state, you will have to pass in a reference to the previous state. This can be checked in the contract to ensure that a newly created state refers to the consumed state; or null, if it's the first state in the chain.
@BelongsToContract(MagicNumberContract::class)
class MagicNumber(
val issuer: AbstractParty,
val magicNumber: Int = 0,
override val previousStateRef: StateRef? = null
) : ChainState {
override val participants: List<AbstractParty>
get() = listOf(issuer)
fun amend(previousState: StateAndRef<MagicNumber>, magicNumber: Int): MagicNumber {
return ExampleChainState(issuer, magicNumber, previousState.ref)
}
}
class MagicNumberContract : Contract {
companion object : ContractID
override fun verify(tx: LedgerTransaction) = tx.allowCommands(
Issue::class.java,
Amend::class.java
)
interface MagicNumberContractCommand : VerifiedCommandData
object Issue : MagicNumberContractCommand {
internal const val CONTRACT_RULE_PREVIOUS_STATE_REF =
"On example chain state issuing, the previous state ref must be null."
override val verify(transaction: LedgerTransaction, signers: Set<PublicKey>) = requireThat {
val output = transaction.outputsOfType<MagicNumber>().single()
CONTRACT_RULE_PREVIOUS_STATE_REF using (output.previousStateRef == null)
}
}
object Amend : MagicNumberContractCommand {
internal const val CONTRACT_RULE_PREVIOUS_STATE_REF =
"On example chain state issuing, the previous state ref must point to the previous state."
override val verify(transaction: LedgerTransaction, signers: Set<PublicKey>) = requireThat {
val input = transaction.inRefsOfType<MagicNumber>().single()
val output = transaction.outputsOfType<MagicNumber>().single()
CONTRACT_RULE_PREVIOUS_STATE_REF using (output.previousStateRef == input.ref)
}
}
The Hashable
interface defines a hash
property. Admittedly it's use isn't immediately apparent but when implemented correctly, a state hash could either represent a ledger-wide unique identifier, or when combined with the ChainState
interface could represent a per-state unique identifier, where the previous state reference deterministically changes the hash for every newly created state.
The following example demonstrates a Hashable
implementation. Note that as this state also implements ChainState
this produces a hash that changes deterministically every time the state evolves, thus providing a unique identifier that can be used to query a specific state.
class MagicNumber(
val issuer: AbstractParty,
val magicNumber: Int = 0,
override val previousStateRef: StateRef? = null
) : ChainState, Hashable {
override val hash: SecureHash
get() = SecureHash.sha256("$issuer$magicNumber$previousStateRef")
override val participants: List<AbstractParty>
get() = listOf(issuer)
fun amend(previousState: StateAndRef<MagicNumber>, magicNumber: Int): MagicNumber {
return ExampleChainState(issuer, magicNumber, previousState.ref)
}
}
Note that if the issuer were to issue the same magic number twice, this would result in a duplicate hash. This should be checked in the flow before a new state is created. You could also implement QueryableState
and include a unique constraint on the hash column, however this alone isn't an entirely fail-safe solution as states are recorded to the vault before queryable tables are updated, leading to inconsistent data in the vault.