Skip to content

Commit

Permalink
[book] Witness (#67)
Browse files Browse the repository at this point in the history
  • Loading branch information
damirka authored May 24, 2024
1 parent 362bc11 commit d28a6a5
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 240 deletions.
2 changes: 1 addition & 1 deletion book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@
- [Dynamic Fields](./programmability/dynamic-fields.md)
- [Dynamic Object Fields](./programmability/dynamic-object-fields.md)
- [Dynamic Collections](./programmability/dynamic-collections.md)
- [Pattern: Witness]() <!-- ()./programmability/witness-pattern.md) -->
- [Pattern: Witness](./programmability/witness-pattern.md)
- [One Time Witness](./programmability/one-time-witness.md)
- [Publisher Authority](./programmability/publisher.md)
- [Display](./programmability/display.md) <!-- End Block: from Witness to Display -->
Expand Down
4 changes: 2 additions & 2 deletions book/src/programmability/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Advanced Programmability

In previous chapters we've covered [the basics of Move](./../move-basics/README.md) and
[Sui Storage Model](./../programmability/README.md). Now it's time to dive deeper into the advanced
In previous chapters we've covered [the basics of Move](./../move-basics) and
[Sui Storage Model](./../storage). Now it's time to dive deeper into the advanced
topics of Sui programmability.

This chapter introduces more complex concepts, practices and features of Move and Sui that are
Expand Down
9 changes: 1 addition & 8 deletions book/src/programmability/one-time-witness.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,7 @@ To check if a type is an OTW, `sui::types` module of the
to check if the type is an OTW.

```move
use sui::types;
const ENotOneTimeWitness: u64 = 1;
/// Takes an OTW as an argument, aborts if the type is not OTW.
public fun takes_witness<T: drop>(otw: T) {
assert!(types::is_one_time_witness(&otw), ENotOneTimeWitness);
}
{{#include ../../../packages/samples/sources/programmability/one-time-witness.move:usage}}
```

<!-- ## Background
Expand Down
298 changes: 69 additions & 229 deletions book/src/programmability/witness-pattern.md
Original file line number Diff line number Diff line change
@@ -1,277 +1,117 @@
# Pattern: Witness

Almost every combination of [abilities](../move-basics/abilities-introduction.md) forms a pattern in
Move. And one of the most important ones is the _Witness pattern_ - a type that with the
[`drop`](./../move-basics/drop-ability.md) ability. It is used to instantiate generic types with a
concrete type parameter by providing a _proof of ownership_ of this type.
Witness is a pattern of proving an existence by constructing a proof. In the context of programming,
witness is a way to prove a certain property of a system by providing a value that can only be
constructed if the property holds.

## Definition
## Witness in Move

A Witness is a type that has the `drop` ability. It is not very useful on its own, within a single
module, but rather serves as an initialization value for generic types. For example, if there is a
generic type `Custom<T>`, where `T` is a type parameter, a witness may be used to instantiate it,
while also ensuring that no other module can create a `Custom<T>` with the same `T`.
In the [Struct](./../move-basics/struct.md) section we have shown that a struct can only be
created - or _packed_ - by the module defining it. Hence, in Move, a module proves ownership of the
type by constructing it. This is one of the most important patterns in Move, and it is widely used
for generic type instantiation and authorization.

```move
{{#include ../../../packages/samples/sources/programmability/witness-pattern.move:definition}}
```

## Use Cases

- **Generic Types**: Witness is a powerful tool to instantiate generic types with a concrete type
parameter. It ensures that the type is created only by the module that owns the type.
- **Proof of Ownership**: Witness acts as a proof of ownership of a concrete type. It is used to
authorize certain actions on the type.
- **Authorization**: Witness can be used to authorize certain actions on the type.
- **Interface-like Behaviors**: Witness can be used to create abstract class-like behaviors in Move.

## Background

