/dsntk-cosmos

DMN Decision Execution on the Cosmos Blockchain

Primary LanguageGoApache License 2.0Apache-2.0

DMN Decision Execution on the Cosmos Blockchain

Source code for this article published on Medium.

Prerequisites

  • Install the newest stable version of Rust.
  • Install the newest version of Go.
  • Install the newest version of Ignite CLI.
  • Clone this repository locally, to experiment with presented examples.

Install DSNTK

$ cargo install dsntk
$ dsntk --version
dsntk 0.0.4

Create a decision table for SLA

The decision table for calculating SLA is presented below. The source is saved in file sla.dtb. This decision table is identical to the one presented in Haarmann's work.

 ┌───────┐
 │  SLA  │
 ├───┬───┴─────────────┬───────────────╥─────┐
 │ U │ YearsAsCustomer │ NumberOfUnits ║ SLA │
 │   ├─────────────────┼───────────────╫─────┤
 │   │    [0..100]     │ [0..1000000]  ║ 1,2 │
 ╞═══╪═════════════════╪═══════════════╬═════╡
 │ 1 │       <2        │    <1000      ║  1  │
 ├───┼─────────────────┼───────────────╫─────┤
 │ 2 │       <2        │   >=1000      ║  2  │
 ├───┼─────────────────┼───────────────╫─────┤
 │ 3 │      >=2        │     <500      ║  1  │
 ├───┼─────────────────┼───────────────╫─────┤
 │ 4 │      >=2        │    >=500      ║  2  │
 └───┴─────────────────┴───────────────╨─────┘

To evaluate this decision table, run:

$ dsntk edt sla.input sla.dtb
2

The sla.input file contains input data presented to decision table during evaluation.

To test this decision table, run:

$ dsntk tdt sla.test sla.dtb
test 1 ... ok
test 2 ... ok
test 3 ... ok
test 4 ... ok
test 5 ... ok
test 6 ... ok
test 7 ... ok
test 8 ... ok
test 9 ... ok
test 10 ... ok
test 11 ... ok

test result: ok. 11 passed; 0 failed.

Create decision table for Fine

The decision table for calculating Fine is presented below. The source is saved in file fine.dtb. This decision table is identical to the one presented in Haarmann's work.

 ┌───────┐
 │ Fine  │
 ├───┬───┴────────────┬─────╥──────┐
 │ U │ DefectiveUnits │ SLA ║ Fine │
 │   ├────────────────┼─────╫──────┤
 │   │  [0.00..1.00]  │ 1,2 ║      │
 ╞═══╪════════════════╪═════╬══════╡
 │ 1 │    < 0.05      │  1  ║ 0.00 │
 ├───┼────────────────┼─────╫──────┤
 │ 2 │  [0.05..0.10]  │  1  ║ 0.02 │
 ├───┼────────────────┼─────╫──────┤
 │ 3 │    > 0.10      │  1  ║ 1.00 │
 ├───┼────────────────┼─────╫──────┤
 │ 4 │    < 0.01      │  2  ║ 0.00 │
 ├───┼────────────────┼─────╫──────┤
 │ 5 │  [0.01..0.05]  │  2  ║ 0.05 │
 ├───┼────────────────┼─────╫──────┤
 │ 6 │    > 0.05      │  2  ║ 1.05 │
 └───┴────────────────┴─────╨──────┘

To evaluate this decision table, run:

$ dsntk edt fine.input fine.dtb
0.02

The fine.input file contains input data presented to decision table during evaluation.

To test this decision table, run:

test 1 ... ok
test 2 ... ok
test 3 ... ok
test 4 ... ok
test 5 ... ok
test 6 ... ok
test 7 ... ok
test 8 ... ok
test 9 ... ok
test 10 ... ok
test 11 ... ok
test 12 ... ok

test result: ok. 12 passed; 0 failed.

Create decision model for calculating SLA and Fine

Decision tables presented above contain properly working decision logic as described in Haarmann's work. This logic must be combined in a decision model, specifying requirements and dependencies as depicted below:

mancus.png

Decision model is identical to the one presented in Haarmann's work. Every decision model, to be evaluated, must be prepared in XML format, compliant with DMN specification. The file mancus.dmn contains such a model. The content of this file is presented below.

<?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>&lt; 2</text>
                </inputEntry>
                <inputEntry>
                    <text>&lt; 1000</text>
                </inputEntry>
                <outputEntry>
                    <text>1</text>
                </outputEntry>
            </rule>
            <rule>
                <inputEntry>
                    <text>&lt; 2</text>
                </inputEntry>
                <inputEntry>
                    <text>&gt;= 1000</text>
                </inputEntry>
                <outputEntry>
                    <text>2</text>
                </outputEntry>
            </rule>
            <rule>
                <inputEntry>
                    <text>&gt;= 2</text>
                </inputEntry>
                <inputEntry>
                    <text>&lt; 500</text>
                </inputEntry>
                <outputEntry>
                    <text>1</text>
                </outputEntry>
            </rule>
            <rule>
                <inputEntry>
                    <text>&gt;= 2</text>
                </inputEntry>
                <inputEntry>
                    <text>&gt;= 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>&lt; 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>&gt; 0.1</text>
                </inputEntry>
                <inputEntry>
                    <text>1</text>
                </inputEntry>
                <outputEntry>
                    <text>1</text>
                </outputEntry>
            </rule>
            <rule>
                <inputEntry>
                    <text>&lt; 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>&gt; 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>

