Skip to content

Programming an IOU

Introduction

Protocols are core constructs of NPL that represent contracts, regulations, and operating procedures. They represent agreements between parties on the deontic modalities of permissions, obligations, and prohibitions.

In this section we'll show how protocols are defined in NPL by programming an IOU.

Creating the IOU

Let's start by creating the definition for our IOU protocol. An IOU represents an acknowledgement of debt between two parties. The party who issues the IOU and owes the money is referred to as the issuer, and the party who lent the money to the issuer is the payee.

An NPL definition starts with the keyword protocol, and is immediately followed by the protocol parties, placed between square brackets. Then follows the name, for which we'll use Iou, and the input parameters required to instantiate the protocol.

For our IOU we'll need to know how much money is owed, so let's add an input parameter of type Number.

protocol[issuer, payee] Iou(var forAmount: Number) {
    // Protocol body
};

Type annotations are important in NPL, because it is a compiled and statically typed language. This means the compiler will verify that you have given it the correct type even before the code is run. For an explanation of NPL's type system, see Type system.

Deriving rights from our IOU

Above we have defined an IOU, but we can't do anything with it yet! For this IOU, we'd like the following behavior:

  • the issuer should be able to pay off the debt all at once, or in increments
  • the payee should be able to forgive the debt at any point in time, unless of course the issuer has already paid
  • either party should be able to request both the currently outstanding amount, and view the payment history

Let's start by adding a way for the issuer to make a payment.

The deontic modalities available in NPL are permissions, obligations, and prohibitions. Permissions and obligations are implemented directly on the protocols (more about prohibitions later on), and are referred to as actions. They start with the keyword permission or obligation, followed by the party that is obliged or permitted placed between square brackets. Then follows the name, along with any input values required to invoke the action.

In this case, we'll use a permission called pay for the payment mechanism, and we'll simply allow the issuer to specify how much has been paid.

protocol[issuer, payee] Iou(var forAmount: Number) {
    permission[issuer] pay(amount: Number) {
        // payment logic
    };
};

In real systems the burden of proof could be placed on external mechanisms instead, but these are beyond the scope of our IOU.

It is important to note that any party referenced in a protocol must be part of the protocol header. Otherwise, parties external to the protocol might accidentally be required to perform actions they have not agreed upon.

Storing data

The pay permission has now been defined, but it doesn't do anything yet. Because incoming payments will affect the outstanding amount, we must keep track of them.

Because we required that both parties need to be able to view the payment history, we have to keep track of both the amount and the time at which the payment was made. To do so, we'll introduce a special data structure that holds a Number and a DateTime called a Struct. Below we've defined a struct called TimestampedAmount that holds two fields: one named amount of type Number, and one named timestamp of type DateTime.

struct TimestampedAmount {
    amount: Number,
    timestamp: DateTime
};

On our IOU each incoming payment corresponds to one struct. We'll introduce a protocol member called payments that represents a List on which each item is a TimestampedAmount.

struct TimestampedAmount {
    amount: Number,
    timestamp: DateTime
};

protocol[issuer, payee] Iou(var forAmount: Number) {
    var payments: List<TimestampedAmount> = listOf<TimestampedAmount>();
};

Because NPL does not allow us to leave protocol members unassigned, we have initialized this list as an empty list of TimestampedAmount. An empty list is instantiated using the listOf function, which is a built-in NPL function. Assignment expressions for protocol members are always run as part of the protocol instantiation.

Adding execution logic to actions

Now that we have a place to store our data, let's make sure the payment is stored on the protocol. First, wrap the incoming payment into a TimestampedAmount, and then add it to the list.

struct TimestampedAmount {
    amount: Number,
    timestamp: DateTime
};

protocol[issuer, payee] Iou(var forAmount: Number) {
    var payments: List<TimestampedAmount> = listOf<TimestampedAmount>();

    permission[issuer] pay(amount: Number) {
        var p = TimestampedAmount(amount = amount, timestamp = now());
        payments = payments.with(p);
    };
};

