Skip to content

Latest commit

 

History

History
821 lines (664 loc) · 34.8 KB

sip-002-smart-contract-language.md

File metadata and controls

821 lines (664 loc) · 34.8 KB

Preamble

SIP Number: 002

Title: The Clarity Smart Contract Language

Author: Aaron Blankstein [email protected], Ludo Galabru [email protected]

Consideration: Technical

Type: Consensus

Status: Ratified

Created: 29 November 2018

License: BSD 2-Clause

Sign-off: Jude Nelson [email protected], Technical Steering Committee Chair

Discussions-To: https://github.com/stacksgov/sips

Abstract

In order to support applications which require validation of some pieces of their logic, we present a smart contracting language for use with the Stacks blockchain. This smart contracting language can be used on the Stacks blockchain to support programatic control over digital assets within the Stacks blockchain (e.g., BNS names, Stacks tokens, etc.)

While application-chains may use any smart-contract language that they like, this smart contracting language's VM will be a part of blockstack-core, and, as such, any blockstack-core node will be able to validate application chains using this smart contracting language with a simple configuration change.

This smart contracting language permits static analysis of any legal smart contract to determine runtime costs. This smart contracting language is not only Turing-incomplete (a requirement for such static analysis to be guaranteed successful), but readily permits other kinds of proofs to be made about the code as well.

License and Copyright

This SIP is made available under the terms of the BSD-2-Clause license, available at https://opensource.org/licenses/BSD-2-Clause. This SIP's copyright is held by the Stacks Open Internet Foundation.

Introduction

A smart contract is composed of two parts:

  1. A data-space, which is a set of tables of data which only the smart contract may modify
  2. A set of functions which operate within the data-space of the smart contract, though they may call public functions from other smart contracts.

Users call smart contracts' public functions by broadcasting a transaction on the blockchain which invokes the public function.

This smart contracting language differs from most other smart contracting languages in two important ways:

  1. The language is not intended to be compiled. The LISP language described in this document is the specification for correctness.
  2. The language is not Turing complete. This allows us to guarantee that static analysis of programs to determine properties like runtime cost and data usage can complete successfully.

Specification

Specifying Contracts

A smart contract definition is specified in a LISP language with the following limitations:

  1. Recursion is illegal and there is no lambda function.
  2. Looping may only be performed via map, filter, or fold
  3. The only atomic types are booleans, integers, fixed length buffers, and principals
  4. There is additional support for lists of the atomic types, however the only variable length lists in the language appear as function inputs (i.e., there is no support for list operations like append or join).
  5. Variables may only be created via let binding and there is no support for mutating functions like set.
  6. Defining of constants and functions are allowed for simplifying code using define-private statement. However, these are purely syntactic. If a definition cannot be inlined, the contract will be rejected as illegal. These definitions are also private, in that functions defined this way may only be called by other functions defined in the given smart contract.
  7. Functions specified via define-public statements are public functions.
  8. Functions specified via define-read-only statements are public functions and perform no state mutations. Any attempts to modify contract state by these functions or functions called by these functions will result in an error.

Public functions return a Response type result. If the function returns an ok type, then the function call is considered valid, and any changes made to the blockchain state will be materialized. If the function returns an err type, it will be considered invalid, and will have no effect on the smart contract's state. So if function foo.A calls bar.B, and bar.B returns an ok, but foo.A returns an err, no effects from calling foo.A materialize--- including effects from bar.B. If, however, bar.B returns an err and foo.A returns an ok, there may be some database effects which are materialized from foo.A, but no effects from calling bar.B will materialize.

Unlike functions created by define-public, which may only return Response types, functions created with define-read-only may return any type.

List Operations

  • Lists may be multi-dimensional (i.e., lists may contain other lists), however each entry of this list must be of the same type.
  • filter map and fold functions may only be called with user-defined functions (i.e., functions defined with (define-private ...), (define-read-only ...), or (define-public ...)) or simple native functions (e.g., +, -, not).
  • Functions that return lists of a different size than the input size (e.g., (append-item ...)) take a required constant parameter that indicates the maximum output size of the function. This is enforced with a runtime check.

