Smart Contracts

Vendia Share makes it easy to share code and files among participants in a Uni. But sometimes data alone isn't enough; some applications also require shared computations, known as smart contracts.

Smart contracts can be used for various purposes. Examples of smart contract usage include:

  • Integrity Constraints: Smart contracts can be used to enforce restrictions or limitations, such as ensuring that the balance across several related accounts never drops below a minimum threshold or ensuring that two flight segments aren't booked closer together than 45 minutes between the arrival of the first and the departure of the second. Making this calculation a smart contract ensures that the data in the Uni can adhere to policy constraints, regardless of its provenance. Participants don't have to just trust that other participants "got it right" when they updated one or more values over time.

  • Derived (Computed) Values: Smart contracts can also be used to create data values that are derived from other information. For example, a Uni may be used to store sales information, in which case a smart contract can be used to accurately calculate sales tax and the total amount, rather than requiring every node to maintain a redundant implementation of the tax calculation.

  • Third-party System Integration: Because a smart contract can be any code that the parties in the Uni agree is legitimate to use for their shared purposes, it can do things like contact third party systems, retrieve information stored outside the Uni, etc. Smart contracts can also be used to update third-party systems based on the data in the Uni.

When to Use Smart Contracts

Not all application data needs to run as a smart contract. The following questions can help determine if a smart contract is indicated:

  • Does the calculation affect the data in the Uni?: If the goal is to add computed values, enforce Uni-wide constraints, or act on behalf of all participants, then a smart contract is likely to be required.

  • Is the result of the computation stored in the Uni?: If a computations output is used to update or add to the Uni's data, then a smart contract may be appropriate.

How Smart Contracts Work

On many blockchain platforms, smart contracts have to be executed by all nodes in parallel - a costly and redundant approach. Vendia only requires executing a smart contract once, which also frees developers from having to ensure that the code in the contract is idempotent and replayable. This allows for freedom of language choice: On Vendia, smart contracts can be written in literally any language (though sticking to one of the built-in ones does make things a little simpler). Vendia permits flexible use of non-idempotent calculations, including random number generators, time of date, arbitrary API calls, and more. Not all Unis and participants may elect to support those features in the smart contracts they use, but they're available if desired.

Vendia Share's existing smart contract platform support contracts expressed as AWS Lambda functions. Importantly, these functions must be versioned. Versioning a Lambda function makes it immutable - not even the owner of the function can change its code. This immutability allows the function to be executed with cross-participant trust, because the function has the same "meaning" regardless of who its owner might be.

Vendia executes a smart contract in several steps:

  1. The contract's parameters, which are specified as GraphQL queries, are executed. Each of these returns a value that forms one of the arguments to the function.

  2. The Lambda function representing the contract is invoked, using the values generated in step one as the arguments.

  3. The result of the function is captured and one or more GraphQL mutations are used to update the Uni with the function's outputs. If the function fails, a special status mutation is used to record that fact instead of updating the Uni with the function's result.

The values passed to a function are computed in the same block in which the smart contract invocation is processed. However, since functions can run for up to 15 minutes, transaction processing does not wait for contracts to complete. The results of a contract will be applied asynchronously, once they become available.

A Smart Contract Example

Building on the inventory track and trace quickstart, smart contracts can be used to check external systems before marking a shipment as delivered. The "Orders" and "Shipments" data models both have a delivered(boolean) property but instead of directly mutating that state, the delivering party can use a smart contract to create a confirmation step for the recipient before a delivered=True state is written to the world.

A smart contract can be used to check with off-chain systems before putting the data into the ledger permanently. Before introducing our contract we can take a look at the state of the world:

query Statuses {
  listWarehouses {
    Warehouses {
      city
      code
      companyName
    }
  }
  listShipments {
    Shipments {
      created
      delivered
      destinationWarehouse
      lastUpdated
      id
      location
      orderId
      originWarehouse
    }
  }
  listOrders {
    Orders {
      delivered
      retailerWarehouseCode
      manufacturerWarehouseCode
      orderId
    }
  }
}

