Skip to content

Protocol

Introduction

Protocols are top-level language constructs and cannot be declared within another protocol.

Note that protocols in NPL are always passed by reference, and that protocol references are assigned unique identifiers automatically.

Protocol header

Constructor

The header of a protocol definition starts with the keyword protocol, and is immediately followed by the protocol parties.

The protocol name and its constructor parameters follow immediately after.

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

A protocol can be instantiated from within the language by invoking the constructor.

SimpleIou[issuer, payee](100);

Parties and constructor parameters can be passed as named arguments. Mixing named with unnamed parties, or mixing named with unnamed parameters is not supported.

SimpleIou[issuer = issuer, payee = payee](forAmount = 100);

or

SimpleIou[payee = payee, issuer = issuer](forAmount = 100);

Parties

Any signatory party referenced in a protocol must be part of the protocol header, and all signatory parties must have agreed to the creation of the protocol for it to become valid/instantiated.

More information on the concept of parties can be found here. More information on the Party type can be found here.

Observers

Observers are parties that have no explicitly assigned rights or obligations. Observers are parties that may observe and read the protocol, but cannot derive any other rights from their involvement as observers.

The compiler automatically creates a field observers for every protocol and initializes it as an empty map.

protocol[issuer, payee] AnotherSimpleIou(var forAmount: Number) {
    permission[*newObserver & issuer] addObserver(name: Text) {
        observers = observers.with(name, newObserver);
    };

    permission[issuer] removeObserver(name: Text) {
        observers = observers.without(name);
    };
};

Note that there is no default way to add or remove observers. This is left up to the programmer using actions.

A protocol can be instantiated with observers through the named argument observers.

SimpleIou[issuer, payee, observers = mapOf(Pair("Eavesdropper", eavesdropper))](100);

Parameters

The protocol header may specify a number of parameters that are required for its instantiation. In the example above, that is forAmount. Some of these parameters may also be fields.

Visibility

Protocol parties and observers are always private and cannot be read from outside the protocol instance. The visibility of constructor parameters as fields is dictated by the visibility modifiers that were chosen.

Protocol body

The body of the protocol may consist of an init block, state definitions, fields, and actions.

Init

Protocols may declare an initializer block, a block intended to run a series of side effects after all of a protocol's fields have been initialized. Its syntax is init, followed by a block containing the initialization steps. All steps that are allowed in protocol functions or actions are allowed in the protocol's init block.

protocol[issuer, payee] InitializingIou(var forAmount: Number) {
    init {
        // initializations
    };
};

The init block may be placed anywhere on the protocol body.

Protocol states

Protocols are modeled as state machines. Protocol states are transient by default.

All possible protocol states are declared at the start of the protocol body with the keyword state.

Exactly one state may be defined as initial state, whereas any number of states may be defined as intermediate or final state. At protocol instantiation, the protocol's state will be initialized to its initial state. It is not possible to transition out of final states, with one exception: the only legal transition out of a final state is into that same final state (which is of course not a meaningful state transition).

States may be viewed as enums on a protocol. Only one state is active at any particular point in time.

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

    // Protocol body
};

State guards

Protocol actions may be conditional upon states. This is done using the pipe symbol | following the optional return type, and followed by a comma-separated list of protocol states. The permission or obligation is only allowed in the listed state(s). If no state guard is specified, the permission or obligation is allowed in all states.

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

    permission[issuer] pay(amount: Number) | unpaid {
        // permission body
    };

    permission[issuer | payee] getAmountOwed() returns Number {
        // permission body
        return 1;
    };

    permission[payee] forgive() | unpaid, processing {
        // permission body
    };

    // Protocol body
};

State transitions

State transitions are explicitly specified in a permission, an obligation, or the otherwise block of an obligation. A protocol can transition to only one (new or same) state. A state transition is declared with the keyword become followed by the new state.

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

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

    // Protocol body
};

Reflection of states

Inspection of protocol states is possible with the built-in methods described in the derived type States section.

Protocol fields

Protocols may contain their own data in the form of protocol fields.