Inter-Contract Calls

A smart contract may call functions from other smart contracts using a (contract-call?) function.

This function returns a Response type result-- the return value of the called smart contract function. Note that if a called smart contract returns an err type, it is guaranteed to not alter any smart contract state whatsoever. Of course, any transaction fees paid for the execution of that function will not be returned.

We distinguish 2 different types of contract-call?:

  • Static dispatch: the callee is a known, invariant contract available on-chain when the caller contract is being deployed. In this case, the callee's principal is provided as first argument, followed by the name of the method and its arguments:
(contract-call?
    'SC3H92H297DX3YDPFHZGH90G8Z4NPH4VE8E83YWAQ.registrar
    register-name
    name-to-register)

This approach must always be preferred, when adequate. It makes static analysis easier, and eliminates the potential for reentrancy bugs when the contracts are being published (versus when being used).

  • Dynamic dispatch: the callee is passed as an argument, and typed as a trait reference ().
(define-public (swap (token-a <can-transfer-tokens>)
                     (amount-a uint)
                     (owner-a principal)
                     (token-b <can-transfer-tokens>)
                     (amount-b uint)
                     (owner-b principal)))
     (begin
         (unwrap! (contract-call? token-a transfer-from? owner-a owner-b amount-a))
         (unwrap! (contract-call? token-b transfer-from? owner-b owner-a amount-b))))

Traits can either be locally defined:

(define-trait can-transfer-tokens (
    (transfer-from? (principal principal uint) (response uint)))

Or imported from an existing contract:

(use-trait can-transfer-tokens
    'SC3H92H297DX3YDPFHZGH90G8Z4NPH4VE8E83YWAQ.contract-defining-trait.can-transfer-tokens)

Looking at trait conformance, callee contracts have two different paths. They can either be "compatible" with a trait by defining methods matching some of the methods defined in a trait, or explicitely declare conformance using the impl-trait statement:

(impl-trait 'SC3H92H297DX3YDPFHZGH90G8Z4NPH4VE8E83YWAQ.contract-defining-trait.can-transfer-tokens)

Explicit conformance should be prefered when adequate. It acts as a safeguard by helping the static analysis system to detect deviations in method signatures before contract deployment.

The following limitations are imposed on contract calls:

  1. On static dispatches, callee smart contracts must exist at the time of creation.
  2. No cycles may exist in the call graph of a smart contract. This prevents recursion (and re-entrancy bugs). Such structures can be detected with static analysis of the call graph, and will be rejected by the network.
  3. contract-call? are for inter-contract calls only. Situations where the caller is also the callee will result in abortion of the ongoing transaction.

Principals and Owner Verification

The language provides a primitive for checking whether or not the smart contract transaction was signed by a particular principal. Principals are a specific type in the smart contracting language which represent a spending entity (roughly equivalent to a Stacks address). The signature itself is not checked by the smart contract, but by the VM. A smart contract function can use a globally defined variable to obtain the current principal:

tx-sender

The tx-sender variable does not change during inter-contract calls. This means that if a transaction invokes a function in a given smart contract, that function is able to make calls into other smart contracts without that variable changing. This enables a wide variety of applications, but it comes with some dangers for users of smart contracts. However, as mentioned before, the static analysis guarantees of our smart contracting language allow clients to know a priori which functions a given smart contract will ever call.

Another global variable, contract-caller, does change during inter-contract calls. In particular, contract-caller is the contract principal corresponding to the most recent invocation of contract-call?. In the case of a "top-level" invocation, this variable is equal to tx-sender.

Assets in the smart contracting language and blockchain are "owned" by objects of the principal type, meaning that any object of the principal type may own an asset. For the case of public-key hash and multi-signature Stacks addresses, a given principal can operate on their assets by issuing a signed transaction on the blockchain. Smart contracts may also be principals (reprepresented by the smart contract's identifier), however, there is no private key associated with the smart contract, and it cannot broadcast a signed transaction on the blockchain.

In order to allow smart contracts to operate on assets it owns, smart contracts may use the special function:

(as-contract (...))

This function will execute the closure (passed as an argument) with the tx-sender and contract-caller set to the contract's principal, rather than the current sender. It returns the return value of the provided closure. A smart contract may use the special variable contract-principal to refer to its own principal.

For example, a smart contract that implements something like a "token faucet" could be implemented as so:

(define-public (claim-from-faucet)
  (if (is-none? (map-get claimed-before (tuple (sender tx-sender))))
      (let ((requester tx-sender)) ;; set a local variable requester = tx-sender
        (map-insert! claimed-before (tuple (sender requester)) (tuple (claimed true)))
        (as-contract (stacks-transfer! requester 1))))
      (err 1))

Here, the public function claim-from-faucet:

  1. Checks if the sender has claimed from the faucet before
  2. Assigns the tx sender to a requester variable
  3. Adds an entry to the tracking map
  4. Uses as-contract to send 1 microstack

The primitive function is-contract? can be used to determine whether a given principal corresponds to a smart contract.

Stacks Transfer Primitives

To interact with Stacks balances, smart contracts may call the (stacks-transfer!) function. This function will attempt to transfer from the current principal to another principal:

(stacks-transfer!
  to-send-amount
  recipient-principal)

This function itself requires that the operation have been signed by the transferring principal. The integer type in our smart contracting language is an 16-byte signed integer, which allows it to specify the maximum amount of microstacks spendable in a single Stacks transfer.

Like any other public smart contract function, this function call returns an ok if the transfer was successful, and err otherwise.

Data-Space Primitives

Data within a smart contract's data-space is stored within maps. These stores relate a typed-tuple to another typed-tuple (almost like a typed key-value store). As opposed to a table data structure, a map will only associate a given key with exactly one value. Values in a given mapping are set or fetched using:

  1. (map-get map-name key-tuple) - This fetches the value associated with a given key in the map, or returns none if there is no such value.
  2. (map-set! map-name key-tuple value-tuple) - This will set the value of key-tuple in the data map
  3. (map-insert! map-name key-tuple value-tuple) - This will set the value of key-tuple in the data map if and only if an entry does not already exist.
  4. (map-delete! map-name key-tuple) - This will delete key-tuple from the data map

We chose to use data maps as opposed to other data structures for two reasons:

  1. The simplicity of data maps allows for both a simple implementation within the VM, and easier reasoning about functions. By inspecting a given function definition, it is clear which maps will be modified and even within those maps, which keys are affected by a given invocation.
  2. The interface of data maps ensures that the return types of map operations are fixed length, which is a requirement for static analysis of smart contracts' runtime, costs, and other properties.

A smart contract defines the data schema of a data map with the define-map call. The define-map function may only be called in the top-level of the smart-contract (similar to define-private). This function accepts a name for the map, and a definition of the structure of the key and value types. Each of these is a list of (name, type) pairs, and they specify the input and output type of map-get. Types are either the values 'principal, 'integer, 'bool or the output of a call to (buffer n), which defines an n-byte fixed-length buffer.

This interface, as described, disallows range-queries and queries-by-prefix on data maps. Within a smart contract function, you cannot iterate over an entire map.

Record Type Syntax

To support the use of named fields in keys and values, our language allows the construction of named tuples using a function (tuple ...), e.g.,

(define-constant imaginary-number-a (tuple (real 1) (i 2)))
(define-constant imaginary-number-b (tuple (real 2) (i 3)))

This allows for creating named tuples on the fly, which is useful for data maps where the keys and values are themselves named tuples. To access a named value of a given tuple, the function (get #name tuple) will return that item from the tuple.

Time-shifted Evaluations

The Stacks language supports historical data queries using the (at-block) function:

(at-block 0x0101010101010101010101010101010101010101010101010101010101010101
  ; returns owner principal of name represented by integer 12013
  ;  at the time of block 0x010101...
  (map-get name-map 12013))

This function evaluates the supplied closure as if evaluated at the end of the supplied block, returning the resulting value. The supplied closure must be read-only (is checked by the analysis).

The supplied block hash must correspond to a known block in the same fork as the current block, otherwise a runtime error will occur and the containing transaction will fail. Note that if the supplied block pre-dates any of the data structures being read within the closure (i.e., the block is before the block that constructed a data map), a runtime error will occur and the transaction will fail.

Library Support and Syntactic Sugar

There are a number of ways that the developer experience can be improved through the careful addition of improved syntax. For example, while the only atomic types supported by the smart contract language are integers, buffers, booleans, and principals, so if a developer wishes to use a buffer to represent a fixed length string, we should support syntax for representing a buffer literal using something like an ASCII string. Such support should also be provided by transaction generation libraries, where buffer arguments may be supplied strings which are then automatically converted to buffers. There are many possible syntactic improvements and we expect that over the course of developing the prototype, we will have a better sense for which of those improvements we should support. Any such synactic changes will appear in an eventual language specification, but we believe them to be out of scope for this proposal.

Static Analysis

One of the design goals of our smart contracting language was the ability to statically analyze smart contracts to obtain accurate upper-bound estimates of transaction costs (i.e., runtime and storage requirements) as a function of input lengths. By limiting the types supported, the ability to recurse, and the ability to iterate, we believe that the language as presented is amenable to such static analysis based on initial investigations.

The essential step in demonstrating the possibility of accurate and useful analysis of our smart contract definitions is demonstrating that any function within the language specification has an output length bounded by a constant factor of the input length. If we can demonstrate this, then statically computing runtime or space requirements involves merely associating each function in the language specification with a way to statically determine cost as a function of input length.

Notably, the fact that the cost functions produced by static analysis are functions of input length means the following things:

  1. The cost of a cross-contract call can be "memoized", such that a static analyzer does not need to recompute any static analysis on the callee when analyzing a caller.
  2. The cost of a given public function on a given input size is always the same, meaning that smart contract developers do not need to reason about different cases in which a given function may cost more or less to execute.

Bounding Function Output Length

Importantly, our smart contracting language does not allow the creation of variable length lists: there are no list or cons constructors, and buffer lengths must be statically defined. Under such requirements (and given that recursion is illegal), determining the output lengths of functions is rather directly achievable. To see this, we'll examine trying to compute the output lengths for the only functions allowed to iterate in the language:

outputLen(map f list<t>)     := Len(list<t>) * outputLen(f t)
outputLen(filter f list<t>)  := Len(list<t>)
outputLen(fold f list<t> s)  := Len(s)

Many functions within the language will output values larger than the function's input, however, these outputs will be bound by statically inferable constants. For example, the data function map-get will always return an object whose size is equal to the specified value type of the map.

A complete proof for the static runtime analysis of smart contracts will be included with the implementation of the language.

Deploying the Smart Contract

Smart contracts on the Stacks blockchain will be deployed directly as source code. The goal of the smart contracting language is that the code of the contract defines the ground truth about the intended functionality of the contract. While seemingly banal, many systems chose instead to use a compiler to translate from a friendly high-level language to a lower-level language deployed on the blockchain. Such an architecture is needlessly dangerous. A bug in such a compiler could lead to a bug in a deployed smart contract when no such bug exists in the original source. This is problematic for recovery --- a hard fork to "undo" any should-have-been invalid transactions would be contentious and potentially create a rift in the community, especially as it will not be easy to deduce which contracts exactly were affected and for how long. In contrast, bugs in the VM itself present a more clear case for a hard fork: the smart contract was defined correctly, as everyone can see directly on the chain, but illegal transactions were incorrectly marked as valid.

Virtual Machine API

From the perspective of other components of blockstack-core, the smart contracting VM will provide the following interface:

connect-to-database(db)

publish-contract(
  contract-source-code)

  returns: contract-identifier

execute-contract(
  contract-identifier,
  transaction-name,
  sender-principal,
  transaction-arguments)

  returns: true or false if the transaction executed successfully

Invocation and Static Analysis

When processing a client transaction, a blockstack-core node will do one of two things, depending on whether that transaction is a contract function invocation, or is attempting to publish a new smart contract.

Contract function invocation

Any transaction which invokes a smart contract will be included in the blockchain. This is true even for transactions which are invalid. This is because validating an invalid transaction is not a free operation. The only exceptions to this are transactions which do not pay more than either a minimum fee or a storage fee corresponding to the length of the transaction. Transactions which do not pay a storage fee and clear the minimum transaction fee are dropped from the mempool.

To process a function invocation, blockstack-core does the following:

  1. Get the balance of the sender's account. If it's less than the tx fee, then RETURN INVALID.
  2. Otherwise, debit the user's account by the tx fee.
  3. Look up the contract by hash. If it does not exist, then RETURN INVALID.
  4. Look up the contract's define-public function and compare the tx's arguments against it. If the tx does not call an existing method, or supplies invalid arguments, then RETURN INVALID.
  5. Look up the cost to execute the given function, and if it is greater than the paid tx fee, RETURN INVALID.
  6. Execute the public function code and commit the effects of running the code and RETURN OK

Publish contract

A transaction which creates a new smart contract must pay a fee which funds the static analysis required to determine the cost of the new smart contract's public functions. To process such a transaction, blockstack-core will:

  1. Check the sender's account balance. If zero, then RETURN INVALID
  2. Check the tx fee against the user's balance. If it's higher, then RETURN INVALID
  3. Debit the tx fee from the user's balance.
  4. Check the syntax, calculating the fee of verifying each code item. If the cost of checking the next item exceeds the tx fee, or if the syntax is invalid, then RETURN INVALID.
  5. Build the AST, and assign a fee for adding each AST item. If the cost of adding the next item to the tree exceeds the tx fee (or if the AST gets too big), then RETURN INVALID.
  6. Walk the AST. Each step in the walk incurs a small fee. Do the following while the tx fee is higher than the total cost incurred by walking to the next node in the AST: a. If the next node calls a contract method, then verify that the contract exists and the method arguments match the contract's define-public signature. If not, then RETURN INVALID. b. Compute the runtime cost of each node in the AST, adding it to the function's cost analysis.
  7. Find all define-map calls to find all tables that need to exist. Each step in this incurs a small fee.
  8. Create all the tables if the cost of creating them is smaller than the remaining tx fee. If not, then RETURN INVALID.
  9. RETURN OK

Database Requirements and Transaction Accounting

The smart contract VM needs to interact with a database somewhat directly: the effects of an map-insert! or map-set! call are realized later in the execution of the same transaction. The database will need to support fairly fine-grained rollbacks as some contract calls within a transaction's execution may fail, triggering a rollback, while the transaction execution continues and successfully completes other database operations.

The database API provided to the smart contract VM, therefore, must be capable of (1) quickly responding to map-get queries, which are essentially simply key-value gets on the materialized view of the operation log. The operation log itself is simply a log of the map-insert! and map-set! calls. In addition to these operations, the smart contract VM will be making token transfer calls. The databasse log should track those operations as well.

In order to aid in accounting for the database operations created by a given transaction, the underlying database should store, with each operation entry, the corresponding transaction identifier. This will be expanded in a future SIP to require the database to store enough information to reconstruct each block, such that the blocks can be relayed to bootstrapping peers.

Clarity Type System

Types

The Clarity language uses a strong static type system. Function arguments and database schemas require specified types, and use of types is checked during contract launch. The type system does not have a universal super type. The type system contains the following types:

  • (tuple (key-name-0 key-type-0) (key-name-1 key-type-1) ...) - a typed tuple with named fields.
  • (list max-len entry-type) - a list of maximum length max-len, with entries of type entry-type
  • (response ok-type err-type) - object used by public functions to commit their changes or abort. May be returned or used by other functions as well, however, only public functions have the commit/abort behavior.
  • (optional some-type) - an option type for objects that can either be (some value) or none
  • (buff max-len) := byte buffer or maximum length max-len.
  • principal := object representing a principal (whether a contract principal or standard principal).
  • bool := boolean value (true or false)
  • int := signed 128-bit integer
  • uint := unsigned 128-bit integer

Type Admission

UnknownType. The Clarity type system does not allow for specifying an "unknown" type, however, in type analysis, unknown types may be constructed and used by the analyzer. Such unknown types are used only in the admission rules for response and optional types (i.e., the variant types).

Type admission in Clarity follows the following rules:

  • Types will only admit objects of the same type, i.e., lists will only admit lists, tuples only admit tuples, bools only admit bools.
  • A tuple type A admits another tuple type B iff they have the exact same key names, and every key type of A admits the corresponding key type of B.
  • A list type A admits another list type B iff A.max-len >= B.max-len and A.entry-type admits B.entry-type.
  • A buffer type A admits another buffer type B iff A.max-len >= B.max-len.
  • An optional type A admits another optional type B iff:
    • A.some-type admits B.some-type OR B.some-type is an unknown type: this is the case if B only ever corresponds to none
  • A response type A admits another response type B if one of the following is true:
    • A.ok-type admits B.ok-type AND A.err-type admits B.err-type
    • B.ok-type is unknown AND A.err-type admits B.err-type
    • B.err-type is unknown AND A.ok-type admits B.ok-type
  • Principals, bools, ints, and uints only admit types of the exact same type.

Type admission is used for determining whether an object is a legal argument for a function, or for insertion into the database. Type admission is also used during type analysis to determine the return types of functions. In particular, a function's return type is the least common supertype of each type returned from any control path in the function. For example:

(define-private (if-types (input bool))
  (if input
     (ok 1)
     (err false)))

The return type of if-types is the least common supertype of (ok 1) and (err false) (i.e., the most restrictive type that contains all returns). In this case, that type (response int bool). Because Clarity does not have a universal supertype, it may be impossible to determine such a type. In these cases, the functions are illegal, and will be rejected during type analysis.

Measuring Transaction Costs for Fee Collection

Our smart contracting language admits static analysis to determine many properties of transactions before executing those transactions. In particular, it allows for the VM to count the total number of runtime operations required, the maximum amount of database writes, and the maximum number of calls to any expensive primitive functions like database reads or hash computations. Translating that information into transaction costs, however, requires more than simply counting those operations. It requires translating the operations into a single cost metric (something like gas in Ethereum). Then, clients can set the fee rate for that metric, and pay the corresponding transaction fee. Notably, unlike Turing-complete smart contracting languages, any such fees are known before executing the transaction, such that clients will no longer need to estimate gas fees. They will, however, still need to estimate fee rates (much like Bitcoin clients do today).

Developing such a cost metric is an important task that has significant consequences. If the metric is a bad one, it could open up the possibility of denial-of-service attacks against nodes in the Stacks network. We leave the development of a cost metric to another Stacks Improvement Proposal, as we believe that such a metric should be designed by collecting real benchmarking data from something close to a real system (such measurements will likely be collected through a combination of hand-crafted benchmarks and fuzzing test suites).

Maximum Operation Costs and Object Sizes

Even with a cost metric, it is a good idea to set maximums for the cost of an operation, and the size of objects (like buffers). Developing good values for constants such as maximum number of database reads or writes per transaction, maximum size of buffers, maximum number of arguments to a tuple, maximum size of a smart contract definition, etc. is a process much like developing a cost metric--- this is something best done in tandem with the production of a prototype. However, we should note that we do intend to set such limits.

Example: Simple Naming System

To demonstrate the expressiveness of this smart contracting language, let's look at an example smart contract which implements a simple naming system with just two kinds of transactions: preorder and register. The requirements of the system are as follows:

  1. Names may only be owned by one principal
  2. A register is only allowed if there is a corresponding preorder with a matching hash
  3. A register transaction must be signed by the same principal who paid for the preorder
  4. A preorder must have paid at least the price of the name. Names are represented as integers, and any name less than 100000 costs 1000 microstacks, while all other names cost 100 microstacks.
  5. Preorder hashs are globally unique.

In this simple scheme, names are represented by integers, but in practice, a buffer would probably be used.

(define-constant burn-address '1111111111111111111114oLvT2)
(define-private (price-function name)
  (if (< name 1e5) 1000 100))

(define-map name-map 
  { name: uint } { buyer: principal })
(define-map preorder-map
  { name-hash: (buff 160) }
  { buyer: principal, paid: uint })

(define-public (preorder 
               (name-hash (buffer 20))
               (name-price integer))
  (if (and (is-ok? (stacks-transfer!
                    name-price burn-address))
           (map-insert! preorder-map
            (tuple (name-hash name-hash))
            (tuple (paid name-price)
                   (buyer tx-sender))))
      (ok 0)
      (err 1)))

(define-public (register 
               (recipient-principal principal)
               (name integer)
               (salt integer))
  (let ((preorder-entry
          (map-get preorder-map
                         (tuple (name-hash (hash160 name salt)))))
        (name-entry 
          (map-get name-map (tuple (name name)))))
    (if (and
         ;; must be preordered
         (not (is-none? preorder-entry))
         ;; name shouldn't *already* exist
         (is-none? name-entry)
         ;; preorder must have paid enough
         (<= (price-funcion name)
             (default-to 0 (get paid preorder-entry)))
         ;; preorder must have been the current principal
         (eq? tx-sender
              (expects! (get buyer preorder-entry) (err 1)))
         (map-insert! name-table
           (tuple (name name))
           (tuple (owner recipient))))
         (ok 0)
         (err 1))))

Note that Blockstack PBC intends to supply a full BNS (Blockstack Naming System) smart contract, as well as formal proofs that certain desirable properties hold (e.g. "names are globally unique", "a revoked name cannot be updated or transferred", "names cost stacks based on their namespace price function", "only the principal can reveal a name on registration", etc.).

Related Work

Smart contract languages are not new at the time of this writing. By far the most well-known one at this time is Solidity [1], a Turing-complete smart contract programming language for the Ethereum blockchain [2]. Solidity is compiled, which makes it hard to determine whether or not any differences between the contract's behavior and its expected behavior are due to bugs in the source code, the compiler, or the EVM itself. Clarity is interpreted, so any bugs that cannot be attributed to a Clarity contract's source code must be due to bugs in the blockchain itself. Removing this ambiguity is important for determining how to triage such problems. A bug in the runtime system would justifiably be fixed with a backwards-incompatible change, whereas a bug in the compiler may not be.

Clarity is decidable, which removes the need for implementing an "out-of-gas" error condition that is common in Turing-complete smart contract languages. Clarity programs either run to completion if they do not fail static analysis checks, or the blockchain transactions that invoke them are never mined in the first place.

There are many other smart contract programming languages with different design goals than Clarity. This section deserves future expansion, and may be added to after this SIP is ratified.

[1] Solidity Language. https://docs.soliditylang.org/en/v0.8.0/ [2] Ethereum blockchain. https://ethereum.github.io/yellowpaper/paper.pdf

Backwards Compatibility

Not applicable

Activation

At least 20 miners must register a name in the .miner namespace in Stacks 1.0. Once the 20th miner has registered, the state of Stacks 1.0 will be snapshotted. 300 Bitcoin blocks later, the Stacks 2.0 blockchain will launch. With Stacks 2.0 will come the Clarity VM.

Reference Implementations

The frst reference implementation can be found at https://github.com/blockstack/stacks-blockchain.