DMN Decision Execution on the Cosmos Blockchain
In May 2018, Stephan Haarmann, Kimon Batoulis, Adriatik Nikaj, and Mathias Weske published — for the first time online — a great article:
“DMN Decision Execution on the Ethereum Blockchain”. The abstract is cited below, but you are strongly encouraged to read the whole article before proceeding.
“Recently blockchain technology has been introduced to execute interacting business processes in a secure and transparent way. While the foundations for process enactment on blockchain have been researched, the execution of decisions on blockchain has not been addressed yet. In this paper we argue that decisions are an essential aspect of interacting business processes, and, therefore, also need to be executed on blockchain. The immutable representation of decision logic can be used by the interacting processes, so that decision taking will be more secure, more transparent, and better auditable. The approach is based on a mapping of the DMN language S-FEEL to Solidity code to be run on the Ethereum blockchain. The work is evaluated by a proof-of-concept prototype and an empirical cost evaluation.”
— Haarmann, S., Batoulis, K., Nikaj, A., Weske, M. (2018). DMN Decision Execution on the Ethereum Blockchain. In: Krogstie, J., Reijers, H. (eds) Advanced Information Systems Engineering. CAiSE 2018. Lecture Notes in Computer Science(), vol 10816. Springer, Cham. https://doi.org/10.1007/978-3-319-91563-0_20
In this article, we present how the idea of separating business processes from decisions can be applied to blockchains built on Cosmos-SDK.
We evaluate a simplistic proof-of-concept prototype that enables executing decisions based on DMN and the Cosmos blockchain. We focus strictly on implementation and execution of the decision logic, leaving the implementation of the business process part (smart contracts) to the reader.
Introduction
Cosmos blockchains built on top of Cosmos-SDK are modular:
“The power of the Cosmos SDK lies in its modularity. Cosmos SDK applications are built by aggregating a collection of interoperable modules. Each module defines a subset of the state and contains its own message/transaction processor, while the Cosmos SDK is responsible for routing each message to its respective module.”
In the following chapters we will develop custom Cosmos module that integrates with DMN runtime, and enables the decision logic to be executed as a simple query.
Prerequisites
To complete our prototype we need the blockchain node with custom module and the DMN runtime. To create a blockchain node with custom module, we will use Ignite CLI. To evaluate decision logic based on DMN standard we will use Decision Toolkit (DSNTK). For simplicity reasons, we will integrate the Cosmos module and DMN runtime using the JSON API provided by DSNTK.
To run the code examples, make sure you have installed:
- newest version of Ignite CLI,
- newest version of Go,
- newest version of Rust,
- newest version of Decision Toolkit (DSNTK).
All sources are available on GitHub.
Building the decision model
The decision model used in our prototype is identical to the one described in “DMN Decision Execution on the Ethereum Blockchain”. It contains two decisions: SLA and Fine implemented as decision tables. There are three input values used to evaluate the decisions. YearsAsCustomer and NumberOfUnits are used to evaluate SLA. Result from SLA decision and the input value DefectiveUnits are used to evaluate Fine.

SLA is modeled as a decision table shown below (check sla.dtb file on GitHub):

Fine is also modeled as a following decision table (check fine.dtb file on GitHub):

