Resources: A Safe Language Abstraction for Money

04/10/2020 ∙ by Sam Blackshear, et al. ∙ Facebook Stanford University 0

Smart contracts are programs that implement potentially sophisticated transactions on modern blockchain platforms. In the rapidly evolving blockchain environment, smart contract programming languages must allow users to write expressive programs that manage and transfer assets, yet provide strong protection against sophisticated attacks. Addressing this need, we present flexible and reliable abstractions for programming with digital currency in the Move language [Blackshear et al. 2019]. Move uses novel linear [Girard 1987] resource types with semantics drawing on C++11 [Stroustrup 2013] and Rust [Matsakis and Klock 2014]: when a resource value is assigned to a new memory location, the location previously holding it must be invalidated. In addition, a resource type can only be created or destroyed by procedures inside its declaring module. We present an executable bytecode language with resources and prove that it enjoys resource safety, a conservation property for program values that is analogous to conservation of mass in the physical world.

READ FULL TEXT VIEW PDF
POST COMMENT

Comments

There are no comments yet.

Authors

page 1

page 2

page 3

page 4

This week in AI

Get the week's most popular data science and artificial intelligence research sent straight to your inbox every Saturday.

1. Introduction

The emergence of Bitcoin (nakamoto) and Ethereum (ethereum) has created significant interest in the computational model of a replicated state machine synchronized by a distributed consensus protocol. In this programming model, a command is executed as an atomic and deterministic transaction that is replicated consistently across all nodes participating in consensus. While cryptocurrency and decentralized finance are the most prominent applications of programmable blockchains, there are other important use-cases such as tracking supply chains (HBR-global-supply-chain) and clearing global markets (JPMcoin).

Transactions are programmed as smart contracts, a catchy name (szabo_smart_contracts) for program units installed for atomic execution on the blockchain. If the contract language is sufficently expressive, then smart contracts are attractive implementaions for a wide variety of conventional functions such as bank deposit and withdrawal, cross-border funds transfer, point-of-sale online payment, escrow agreements, futures contracts, and derivatives. To meet these goals, a smart-contract programming language must allow users to write programs that manage and transfer assets while providing extremely trustworthy protection against sophisticated attacks.

In this paper, we describe and analyze flexible and reliable abstractions for programming with digital currency and other assets in the Move language (move_white). Move uses novel linear (linear_logic) resource types that draw on experience with C++11 (c++) and Rust (rust) to preserve integrity and prevent copying of assets. When combined with other abstraction features of Move, linearity ensures resource conservation. Whereas data abstraction ensures that a resource may only be created and destroyed by the defining module, linearity further prevents duplication and unintended loss. We present an executable Move bytecode language with move semantics and show that it satisfies a set of resource safety guarantees.

Contributions

This paper adds rigor to the informal description of Move (move_white). Its key contributions are:

  • We introduce resources, an intuitive abstraction for currency-like values, and demonstrate their utility compared to existing language constructs (Section 2).

  • We explain the key features of the Move bytecode language and explain how their design supports support resource-oriented programming (Section 3)

  • We formalize the semantics of the Move bytecode interpreter for the subset of Move analyzed in this paper (Section 4).

  • We formally define resource safety properties and prove that execution of Move bytecode programs is resource-safe (Section 5).

  • We describe our implementation of the Move virtual machine, its integration in the Libra blockchain (libra_blockchain_white), and the adoption of Move in other contexts (Section 6).

2. Programming With Money

Move is designed to support a rich variety of economic and financial activities by supporting fundamental conservation properties, not only for built-in currencies, but also for programmer-defined assets. We believe this is essential. To begin with, smart contracts provide customizable logic for sending, receiving, storing, and apportioning digital funds that cannot be arbitrarily created, lost, or destroyed. Further, the internal balance in a bank account, the monetary value inherent in a contract for future payment, or an escrow contract all represent assets that must be conserved in the same ways as conventional currency. Thus, smart contracts must be able to implement new assets with expected conservation properties and appropriately control the exchange of one asset for another.

2.1. Savings Bank Example

With this goal in mind, we use a simple bank account contract to illustrate the key features of Move for programming with assets and demonstrate by example the advantages of Move over two alternative contract programming languages where notable problems have occurred in practice. Figure 1 implements a savings bank with the following requirements:

  • A customer should be able to deposit money worth via the deposit procedure and subsequently extract money worth via the withdraw procedure.

  • No customer should be able to withdraw money deposited by another customer.

