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, usingthis
to refer to the protocol itself
protocolFieldValue = 5.3;
this.protocolFieldValue = 5.3; // if protocol field amount is shadowed
-
Transition its protocol state. See state transitions
-
Variable declaration and assignment. See Assignments.
-
Function calls, see Functions.
-
Conditional execution of built-in actions, see Control Flow
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
A function may be qualified using one or more require
conditions and may be placed anywhere inside its block body.
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 anywhere inside the block body of an action.
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)
allStates
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)