Declarative Permissions
By default, all contracts are fully public . This is semantically equivalent to using the @public
directive.
Polylang
controls access (read/write) to data in contracts through its permissions model. This is achieved via the following directives - @public
(default), @private
, @read
, @call
, and @delegate
,
as well as secondary methods such as checking authorization (using a public key, for instance).
Polylang
Access Control is best understood in terms of what the user is allowed to do (or not do). This leads to the following permission modes:
-
Allow all
-
Delegation
-
Using custom code (for more granular controls)
Each of these modes is described in detail in the following sections.
Allow all
This is the least constrained mode of access to contracts data. This mode allows anyone to read from and write to (i.e., call Polylang
functions which modify the record data) contracts in varying levels of granularity
using directives.
This is the default mode of operation.
The following are equivalent:
contract MyContract {
...
}
and
@public
contract MyContact {
...
}
@read
This directive allows anyone to read contract data, but not to write data (invoke functions). This directive can be applied to both contracts as well as to a specific field (which must be of type PublicKey
).
To signify read permission on the overall contract:
@read
@private
contract Person {
id: string;
name: string;
age: number;
constructor (id: string name: string, age: number) {
this.id = id;
this.name = name;
this.age = age;
}
setName(newName: string) {
this.name = newName;
}
setAge(newAge: number) {
this.age = newAge;
}
del () {
selfdestruct();
}
}
In this case, anyone can read Person
data, but no one (including the user who created the Person
contract) can invoke the setName
, setAge
, and del
functions (since the contract is marked @private
).
If we wish to enforce a stronger constraint that only the creator of the Person
contract be allowed to read the contract, we can modify the contract like so:
@private
contract Person {
id: string;
name: string;
age: number;
@read
creator: PublcKey;
constructor (id: string name: string, age: number) {
this.id = id;
this.creator = ctx.publicKey;
this.name = name;
this.age = age;
}
setName(newName: string) {
this.name = newName;
}
setAge(newAge: number) {
this.age = newAge;
}
del () {
selfdestruct();
}
}
Since the contract above is marked as @private
, no one other than the creator of the contract, i.e., the user whose public key matches the public key stored in the creator
field can read the contract data.
@call
This directive allows anyone to invoke functions on the contract record (if the contract is marked @private
), but not read data from the contract. This directive may be applied on contracts or on individual functions.
We will consider both cases using the same example.
Consider the following contract:
@call
@private
contract Person {
id: string;
name: string;
age: number;
// allows anyone to read this field
@read
creator: PublicKey;
constructor (id: string name: string, age: number) {
this.creator = ctx.publicKey;
this.id = id;
this.name = name;
this.age = age;
}
setName(newName: string) {
this.name = newName;
}
setAge(newAge: number) {
this.age = newAge;
}
// this function can only be called by the user whose public keys matches the one in the `creator` field.
@call(creator)
del () {
selfdestruct();
}
}
In the example above, no one can read Person
data, but anyone can invoke the functions setName
and setAge
, but not the del
function (since it has a @call
directive on it, which overrides the directive on the contract).
@private
This directive is the most restrictive of all, allowing no one (including the creator of the contract
) to read from or write to the contract
post creation. This directive can only be applied on contracts.
For example:
@private
contract Person {
id: string;
name: string;
age: number;
constructor (id: string name: string, age: number) {
this.id = id;
this.name = name;
this.age = age;
}
setName(newName: string) {
this.name = newName;
}
setAge(newAge: number) {
this.age = newAge;
}
del () {
selfdestruct();
}
}
Once the Person
contract record has been created, no one can read its data (or update it).
Delegation
Delegation involves delegating or offloading the responsibility of checking for read/write permissions onto a field which is either a PublicKey
, or an arbitrary long chain of fields of contract types which
ultimately ends in a field of type PublicKey
. Delegation makes use the @delegate
directive coupled with @read
and/or @call
.
Suppose we wish to read data from the Response
contract, we can specify that the read permisson check be handled by the form
field (a Form
contract), and Form
in turns delegates the
responsibility of checking read permissions to its creator
field (or type User
). Finally, the User
contract has a publicKey
field which handles the actual read permission checks.
So when a user tries to read from the Response
contract, the read will only succeed if the publicKey
field in the transitively associated contract, User
matches that or the user attempting the operation.
Likewise, the approve
function may only be called by a user if the delegation chain ends up confirming that the publicKey
field in User
is the same as that of the user attempting the function call operation.
Additionally, note that the someOtheFunction
in the Response
contract cannot be called by anyone.
contract Response {
// delegate read permission to the form field
@read
form: Form;
@call(form)
approve() {
...
}
someOtheFunction() {
...
}
...
}
contract Form {
// delegate read permission to the User field
@delegate
creator: User;
...
}
contract User {
// delegate read permission to publicKey
@delegate
publicKey: PublicKey;
...
}
Since the chain of delegates must end up on a field of type PublicKey
, the delegation model requires the usage of public keys. This means that the
user must be authenticated before attempting the operation. This is outside the purview of Polylang
.
Using Custom Code
This mode only works for write access, not for read access.
While delegation provides an elegant way of controlling permissions in Polylang
, for more complicated scenarios, checking permissions using custom code is often the most flexible approach. This can also turn out to
be the easiest to understand in many cases.
The usual way is provide tighter constraints via custom checks. For example:
contract Team {
id: string
members: string[]
publicKey: PublicKey;
// Anyone can call this function because the contract is public by default
addMember (id: string) {
// But you must be signing using the correct key
if (this.publicKey == ctx.publicKey) {
throw error('invalid user');
}
// And there must not be already more than 5 members
if (this.members.length > 5) {
throw error('too many members');
}
// Now we have checked rules, we can write the data
this.members.push(id);
}
}
In the example above, anyone can read the data since the contract
is public by default. However, for the addMember
function which modifies data, we wish to have tighter constrainsts. As such, we have two checks in
place - the first check ensures that the public key of the user invoking the function matches the public key stored in the contract during the creation of the record.
Secondly, we have a custom check that allows the operation to proceed only if we have enough members in the team. This second form of checks cannot be done using directives and/or delegates, and is domain-specific in nature.
The flexibility afforded by the Polylang
permissions model allows the user to specify constraints depending on their domain of interest.
Summary of Permission modes
The various basic modes of permissions provided by the use of directives is summarised in the following table. Note that there are several more ways in which these modes can be combined to obtain varying granularities of permissions.
Directive | Entity | Read | Write |
---|---|---|---|
None / @public | Contract | Anyone | Anyone |
@private | Contract | No one | No one |
@read | Contract | Anyone | No one |
@read | field | Only user with matching public key | No one |
@call | Contract | No one | Anyone |
@call | function | No one | Only user with matching public key |
@delegate + @read | function | Only user with a transitively matching public key | No one |
@delegate + @call | function | No one | Only user with a transitively matching public key |
Declarative Permissions and Proofs
During compilation, the Polylang
compiler evaluates the AST (Abstract Syntax Tree) and selectively generates Miden VM assembly instructions
based on the declarative permissions in the contract (such as the@call
directive) by checking the various rules mentioned in the sections above.
When the program is run by the Polylang
prover, the generated code is evaluated, run, and becomes part of the generated proof.