Even in this simplest of examples, there are already two assets: the funds deposited into the bank contract, and the bank credit that the customer can use to withdraw the funds in the future. Most smart contract platforms have a native asset such as Ether in Ethereum (ethereum) that is implemented as part of the core platform and guarantees conservation. But even if the deposited funds are represented using the native asset, the bank contract must correctly implement deposit and withdraw to ensure conservation for the bank credit asset. Programming mistakes in this setting can be extremely costly;high-profile bugs in Ethereum, e.g., (re_dao; parity_hack; eth_vulns), have resulted in the theft of digital assets worth tens of millions of dollars. To summarize, programming challenges in this environment include:

  1. Conservation. Transfers must preserve the total supply of money in the system, including custom assets defined by contracts.

  2. Unique atomic transfer. The sender of an asset must relinquish all control of the asset. This ownership transfer should be atomic because any non-atomic exchange risks leaving one or both parties empty-handed.

  3. Authority. Smart contract programmers must represent authority carefully and restrict access to privileged operations. Contracts are deployed on a public platform open to both benign customers and bad actors.

Move represents money using user-defined linear resource types. Move has ordinary types like integers and addresses that can be copied, but resources can only be moved. Linearity prevents “double spending” by moving a resource twice (e.g., into two different callees) and forces a well-typed procedure to move all of its resources, avoiding accidental loss.

Figure 1 provides a Move representation of the simple bank along with an implementation in Solidity (solidity) and Scilla (scilla). Solidity is a source language for Ethereum (ethereum) and the first to provide an expressive smart contract programming model. Scilla is a newer language designed by programming language researchers to simplify formal verification of contracts and incorporate lessons learned from Solidity design flaws. Although many other contract languages have been proposed (see Section 7), these two represent the state of practice (Solidity) and the state of the art (Scilla).

Solidity, Scilla and other account-based languages often use a model in which each contract has an implicit balance in the platform’s native currency. This balance can only be modified by special instructions. However, the properties ensured by these special instructions are not available to programmers that wish to implement custom currencies such as bank credits. A common strategy used instead is illustrated in Figure 1: a map, credit, is employed to map creditor identities to integers. The integers in the range of the map represent money and must be manipulated carefully to provide the global conservation invariants associated with monetary assets. However, as we will see by examining the code samples, properties guaranteed by construction in Move are more difficult to ensure via ad hoc programming in other languages. Although the bank is a somewhat artificial example, it is adapted from similar examples in the Solidity/Scilla documentation and concisely captures the key idioms of typical contracts: sending/receiving/atomically exchanging money and implementing a new money-like construct.

contract Bank
mapping (address => uint) credit;
function deposit() payable {
  amt =
    credit[msg.sender] + msg.value
  credit[msg.sender] = amt
}
function withdraw() {
  uint amt = credit[msg.sender];
  msg.sender.transfer(amt);
  credit[msg.sender] = 0;
}
contract Bank
field credit: Map Address Uint;
transition deposit()
  accept;
  match credit[_sender] with
  Some(amt) =>
    credit[_sender] :=
      amt + _amount
  None =>
    credit[_sender] := _amount
  end
end
transition withdraw()
  match credit[_sender] with
  Some(amt) =>
    msg = {
      _recipient: _sender;
      _amount: amt
    };
    credit[_sender] := 0;
    send msg
  None => ()
  end
end
module Bank
use 0x0::Coin;
resource T { balance: Coin::T }
resource Credit { amt: u64, bank: address }
fun deposit(
  coin: Coin::T,
  bank: address
): Credit {
  let amt = Coin::value(&coin);
  let t = borrow_global<T>(copy bank);
  Coin::deposit(&mut t.balance, move coin);
  return Credit {
    amt: move amt, bank: move bank
  };
}
fun withdraw(credit: Credit): Coin::T {
  Credit { amt, bank } = move credit;
  let t = borrow_global<T>(move bank);
  return Coin::withdraw(
    &mut t.balance, move amt
  );
}
Figure 1. A simple bank contract in Solidity (left), Scilla (middle), and Move (right). Each code snippet must implement bidirectional exchanges of the language’s native currency for a bank credit currency defined by the contract. In Solidity and Scilla, both native and custom currencies are represented indirectly via maps of identities to integers, whereas in Move, currency is represented directly with resources.

Solidity and Scilla deposit

The first task of the deposit procedure in Figure 1 is to accept the language’s native currency. In Solidity, native currency sent by a caller is implicitly deposited into the contract’s balance before the callee code is executed, provided the receiving function is marked as payable. If not, an attempted deposit causes a runtime failure that reverts all changes performed by the current transaction.

In Scilla, money is transferred from caller to callee via an explicit accept construct which avoids runtime failures but introduces other problems. Although money not accepted by the callee will be silently returned, bugs may occur if the programmer forgets to accept funds. For example, accepting on one control-flow path but not another (e.g., only in the None branch) would allow the caller to steal funds deposited by another user by subsequently invoking Withdraw.