Execute the decision model

To execute the decision model using DSNTK, run:

$ 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

DSNTK, run with command srv searches for DMN models in current directory. While there is exactly one in file mancus.dmn, DSNTK loads the model and deploys invocables (decision tables in our case) and prepares JSON API endpoints to evaluate those invocables.

To test SLA decision, open another terminal and run:

$ curl -s \
       -d "{ YearsAsCustomer: 1, NumberOfUnits: 1000 }" \
       -H "Content-Type: application/json" \
       -X POST http://0.0.0.0:22022/evaluate/io/dsntk/DecisionContract/SLA
{"data":2}

The result SLA is 2.

To test Fine decision, run:

$ curl -s \
       -d "{ YearsAsCustomer: 1, NumberOfUnits: 1000, DefectiveUnits: 0.034 }" \
       -H "Content-Type: application/json" \
       -X POST http://0.0.0.0:22022/evaluate/io/dsntk/DecisionContract/Fine
{"data":0.05}       

The result Fine is 0.05 that is 5%.

To run both tests shown above:

$ chmod +x mancus.sh
$ ./mancus.sh
Calculating SLA:
{"data":2}
Calculating fine:
{"data":0.05}

Now we have the DMN decision model up and running. This model can be evaluated by executing two JSON API endpoints using curl:

Create Go client to invoke the decision model

The example Go application that evaluates decision model is prepared in file client/main.go. The source is presented below:

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"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"

type SlaParams struct {
	YearsAsCustomer int64 `json:"YearsAsCustomer"`
	NumberOfUnits   int64 `json:"NumberOfUnits"`
}

type SlaResult struct {
	Data int64 `json:"data"`
}

type FineParams struct {
	YearsAsCustomer int64   `json:"YearsAsCustomer"`
	NumberOfUnits   int64   `json:"NumberOfUnits"`
	DefectiveUnits  float64 `json:"DefectiveUnits"`
}

type FineResult struct {
	Data float64 `json:"data"`
}

func querySla(yearsAsCustomer int64, numberOfUnits int64) int64 {
	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 int64, numberOfUnits int64, defectiveUnits float64) float64 {
	fineParams := FineParams{
		YearsAsCustomer: yearsAsCustomer,
		NumberOfUnits:   numberOfUnits,
		DefectiveUnits:  defectiveUnits,
	}

	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 fineResult.Data
}

func main() {
	fmt.Printf("SLA = %d\n", querySla(1, 1000))
	fmt.Printf("Fine = %.0f%%\n", queryFine(1, 1000, 0.034)*100)
}

To test this application, run:

$ cd client
$ go run dsntk/client
SLA = 2
Fine = 5%

NOTE: The DSNTK server must be running with decision model deployed, but this is obvious ;-)

Now we have the DMN decision model up and running. this model can be executed from Go application. We will use this Go code to implement query in Cosmos blockchain.

Create example blockchain named decon

Check ignite version:

$ ignite ignite version
Ignite CLI version:             v28.1.1
Cosmos SDK version:             v0.50.3
Your OS:                        linux
Your arch:                      amd64
Your Node.js version:           v20.10.0
Your go version:                go version go1.21.6 linux/amd64
Is on Gitpod:                   false 

Create a chain named decon with custom module named decon:

$ ignite scaffold chain decon

Create a custom query named sla:

$ cd decon
$ 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`.

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

This file was added from Go client and slightly modified: x/decon/keeper/dsntk_client.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)
}

This file is modified: 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)

	// TODO: Process the query
	_ = ctx

	sla := querySla(req.YearsAsCustomer, req.NumberOfUnits)
	return &types.QuerySlaResponse{Sla: sla}, nil
}

This file is modified: 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)

	// TODO: Process the query
	_ = ctx

	fine := queryFine(req.YearsAsCustomer, req.NumberOfUnits, req.DefectiveUnits)
	return &types.QueryFineResponse{Fine: fine}, nil
}

Start DSNTK server in one terminal (if not already started):

$ dsntk srv

Start the chain (in second terminal:

$ ignite chain serve

Query SLA (in third terminal):

$  ~/go/bin/decond query decon sla 1 1000
sla: "2"

Query Fine (in fourth terminal):

$ ~/go/bin/decond query decon fine 1 1000 3400000
fine: "5000000"

Now we have a chain with custom module, that executes DMN decision model.

References