An instance of TimestampedAmount is created by using its name, followed by the named parameters and their values in braces. For the timestamp field, we invoke the now function. This is another NPL built-in function that returns the current DateTime. Then, we invoke the .with method, an NPL built-in on lists that returns an existing list with a new element added. The result is then assigned to the payments field on the protocol.

Prohibitions using state guards

Another requirement for our IOU was that the payee should be able to forgive the debt at any point in time. Let's introduce this as a protocol permission.

struct TimestampedAmount {
    amount: Number,
    timestamp: DateTime
};

protocol[issuer, payee] Iou(var forAmount: Number) {
    var payments: List<TimestampedAmount> = listOf<TimestampedAmount>();

    permission[issuer] pay(amount: Number) {
        var p = TimestampedAmount(amount = amount, timestamp = now());
        payments = payments.with(p);
    };

    permission[payee] forgive() {
        // forgive logic
    };
};

What should the forgive permission body do? Forgiving means the issuer no longer needs to pay, and in fact should no longer be able to pay.

In order to accommodate such protocol logic, NPL natively supports states as an action prohibition mechanism. States are defined on the protocol using the keyword state, followed by the name. One of the states must be prefixed by the keyword initial, indicating that the protocol will in that state upon creation. Other states may optionally be labelled final, indicating that once reached, no further state transitions are allowed.

Transitioning to another protocol state is achieved using the keyword become, followed by the name of the state. States may also be used as guards on actions (a form of prohibition) using the | stateName on protocol actions.

Let's use states for our IOU. We introduce an initial state called unpaid, and two final states paid and forgiven. Invoking the forgive permission causes a state transition to forgiven. We'll also add a state guard such that pay and forgive may only be invoked as long as the protocol state is unpaid.

struct TimestampedAmount {
    amount: Number,
    timestamp: DateTime
};

protocol[issuer, payee] Iou(var forAmount: Number) {
    initial state unpaid;
    final state paid;
    final state forgiven;

    var payments: List<TimestampedAmount> = listOf<TimestampedAmount>();

    permission[issuer] pay(amount: Number) | unpaid {
        var p = TimestampedAmount(amount = amount, timestamp = now());
        payments = payments.with(p);
    };

    permission[payee] forgive() | unpaid {
        become forgiven;
    };
};

Now the invocation of forgive will automatically cause pay to become unavailable!

Helper functions

Our pay permission still lacks certain features. Specifically, regardless of how much is paid, we have not added a state transition to paid. We have also not added a permission that shows the currently outstanding amount. Both of these features rely on the currently outstanding amount, and are a prime candidate for helper functions.

Helper functions are defined using the keyword function, and they do not specify any parties. Functions are chiefly intended to facilitate the reuse of shared logic, and to separate code into smaller parts. If defined on a protocol they may access the protocol's members, but they may only be invoked from within other permissions or functions in that protocol. Note that helper functions may also be defined outside of the protocol.

Let's add two helper functions. The first is called total and simply sums up all amounts on a list of TimestampedAmount. The second is amountOwed and is defined on the protocol itself. Note how it uses the protocol's forAmount member directly.

struct TimestampedAmount {
    amount: Number,
    timestamp: DateTime
};

function total(entries: List<TimestampedAmount>) returns Number -> {
    return entries.map(function(p: TimestampedAmount) returns Number -> p.amount).sum();
};

protocol[issuer, payee] Iou(var forAmount: Number) {
    initial state unpaid;
    final state paid;
    final state forgiven;

    var payments: List<TimestampedAmount> = listOf<TimestampedAmount>();

    function amountOwed() returns Number -> {
        return forAmount - total(payments);
    };

    permission[issuer] pay(amount: Number) | unpaid {
        var p = TimestampedAmount(amount = amount, timestamp = now());
        payments = payments.with(p);
    };

    permission[payee] forgive() | unpaid {
        become forgiven;
    };
};