In the below mutation, the contract will call the ContractEnforcement Lambda function with the information coming from the Arguments query, getting up-to-date data about the shipment from the ledger and passing that on as JSON input to the function.

mutation confirmDelivery {
  add_Contract_async(
    input: {
      Function: "arn:aws:lambda:us-east-2:123456789012:function:ContractEnforcement:9",
      Arguments: {
        Name: "DeliveredShipment",
        Query: "getShipment(id: \"d34c0292-5d6e-4a08-b1cb-529c72d461ac\") { id orderId destinationWarehouse }"
      },
      Results: {
        Mutation: "updateShipment_async(
          id: \"d34c0292-5d6e-4a08-b1cb-529c72d461ac\"
          input: { delivered:$name, lastUpdated:$lastUpdated, orderId:$orderId }
        )",
        Arguments: [
          {Name: "delivered", Type: "Boolean"},
          {Name: "lastUpdated", Type: "String"}
          {Name: "orderId", Type: "String"}
        ]
      },
      InvocationUID: "some-idempotency-token"
    }
  ) {
    result {
      id
      node_owner
      submission_time
      tx_id
      tx_version
    }
    error
  }
}

When invoked, the Lambda function will receive an invocation event corresponding based upon the Arguments defined in the smart contract mutation and the output of the defined query. The Name argument will be used as the top-most property of the JSON document in the event. For example:

{
  "DeliveredShipment": {
    "data": {
      "getShipment": {
        "id": "d34c0292-5d6e-4a08-b1cb-529c72d461ac",
        "orderId: "123456",
        "destinationWarehouse": "wh12345"
      }
    }
  }
}

The function must then return a JSON object that fills in the Arguments to the mutation. These arguments are mapped in to the mutation updateShipment_async and is the result we want our smart contract to have after checking the external inventory system. For example:

{
  "delivered": true,
  "lastUpdated": "2021-....",
  "orderId": "71bd99f5-8157-4ead-9d77-6bc854d89c58"
}

The call to the smart contract and the mutation result will be saved to the ledger, making it easy to resolve any future disputes and audit the usage of the smart contract. Supporting Lambda execution makes it easy to integrate any external system into your Uni's consensus process and leave a trail of decisions auditable by any node.

The function that executes the smart contract can be written in any language that is supported by AWS Lambda. For our shipment example, the Python code might look like the below.

import json
from datetime import datetime, timezone

def lambda_handler(event, context):
    # printing out the event is useful for development, but you may not want to
    # do this for customer data
    print(json.dumps(event, sort_keys=True))
    # read the incoming arguments to get information about the order
    warehouse_id = event['DeliveredShipment']['data']['getShipments']['destinationWarehouse']
    order_id = event['DeliveredShipment']['data']['getShipments']['orderId']
    delivery_received = True # TODO call out to the inventory system
    return {
        'delivered': delivery_received,
        'lastUpdated': datetime.now(timezone=timezone.utc).isoformat(),
        'orderId': order_id,
    }

Smart Contract Function Deployment and Permissions

The Lambda function supporting a smart contract is a customer-owned resource. As such it is deployed to an AWS Account outside of Vendia. This allows you to retain complete control over the function configuration, code, versioning, and permissions.

The resource policy of a function support a smart contract must be updated so that the Uni can invoke the Function. Specifically, permissions for lambda:GetFunction and lambda:InvokeFunction are required.

The fastest way to add the required permissions for you lambda function is via the AWS CLI. The following code block provides an example of the CLI commands necessary.

You must update the example CLI commands such that YourAWSRegion is replaced with the AWS region where your function resides, such as us-east-1. You must also replace YourFunctionName with the name of your lambda function. Finally, UniAccount must be replaced with the correct AWS account number for your smart contract.

Note: The correct AWS account number can be found by looking at the cloud resources of the initial node (the first you created) on the Uni dashboard. For example if the cloud resources include an Block Notification resource like "arn:aws:sns:us-east-2:123456789:test-..." then the account number will be 123456789 as the 5'th colon-delimited value in that string. This value is also available using the "share uni get" CLI action.

