Skip to contents

Introduction

In MultiChain, Smart Filters provide a way to enforce business rules directly on the ledger. For example, you can ensure that a sensor only records values within a specific range or that a transaction is only valid if it contains certain metadata. To keep code clean, common logic can be stored in Libraries.

library(multichainr)

# Set the path to your MultiChain binaries
mc_set_path(Sys.getenv("MULTICHAIN_PATH"))

1. Node Initialization

We start by setting up a local node. Governance permissions (Admin) are required to create filters and libraries.

chain_name <- "logic_demo_chain"

# Create and start the node
mc_node_init(chain_name)
mc_node_start(chain_name)

# Wait for the node to initialize
Sys.sleep(3) 

# Connect to the local node
config <- mc_get_config(chain_name)
conn <- mc_connect(config)

2. Creating a JavaScript Library

A library contains helper functions. Here, we create a library called validator that checks if a temperature reading is within a safe physical range.

# Define the JavaScript code for the library
lib_js <- "
function isSafeTemp(temp) {
    return temp >= -50 && temp <= 100;
}
"

# Create the library on the blockchain
# updatemode = 'instant' means updates take effect immediately without voting
mc_create_library(conn, "validator", updatemode = "instant", js_code = lib_js)

cat("Library 'validator' created on-chain.\n")

3. Writing and Testing a Stream Filter

Now we want to create a filter for a stream called telemetry. This filter will use our library to reject any data that has an “impossible” temperature.

Defining the Filter

The filter logic must be contained in a function named filterstreamitem().

# The filter code imports the 'validator' library
filter_js <- "
function filterstreamitem() {
    var item = getfilterstreamitem();
    
    if (item.data.json && typeof item.data.json.temp !== 'undefined') {
        if (!isSafeTemp(item.data.json.temp)) {
            return 'Invalid temperature detected by on-chain logic';
        }
    }
    return true;
}
"

Local Testing (Dry Run)

Before deploying a filter to the whole network, it is best practice to test it locally using mc_test_stream_filter. To do this, we first publish a sample item to get a valid transaction ID.

# 0. Setup a temporary stream for testing
mc_create_stream(conn, "test_stream", open = TRUE)
mc_subscribe(conn, "test_stream")

# 1. Publish valid data and get its TXID
txid_valid <- mc_publish(conn, "test_stream", "key1", list(json = list(temp = 25.5)))

# Test the filter against the valid transaction
test_valid <- mc_test_stream_filter(conn, 
                                    options = list(libraries = list("validator")), 
                                    js_code = filter_js, 
                                    tx = txid_valid)
print(test_valid) # Should be TRUE (logical) or 'true' (string)

# 2. Publish invalid data and get its TXID
txid_invalid <- mc_publish(conn, "test_stream", "key1", list(json = list(temp = 500)))

# Test the filter against the invalid transaction
test_invalid <- mc_test_stream_filter(conn, 
                                      options = list(libraries = list("validator")), 
                                      js_code = filter_js, 
                                      tx = txid_invalid)
print(test_invalid) # Should return our error message string

4. Deploying and Using the Filter

Once tested, we create the filter on the blockchain and attach it to our stream.

# 1. Create the telemetry stream
mc_create_stream(conn, "telemetry", open = TRUE)

# 2. Create the stream filter globally
# Here 'options' only specifies the libraries used.
mc_create_stream_filter(conn, "temp_range_check", 
                        options = list(libraries = list("validator")), 
                        js_code = filter_js)

# 3. Activate the filter for the 'telemetry' stream
# In MultiChain, filters must be approved for specific entities.
admin_addr <- mc_get_addresses(conn)[1]

mc_approve_from(conn, 
                from_address = admin_addr, 
                entity = "temp_range_check", 
                approve = list("for" = "telemetry", approve = TRUE))

5. Verifying the Logic

After the filter is active, any attempt to publish invalid data to the telemetry stream will be rejected by every node in the network.

# This will SUCCEED (20 is within range)
txid_ok <- mc_publish(conn, "telemetry", "sensor_01", list(json = list(temp = 20)))
print(paste("Successful publish TXID:", txid_ok))

# This will FAIL at the protocol level. 
# We use try() to catch the RPC error so the vignette can continue.
publish_error <- try(mc_publish(conn, "telemetry", "sensor_01", list(json = list(temp = 999))), 
                     silent = TRUE)

if (inherits(publish_error, "try-error")) {
    cat("Rejected! As expected, the filter blocked the invalid data.\n")
}

# Retrieve the filter code from the blockchain to verify
code_info <- mc_get_filter_code(conn, "temp_range_check")
cat("Stored filter code length:", nchar(code_info), "characters.\n")

6. Cleanup

Always shut down the node and clean up the temporary directory.

# Stop the node
mc_node_stop(conn)
Sys.sleep(2)

# Determine data directory
if (.Platform$OS.type == "windows") {
  base_dir <- file.path(Sys.getenv("APPDATA"), "MultiChain")
} else if (Sys.info()["sysname"] == "Darwin") {
  base_dir <- file.path(Sys.getenv("HOME"), "Library/Application Support/MultiChain")
} else {
  base_dir <- file.path(Sys.getenv("HOME"), ".multichain")
}

chain_dir <- file.path(base_dir, chain_name)
if (dir.exists(chain_dir)) {
  unlink(chain_dir, recursive = TRUE)
}

Summary

In this vignette, we demonstrated the advanced logic capabilities of multichainr:

  1. Code Reuse: Using mc_create_library to store JavaScript functions.
  2. Logic Definition: Writing a stream filter that intercepts and validates incoming data.
  3. Prototyping: Using mc_test_stream_filter to debug JavaScript logic without making on-chain transactions.
  4. Deployment: Attaching validation rules to specific streams using mc_create_stream_filter.