All protocol constructor parameters which contain the var qualifier are protocol fields. The protocol body may declare and initialize other protocol fields that do not get explicitly initialized by arguments at protocol instantiation. They might be initialized indirectly by protocol arguments by referring to protocol parameters in the initialization definition. These fields do not need the var qualifier.

protocol[issuer, payee] FieldsIou(var forAmount: Number) {
    var amountOwed: Number = forAmount;

    // Protocol body
};

The protocol constructor parameters which do not contain the var qualifier are not transformed into fields, and may only be used in the init block, protocol level require conditions, and while initializing other protocol fields:

protocol[p] NonField(x: Number) {
    // OK, using x in protocol `require`
    require(x > 0, "x must be positive");

    // OK, using x in a field initializer
    var y: Number = x;

    init {
        // OK, using x in init block
        debug(x);
    }
}

In the example below, x is not a field and cannot be used as such:

protocol[p] NonField(x: Number) {
    function foo() {
        // ERROR, x is not a field
        debug(x);
    }

    permission[p] bar() {
        // ERROR, x is not a field
        require(x > 0, "x must be positive");

        // ERROR, x is not a field
        debug(x);
    }
}

function non_field() {
    var nf = NonField['a'](10);

    // ERROR, x is not a field
    debug(nf.x);
}

Field visibility

The default scope of protocol parameters and fields is public, i.e. every party (and only these parties) on the protocol instance has read access to these fields. This default scope for protocol parameters and fields can be changed with the private keyword, which limits the visibility to local scope in the protocol.

protocol[party] AccessProtocol(var a: Number, private var b: Number) {
    var c: Number = 1;
    private var d = 2;

    permission[party] bar(e: Number) returns Number {
        var f = 3;
        return a + b + c + d + e + f;
    };

    permission[party] bar2() returns Number {
        return d;
    };

    permission[party] bar3(a: Number) returns Number {
        return this.a + this.c + a;
    };
};

In above example, a and c are accessible from within and outside of the protocol (by party), but at the same time shared across the permissions and obligations of the protocol.

b and d are only accessible from with the protocol, also by its permissions and obligations.

a, b, c, d or derived information can also be exposed to other other protocols through a permission or obligation with a return type (here: bar() and bar2()).

Protocol parameters can be shadowed by parameters, variables or struct member fields in permissions or obligations when they use the same name. These protocol parameters can be explicitly referred to by prefixing it with the this keyword. Permission bar3 has a parameter a which shadows the protocol parameter a. In the permission body one can still refer to the protocol parameter with this.a. Although protocol parameter c is not shadowed, once can still refer to it as this.c.

The permission parameters and fields e, f and a are only accessible within the scope of the permissions they are declared in. They are not shared within the protocol nor shared and accessible from outside. Their value can be exposed directly or indirectly by using it as (part of) the return value of the permission/obligation.

Protocol actions

Protocol permissions and obligations, collectively called actions, are represented by the permission and obligation keywords respectively. The difference between permissions and obligations is explained below.

Actions appear as methods within a protocol body, and always state which party or parties are allowed to invoke them. They furthermore have a name, list the input arguments, and denote an optional return type (prefixed with the returns keyword).

protocol[issuer, payee] ActionIou(var forAmount: Number) {
    var amountOwed: Number = forAmount;

    permission[issuer] pay(amount: Number) returns Number {
        // ...
        return 1;
    };

    // rest of protocol body
};

Party expressions

Action party access may be described as simple expressions. These operators may not be mixed, meaning | cannot be mixed with &.

Operator Explanation Example
| unilateral p1 | p2
& unanimous p1 & p2

Actions may be assigned to a single signatory party.

protocol[p1, p2] Example(var forAmount: Number) {
    permission[p1] exclusiveAction() {
        // accessible only to p1
    };
}

Actions may be assigned to one of several signatory parties by using the | (or) separator.

protocol[p1, p2] Exclusive(var forAmount: Number) {
    permission[p1 | p2] exclusiveAction() {
        // accessible to p1 or p2
    };
}

Actions may also be assigned to multiple signatory parties by using the & (and) separator.