To better understand the Witness pattern, it is important to go back to the basics of
[generic types](./../move-basics/generics.md) in Move. Generic types are defined with a type
parameter, which can be any type with any constraints on its abilities. Previously we looked into
the [`Container<T>`](./../move-basics/generics.md#generic-syntax) example, where `T` was a type
_stored_ inside the container. Hence, there wasn't a question of how to initialize it, as the value
was passed to the constructor. However, in some cases, the type can be
[_phantom_](./../move-basics/generics.md#phantom-type-parameters) - it is not stored in the struct,
but used to differentiate between different instances of the same base type. And this is where the
Witness pattern finds its place.

> In this section we use a generic `Coin<T>` as an example of a generic type, however, it should not
> be mistaken for the actual [Coin](./balance-and-coin.md) type in the
> [Sui Framework](./sui-framework.md). We will go deeper into the `Coin` type in the
> [Balance & Coin](./balance-and-coin.md) section.
```move
/// Container always stores the `T`!
public struct Container<T> { inner: T }
/// The initialization is straightforward: the value is packed into the struct.
public fun new_container<T>(inner: T): Container<T> { Container { inner } }
```
Practically speaking, for the witness to be used, there has to be a function that expects a witness
as an argument. In the example below it is the `new` function that expects a witness of the `T` type
to create a `Instance<T>` instance.

Compare the example above with the following one:
> It is often the case that the witness struct is not stored, and for that the function may require
> the [Drop](./../move-basics/drop-ability.md) ability for the type.
```move
/// Coin does not store the `T`, and we need some way to
/// initialize it with a concrete type!
public struct Coin<phantom T> { value: u64 }
module book::witness {
/// A struct that requires a witness to be created.
public struct Instance<T> { t: T }
/// Something is missing here! Anyone can create a `Coin<T>` with any `T`.
public fun new_coin<T>(value: u64): Coin<T> { Coin { value } }
```

While the `new_container` function requires the `T` value to be passed, the `new_coin` function is
missing this part. And this implementation of the `new_coin` allows anyone to create a `Coin<T>`
with any `T`. And, naturally, this is not the behaviour we want.

Now, how can we fix this? We want to ensure that only a specific module can create a `Coin<T>` with
its `T`. Otherwise, the type signature of `Coin<T>` makes little sense. And similar to the
`Container<T>`, we need a way to present some `T` to the `new_coin` function. And given that this
`T` is not stored in the struct and not stored to be used later (important security property), we
would want to use a type that has the `drop` ability - a Witness.

An updated signature of the `new_coin` would then look like this:

```move
/// Requires a `T` with the `drop` ability as a proof of ownership of `T`.
/// However, the value is not used and discarded right away.
public fun new_coin<T: drop>(_witness: T, value: u64): Coin<T> {
Coin { value }
/// Create a new instance of `Instance<T>` with the provided T.
public fun new<T>(witness: T): Instance<T> {
Instance { t: witness }
}
}
```

And unlike the `Container<T>`, where the `T` can be any value, including an
[object](./../storage/key-ability.md) (hence, it can be supplied _dynamically_), the `T` in the
`new_coin` function has to be created and consumed _statically_ - by implementing a function in the
module that owns the `T`. So, if the type `MyCoin` is defined in the `my_coin` module, only this
module can create a new `Coin` with `T` being `MyCoin`.
The only way to construct an `Instance<T>` is to call the `new` function with an instance of the
type `T`. This is a basic example of the witness pattern in Move. A module providing a witness often
has a matching implementation, like the module `book::witness_source` below:

```move
module book::my_coin {
use book::simple_coin::{Self, Coin};
module book::witness_source {
use book::witness::{Self, Instance};
/// The Witness type for the Coin.
public struct MyCoin has drop {}
/// A struct used as a witness.
public struct W {}
/// The implementation of the `new_coin` function. Witness is not storable,
/// thus the only way to create a `Coin<MyCoin>` is to call this function in
/// this module.
public fun new_mycoin(value: u64): Coin<MyCoin> {
simple_coin::new_coin(MyCoin {}, value)
/// Create a new instance of `Instance<W>`.
public fun new_instance(): Instance<W> {
witness::new(W {})
}
}
```

## "Abstract Class"
The instance of the struct `W` is passed into the `new` function to create an `Instance<W>`, thereby
proving that the module `book::witness_source` owns the type `W`.

The module implementing a generic type may allow implementing certain functions on top of it. So
that some of the functions are publicly available on the generic type, while others can be
implemented by a specific module. This way, the generic type acts similarly to an _abstract class_
in object-oriented languages.
## Instantiating a Generic Type

The following example demonstrates a custom `RegulatedCoin` type with implementable functions. The
actual implementation of the `mint`, `burn`, and `transfer` functions is left to the application
module, while the `join` function is publicly available.
Witness allows generic types to be instantiated with a concrete type. This is useful for inheriting
associated behaviors from the type with an option to extend them, if the module provides the ability
to do so.

```move
/// A custom RegulatedCoin type with implementable functions.
public struct RegulatedCoin<phantom T> has key {
// File: sui-framework/sources/balance.move
/// A Supply of T. Used for minting and burning.
public struct Supply<phantom T> has key, store {
id: UID,
value: u64
}
/// Protected function - requires a Witness.
/// Mints a new `RegulatedCoin` with the value.
public fun mint<T: drop>(_: T, value: u64, ctx: &mut TxContext): RegulatedCoin<T> {
RegulatedCoin { id: object::new(ctx), value }
}
/// Protected function - requires a Witness.
/// Burns the `RegulatedCoin` and returns the value.
public fun burn<T: drop>(_: T, coin: RegulatedCoin<T>): u64 {
let RegulatedCoin { id, value } = coin;
id.delete();
value
}
/// Protected function - requires a Witness.
public fun transfer<T: drop>(_: T, coin: RegulatedCoin<T>, to: address) {
transfer::transfer(coin, to)
/// Create a new supply for type T with the provided witness.
public fun create_supply<T: drop>(_w: T): Supply<T> {
Supply { value: 0 }
}
/// Public API - does not require a Witness.
public fun join<T>(coin: &mut RegulatedCoin<T>, other: RegulatedCoin<T>) {
let RegulatedCoin { id, value } = other;
coin.value = coin.value + value;
id.delete();
/// Get the `Supply` value.
public fun supply_value<T>(supply: &Supply<T>): u64 {
supply.value
}
```

In the example above, the `mint`, `burn`, and `transfer` functions require a Witness of the `T` type
to be called. This way, the application module can implement these functions and ensure that only
the module that owns the `T` can call them. The `join` function, on the other hand, is publicly
available and does not require a Witness.
In the example above, which is borrowed from the `balance` module of the
[Sui Framework](./sui-framework.md), the `Supply` a generic struct that can be constructed only by
supplying a witness of the type `T`. The witness is taken by value and _discarded_ - hence the `T`
must have the [drop](./../move-basics/drop-ability.md) ability.

This pattern allows sharing base type and details of the implementation between different
applications, while still allowing the applications to implement their own logic on top of the base
type.

## Storable Witnesses + Metadata

It is possible to extend the Witness type by adding the [`store`](./../storage/store-ability.md)
ability, which would allow the type to be stored in objects. We call this variation of the Witness a
_Transferable Witness_. However, while this method can be useful in some applications by providing
the caller a way to perform a _delayed_ authorization or submit a combination of different
witnesses; it is not recommended to use it in open or sensitive systems, as it may be a security
risk. The callee function may end up storing the Witness in an object and using it later, which may
lead to unexpected behaviour.

With that in mind, the _Transferable Witness_ still has some benefits, such as the ability to reuse
a "metadata" or "configuration" struct and pass it as a Witness. This way, the application can avoid
unnecessary linking of the metadata with an extra type, and the metadata can _act like a Witness_.
To illustrate this, let's consider a generic ticketing application: the `Ticket` is a generic type
which stores metadata, and the application can implement some protected functions for it, such as
`transfer`.

> In the implementation below we omit certain features (like getter functions and the ability to
> consume a Ticket) for the sake of clarity and simplicity. The full example is available in the
> [Move Book](https://github.com/MystenLabs/move-book/tree/main/packages/samples/examples)
> repository.
The instantiated `Supply<T>` can then be used to mint new `Balance<T>`'s, where `T` is the type of
the supply.

```move
/// A Ticket with the metadata of type `T`. Very similar to `Container<T>`.
/// Note that the `Ticket` only has `key`, hence it's not publicly transferable!
public struct Ticket<T: store + drop> has key {
id: UID,
used: bool,
metadata: T
}
/// Create a new Ticket with the metadata and the context.
/// No need for a Witness here!
public fun new<T: store + drop>(metadata: T, ctx: &mut TxContext): Ticket<T> {
Ticket {
id: object::new(ctx),
used: false,
metadata
}
}
/// Transfer the Ticket to another user. Requires a Witness of the metadata type.
/// May or may not be implemented by the application.
public fun transfer<T: store + drop>(_metadata: T, ticket: Ticket<T>, to: address) {
transfer::transfer(ticket, to)
// File: sui-framework/sources/balance.move
/// Storable balance.
struct Balance<phantom T> has store {
value: u64
}
```

In the example above, the instantiation of the `Ticket` in the `new` function is straightforward, as
the metadata is passed as an argument. However, the `transfer` function, which can be implemented by
the application, requires some kind of authorization. And the only authorization method we have here
is the metadata itself. So, the application can pass an empty metadata struct as a Witness to the
`transfer` function, hence proving that it has the right to transfer the ticket of this type.

```move
module book::concert {
use book::ticket::{Self, Ticket};
/// The metadata for the concert ticket.
public struct Metadata has store, drop {
name: String,
date: String,
event_id: u64
}
/// Purchase a ticket for the concert. (omits the actual purchase logic)
public fun purchase(/* take a Coin */ ctx: &mut TxContext) {
let ticket = ticket::new(Metadata {
name: b"Mysties".to_string(),
date: b"2023-05-03".to_string(),
event_id: 1
});
// Transfer the ticket to the user.
ticket::transfer(empty(), ticket, ctx.sender())
}
/// Create an empty Metadata struct to act as a Witness.
fun empty(): Metadata {
Metadata { name: b"".to_string(), date: b"".to_string(), event_id: 0 }
}
/// Increase supply by `value` and create a new `Balance<T>` with this value.
public fun increase_supply<T>(self: &mut Supply<T>, value: u64): Balance<T> {
assert!(value < (18446744073709551615u64 - self.value), EOverflow);
self.value = self.value + value;
Balance { value }
}
```

## Limitations

- Witnesses are a powerful tool, but they have their limitations. The most important one is that
they do not guarantee uniqueness. The module owning the `T` can create multiple witnesses of the
same type, and if the library / module functionality assumes that the witness is unique and
instantiated only once, it may lead to unexpected behaviour. To address this issue, the
[One Time Witness](./one-time-witness.md) pattern is introduced, which ensures that the witness is
created only once.
## One Time Witness

- In many cases Witness requires a function to be implemented in the instantiating module. This can
be seen as a limitation, as it requires additional boilerplate code to be written. However, it is
not the case for [One Time Witness](./one-time-witness.md), and in the cases where a regular
Witness can be used, the Witness can be created in the [module `init`](./module-initializer.md)
function.
While a struct can be created any number of times, there are cases where a struct should be
guaranteed to be created only once. For this purpose, Sui provides the "One-Time Witness" - a
special witness that can only be used once. We explain it in more detail in the
[next section](./one-time-witness.md).

## Summary

- Witness is a type with the `drop` ability, used to instantiate generic types with a concrete type
parameter.
- It acts as a proof of ownership of a type and may be used for type-based authorization.
- Witness can be also used to create abstract class-like behaviors in Move.
- Transferable Witness is a variation of the Witness that has the `store` ability, allowing it to be
stored in objects. It may be useful in certain applications, but not encouraged.
- Metadata types can be used as _Witnesses_. This can be useful in consumer-facing applications,
such as ticketing systems, or NFTs.
- Witnesses do not guarantee uniqueness, and the [One Time Witness](./one-time-witness.md) pattern
is introduced to address this issue.
- Witness is a pattern of proving a certain property by constructing a proof.
- In Move, a module proves ownership of a type by constructing it.
- Witness is often used for generic type instantiation and authorization.

## Next Steps

In the next section we will explain the [One Time Witness](./one-time-witness.md) (OTW) pattern,
which guarantees that a certain witness was created only once. The OTW pattern opens the door to
more complex topics, such as [Publisher](./publisher.md) and
[Balance & Coin](./balance-and-coin.md).
In the next section, we will learn about the [One Time Witness](./one-time-witness.md) pattern.
13 changes: 13 additions & 0 deletions packages/samples/sources/programmability/one-time-witness.move
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,16 @@ module book::one_time {
}
}
// ANCHOR_END: definition

module book::one_time_usage {
// ANCHOR: usage
use sui::types;

const ENotOneTimeWitness: u64 = 1;

/// Takes an OTW as an argument, aborts if the type is not OTW.
public fun takes_witness<T: drop>(otw: T) {
assert!(types::is_one_time_witness(&otw), ENotOneTimeWitness);
}
// ANCHOR_END: usage
}

0 comments on commit d28a6a5

Please sign in to comment.