Liquibase to the rescue of Off-Ledger State Persistence in R3 Corda 4.6+ — Resolving Schema-validation: missing table Issue

RajaPandian
5 min readFeb 20, 2021

I’ve been working on off-ledger state persistence in R3 Corda 4.6 community edtion and I had hit a blocker when starting up the nodes.

Why do we need to store state data off-ledger?

State data stored to the corda ledger are encoded and stored in binary format, which is not readable. Also, in enterprises, the business data might be needed to perform analysis and/ or reporting. In such cases, we might need to store certain business specific data off-ledger, readable in a custom table.

Issue: Upon starting the nodes via runnnodes command, the nodes throws an exception — Caused by: org.hibernate.tool.schema.spi.SchemaManagementException: Schema-validation: missing table [<table_name>] and terminates.

Scenario: To setup a off-ledger persistence table where the state transaction information can be stored for analytics and reporting. To do so, I did the following:

  1. Create a family schema marker class that could collectively refer to the schema versions
package com.crpdev.corda.tododist.states.todo;public class ToDoSchema {
}

2. Create a schema class V1 that will hold the Model [Entity extending the PersistentState class] as an inner class. This will ensure the schema version is bound to the entity and its properties. The versioned schema class extends MappedSchema, which will enforce a default constructor to associate the Model and Schema classes.

package com.crpdev.corda.tododist.states.todo;import net.corda.core.schemas.MappedSchema;
import net.corda.core.schemas.PersistentState;
import org.hibernate.annotations.Type;
import javax.annotation.Nullable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
import java.util.Arrays;
import java.util.UUID;
public class ToDoSchemaV1 extends MappedSchema { public ToDoSchemaV1(){
super(ToDoSchema.class, 1, Arrays.asList(ToDoModel.class));
}
@Entity
@Table(name="todo_model")
public static class ToDoModel extends PersistentState {
@Column(name = "task")
private final String task;
@Column(name = "id")
@Type(type = "uuid-char")
private final UUID linearId;
public ToDoModel (String task, UUID linearId){
this.task = task;
this.linearId = linearId;
}
public String getTask() {
return task;
}
public UUID getLinearId() {
return linearId;
}
}
}

3. To make a state persist the data off-ledger, the state must implement QueryableState interface, thereby overriding 2 methods: generateMappedObject and supportedSchemas

  • generateMappedObject returns a PersistentState object [Model class in Step 2]
  • supportedSchemas returns the list of versioned Schemas defined [schema class in Step 1]
package com.crpdev.corda.tododist.states;import com.crpdev.corda.tododist.contracts.ToDoContract;
import com.crpdev.corda.tododist.states.todo.ToDoSchemaV1;
import net.corda.core.contracts.BelongsToContract;
import net.corda.core.contracts.ContractState;
import net.corda.core.contracts.LinearState;
import net.corda.core.contracts.UniqueIdentifier;
import net.corda.core.identity.AbstractParty;
import net.corda.core.identity.Party;
import net.corda.core.schemas.MappedSchema;
import net.corda.core.schemas.PersistentState;
import net.corda.core.schemas.QueryableState;
import net.corda.core.serialization.ConstructorForDeserialization;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.List;
@BelongsToContract(ToDoContract.class)
public class ToDoState implements ContractState, LinearState, QueryableState {
private final Party assignedBy;
private final Party assignedTo;
private final String taskDescription;
private final UniqueIdentifier linearId;
public ToDoState(Party assignedBy, Party assignedTo, String taskDescription){
this.assignedBy = assignedBy;
this.assignedTo = assignedTo;
this.taskDescription = taskDescription;
this.linearId = new UniqueIdentifier();
}
@ConstructorForDeserialization
public ToDoState(Party assignedBy, Party assignedTo, String taskDescription, UniqueIdentifier linearId){
this.assignedBy = assignedBy;
this.assignedTo = assignedTo;
this.taskDescription = taskDescription;
this.linearId = linearId;
}
public Party getAssignedBy() {
return assignedBy;
}
public Party getAssignedTo() {
return assignedTo;
}
public String getTaskDescription() {
return taskDescription;
}
@Override
public UniqueIdentifier getLinearId() {
return linearId;
}
@Override
public List<AbstractParty> getParticipants() {
return Arrays.asList(assignedBy, assignedTo);
}
public ToDoState assignTo(Party assignedTo){
return new ToDoState(
assignedBy, assignedTo, taskDescription, linearId
);
}
@NotNull
@Override
public PersistentState generateMappedObject(@NotNull MappedSchema schema) {
if (schema instanceof ToDoSchemaV1){
return new ToDoSchemaV1.ToDoModel(taskDescription, linearId.getId());
} else {
throw new IllegalArgumentException("No Supported Schema Found!");
}
}
@NotNull
@Override
public Iterable<MappedSchema> supportedSchemas() {
return Arrays.asList(new ToDoSchemaV1());
}
}