protocol[p1, p2] Unanimous(var forAmount: Number) {
    permission[p1 & p2] unanimousAction() {
        // accessible only if p1 and p2 both invoke
    };
}

Note

Unanimous actions cannot be invoked directly via the API, as an API action invocation is associated with a single party. They can, however, be called from within NPL itself and thereby be indirectly reachable via API action invocations, e.g.:

@api
protocol[a, b] Baz() {
    permission[a & b] foo() {};

    @api
    permission[a] bar() {
        this.foo[a, b]();
    };
};

Actions may also include external parties, which are non-signatory parties. These are parties that are prefixed by *. The following permission is accessible to the signatory party p1 and the external party declared as p2.

protocol[p1] UnanimousExternal(var forAmount: Number) {
    permission[p1 & *p2] unanimousAction() {
        // accessible to p1 and an external party declared as 'p2'
    };
}

Typically external parties are only listed in the presence of at least one signatory party to prevent non-signatory parties from being able to unilaterally make protocol modifications. However, this is not required, and doing so is legal.

Caution is recommended when using external parties because they will have access to view or modify any protocol member. It will be the developer's responsibility to restrict access to private member data. If a permission of a member protocol reference is invoked, the normal process for evaluating claims will occur.

protocol[p1] Open(var forAmount: Number) {
    permission[*reader] read() returns Number {
        // accessible to an external party declared as 'reader'
        return forAmount;
    };
};

Note that it is not legal to use external parties for exclusive actions. Therefore, an action specification of the type permission[*reader | p1] read() { ... } is illegal. To have this behavior, specify two separate actions instead.

Action body

The action body defines a set of steps that are executed consecutively by the engine after a party invokes the action with valid arguments. If the action header is annotated with a return type, the executed action has to explicitly return a value of the specified return type.

Action bodies are atomic. This means that once invoked, all steps either complete successfully, or the action fails.

Some actions that can be performed in an action body include:

  • Instantiate a protocol. The parties of the protocol instance have to be a subset of the parties of the protocol to which the permission belongs. Syntax: protocol name followed by a list of instantiated parties between square brackets, followed by a list of parameters between round brackets
var otherProtocol = Contract['SomeParty']();
  • Invoke a permission or obligation of its protocol or another protocol Syntax: protocol instance variable (or keyword this), followed by a dot and the permission or obligation, followed by a list of named or unnamed parameters between round brackets.
  • Mixing named with unnamed parties, or mixing named with unnamed parameters is not supported.
otherProtocol.increment[employee]();
this.processNumber[manager](10);

or

otherProtocol.increment[holder = employee]();
this.processNumber[manager = manager](v = 10);
  • Modify a protocol field, party, variable, or struct member field Syntax: field/variable followed by = and an expression, using this to refer to the protocol itself
protocolFieldValue = 5.3;
this.protocolFieldValue = 5.3; // if protocol field amount is shadowed
if (equals("23", 12)) {
    become success;
} else {
    become failed;
};
  • Return a value. If the permission has a return type, the last action has to be a return action. It will return a value of the return type to the caller of the permission.
permission[employee] readAnnouncement() returns Text {
    return "Out of coffee!";
};
  • Any number of the above. An action body can perform multiple things. The individual statements or expressions are separated by a semi-colon, and are executed in sequence in a single transaction.
doThis();
doThat();

Shadowing and this

Protocol fields may be shadowed by permission/obligation arguments (or struct member fields in a copy() construct) . The protocol field can then still be referred to by prefixing it with this.

protocol[issuer, payee] Iou(var amount: Number, var deadLine: DateTime) {
    permission[issuer] pay(amount: Number) {
        this.amount = this.amount - amount;
    };

    // Protocol body
};

Temporal constraints

Temporal constraints may be imposed on any protocol action. Its related keywords are before, after, and between. Such constraints always pertain to values of type DateTime. These values may either be literals, or variables.

The before <time> constraint is a deadline constraint, stating that an action may only take place before some point in time. This constraint is mandatory for obligations.

permission[party] mayDoBefore() before 2025-01-25T12:00:00Z {
    // ...
};