The syntax for functions is such that it specifies its input values, and explicitly defines its return type using the returns keyword. It explicitly uses the return keyword to return a result.

Next we'll add a permission called getAmountOwed that returns the currently outstanding amount.

struct TimestampedAmount {
    amount: Number,
    timestamp: DateTime
};

function total(entries: List<TimestampedAmount>) returns Number -> {
    return entries.map(function(p: TimestampedAmount) returns Number -> p.amount).sum();
};

protocol[issuer, payee] Iou(var forAmount: Number) {
    initial state unpaid;
    final state paid;
    final state forgiven;

    var payments: List<TimestampedAmount> = listOf<TimestampedAmount>();

    function amountOwed() returns Number -> {
        return forAmount - total(payments);
    };

    permission[issuer] pay(amount: Number) | unpaid {
        var p = TimestampedAmount(amount = amount, timestamp = now());
        payments = payments.with(p);
    };

    permission[payee] forgive() | unpaid {
        become forgiven;
    };

    permission[issuer|payee] getAmountOwed() returns Number {
        return amountOwed();
    };
};

The getAmountOwed permission simply returns the result of the amountOwed function. Unlike the other permissions, this permission returns a result, and it is therefore annotated using returns. It also lists two parties, making this permission available to either party.

Note that the permission explicitly uses the return keyword to return a result.

Our helper function also allows us to add the state transition to the pay permission.

struct TimestampedAmount {
    amount: Number,
    timestamp: DateTime
};

function total(entries: List<TimestampedAmount>) returns Number -> {
    return entries.map(function(p: TimestampedAmount) returns Number -> p.amount).sum();
};

protocol[issuer, payee] Iou(var forAmount: Number) {
    initial state unpaid;
    final state paid;
    final state forgiven;

    var payments: List<TimestampedAmount> = listOf<TimestampedAmount>();

    function amountOwed() returns Number -> {
        return forAmount - total(payments);
    };

    permission[issuer] pay(amount: Number) | unpaid {
        var p = TimestampedAmount(amount = amount, timestamp = now());
        payments = payments.with(p);

        if (amountOwed() == 0) {
            become paid;
        };
    };

    permission[payee] forgive() | unpaid {
        become forgiven;
    };

    permission[issuer|payee] getAmountOwed() returns Number {
        return amountOwed();
    };
}

Prohibitions using require conditions

There are still some loopholes in our protocol, the biggest of which is that the issuer is allowed to make negative payments. Furthermore, our amountOwed() == 0 check will only work if the issuer never pays more than what is owed.

NPL uses require conditions to prohibit certain parameter inputs. A require is a boolean expression that serves as precondition. Let's add them for the pay permission.

struct TimestampedAmount {
    amount: Number,
    timestamp: DateTime
};

function total(entries: List<TimestampedAmount>) returns Number -> {
    return entries.map(function(p: TimestampedAmount) returns Number -> p.amount).sum();
};

protocol[issuer, payee] Iou(var forAmount: Number) {
    initial state unpaid;
    final state paid;
    final state forgiven;

    var payments: List<TimestampedAmount> = listOf<TimestampedAmount>();

    function amountOwed() returns Number -> {
        return forAmount - total(payments);
    };

    permission[issuer] pay(amount: Number) | unpaid {
        require(amount > 0, "Payments must be strictly positive");
        require(amount <= amountOwed(), "Cannot pay more than is owed");

        var p = TimestampedAmount(amount = amount, timestamp = now());
        payments = payments.with(p);

        if (amountOwed() == 0) {
            become paid;
        };
    };

    permission[payee] forgive() | unpaid {
        become forgiven;
    };

    permission[issuer|payee] getAmountOwed() returns Number {
        return amountOwed();
    };
}

Our protocol is based on the premise that forAmount is strictly positive. require conditions may also be defined on a protocol level, so we'll add one there as well.

struct TimestampedAmount {
    amount: Number,
    timestamp: DateTime
};

function total(entries: List<TimestampedAmount>) returns Number -> {
    return entries.map(function(p: TimestampedAmount) returns Number -> p.amount).sum();
};