aws lambda add-permission --region YourAWSRegion --function-name YourFunctionName --statement-id InvokeForVendia --action lambda:InvokeFunction --principal UniAccount
aws lambda add-permission --region YourAWSRegion --function-name YourFunctionName --statement-id GetForVendia --action lambda:GetFunction --principal UniAccount

Smart Contract Reference

Schema

For reference, here is the full JSON schema for a contract expression. This can also be reviewed in GraphQL format from your node's GraphQL Explorer.

"_Contract": {
  "description": "Smart Contract Invocations",
  "type": "array",
  "items": {
    "type": "object",
    "properties": {
      "InvocationUID": {
        "type": "string"
      },
      "Function": {
        "type": "string"
      },
      "Arguments": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "Name": {
              "type": "string"
            },
            "Query": {
              "type": "string"
            }
          },
          "required": ["Name", "Query"],
          "uniqueItems": false
        }
      },
      "Results": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "Mutation": {
              "type": "string"
            },
            "Arguments": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "Name": {
                    "type": "string"
                  },
                  "Type": {
                    "type": "string"
                  },
                  "ResultPath": {
                    "type": "string"
                  }
                },
                "required": ["Name", "Type"],
                "uniqueItems": true
              }
            },
          },
          "required": ["Mutation"],
          "uniqueItems": false
        }
      },
    },
    "required": ["InvocationUID", "Function"]
  },
  "uniqueItems": false
}

Arguments

For reference, the following figure depicts the arguments passed to a Smart Contract mutation and descriptive text regarding their use given an example smart contract mutation.

Note that comment lines beginning with "#" are not valid in the GraphQL specification and are used here for documentation purposes only

# Smart Contracts are invoked by posting a Mutation to the GraphQL API endpoint. This mutation can optionally have a name like 'confirmDelivery'
mutation confirmDelivery {

  # The add_Contract_async operation informs the Uni that this operation is a Smart Contract Invocation
  add_Contract_async(

    # The input block defines the location of the function and the inputs that will generate the Event object sent to the function when invoked.
    input: {

      # The function must be a ARN that points to a version of an AWS Lambda function
      Function: "arn:aws:lambda:us-east-2:123456789012:function:ContractEnforcement:9",

      # The arguments will generate the event sent to the function
      Arguments: {

        # The Name argument becomes the top-level property of the event object such that the event will be like {"DeliveredShipment":{"data":{...}}}
        Name: "DeliveredShipment",

        # The Query argument should be a single GraphQL operation applicable to the Uni schema. The result of this query will be presented to the event object inside the data property such that the event will be like  {"DeliveredShipment":{"data":{"getShipment":{...}}}}. 
        Query: "getShipment(id: \"d34c0292-5d6e-4a08-b1cb-529c72d461ac\") { id orderId destinationWarehouse }"
      },

      # The results will be used to construct a new Mutation processed in the Uni after the Lambda function invocation returns a result
      Results: {

        # The Mutation defines the mutation to execute. $variable notation is used to extract values returned from the lambda function invocation.
        Mutation: "updateShipment_async(
          id: \"d34c0292-5d6e-4a08-b1cb-529c72d461ac\"
          input: { delivered:$name, lastUpdated:$lastUpdated, orderId:$orderId }
        )",

        # Arguments defines the Name and Type of properties in the return value from the lambda invocation such that they can be mapped to the variables above.
        Arguments: [
          {Name: "delivered", Type: "Boolean"},
          {Name: "lastUpdated", Type: "String"}
          {Name: "orderId", Type: "String"}
        ]
      },

      # The InvocationUID allows a customer to define a string token for reference in logs, during lambda execution, and in any integrated systems.
      InvocationUID: "some-idempotency-token"
    }
  ) 

  # The remaining portion is the standard GraphQL result object that will indicate details regarding the post of the Smart Contract mutation to the Uni.
  {
    result {
      id
      node_owner
      submission_time
      tx_id
      tx_version
    }
    error
  }
}