The after <time> constraint is a constraint stating that an action may only take place after some point in time. Note that after is only legal for permissions: the presence of a deadline is a key characteristic of obligations.

permission[party] mayDoAfter() after 2025-01-25T12:00:00Z {
    // ...
};

The between <time-1> and <later-than-time-1> constraint states that an action may be invoked only between two points in time.

permission[party] mayDoBetween() between 2020-01-01T12:00Z and 2025-01-01T12:00Z {
    // ...
}

The before <time-1> after <later-than-time-1> constraint states that an action may be invoked only before some point in time, or after a later point in time. This therefore precludes the action from taking place during some time frame.

permission[party] mayDoBeforeAfter() before 2020-01-01T12:00Z after 2025-01-01T12:00Z {
    // ...
}

Note that if time 2 is not after time 1, this is simply an alternative syntax to between.

Instead of literals, variables may be used as well.

protocol[party] Deadline(var deadline: DateTime) {
    initial state open;
    final state fulfilled;
    final state missed;

    obligation[party] mustDoBefore() before deadline {
        become fulfilled;
    } otherwise become missed;
};

Obligations

Note

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

The syntax and semantics of obligations are largely the same to those of permissions, with some exceptions.

Obligations are specified with the keyword obligation. The mandatory presence of a deadline and predefined consequences in case of breaches is what distinguishes an obligation from a permission.

An obligation represents a predefined condition, predicated on reaching a certain point in time, for which parties have potentially agreed to elevate the resolution outside the scope of the defined contract. The deadline is specified with the keyword before, and it is the only time constraint applicable for obligations.

Obligations must have a predefined consequence in the form of an otherwise clause, and this consequence always represents a state transition (using the become keyword).

An obligation and its invoked state transition is only present given that the state upon which it is predicated is active. In the following example, the obligation only exists in state unpaid. The use of state guards is optional (it is conceivable that the deadline is simply shifted, but the obligation is eternal), but strongly recommended.

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

    obligation[issuer] pay() before payDeadline | unpaid {
        // Payment logic
        become paid;
    } otherwise become breached;

    // Protocol body
};

Read about how engine is handling obligations here.

Guards

Deprecated syntax (since 2024.1.1, to be removed in 2025.1.0)

The guard keyword has been deprecated: use require instead (see below)

Require conditions

Functions may be qualified using one or more require clauses that serve as preconditions.

Each require consists of a boolean expression and a message.

The boolean expression must evaluate to true for the condition to pass. If the expression were to evaluate to false, the check would fail and the specified message would be returned.

require(forAmount > 0, "Amount must be strictly positive");

Require statements provide a mechanism to specify prohibitions and must be placed at the start of a protocol or action block.

protocol[issuer, payee] IouWithRequirements(var forAmount: Number, var deadLine: DateTime) {
    require(forAmount > 0, "Amount must be strictly positive");

    permission[issuer] pay(amount: Number) {
        require(amount > 0, "Payments must be strictly positive");
        require(now().isBefore(deadLine, true), "Payment must come in before deadline");
        // permission body
    };
    // Protocol body
};

A function with require statements may be called any number of times anywhere in the body of a Protocol init block or a Protocol action.

Equality and inequality

Protocols are only considered equal if they refer to the same instance.

Given

protocol[p] P() {};

and

var p = P['007']();
var q = P['007']();
var r = p;

then

p == q // false, different protocol instances
p == r // true, same protocol instance

Generated derived types

The NPL compiler automatically generates certain derived types based on a protocol type declaration. Such derived types are unique to the defined protocol type: the generated derived types for two different protocol types are different and therefore incompatible with one another.

Derived types cannot be referenced in a use-clause directly, but are imported whenever the protocol is. A use iou.SimpleIou therefore automatically imports SimpleIou.PartyId and SimpleIou.States.

PartyId

The PartyId type is an enum uniquely derived from a protocol type that contains all the protocol's party identifiers.

For example, the protocol declaration

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

generates the derived enum PartyId

enum PartyId { issuer, payee };

which can be used within a protocol as the unqualified PartyId or inside and outside a protocol as the fully qualified SimpleIou.PartyId.