@api
protocol[issuer, payee] Iou(var forAmount: Number) {
    require(forAmount > 0, "Initial amount must be strictly positive");

    initial state unpaid;
    final state paid;
    final state forgiven;

    private var payments = listOf<TimestampedAmount>();

    function amountOwed() returns Number -> {
        return forAmount - total(payments);
    };

    @api
    permission[issuer] pay(amount: Number) | unpaid {
        require(amount > 0, "Amount must be strictly positive");
        require(amount <= amountOwed(), "Amount may not exceed amount owed");

        var p = TimestampedAmount(amount = amount, timestamp = now());
        payments = payments.with(p);

        if (amountOwed() == 0) {
            become paid;
        };
    };

    @api
    permission[payee] forgive() | unpaid {
        become forgiven;
    };

    @api
    permission[issuer|payee] getAmountOwed() returns Number {
        return amountOwed();
    };
}

Obligations

Note

Obligations are under active development: Some features are subjects to change.

Now that everything is in place, it is time to reconsider the rather informal nature of the pay permission. Instead, we'd really like to make sure that the issuer has some motivation to pay back quickly.

To accomplish this, the payee and issuer agree on a reasonable payment deadline and a late payment fee. These are part of the protocol's instantiation, and are added to the constructor.

protocol[issuer, payee] Iou(var forAmount: Number, var paymentDeadline: DateTime, var lateFee: Number) {

If the payment is not complete before the deadline, the payee has the right (but not the obligation) to charge a late payment fee the total amount by some pre-agreed late payment fee. If the payment is indeed raised, the deadline is automatically extended by one month.

The corresponding NPL permission relies on an additional state default (indicating that the issuer's payment is late) and looks as follows.

@api
permission[payee] chargeLatePaymentFee() | default {
    forAmount = forAmount + lateFee;
    paymentDeadline = now() + months(1);

    become unpaid;
};

There may be other factors that lead to the final decision to charge the payment fee, but the issuer should be able to pay again as soon as the payee has invoked the right to charge the late payment fee.

@api
obligation[issuer] pay(amount: Number) before paymentDeadline | unpaid, default {
    require(amount > 0, "Amount must be strictly positive");
    require(amount <= amountOwed(), "Amount may not exceed amount owed");

    var p = TimestampedAmount(amount = amount, timestamp = now());
    payments = payments.with(p);

    if (amountOwed() == 0) {
        become paid;
    };
} otherwise become default;

Now pay is an obligation with a deadline as specified. Note that forgive may also be invoked if the state is default.

@api
protocol[issuer, payee] Iou(var forAmount: Number, var paymentDeadline: DateTime, var lateFee: Number) {
    require(forAmount > 0, "Initial amount must be strictly positive");
    require(paymentDeadline.isAfter(now() + months(1), false), "Payment deadline must be at least one month in the future");
    require(lateFee > 0, "Late fee must be strictly positive");

    initial state unpaid;
    state default;
    final state paid;
    final state forgiven;

    private var payments = listOf<TimestampedAmount>();

    function amountOwed() returns Number -> {
        return forAmount - total(payments);
    };

    @api
    obligation[issuer] pay(amount: Number) before paymentDeadline | unpaid, default {
        require(amount > 0, "Amount must be strictly positive");
        require(amount <= amountOwed(), "Amount may not exceed amount owed");

        var p = TimestampedAmount(amount = amount, timestamp = now());
        payments = payments.with(p);

        if (amountOwed() == 0) {
            become paid;
        };
    } otherwise become default;

    @api
    permission[payee] chargeLatePaymentFee() | default {
        forAmount = forAmount + lateFee;
        paymentDeadline = now() + months(1);

        become unpaid;
    };

    @api
    permission[payee] forgive() | unpaid, default {
        become forgiven;
    };

    @api
    permission[issuer|payee] getAmountOwed() returns Number {
        return amountOwed();
    };
}

Our protocol definition is now complete!