The project refers to the book Mastering Corda by Jamiel Sheikh which I would definitely recommend if you prefer to have a solid foundation on Blockchain concepts and work with Corda in Java. The book discusses the topics in Corda 4.3, however the latest Corda Template Java [as on Feb 20, 2021] is 4.6.

In the book, Chapter 07 discusses about advanced concepts in Corda, to implement a custom table to store state data off-ledger. When implementing this case, I hit a block with an exception — Caused by: org.hibernate.tool.schema.spi.SchemaManagementException: Schema-validation: missing table [todo_model]

Until version 4.6, the steps laid above were sufficient to store state data off-ledger. However from Corda 4.6, liquibase scripts must be put in place to do the same.

Liquibase scripts for database management and migration was already present in previous versions of Corda enterprise but not in community.

Implementing Liquibase scripts for custom models in Corda 4.6+

  • Create a folder “/src/main/resources/migration” in the contracts directory of the project
  • In the folder are 2 xml files to be created — <project-name>.changelog-master.xml and <model-name>.changelog-v1.xml

<project-name>.changelog-master.xml — holds the changelog xml information

<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
<include file="migration/todo-model.changelog-v1.xml"/></databaseChangeLog>

<model-name>.changelog-v1.xml — holds the table schema to be created in the database

<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
<changeSet author="R3.Corda" id="create_todo_model_state">
<createTable tableName="todo_model">
<column name="output_index" type="INT"/>
<column name="transaction_id" type="NVARCHAR(64)"/>
<column name="task" type="NVARCHAR(64)"/>
<column name="id" type="NVARCHAR(64)"/>
</createTable>
</changeSet>
</databaseChangeLog>
  • Update versioned schema class to refer to the liquibase script using method getMigrationResource
package com.crpdev.corda.tododist.states.todo;import net.corda.core.schemas.MappedSchema;
import net.corda.core.schemas.PersistentState;
import org.hibernate.annotations.Type;
import javax.annotation.Nullable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
import java.util.Arrays;
import java.util.UUID;
public class ToDoSchemaV1 extends MappedSchema { public ToDoSchemaV1(){
super(ToDoSchema.class, 1, Arrays.asList(ToDoModel.class));
}
@Entity
@Table(name="todo_model")
public static class ToDoModel extends PersistentState {
@Column(name = "task")
private final String task;
@Column(name = "id")
@Type(type = "uuid-char")
private final UUID linearId;
public ToDoModel (String task, UUID linearId){
this.task = task;
this.linearId = linearId;
}
public String getTask() {
return task;
}
public UUID getLinearId() {
return linearId;
}
}
@Nullable
@Override
public String getMigrationResource() {
return "tododist.changelog-master";
}
}

This is all that is to be done to store your state data off-ledger using a custom table.

Note: the project build.gradle file is already defined with the property runSchemaMigration = true

You should now be able to run your nodes via runnodes command, perform transactions and see the state data stored in a readable format off-ledger.

If you prefer not to use liquibase scripts, you could overcome the exception by running each node using the command java -jar corda.jar — allow-hibernate-to-manage-app-schema, which may be cubersome as the numder of nodes increases

Github link to the project — todoist-cordapp

Connect with me!

LinkedIn: https://www.linkedin.com/in/rajapandianc/

Twitter: Rajapandian

--

--

RajaPandian

Blockchain evangelist and Integrations Consultant