protocol[issuer, payee] PartyIdUsage(var forAmount: Number) {
    function getIssuerId() returns PartyIdUsage.PartyId -> {
        return PartyId.issuer;
    };
};

Because PartyId is uniquely derived, the type PartyId for one protocol type is not compatible with the PartyId of a different protocol type.

var p: SimpleIou.PartyId = OtherIou.PartyId.issuer // Error, different types.

States

The States type is an enum uniquely derived from a protocol type that contains all the protocol's state identifiers.

For example, the protocol declaration

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

    // Protocol body
};

will generate the derived enum States

enum States { unpaid, processing, paid, forgiven };

which can be used within a protocol as the unqualified States, or inside and outside a protocol as the fully qualified StatesIou.States.

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

    function messageToSend(st: StatesIouUsage.States) returns Text -> {
        match(st) {
            unpaid -> { return "Where's our money?"; }
            processing -> { return "Still waiting"; }
            paid -> { return "Thanks!"; }
            forgiven -> { return "Whatever, keep it"; }
        };
    };
};

Because States is uniquely derived, the type States for one protocol type is not compatible with the States of a different protocol type.

var p: SimpleIou.States = OtherIou.States.paid // Error, different types.

Inspection of protocol states is possible with the built-in protocol methods activeState(), initialState(), and finalStates(). You can also use the standard variants() method on the States enum type. Refer to method documentation below for usage and description details.

Methods

parties

<function> Returns the protocol instance's bound parties. PartyId is a auto-generated derived enum type that is unique to each protocol type.

Receiver

R

Type Arguments

R - the protocol instance on which this function is called (the receiver)

Returns

Map<R.PartyId, Party> - the protocol instance's party identifiers mapped to its bound parties

Usage
protocol[issuer, payee] PartiesUsage() {
    function findIdFor(party: Party) returns Optional<PartiesUsage.PartyId> -> {
        return parties().filter(function(id: PartyId, p: Party) -> p == party).keys().toList().firstOrNone();
    };
};

activeState

<function> Returns the protocol instance's currently active state if it exists. States is an auto-generated derived enum type that is unique to each protocol type.

Receiver

R

Type Arguments

R - the protocol instance on which this function is called (the receiver)

Returns

Optional<R.States> - the protocol instance's currently active state, or None if the protocol has no states

Usage
protocol[p] Foo() {
    initial state s;
    state t;

    function foo() -> {
        match(activeState().getOrFail()) {
            s -> doImportantStuff(true)
            t -> doImportantStuff(false)
        };
    };

    function doImportantStuff(b: Boolean) returns Unit -> { };
};

allStates (deprecated)

Deprecated: Use the R.States.variants() function.

<function> Returns the protocol instance's states. States is an auto-generated derived enum type that is unique to each protocol type.

Receiver

R

Type Arguments

R

Returns

Set<R.States> - a set of the protocol instance's states

Usage

protocol[p] Bar() {
    initial state a;
    state b;
    state c;
    final state d;
};
var bar = Bar['p']();
bar.allStates() == setOf(Bar.States.a, Bar.States.b, Bar.States.c, Bar.States.d)

initialState

<function> Returns the protocol instance's initial state if it exists. States is an auto-generated derived enum type that is unique to each protocol type.

Receiver

R

Type Arguments

R

Returns

Optional<R.States> - the protocol instance's initial state, or None if the protocol has no states

Usage

protocol[p] Bar() {
    initial state a;
    state b;
    state c;
    final state d;
};
protocol[p] Quux() {
    // No states defined
};
var bar = Bar['p']();
var quux = Quux['p']();
bar.initialState().getOrFail() == Bar.States.a
quux.initialState().isPresent() == false

finalStates

<function> Returns the protocol instance's final states. States is an auto-generated derived enum type that is unique to each protocol type.

Receiver

R

Type Arguments

R

Returns

Set<R.States> - a set of the protocol instance's final states

Usage

protocol[p] Bar() {
    initial state a;
    state b;
    state c;
    final state d;
};
var bar = Bar['p']();
bar.finalStates() == setOf(Bar.States.d)