The second task of deposit is to update the caller’s bank credits by the transferred amount. In both languages, the amount sent by the caller is available through special integer-typed expressions: msg.value in Solidity and _amount in Scilla. The identity of the caller is represented by msg.sender or _sender, respectively. The programmer must be careful to increment the caller’s credit balance by the transferred quantity exactly once. Forgetting to update the balance is stealing funds from the caller, whereas updating more than once allows the caller to steal funds from other customers. There are no special checks on integer expressions to prevent either programmer error from violating conservation of funds.

Solidity and Scilla withdrawal

The withdraw procedure exchanges bank credits for native currency. Although this is logically the inverse of deposit, the implementation looks quite different. This is because Solidity and Scilla do not have language support for returning native or custom currency to the calling procedure. Instead, the code uses language primitives for sending currency to the address that stores the contract whose procedure invoked withdraw.

In Solidity, the relevant primitive is msg.sender.transfer. Subtly, this is a virtual call that invokes a user-defined procedure known as a fallback function in the callee. The decision to make every payment of native currency a virtual call has led to infamous re-entrancy vulnerabilities such as the DAO (re_dao) attack that led to theft of digital assets worth over $60 million. The key issues are that (a) the update to the credit map via credit[msg.sender] =  and the sending of funds via transfer are not atomic, and (b) the map update occurs after the virtual transfer call. If the virtual call invokes a user-defined function that calls back into withdraw, the caller can steal funds deposited by a different customer.

Scilla improves on Solidity by defining a more restricted message-passing primitive for sending money to addresses. The _amount: amt code snippet implicitly withdraws amt units of money from the contract’s available balance. Then, the _sender’s balance in the credit map is zeroed out before using the send primitive to transfer the money to its recipient. Scilla’s type system forces any global side effect like a message send to occur at the end of the procedure; for our example, it would not allow the update to credit to occur after the send. In addition, Scilla does not have virtual calls. These restrictions prevent re-entrancy issues.

However, the Scilla design introduces a new kind of issue: using emit msg instead of send msg in the example would cause the money in the message to be destroyed. The emit construct emits the message as a client-facing event rather than sending it to an address. This mistake permanently reduces the supply of money in the system. Scilla programmers have encountered this problem in practice ((scilla), Section 5.2), though Scilla has an auxiliary “cashflow” static analyzer for detecting problems like this.

Move Bank

The Move implementations of the deposit and withdraw procedures are symmetric. The deposit procedure says that it requires payment by declaring a parameter of type Coin::T and that it intends to credit the caller by declaring a return value of type Bank::Credit. The withdraw procedure does the inverse. Coin::T represents native currency; it is a resource type defined in a separate Coin module that we describe in Section 6.2. Since both Coin::T and Bank::Credit are resources, the type system will reject any implementation that fails to consume the input resource or return ownership of the output resource. Both resources can leverage the same language feature (move semantics) for atomic ownership transfer into and out of the procedure.

The deposit code consumes its input resource by acquiring a reference to a Bank::T value published in global storage and moving the coin resource into the bank’s balance via the call to Coin::deposit. It then packs (constructs) a Credit resource and returns it to the caller.

The withdraw code consumes its input Credit resource by unpacking it. Unpacking destroys a resource and returns its contents. Only the Bank module can pack, unpack, and acquire references to the fields of the Credit resource; code outside the module can only access Credit through the procedures exposed by Bank. Finally, withdraw extracts native currency from the bank’s balance via Coin::withdraw and returns it to the caller.

Resources as Capabilities

We conclude our discussion of the Move bank by noting that the advantage of an explicit type for money goes beyond safety: resources enable flexible programming patterns that would not be possible with an implicit representation of money. For example: say that Alice is a customer of the Bank and wants to give another user Bob permission to withdraw the funds she has deposited. Alice can simply transfer ownership of her Bank::Credit to Bob, who can use it to invoke withdraw at his leisure—no change to the Bank code is required. Bob could also choose to store his Bank::Credit in another resource that (e.g.) allows multiple parties to access it or prevents it from being redeemed until a certain time.

By contrast, the Solidity and Scilla implementations of the Bank cannot support this feature without modifying the original contract to support it. In essence, the credit map approach implements an access control list for withdrawing native currency, whereas the resource approach implements a linear capability for withdrawals (Hardy94theconfused; capability-myths; DBLP:journals/pacmpl/Swasey0D17). Capability-based programming enables some powerful design patterns, as we will see in Section 6.2.

3. Move Overview

This section provides an informal overview of the key concepts and design decisions of the Move language that support safe and expressive programming with resources.

3.1. Executable Bytecode With Resources

The Move execution platform relies on a compiler to transform source language programs into programs in the Move bytecode language. For example, Figure 1 contains Move source code that compiles to an executable bytecode representation (see Figure 2 for an example). Bytecode – not source code – is stored and executed on the Libra blockchain.