The complete decision model is presented below (see mancus.dmn file on GitHub):
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<definitions namespace="https://dsntk.io"
name="DecisionContract"
id="_f78964ab-4b04-4dee-b9b0-fa3db9b2e499"
xmlns="https://www.omg.org/spec/DMN/20191111/MODEL/"
xmlns:di="http://www.omg.org/spec/DMN/20180521/DI/"
xmlns:dmndi="https://www.omg.org/spec/DMN/20191111/DMNDI/"
xmlns:dc="http://www.omg.org/spec/DMN/20180521/DC/">
<description>
Decision contract for calculating the _fine_.
</description>
<decision name="SLA" label="SLA" id="_822e095e-a12e-4de4-9468-14c059e354c3">
<description>
Calculates the **SLA**.
</description>
<variable typeRef="number" name="SLA">
<description>
Calculated SLA.
</description>
</variable>
<informationRequirement id="_a5c2170c-8187-43f6-9a70-53ad64a8446b">
<requiredInput href="#_32873537-d1f7-4305-9d2f-6b1b0ab91dc1"/>
</informationRequirement>
<informationRequirement id="_9e70e348-4e66-485d-8dc4-6a16cc65fa05">
<requiredInput href="#_dd4cf4f2-92a4-4f97-96f3-0458c3c32d25"/>
</informationRequirement>
<decisionTable outputLabel="SLA">
<input>
<inputExpression typeRef="number">
<text>YearsAsCustomer</text>
</inputExpression>
<inputValues>
<text>[0..100]</text>
</inputValues>
</input>
<input>
<inputExpression typeRef="number">
<text>NumberOfUnits</text>
</inputExpression>
<inputValues>
<text>[0..1000000]</text>
</inputValues>
</input>
<output>
<outputValues>
<text>1,2</text>
</outputValues>
</output>
<rule>
<inputEntry>
<text>< 2</text>
</inputEntry>
<inputEntry>
<text>< 1000</text>
</inputEntry>
<outputEntry>
<text>1</text>
</outputEntry>
</rule>
<rule>
<inputEntry>
<text>< 2</text>
</inputEntry>
<inputEntry>
<text>>= 1000</text>
</inputEntry>
<outputEntry>
<text>2</text>
</outputEntry>
</rule>
<rule>
<inputEntry>
<text>>= 2</text>
</inputEntry>
<inputEntry>
<text>< 500</text>
</inputEntry>
<outputEntry>
<text>1</text>
</outputEntry>
</rule>
<rule>
<inputEntry>
<text>>= 2</text>
</inputEntry>
<inputEntry>
<text>>= 500</text>
</inputEntry>
<outputEntry>
<text>2</text>
</outputEntry>
</rule>
</decisionTable>
</decision>
<decision name="Fine" label="Fine" id="_77a97976-3140-4a91-9b47-c3d3587f3065">
<description>
Calculates the **fine**.
</description>
<variable typeRef="number" name="Fine">
<description>
Calculated fine.
</description>
</variable>
<informationRequirement id="_738f8936-85ac-4f8c-9bc2-b2e2ed9e1f80">
<requiredInput href="#_ab93cef8-48c2-4c79-9165-12531c4a4b3f"/>
</informationRequirement>
<informationRequirement id="_6e20677f-f7c1-4000-9acb-b063fa35af16">
<requiredDecision href="#_822e095e-a12e-4de4-9468-14c059e354c3"/>
</informationRequirement>
<decisionTable outputLabel="Fine">
<input>
<inputExpression typeRef="number">
<text>DefectiveUnits</text>
</inputExpression>
<inputValues>
<text>[0.00 .. 1.00]</text>
</inputValues>
</input>
<input>
<inputExpression typeRef="number">
<text>SLA</text>
</inputExpression>
<inputValues>
<text>1,2</text>
</inputValues>
</input>
<output/>
<rule>
<inputEntry>
<text>< 0.05</text>
</inputEntry>
<inputEntry>
<text>1</text>
</inputEntry>
<outputEntry>
<text>0</text>
</outputEntry>
</rule>
<rule>
<inputEntry>
<text>[0.05 .. 0.1]</text>
</inputEntry>
<inputEntry>
<text>1</text>
</inputEntry>
<outputEntry>
<text>0.02</text>
</outputEntry>
</rule>
<rule>
<inputEntry>
<text>> 0.1</text>
</inputEntry>
<inputEntry>
<text>1</text>
</inputEntry>
<outputEntry>
<text>1</text>
</outputEntry>
</rule>
<rule>
<inputEntry>
<text>< 0.01</text>
</inputEntry>
<inputEntry>
<text>2</text>
</inputEntry>
<outputEntry>
<text>0</text>
</outputEntry>
</rule>
<rule>
<inputEntry>
<text>[0.01 .. 0.05]</text>
</inputEntry>
<inputEntry>
<text>2</text>
</inputEntry>
<outputEntry>
<text>0.05</text>
</outputEntry>
</rule>
<rule>
<inputEntry>
<text>> 0.05</text>
</inputEntry>
<inputEntry>
<text>2</text>
</inputEntry>
<outputEntry>
<text>1.05</text>
</outputEntry>
</rule>
</decisionTable>
</decision>
<inputData name="YearsAsCustomer" label="years as customer" id="_32873537-d1f7-4305-9d2f-6b1b0ab91dc1">
<variable typeRef="number" name="YearsAsCustomer">
<description>
Number of years the customer buys units from the manufacturer.
**Value provided by the manufacturer.**
</description>
</variable>
</inputData>
<inputData name="NumberOfUnits" label="number of units" id="_dd4cf4f2-92a4-4f97-96f3-0458c3c32d25">
<variable typeRef="number" name="NumberOfUnits">
<description>
Total number of units bought by the customer during whole cooperation with the manufacturer.
**Value provided by the manufacturer.**
</description>
</variable>
</inputData>
<inputData name="DefectiveUnits" label="defective units" id="_ab93cef8-48c2-4c79-9165-12531c4a4b3f">
<variable typeRef="number" name="DefectiveUnits">
<description>
Number of defective units.
**Value provided by the customer.**
</description>
</variable>
</inputData>
<dmndi:DMNDI>
<dmndi:DMNDiagram sharedStyle="style1">
<dmndi:Size height="340.0" width="680.0"/>
<dmndi:DMNShape dmnElementRef="_822e095e-a12e-4de4-9468-14c059e354c3">
<dc:Bounds height="80.0" width="100.0" x="200.0" y="60.0"/>
</dmndi:DMNShape>
<dmndi:DMNShape dmnElementRef="_77a97976-3140-4a91-9b47-c3d3587f3065">
<dc:Bounds height="80.0" width="100.0" x="470.0" y="60.0"/>
</dmndi:DMNShape>
<dmndi:DMNShape dmnElementRef="_32873537-d1f7-4305-9d2f-6b1b0ab91dc1">
<dc:Bounds height="60.0" width="160.0" x="80.0" y="220.0"/>
</dmndi:DMNShape>
<dmndi:DMNShape dmnElementRef="_dd4cf4f2-92a4-4f97-96f3-0458c3c32d25">
<dc:Bounds height="60.0" width="160.0" x="260.0" y="220.0"/>
</dmndi:DMNShape>
<dmndi:DMNShape dmnElementRef="_ab93cef8-48c2-4c79-9165-12531c4a4b3f" sharedStyle="style2">
<dc:Bounds height="60.0" width="160.0" x="440.0" y="220.0"/>
</dmndi:DMNShape>
<dmndi:DMNEdge dmnElementRef="_a5c2170c-8187-43f6-9a70-53ad64a8446b">
<di:waypoint x="160.0" y="220.0"/>
<di:waypoint x="230.0" y="140.0"/>
</dmndi:DMNEdge>
<dmndi:DMNEdge dmnElementRef="_9e70e348-4e66-485d-8dc4-6a16cc65fa05">
<di:waypoint x="340.0" y="220.0"/>
<di:waypoint x="270.0" y="140.0"/>
</dmndi:DMNEdge>
<dmndi:DMNEdge dmnElementRef="_738f8936-85ac-4f8c-9bc2-b2e2ed9e1f80">
<di:waypoint x="520.0" y="220.0"/>
<di:waypoint x="520.0" y="140.0"/>
</dmndi:DMNEdge>
<dmndi:DMNEdge dmnElementRef="_6e20677f-f7c1-4000-9acb-b063fa35af16">
<di:waypoint x="300.0" y="100.0"/>
<di:waypoint x="470.0" y="100.0"/>
</dmndi:DMNEdge>
</dmndi:DMNDiagram>
<dmndi:DMNStyle id="style1" fontSize="12"/>
<dmndi:DMNStyle id="style2">
<dmndi:FillColor red="220" green="220" blue="220"/>
</dmndi:DMNStyle>
</dmndi:DMNDI>
</definitions>
Assuming, that the complete DMN decision model is saved in file mancus.dmn, running DSNTK in the same directory will deploy this model and start JSON API server, exposing two invocables:
- io/dsntk/DecisionContract/Fine
- io/dsntk/DecisionContract/SLA
$ dsntk srv -v
Found 1 model.
Loaded 1 model.
Deployed 2 invocables.
Deployed invocables:
io/dsntk/DecisionContract/Fine
io/dsntk/DecisionContract/SLA
dsntk 0.0.0.0:22022
Now, the decision model is ready to be evaluated from custom module on Cosmos blockchain.
Building the Cosmos blockchain with custom module
First, let’s create a chain named decon
with custom module also named decon
, using Ignite CLI:
$ ignite scaffold chain decon
Change the current directory to decon:
$ cd decon
Now, let’s create a custom query named sla
:
$ ignite scaffold query sla yearsAsCustomer:uint numberOfUnits:uint --response sla:uint
modify proto/decon/decon/query.proto
create x/decon/keeper/query_sla.go
modify x/decon/module/autocli.go
🎉 Created a query `sla`.
Being still in decon directory, create a custom query named fine
:
$ ignite scaffold query fine yearsAsCustomer:uint numberOfUnits:uint defectiveUnits:uint --response fine:uint
modify proto/decon/decon/query.proto
create x/decon/keeper/query_fine.go
modify x/decon/module/autocli.go
🎉 Created a query `fine`.
Before starting the chain, we have to add one file and modify two other files to make our custom module work with DMN runtime. While custom modules are stored in directory named x in every Cosmos chain, our custom module named decon
can be found in x/decon directory.
First, let’s add a file x/decon/keeper/dsntk_client.go that implements calling JSON API of the DMN runtime from Go:
package keeper
import (
"bytes"
"encoding/json"
"net/http"
)
const Uri = "http://0.0.0.0:22022/evaluate/io/dsntk/DecisionContract/"
const SlaUri = Uri + "SLA"
const FineUri = Uri + "Fine"
const ContentType = "application/json"
const Multiplier = 100000000.0
type SlaParams struct {
YearsAsCustomer uint64 `json:"YearsAsCustomer"`
NumberOfUnits uint64 `json:"NumberOfUnits"`
}
type SlaResult struct {
Data uint64 `json:"data"`
}
type FineParams struct {
YearsAsCustomer uint64 `json:"YearsAsCustomer"`
NumberOfUnits uint64 `json:"NumberOfUnits"`
DefectiveUnits float64 `json:"DefectiveUnits"`
}
type FineResult struct {
Data float64 `json:"data"`
}
func querySla(yearsAsCustomer uint64, numberOfUnits uint64) uint64 {
slaParams := SlaParams{
YearsAsCustomer: yearsAsCustomer,
NumberOfUnits: numberOfUnits,
}
var body bytes.Buffer
err := json.NewEncoder(&body).Encode(&slaParams)
if err != nil {
panic(err)
}
response, err := http.Post(SlaUri, ContentType, &body)
if err != nil {
panic(err)
}
slaResult := SlaResult{}
err = json.NewDecoder(response.Body).Decode(&slaResult)
if err != nil {
panic(err)
}
return slaResult.Data
}
func queryFine(yearsAsCustomer uint64, numberOfUnits uint64, defectiveUnits uint64) uint64 {
fineParams := FineParams{
YearsAsCustomer: yearsAsCustomer,
NumberOfUnits: numberOfUnits,
DefectiveUnits: float64(defectiveUnits) / Multiplier,
}
var body bytes.Buffer
err := json.NewEncoder(&body).Encode(&fineParams)
if err != nil {
panic(err)
}
response, err := http.Post(FineUri, ContentType, &body)
if err != nil {
panic(err)
}
fineResult := FineResult{}
err = json.NewDecoder(response.Body).Decode(&fineResult)
if err != nil {
panic(err)
}
return uint64(fineResult.Data * Multiplier)
}
Now, let’s modify sla
custom query file: x/decon/keeper/query_sla.go
package keeper
import (
"context"
"decon/x/decon/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (k Keeper) Sla(goCtx context.Context, req *types.QuerySlaRequest) (*types.QuerySlaResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "invalid request")
}
ctx := sdk.UnwrapSDKContext(goCtx)
_ = ctx
sla := querySla(req.YearsAsCustomer, req.NumberOfUnits)
return &types.QuerySlaResponse{Sla: sla}, nil
}
And next, modify fine
custom query file: x/decon/keeper/query_fine.go
package keeper
import (
"context"
"decon/x/decon/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (k Keeper) Fine(goCtx context.Context, req *types.QueryFineRequest) (*types.QueryFineResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "invalid request")
}
ctx := sdk.UnwrapSDKContext(goCtx)
_ = ctx
fine := queryFine(req.YearsAsCustomer, req.NumberOfUnits, req.DefectiveUnits)
return &types.QueryFineResponse{Fine: fine}, nil
}
That’s it. Now we can start the chain:
$ ignite chain serve
In another terminal we can test our decision logic, by executing queries implemented in our custom module:
$ ~/go/bin/decond query decon sla 1 1000
sla: "2"
$ ~/go/bin/decond query decon fine 1 1000 3400000
fine: "5000000"
Smart contracts deployed on Cosmos chain may also evaluate the decision logic, just by querying our custom module. The same way smart contracts query standard modules provided by Cosmos-SDK, like the bank.
Summary
In this article we have presented how DMN decision model may be used on Cosmos blockchain. The presented prototype uses external DMN runtime written in Rust, so the simplest integration with Cosmos module written in Go is by calling JSON API. Although this prototype is simple and straightforward, it may be a good starting point for building a custom module that enables the execution of decision logic on Cosmos blockchains without sacrificing any of the powerful features of the DMN standard.
All artifacts presented in this article are available on GitHub.