Corda State Unique Constraints
See original GitHub issueThere is only one mechanism which can be applied to a Corda state in order to ensure uniqueness which is the LinearState
. This provides the state with a pseudo-randomly generated, 128 bit, universally unique identifier (UUID) - similar to a primary key in a SQL database.
Problem
Sometimes it makes sense for states to be uniquely constrained on other properties, or composites thereof but unlike a SQL database, there is no mechanism by which Corda states can be uniquely constrained on properties other than the linear ID.
Example
The following example illustrates a simple state that models a friendship between two participants on the network - akin to friendships on Facebook.
data class Friendship(
override val linearId: UniqueIdentifier,
val partyA: AbstractParty,
val partyB: AbstractParty
) : LinearState { ... }
The problem here is that I can issue the same friendship twice, just using two different linear IDs…
val initialFriendship = Friendship(UniqueIdentifier(), alice, bob)
val duplicateFriendship1 = Friendship(UniqueIdentifier(), alice, bob)
val duplicateFriendship2 = Friendship(UniqueIdentifier(), bob, alice)
As far as Corda is concerned they are different enough, however from a business perspective, this wouldn’t make sense as you can’t be friends with the same person more than once. Notice that in this case, I have deliberately added two duplicates, because the ordering of the participants means the same thing.
Corda does allow us to implement QueryableState
which allows states to be mapped to a database table, and this feels like a natural place where constraints should apply. With respect to the example above, we can apply the following constraints to our schema:
@Entity
@Table(name = "friendship_states")
@UniqueConstraint(columnNames = ["partyA", "partyB"])
class FriendshipEntity(
...
@Column(name = "partyA", unique = true)
val partyA: AbstractParty,
@Column(name = "partyB", unique = true)
val partyB: AbstractParty
) : PersistentState()
This works from the perspective of persisting mapped states (entities), however it still has no impact on the states themselves, the reason being is that persistence of the entity is performed only after the signed transaction has been committed to the vault.
This causes two problems:
- The ledger and the entity table are not synchronised.
- It causes the node to fail as a result.
Solution
One solution to this would be to check whether any of the states in the transaction implement QueryableState
and persist the mapped entities before committing the signed transaction to the vault, thus constituting a single successful (or unsuccessful) transaction, rather than a successful transaction to commit the signed transaction and a potentially unsuccessful transaction to persist the entity.
Workaround
It’s not the most elegant way to enforce this sort of rule at the state level, but the properties at the state level could be hashed together to form a 128 bit hash, which could then by applied to the linear ID…
val initialFriendship(UniqueIdentifier(id = uuid(hash128(sort(alice, bob)))), alice, bob)
val duplicateFriendship1(UniqueIdentifier(id = uuid(hash128(sort(alice, bob)))), alice, bob)
// ^ Should fail because it has a duplicate linear ID
val duplicateFriendship2(UniqueIdentifier(id = uuid(hash128(sort(bob, alice)))), alice, bob)
// ^ Should fail because it has a duplicate linear ID (alice and bob sort to the same order)
Again, not the nicest solution, but I can’t see any other way to force constraints at the moment.
Issue Analytics
- State:
- Created 4 years ago
- Reactions:4
- Comments:12 (7 by maintainers)
Ah, got it. So you rely on schema constraints. During FinalityFlow the transaction will get recorded, but when updating the vault tables the uniqueness constraint violation will end up rolling back the db transaction and killing the flow. Thanks
Noticed this too, currently we added a PersistentState with a version number which increases every time a state is consumed/produced. Together with a compound unique key, this solves it for now.