Because Move programs are deployed in the open alongside other (potentially untrusted) Move programs, it is important for key properties like resource safety to hold for all Move bytecode programs. If the safety guarantees were only enforced by the source language compiler, an adversary could subvert them by writing malicious bytecode directly and entering it into the execution environment without using a compiler. Thus, we focus on the design and semantic properties of the Move bytecode language here, although we write illustrative examples in the source language for readability.

The Move execution platform relies on a load-time bytecode verifier, in a manner similar to the Java Virtual Machine (jvm) and Common Language Runtime (clr). The bytecode verifier enforces type, memory, and resource safety. Because the goal of the present paper is to explain and formalize properties of Move that provide key advantages over prior smart contract languages (i.e. resource values with ironclad safety guarantees), we focus on a concrete semantics for Move with dynamic checks for type, resource, and memory safety and leave formalization of the bytecode verifier to future work. Our formalization and resource safety theorem (Theorem 5.10) therefore do not depend on any of the invariants ensured by the bytecode verifier; the presence of the verifier just allows an optimized implementation to skip these checks. The analyses performed by the bytecode verifier are sufficiently interesting and complex to fill a paper of their own (particularly reference safety, which has similarities to the Rust borrow checker; see Section 7).

Persistent Global State

Bytecode Source code move credit Credit \{amt, bank\}=... borrow_global<T>(...) &mut t.balance Coin::withdraw(...) return
Figure 2. Bytecode for the withdraw procedure from Figure 1 (left) and an example global state containing resources from Figure 1 (right). The global state contains three account addresses with different combinations of resources. Address 0x1 has a Coin resource with value 5 and a Credit with value 10 that can be redeemed at the Bank resource owned by 0x2. Address 0x3 also has a Bank resource, but it holds a Coin with value 0.

Move execution occurs in the context of a persistent global state organized as a partial map from account addresses to resource data values. Each address can store an arbitrary number of resources, but at most one of any given type at the top level. For example, the account address 0x in Figure 2 holds two Coin::T resources, but one is at the top level and one is stored inside a Bank::T resource.

In addition, an address can store zero or more code modules. The global state is updated via transactions that contain a sender account address and a transaction script consisting of a single main procedure. Transaction scripts update the global state by invoking procedures of published modules that mutate stored resources, add new resources to an address, or remove existing resources from an address. A transaction has all-or-nothing semantics; either the entire script is executed without errors or it aborts and reverts all changes to the global state.

Procedure Calls

Operand stack

Locals

Locals

Global resources

 

Figure 3. Execution mechanics of the Move bytecode interpreter. The global state holds resources that can be moved onto the operand stack or borrowed by pushing a reference onto the stack. Resources can be published to the global state by moving them from the stack into an account address. Each call stack frame (blue) has its own local variables to store values popped off the stack. Formal parameters and return values are passed between caller and callee using the shared operand stack.
local variable instructions
reference instructions
record instructions
global state instructions
stack instructions
procedure instructions
Figure 4. List of Move instructions. The local variable instructions move or copy values between local variables and the operand stack. Reference instructions operate on reference values stored on the operand stack. The global state instructions move values between the operand stack and persistent global storage. Stack instructions manage the operand stack by popping unused values, pushing constants, and performing arithmetic/bitwise operations via . Finally, the procedure instructions create and destroy call stack frames.

Execution of a Move program begins by executing the distinguished main procedure of the transaction script and proceeds via the evaluation mechanics shown in Figure 3. A procedure is defined by a type signature and an executable body comprising a linear sequence of Move bytecode commands. Procedure calls are implemented using a standard call stack containing frames with a procedure name, a set of local variables, and a return address. When one procedure calls another, the calling procedure pushes its callee’s arguments onto the operand stack and invokes the bytecode command, which pops the arguments off the stack and stores them in the actuals of the callee (which become a subset of the callee’s local variables). Before returning, the callee pushes its return values on the stack and invokes the bytecode command, which pops the current stack frame and returns control to the return address.

Modules

A Move module such as our Bank from Figure 1 can declare both record types and procedures. Records can store primitive data values (including booleans, unsigned integers, and account addresses) as well as other record values, but not references. Each record is nominally declared as a resource or non-resource. Non-resource records cannot store resource records, and only resources can be stored in the global state.

Modules support strong encapsulation for their declared types. Consider the bytecode translation of the withdraw procedure from our running example shown in Figure 2. The struct definitions and field definitions used by the bytecode instructions are implemented as integer indexes into internal tables of the current module. This design ensures that privileged operations on the module’s declared types can only be performed by procedures in the module, encapsulating creation via , destruction via , accessing fields via , publishing via , removing via , and accessing (either to read or write) via . For example: the withdraw bytecode is able to access a field of its declared T type, but it would not be able to access a field of the Coin::T type except via the API exposed by the Coin module.

A module may import a type or procedure declared in another module using its storing address as a namespace. For example, the use 0x0::Coin line from our running example indicates that the current module should link against the module named Coin stored at account address 0x0. The combination of encapsulation and resource safety enables modules to safely interoperate while maintaining strong internal invariants.

References

Move supports references to records and primitive values (but not to other references). In a manner similar to Rust, references are either exclusive/mutable (written &mut) or read-only (written &). All reads and writes of record fields occur through a reference.

References are different from other Move values because they are transient: as explained above, persistent global state consists of resource records, which cannot have fields of reference type. This means that each reference must be created during the execution of a transaction script and released before the end of that transaction script. Thus, each individual record value is a tree, and the global state is a forest whose roots are account addresses.

3.2. Language Design for Resource Safety

resource R {} fun copy_resource_bad(r: R) {   let x = copy r; // no; copies r } fun deref_resource_bad(ref: &R): R {   return *ref; // no; copies target of ref } resource R {} fun double_move_bad(r: R): R {   let x = move r;   let y = move r; // no; r already moved   let z = move_from<R>(0x1);   return move_from<R>(0x1); // no; 0x1.R moved }
resource R {} fun destroy_via_assign_bad(r1: R, r2: R) {   let loc = move r1;   loc = move r2; // no; destroys old value of loc } fun destroy_via_write_bad(ref: &mut R, r: R) {   *ref = move r; // no; destroys target of ref } fun unused_resource_local_bad(r: R) {   let local = move r;   return; // no; would destroy resource in local } resource R {} fun double_move_to_bad(r1: R, r2: R)) {    move_to<R>(0x1, move r1);    move_to<R>(0x1, move r2); // no; overwrites r1 }
Figure 5. Top: Examples of bad Move code that must be rejected due to resource duplication. Bottom: examples of bad Move code that must be rejected due to destruction of resources. The programs on the left would all be accepted if type R was declared as a struct instead of a resource, or if R was replaced with a primitive type like u64.

At the beginning and end of a transaction script, all of the resources in the system reside in the global state . Resource safety is a conservation property that relates the set of resources present in state before the script to the set of resources present in state after the script. In general terms, we would like the language to guarantee that:

  1. A resource M::T that is present in post-state was also present in pre-state unless it is introduced by a inside M during script execution

  2. A resource M::T that was present in pre-state is also present in post-state unless it is eliminated by an inside M during script execution

It is helpful to look at each of the instructions in Figure 4 and consider what precautions must be taken in order to ensure that properties (1) and (2) hold. For property (1), we must be careful not to introduce instructions that can duplicate a resource value. Move achieves this by providing both and instructions for transferring a value from a local variable to the operand stack. As the copy_resource_bad function in Figure 5 demonstrates, the instruction cannot be applied to a resource value. The , , and instructions for transferring values prevent double moves that would allow a programmer to “spend” the same resource multiple times (see double_move_bad).

References must also be managed carefully to avoid duplication. The for dereferencing a reference value can only be applied to a non-resource reference. Allowing a dereference of a resource like deref_resource_bad in Figure 5 would copy the resource value behind the reference.

Property (2) is further challenging because conventional languages provide a number of ways to indiscriminately discard values. At the instruction level, restrictions must be placed on , , , and . Most obviously, popping a resource off the operand stack with must be disallowed. If a local variable is of type resource, can only be applied when the variable is uninitialized. Code like destroy_via_assign_bad in Figure 5 would violate property (2) by discarding the old value stored in the local. Similarly, a like *ref = move r in destroy_via_write_bad must not execute if ref points to a resource. This destructive update would destroy the value previously pointed to by ref. Finally, the instruction for moving a resource into global storage aborts if the move would overwrite an existing resource at the given address. For example, double_move_to_bad would fail at runtime because the memory 0x1.R is already occupied.

Instruction-level protections are not quite enough to ensure property (2). There are two remaining holes that could allow resource destruction: values left in local variables when a procedure returns (e.g., unused_resource_local_bad in Figure 5), and values left on the operand stack at the end of script execution. Move prevents both with extra discipline in the calling convention:

  • The values on the operand stack match the types of formal parameters/return values before a / (respectively).

  • cannot be invoked if a local variable holds a resource value or the operand stack holds extra (non-return) values.

  • A script terminates in a non-aborting state only when both the call stack and operand stack are empty.

The reader might wonder: can resources left on the stack and in locals be destroyed by a mid-script abort? This would be indeed be a problem in a conventional language, but the all-or-nothing semantics of Move transactions saves us. An aborting transaction script evaluates to the pre-state of the script, at which point all resources reside safely in global storage.

What Resource Safety Accomplishes for Programmers

At this point, it’s worthwhile to take a step back and briefly discuss what resource safety does and does not guarantee. For concreteness, let’s consider our running example in Figure 1 once more. Resource safety would not preclude an implementation of deposit whose first line was let amt = 7; that is, it cannot protect the programmer from mistakes in implementing a custom asset. It does, however, isolate and localize such decisions. For example, it prevents the Bank from violating the invariants established for the imported Coin::T type inside its own declaring module.

This observation suggests a clear division of responsibilities. It is the module author’s job to define and correctly implement safety invariants for the types inside her module. Once she has done so, encapsulaton and resource safety will ensure that her local invariants are also global invariants—no possible client can ever violate them (similar to the “robust safety” of (DBLP:journals/pacmpl/Swasey0D17)). This is quite powerful because Move modules give programmers an unusual amount of control over declared types (e.g., restricting publishing and destroying types as described above), and this control can be used to establish strong invariants. For example, it is possible to define a type that can only be created after a certain time, a type that can never be destroyed, or a type that can only be created by a caller that has paid ten coins. In Section 6.2, we will show how resource safety allows us to establish global conservation of native currency in the Libra platform via a local invariant of the Coin module.

4. Move Bytecode Interpreter

Next, we present operational semantics for a call-free subset of the Move bytecode that simulates a single transaction of arbitrary length. Generalizing to multiple transactions with procedure calls is conceptually straightforward, but would be significantly less concise. This semantics will be used in Section 5 to formalize and prove resource safety.

As explained in Section 3.1, Move uses a bytecode verifier to ensure type safety and memory safety of smart contracts. Our formalism here, focusing on resource safety, does not depend on the bytecode verifier. Instead, our semantics gets stuck in errournous states, e.g. when encountering a dangling reference or an ill-typed operation. The bytecode verifier ensures additional invariants (e.g., no dangling references, well-typedness) such that programs that pass the bytecode verifier cannot get stuck due to memory or type errors. As a result, our resource safety theorem (Theorem 5.10) does not depend on the bytecode verifier.

We will begin with preliminary definitions and notation for values, types, memory, and persistent global state, before introducing evaluation rules. The notation is summarized in Figure 6.

locations
primitive data values
addresses
resource types
resource tags
tags
field names  (finite)
paths
values  (see Definition 4.1)
tagged values  (see Definition 4.1)
record values
memories
references
stack values
local values
local variables
local states
global resource ids
global states
program locations
program states
Figure 6. Definitions for the semantics of Move. For a set , denotes the set of (finite) lists of elements from .

4.1. Definitions and Notation

Notation for partial functions and lists

We use standard operations on partial functions (used to represent record values or mappings in local and global states); operations on lists are similarly standard and used in several ways.

Following common convention, if is a partial function from to , then is the set of all for which is defined, and is the set of all for which for some . We use to denote the function that is equivalent to on every input except and which maps to . Similarly, is the partial function equivalent to except that it is undefined at .

We use lists to represent a sequece of field accesses and in components of semantic states. We write for the empty list and for the result of placing at the front of list . Similarly, is the list with appended to and, by slight abuse of notation, the concatenation of lists and .

Values and their types

We begin with primitive types, field names, and tags, using these three elements to define the values used in computation. While tags are used to state and prove semantic properties, tags are not needed in the Move virtual machine.

Let be the set of primitive data values, including Booleans, integers, and addresses, a fixed, finite set of field names and the set of tags, where each tag may be a resource tag from a set or the distinguished element indicating a value that is not a resource.

Definition 4.1 ().

The set of values and the set of tagged values are defined together from the primitive values, tags and field names as the least sets satisfying:

  1. ;

  2. for every and , ; and

  3. if , are pair-wise distinct, and , then .

The values arising from condition (iii) are non-empty partial functions from to , which we call record values. We use ordinary function notation when using them (e.g., when writing to refer to the value associated with field in the record value ).

Although types are not used extensively in this paper, we leverage the fact that typing distinguishes resource values from non-resource values. We write to indicate that value has a type . If is the set of resource types, , and , then we say that is a resource value and is a resource tagged value, or simply resource; otherwise, is a non-resource value and is a non-resource tagged value. 111 In a well-formed tagged value, the type of the value must be consistent with the tag (see Definition 5.1).

Paths and Trees

In the semantics, a path is a possibly empty list of field names, which we think of as representing a sequence of field selections.

A tagged value may be regarded as a labeled tree, in the usual way that expressions are parsed as trees, with nodes labeled by tags and edges labeled by field names. Specifically, a primitive value is a tree consisting of a leaf. The tree associated with a tagged record value consists of a node labeled with the tag and a subtree for each record component. If is a record value, then for each , there is an edge from to the subtree for labeled by .

Two useful operations on values and paths are (i) the subterm of located at path , and (ii) the term obtained by replacing the subterm at path with term . The subterm identified by following the empty path is the term itself, i.e. . These operations are formalized as follows.

Definition 4.2 ().

If , then is defined inductively by:

  1. if is a record value and

  2. undefined otherwise

Similarly, if and are both tagged values, then is defined inductively by:

  1. if is a record value and

  2. undefined otherwise

States

In the Move semantics, a state comprises persistent global storage, local memory, operand stack and local variables.

If is the set of memory locations, then a reference is a triple consisting of a location , path , and mutability qualifier .

Local states and global states include memories, which are mappings from locations to values, and stacks. Specifically, a memory is a partial function from to . Defining local values to be locations or references, a local memory is similarly a partial function from to local values, where is a set of local variables. A local stack is a list of stack values, which may be tagged values or references.

Global resources are identified by an address and a resource type. If is the set of addresses, then the set of global resource ids consists of pairs , each associating a primitive value of type addresses with a resource type . A global store is a partial function from global resource ids to .

A global state is a tuple , where is a memory, is a local memory, is a local stack, and is a global store. A local state is similar with the global store omitted.

A Move program is a mapping from program locations to operations and, if represents the current program counter, then is the current instruction and is the next instruction under normal execution. A program state consists of a program counter and a global state.





       













           








Figure 7. Operational Semantics of Move: operations on local state

4.2. Local State Rules

Each rule in Figure 7 operates on local states (global storage is unchanged and thus omitted to keep the presentation simple) and takes the form

where Rule1 is the name of the rule, is a precondition for applying it, and are local states, and is an operation parameterized by a field, variable, or record declaration of the current module. When there are no parameters, we simply write . We use the following variable conventions: ; ; ; ; ; ; is a path; is a mutability qualifier; is a stack value; and is a type.

The MvLoc rules show how the state changes when a local value is moved from a local variable onto the stack. Note that if the value moved is not a reference, it is removed from memory when it is placed on the stack. The CpLoc rules copy local values to the stack. In this case, the local variable (and memory if applicable) retain their values. Note that these rules can only be applied if the local value is not a resource. The StLoc rules take the top stack value and store it in the local variable . There are two versions of the rule, depending on the current local value of . If has no value or contains a reference, it is always possible to store the stack value in (note that we can always choose a not currently in the domain of ). However, if contains a tagged value, then the rule can only be applied if the tagged value is not a resource.

BorrowLoc pushes a reference to the local value in onto the stack. BorrowField takes a reference from the top of the stack and pushes a new reference onto the stack that points to the tagged value in field of the record pointed to by . FreezeRef turns a mutable reference into an immutable reference. ReadRef makes a copy of the tagged value pointed to by a reference on top of the stack and pushes it onto the stack (note that the value must be a non-resource). can be applied to either a mutable reference or an immutable reference. WriteRef takes a non-resource tagged value and a reference from the stack, and replaces the tagged value pointed to by (which must also be a non-resource and of the same type) by . It can only be applied when is a mutable reference. The distinction between mutable and immutable references is not particularly useful in the semantics. However, we include these qualifiers because they are crucially important for the correct operation of the Move bytecode verifier and because we want the semantics to accurately reflect the real implementaion.

The Pop rules pop a stack value off the top of the stack (so long as it is not a resource). The rules (Pack-R and Pack-U) create a record of a given type . The Unpack rule decomposes a record into its fields. For resources, Pack-R pairs the record with a fresh resource tag, i.e., in each application of the rule a new unique tag is created. The Unpack rule discards the tag associated with the unpacked record, but freshness guarantees the discarded tag will not be reused. LoadConst places a constant primitive value onto the stack. StackOp is a meta-rule: there is an instance of the rule for every operation on primitive data values (e.g. negation and conjunction on Booleans, addition and subtraction on integers, etc.). The instantiated rules are formed by replacing by the specific operation and replacing by a condition that specifies legal operands for each (e.g. that the divisor is non-zero for a division operation).

4.3. Global State Rules

The rules of Figure 8 are similar except that they operate on global states. takes an address and a resource from the stack and puts the resource in the global storage at the location indexed by the address and the resource type. Conversely, removes a resource from global storage and puts it on the stack. gets a reference to a resource in global storage. And finally, checks whether the global storage currently contains a resource for a particular global resource id. In this rule, is set to true if is in the domain of , and is false otherwise.







Figure 8. Operational Semantics of Move: Global state operations

4.4. Program State Rules

The rules of Figure 9 lift the small-step semantics presented so far to semantics of call-free Move programs that can model loops and conditional branching using unstructured control-flow. They assume an abstract set of program locations over which a program counter ranges. A program is a mapping from such program locations to operations. The combined global, local, and memory state (represented in the rule as ) is extended with the to obtain a program state, and the rules simply implement sequential and branching control flow in a straightforward way.



Figure 9. Program counter rules

These evaluation rules intentionally get stuck in the presence of resource, type, or memory errors (e.g., on a variable that contains a resource). As we mentioned in Section 3.1, the Move bytecode verifier performs checks that preclude these errors. However, there are two kinds of runtime errors not caught by the bytecode verifier that we also model as stuck execution for convenience:

  1. Errors in such as division by zero and arithmetic over/underflow (which Move chooses to treat as errors);

  2. Overwriting an existing global resource id in or accessing a global resource id that does not exist in or .

In practice, these runtime errors trigger an abort that terminates the current transaction and reverts any changes to global state.

5. Resource Safety

In this section, we prove that the operational semantics introduced above enforces a conservation property: a resource cannot be created or destroyed except by the privileged and constructs available in its declaring module. We define a set of well-formed states (Definition 5.5), show that the semantic rules preserve well-formedness (Proposition 5.7), and finally, that well-formedness guarantees resource safety (Theorem 5.10). We start by introducing the parts of a well-formed state.

Definition 5.1 (Well-formed tagged value).

A tagged value with is well-formed if iff , and in addition, one of the following holds: (i)  is primitive; (ii)  such that every is well-formed, and if then for every , is not a resource.

Intuitively, in a well-formed tagged value, a resource value is never nested inside of an non-resource value, and the tag corresponds to the type.

Definition 5.2 (Globally consistent state).

We say that a state is globally consistent if the following holds: (i) Every tagged value in or in is well-formed; (ii) ; (iii) For every , with .

Intuitively, (i) means that tagged values in the state are well-formed; (ii) means that global resource ids and local variables only point to locations in the memory (no dangling references) and the memory only contains locations pointed to by some global resource id or local variable (no garbage); (iii) means that global values have their expected types.

Definition 5.3 (Tag-consistent state).

A state is tag-consistent if the following holds: (i) if , , and , then and ; (ii) If , , , and , then and ; and (iii) It is never the case that , , , and .

Intuitively, being tag-consistent means that resource tags are unique, i.e. a resource tag can appear in the memory and the stack at most once.

Definition 5.4 (Non-aliasing).

A state is non-aliasing if the following holds: (i) If with and , then ; (ii) If with , then ; and (iii) If and then .

Intuitively, a state is non-aliasing if different global or local identifiers cannot point to the same memory location.

Definition 5.5 (Well-formed state).

A state is well-formed if it is globally consistent, tag-consistent, and non-aliasing.

Well-formed states ensure that global resource identifiers and local variables only point to locations that are in the memory, and do not alias. Note, however, that according to these semantics, a well-formed state may still contain dangling references i.e., s.t. , as well as aliasing between references. As explained in Section 3.1, the bytecode veirifer ensures stronger guarantees (e.g., no dangling references), but in this section we do not depend on these stronger invariants.

We now show that the operational semantics preserves well-formedness of states.

Definition 5.6 (Well-formed execution sequence).

Let be a program. An execution sequence of is such that for every and for every . An execution sequence is called well-formed if each is well-formed.

Proposition 5.7 ().

Let be a program and an execution sequence of . If is well-formed, then is well-formed, i.e., are all well-formed.

Proof.

The proof is by induction on , and amounts to a routine check that the rules of Figure 7 and Figure 8, as well as the Branch-F and Branch-T rules of Figure 9 preserve well-formedness. We explicitly prove this for MvLoc. The rest are verified similarly. Global Consistency: (i) follows from the induction hypothesis since the set of tagged values in the memory and the stack are not changed. (ii) is also preserved: by the induction hypothesis, is non-aliasing. Thus, the fact that is removed from also means that is removed from . Since is also removed from , it follows that (ii) holds. (iii) is preserved since is unchanged and no locations are added to . Tag Consistency: (i) is preserved as the memory only gets smaller after . (ii) and (iii) hold initially by the induction hypotehsis; it is easy to see that both must also hold after moving a value from memory to the stack. Non-Aliasing: (ii) holds since the global state is unchanged. Additionally, (i) and (iii) are preserved as only gets smaller after . ∎

Next, we define the resources of a state, and what it means for resources to be introduced or eliminated in an execution sequence. We can then prove the resource safety theorem.

Definition 5.8 (State Resources).

Let be a state. The resources of , denoted , are defined as follows:

Intuitively, resources of a state are the resource tags that occur in a tagged value of the state.

Definition 5.9 (Resources Introduced and Eliminated).

Let be a program and an execution sequence of . The set of resources introduced in , denoted , is: and . The set of resources eliminated in , denoted , is: and .

Intuitively, collects all resource tags that were created (using ) during the execution; similarly, collects all resource tags that were consumed (using ) during the execution. Notice that these sets are not necessarily disjoint. That is, a resource that is created and later consumed during will appear both in and in .

Theorem 5.10 (Resource Safety).

Let be a program and a well-formed execution sequence of . Then, .

Proof.

The proof is by induction on . The base case () is straightforward (in this case, ). For the induction step, the induction hypothesis provides:

where . If , then examination of the rules shows that (i.e. for all rules other than the and rules, the set of resource tags in the global state remains the same after the application of the rule). By Definition 5.9, and