The Tari RFCs

Tari is a community-driven project. The documents presented in this RFC collection have typically gone through several iterations before reaching this point:

  • Ideas and questions are posted in #tari-dev on #FreeNode IRC. This is typically short-form content with rapid feedback. Often, these conversations will lead to someone posting an issue or RFC pull request.
  • RFCs are "Requests for Comment", so although the proposals in these documents are usually well-thought out, they are not cast in stone. RFCs can, and should, undergo further evaluation and discussion by the community. RFC comments are best made using Github issues.

New RFC's should follow the format given in the RFC template.

Lifecycle

RFCs go through the following lifecycle, which roughly corresponds to the COSS:

StatusDescription
DraftdraftChanges, additions and revisions can be expected.
StablestableTypographical and cosmetic changes aside, no further changes should be made. Changes to the Tari code base w.r.t. a stable RFC will lead to the RFC becoming out of date, deprecated, or retired.
Out of dateout of dateThis RFC has become stale due to changes in the code base. Contributions will be accepted to make it stable again if the changes are relatively minor, otherwise it should eventually become deprecated or retired.
DeprecateddeprecatedThis RFC has been replaced by a newer RFC document, but is still is use in some places and/or versions of Tari.
RetiredretiredThe RFC is no longer in use on the Tari network.

RFC-0001/Overview

Overview of Tari Network

status: stable

Maintainer(s): Cayle Sharrock

Licence

The 3-Clause BSD Licence.

Copyright 2018 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

The aim of this proposal is to provide a very high-level perspective of the moving parts of the Tari protocol.

Description

Abstract

Tari's aims to be the most useful, decentralized platform that empowers anyone to create digitally scarce things people love.

This statement packages some important concepts.

To be useful, creators and users need to be able to interact with their digital assets in the way that they've been accustomed to in Web 2.0. This means that there is a safe, secure, responsive interface between human and the machines that manage their information. It means that the user experience is smooth and intuitive. It means that the network protocol itself is flexible and capable enough to provide every type of assets creators can imagine.

It also scales, so that the entire network does not become a victim of its own success and grind to a halt when more than a few hundred people actually try to use it.

Decentralisation is central to Tari's philosophy (pun intended). The tape is wearing thin on the newsreel that reports yet another centralised custodian, gatekeeper or "authority" becoming a single point of failure. This has led to thousands of people losing their crypto-assets, as well as their trust in the ideas of crypto-assets.

Tari has a strong focus on developer experience. The Tari community obsesses over building beautiful, clean, intuitive APIs and templates, which lead to beautiful developer tools. The idea is to widen the pool and allow anyone to create safe, secure, performant digital assets, rather than provide a gated community of rent-extracting "smart-contract developers".

In general, some of these goals are in direct opposition to each other. Speed, security and decentralisation typically form a trilemma that means that you need to settle for two out of the three.

Tari attempts to resolve the dilemma by splitting operations over two discrete layers. Underpinning everything is a base layer that focuses on security and manages global state, and a second, digital assets layer that focuses on rapid finalisation and scalability.

Multiple Layers

The distributed system trilemma tells us that these requirements are mutually exclusive.

We can't have fast, cheap digital assets and also highly secure and decentralized currency tokens on a single system.

Tari overcomes this constraint by building two layers:

  1. A base layer that provides a public ledger of Tari coin transactions, secured by PoW to maximize security.
  2. A DAN consisting of a highly scalable, efficient side-chain that each manages the state of all digital asset.

The DAN layer gives up some security guarantees in exchange for performance. However, in the case of a liveness failure (wherein parts of the DAN cannot make progress), the base layer intervenes to break the deadlock and allow the DAN to continue.

Base Layer

The Tari base layer has the following primary features:

  • PoW-based blockchain using Nakamoto consensus
  • Transactions and blocks based on the Mimblewimble protocol

Mimblewimble is a blockchain protocol that offers some key advantages over other UTXO-based cryptocurrencies such as Bitcoin:

  • Transactions are private. This means that casual observers cannot ascertain the amounts being transferred or the identities of the parties involved.
  • Mimblewimble has a different set of security guarantees to Bitcoin. The upshot of this is that you can throw away UTXOs once they are spent and still verify the integrity of the ledger.
  • Multi-signature transactions can be easily aggregated, making such transactions very compact, and completely hiding the parties involved, or the fact that there were multiple parties involved at all.

"Mimblewimble is the most sound, scalable 'base layer' protocol we know" -- @fluffypony

In addition to this, Tari has made some novel additions to the basic Mimblewimble protocol. Primarily, these were invented to allow the DAN to be built on top of Tari, but have found some great applications generally:

  • TariScript. Similar to Bitcoin script, TariScript (RFC-201, RFC-202) provides limited "smart contract" functionality on the base layer protocol.
  • One-sided payments. Vanilla Mimblewimble requires both sender and receiver to participate in the creation of transactions. It is impossible in MimblewimbleCoin, for example, to post a "tip jar" address and let people unilaterally send you funds. However, this is possible in Tari, thanks to TariScript.
  • Stealth addresses. Want to allow people to send you funds using one-sided payments without revealing your public key (and thus, who you are) to the world? Stealth addresses have you covered.
  • Covenants. Covenants allow you to construct complex chains of transactions that follow predefined rules.
  • Burn transactions. Tari offers unequivocal "burn" transactions that render the burnt outputs permanently unspendable. This is an important mechanism that underpins the DAN economy.

Proof of Work

Tari is mined using a hybrid approach. On average, 60% of block rewards come from Monero merge-mining, while 40% come from the Sha3x algorithm. Blocks are produced every 2 minutes, on average.

The role of the base layer

The Base Layer fulfils these, and only these, major roles:

  1. It manages and enforces the accounting and consensus rules of the base Tari (XTR) token. This includes standard payments, and simple smart contracts such as one-sided payments and cross-chain atomic swaps.
  2. It maintains the Validator node register.
  3. Maintain a register of smart contract templates. This allows users to verify that digital assets are running the code that they expect and includes functionality like version tracking.
  4. Provides a global reference clock for the digital assets layer to help it resolve certain operational failure modes.

Digital Assets Network

The DAN is focused on achieving high speed and scalability while maintaining a high degree of decentralisation.

The DAN itself is made up of two conceptual levels. On the more fundamental level, the consensus layer uses Cerberus and emergent HotStuff to reach consensus on state changes in the DAN in a highly scalable, decentralised way.

A big, and probably the biggest, advantage of this approach is that assets in disparate smart contracts can easily interact, without the need for slow, honey pot-shaped bridges.

Then there is the semantic layer, which is where the Tari contracts are compiled, run and verified inside sandboxed Tari virtual machines.

Together, these levels provide that smart contract enabled digital assets layer, that we've simply been calling the DAN.

Interplay of the layers

In general, the base layer knows nothing about the specifics of what is happening on the side-chain. It only cares that no Tari is created or destroyed, and that the flow of funds in and out of side chains are carried out by the appropriate authorised agents.

This is by design: the network cannot scale if details of digital asset contracts have to be tracked on the base layer. We envisage that there could be hundreds of thousands of contracts deployed on Tari. Some of those contracts may be enormous; imagine controlling every piece of inventory and their live statistics for a massively multiplayer online role-playing game (MMORPG). The base layer is also too slow. If any state relies on base layer transactions being confirmed, there is an immediate lag before that state change can be considered final, which kills the latency properties we seek for the DAN.

It is better to keep the two networks almost totally decoupled from the outset, and allow each network to play to its strength.

Change Log

DateChangeAuthor
18 Dec 2018First outlineCjS77
30 Mar 2019First draft v0.0.1CjS77
19 Jun 2019Propose payment channel layerCjS77
22 Jun 2021Remove payment channel layer proposalSimianZa
14 Jan 2022Update image. Expound on Base layer responsibilitiesCjS77
10 Nov 2022Update overview for CerberusCjS77

The Tari Base Layer

The Tari Base Layer network comprises the following major pieces of software:

  • Base Layer full node implementation. The base layer full nodes are the consensus-critical pieces of software for the Tari base layer and cryptocurrency. The base nodes validate and transmit transactions and blocks, and maintain consensus about the longest valid proof-of-work blockchain.
  • Peer-to-peer communications network. All blockchain systems need a messaging mechanism. The Tari project has built its own peer-to-peer, end-to-end encrypted, DHT-based communications platform. It utilises the Noise protocol and Tor to be highly secure and anonymous. Devices behind NATs and firewalls can use Tari's communication tools with ease.
  • Mining software. Miners perform proof-of-work to secure the base layer and compete to submit the next valid block into the Tari blockchain. Tari uses two Proof of Work (PoW) algorithms, the first is merge-mined with Monero, and the second is the native SHA3x algorithm. The Tari source provides three alternatives for Tari miners:
    • A standalone miner for SHA3 mining
    • A merge-mining proxy to be used with XMRig to merge mine Tari with Monero
  • Wallet software. Client software and Application Programming Interfaces (APIs) offering means to construct transactions, query nodes for information and maintain personal private keys. The reference design includes a wallet library, an C FFI interface, gRPC client and server code, a multi-platform console text-based wallet, and Aurora, the mobile wallet.

The RFCs in this section go into great describing how the various components work, and how they fit together to provide the backbone of the Tari ecosystem.

RFC-0110/BaseNodes

Base Layer Full Nodes (Base Nodes)

status: stable

Maintainer(s): Cayle Sharrock, S W van heerden and Stanley Bondi

Licence

The 3-Clause BSD Licence.

Copyright 2019 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

The aim of this Request for Comment (RFC) is to describe the roles that base nodes play in the Tari network as well as their general approach for doing so.

$$ \newcommand{\so}{\gamma} % script offset $$

Description

Broad Requirements

Tari Base Nodes form a peer-to-peer network for a proof-of-work based blockchain running the Mimblewimble protocol. The proof-of-work is performed via hybrid mining, that is merge mining with Monero and stand-alone SHA 3. Arguments for this design are presented in the overview.

Tari Base Nodes MUST carry out the following tasks:

  • validate all Tari coin transactions;
  • propagate valid transactions to peer nodes;
  • validate all new blocks received;
  • propagate validated new blocks to peer nodes;
  • connect to peer nodes to catch up (sync) with their blockchain state;
  • provide historical block information to peers that are syncing.

Once the Digital Assets Network (DAN) goes live, Base Nodes will also need to support the tasks described in RFC-0303_DAN. These requirements may involve but are not limited to:

  • maintain an index of validator node registrations;

To carry out these tasks effectively, Base Nodes SHOULD:

  • save the blockchain into an indexed local database;
  • maintain an index of all Unspent Transaction Outputs (UTXOs);
  • maintain a list of all pending, valid transactions that have not yet been mined (the mempool);
  • manage a list of Base Node peers present on the network.

Tari Base Nodes MAY implement chain pruning strategies that are features of Mimblewimble, including transaction block compaction techniques.

Tari Base Nodes MAY also implement the following services via an Application Programming Interface (API) to clients:

  • Block queries
  • Kernel data queries
  • Transaction queries
  • Submission of new transactions

Such clients may include "light" clients, block explorers, wallets and Tari applications.

Transaction Validation and Propagation

Base nodes can be notified of new transactions by:

  • connected peers;
  • clients via APIs.

When a new transaction has been received, it is then passed to the mempool service where it will be validated and either stored or rejected.

The transaction is validated as follows:

  • All inputs to the transaction are valid UTXOs in the UTXO set or are outputs in the current block.
  • No inputs are duplicated.
  • All inputs are able to be spent (they are not time-locked).
  • All inputs are signed by their owners.
  • All outputs have valid range proofs.
  • No outputs currently exist in the current UTXO set.
  • The transaction does not have timelocks applied, limiting it from being mined and added to the blockchain before a specified block height or timestamp has been reached.
  • The transaction excess has a valid signature.
  • The transaction weight does not exceed the maximum permitted in a single block as defined by consensus.
  • The transaction excess is a valid public key. This proves that: $$ \Sigma \left( \mathrm{inputs} - \mathrm{outputs} - \mathrm{fees} \right) = 0 $$.
  • The transaction excess has a unique value across the whole chain.
  • The Tari script of each input must execute successfully and return the public key that signs the script signature.
  • The script offset \( \so\) is calculated and verified as per RFC-0201_TariScript.

Rejected transactions are dropped without service interruption and noted in log files.

Timelocked transactions are rejected by the mempool. The onus is on the client to submit transactions once they are able to be spent.

Note: More detailed information is available in the timelocks RFC document.

Valid transactions are:

Block/Transaction Weight

The weight of a transaction / block measured in "grams". Input, output and kernel weights reflect their respective relative storage and computation cost. Transaction fees are typically proportional to a transaction body's total weight, creating incentive to reduce the size of the UTXO set.

With a target block size of S and 1 gram to represent N bytes, we have a maximum block weight of S/N grams.

With an S of 1MiB and N of 16, the block and transaction body weights are as follows:

Byte sizeNatural WeightAdjustFinal
Output
- Per output83252052
- Tari Scriptvariablesize_of(script) / 160size_of(script) / 16
- Output Featuresvariablesize_of(features) / 160size_of(features) / 16
Input16911-29
Kernel size1138210

Pseudocode:

    output_weight = num_outputs * PER_OUTPUT_GRAMS(53)
    foreach output in outputs:
        output_weight += serialize(output.script) / BYTES_PER_GRAM
        output_weight += serialize(output.features) / BYTES_PER_GRAM
        
    input_weight = num_inputs * PER_INPUT_GRAMS(9)
    kernel_weight = num_kernels * PER_KERNEL_GRAMS(10)
    
    weight = output_weight + input_weight + kernel_weight

where the capitalized values are hard-coded constants.

Block Validation and Propagation

The block validation and propagation process is analogous to that of transactions. New blocks are received from the peer-to-peer network, or from an API call if the Base Node is connected to a Miner.

When a new block is received, it is passed to the block validation service. The validation service checks that:

  • The block has not been processed before.
  • Every transaction in the block is valid.
  • The proof-of-work is valid.
  • The block header is well-formed.
  • The block is being added to the chain with the highest accumulated proof-of-work.
    • It is possible for the chain to temporarily fork; Base Nodes SHOULD store orphaned forks up to some configured depth.
    • It is possible that blocks may be received out of order. Base Nodes SHOULD keep blocks that have block heights greater than the current chain tip for some preconfigured period.
  • The sum of all excesses is a valid public key. This proves that: $$ \Sigma \left( \mathrm{inputs} - \mathrm{outputs} - \mathrm{fees} \right) = 0$$.
  • That all kernel excess values are unique for that block and the entire chain.
  • Check if a block contains already spent outputs, reject that block.
  • The Tari script of every input must execute successfully and return the public key that signs the script signature.
  • The script offset \( \so\) is calculated and verified as per RFC-0201_TariScript.

Because Mimblewimble blocks can simply be seen as large transactions with multiple inputs and outputs, the block validation service checks all transaction verification on the block as well.

Rejected blocks are dropped silently.

Base Nodes are not obliged to accept connections from any peer node on the network. In particular:

  • Base Nodes MAY refuse connections from peers that have been added to a denylist.
  • Base Nodes MAY be configured to exclusively connect to a given set of peer nodes.

Validated blocks are

In addition, when a block has been validated and added to the blockchain:

  • The mempool MUST also remove all transactions that are present in the newly validated block.
  • The UTXO set MUST be updated by removing all inputs in the block, and adding all the new outputs into it.

Peer Validation and Propagation

When a peer attempts to connect or is shared by a trusted peer the base node will perform a peer validation. Ensuring the peer has a valid id, signature, and peer address before adding or propagating the peer back to the network.

Synchronizing and Pruning of the Chain

Syncing and pruning are discussed in detail in RFC-0140.

Archival Nodes

Archival nodes are used to keep a complete history of the blockchain since genesis block. They do not employ pruning at all. These nodes will allow full syncing of the blockchain, because normal nodes will not keep the full history to enable this. These nodes must sync from another archival node.

Pruned Nodes

Pruned nodes take advantage of the cryptography of mimblewimble to allow them to prune spent inputs and outputs beyond the pruning horizon and still validate the integrity of the blockchain i.e. no coins were destroyed or created beyond what is allowed by consensus rules. A sufficient number of blocks back from the tip should be configured because reorgs are no longer possible beyond that horizon. These nodes can sync from any other base node (archival and pruned).

Change Log

DateChangeAuthor
07 Jan 2019First draftCjS77
25 Jan 2019Pruning and cut-throughSWvheerden
07 Feb 2021SyncingSWvheerden
23 Sep 2021Block weightssbondi
19 Oct 2022Stabilizing updatesbrianp

RFC-0111/BaseNodesArchitecture

Base Node Architecture

status: stable

Maintainer(s): Cayle Sharrock

Licence

The 3-Clause BSD Licence.

Copyright 2022 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

The aim of this Request for Comment (RFC) is to describe the high-level Base Node architecture.

Architectural Layout

The Base Node architecture is designed to be modular, robust and performant.

Base Layer architecture

The major components are separated into separate modules. Each module exposes a public Application Programming Interface (API), which communicates with other modules using asynchronous messages via futures.

Base Node Service

The Base Node Service fields requests for the local nodes chain state and also accepts newly mined blocks that are propagating across the network. The service subscribes to NewBlock and BaseNodeRequest messages via the P2P comms interface. These messages are propagated across the P2P network and can also be received directly from other nodes. The service also provides a local interface to its functionality via an asynchronous Request-Response API.

The P2P message types this service subscribes to are:

  • NewBlock: A newly mined block is being propagated over the network. If the node has not seen the block before, the node will validate it. Its action depends on the validation outcome:

    • Invalid block - drop the block.
    • Valid block appending to the longest chain - add the block to the local state and propagate the block to peers.
    • Valid block forking off main chain - add the block to the local state and propagate the block to peers.
    • Valid block building off unknown block - add the orphan block to the local state.
  • BaseNodeServiceRequest: A collection of requests for chain data from the node.

Base Node State Machine Service

This service is essentially a finite state machine that synchronises its blockchain state with its peers. When the state machine decides it needs to synchronise its chain state with a peer it uses the Base Node Sync RPC service to do so. The RPC service allows for streaming of headers and blocks in a far more efficient manner than using the P2P messaging.

This service does not provide a local API but does provide an event stream and Status Info watch channel for other modules to subscribe to.

Mempool and Mempool Sync Services

The mempool service tracks valid transactions that the node knows about, but that have not yet been included in a block. The mempool is ephemeral and non-consensus critical, and as such may be a memory-only data structure. Maintaining a large mempool is far more important for Base Nodes serving miners than those serving wallets. The mempool structure itself is a set of hash maps as described in RFC-0190

When either the node reboots, or it synchronises a default number of 5 blocks, the Mempool sync service will contact peers and sync valid mempool transactions from them. After it has synced this service runs to field such requests from other peers.

The Mempool service handles Mempool Service Requests which it can receive from the P2P comms stack via its subscriptions, via the Mempool RPC service and via an internal Request-Response API. All these interfaces provide the following calls:

  • SubmitTransaction: Submit a transaction to be validated and included in the mempool. If the transaction is invalid it will be rejected with a reason.
  • GetTxStateByExcess: Request the state of a transaction if it exists in the mempool using its excess signature
  • GetStats and getState: Request information about the current status of the mempool.

Liveness Service

The Liveness service can be used by other modules to test the liveness of a specific peer and also periodically tests a set of its connected peers for liveness. This service subscribes to Ping P2P messages and responds with Pongs. The service gathers data about the monitored peer's liveness such as its latency. The Ping and Pong` messages also contain a copy of this nodes current Chain Metadata for use by the receiving nodes Chain Metadata Service.

Chain Metadata Service

The Chain Metadata Service maintains this nodes current Chain Metadata state to be sent out via Ping and Pong messages by the Liveness service. This node also monitors the Chain Metadata received from other peers in the Ping and Pong messages received by the Liveness service. Once a full round of Pong messages are received this service will emit this data as an event which the Base Node State Machine monitors.

Distributed Hash Table (DHT) Service

Peer discovery is a key service that blockchain nodes provide so that the peer mesh network can be navigated by the full nodes making up the network.

In Tari, the peer-to-peer network is not only used by full nodes (Base Nodes), but also by Validator Nodes, and Tari and Digital Assets Network (DAN) clients.

For this reason, peer management is handled internally by the Comms layer. If a Base Node wants to propagate a message, new block or transaction, for example, it simply selects a BROADCAST strategy for the message and the Comms layer will do the rest.

When a node wishes to query a peer for its peer list, this request will be handled by the DHTService. It will communicate with its Comms module's Peer Manager, and provide that information to the peer.

Blockchain Database

The blockchain database module is responsible for providing a persistent storage solution for blockchain state data. This module is used by the Base Node Service, Base Node State Machine, Mempool Service and the RPC servers. For Tari, this is delivered using the Lightning Memory-mapped Database (LMDB). LMDB is highly performant, intelligent and straightforward to use. An LMDB is essentially treated as a hash map data structure that transparently handles memory caching, disk Input/Output (I/O) and multi-threaded access. This module is shared by many services and so must be thread-safe.

Communication Interfaces

P2P communications

The Tari Peer to Peer messaging protocol is defined in RFC-0172. It is a fire-and-forget style protocol. Messages can be sent directly to a known peer, sent indirectly to an offline or unknown peer and broadcast to a set of peers. When a message is sent to specific peer it is propagated to the peers local neighbourhood and stored by those peers until it comes online to receive the message. Messages that are broadcast will be propagated around the network until the whole network has received them, they are not stored.

RPC Services

Fire-and-forget messaging is not efficient for point to point communications between online peers. For these applications the Base Node provides RPC services that present an API for clients to interact with. These RPC services provide a Request-Response interface defined by Profobuf for clients to use. RPC also allows for streaming of data which is much more efficient when transferring large amounts of data.

Examples of RPC services running in Base Node are:

  • Wallet RPC service: An RPC interface containing methods used by wallets to submit and query transactions on a Base Node
  • Base Node Sync RPC Service: Used by the Base Node State Machine Service to synchronise blocks
  • Mempool RPC Service: Provides the Mempool Service API via RPC

gRPC Interface

Base Nodes need to provide a local communication interface in addition to the P2P and RPC communication interface. This is best achieved using gRPC. The Base Node gRPC interface provides access to the public API methods of the Base Node Service, the mempool module and the blockchain state module, as discussed above.

gRPC access is useful for tools such as local User Interfaces (UIs) to a running Base Node; client wallets running on the same machine as the Base Node that want a more direct communication interface to the node than the P2P network provides; third-party applications such as block explorers; and, of course, miners.

A non-exhaustive list of methods the base node module API will expose includes:

  • Blockchain state calls, including:
    • checking whether a given Unspent Transaction Output (UTXO) is in the current UTXO set;
    • requesting the latest block height;
    • requesting the total accumulated work on the longest chain;
    • requesting a specific block at a given height;
    • requesting the Merklish root commitment of the current UTXO set;
    • requesting a block header for a given height;
    • requesting the block header for the chain tip;
    • validating signatures for a given transaction kernel;
    • validating a new block without adding it to the state tree;
    • validating and adding a (validated) new block to the state, and informing of the result (orphaned, fork, reorg, etc.).
  • Mempool calls
    • The number of unconfirmed transactions
    • Returning a list of transaction ranked by some criterion (of interest to miners)
    • The current size of the mempool (in transaction weight)
  • Block and transaction validation calls
  • Block synchronisation calls

Change Log

DateChangeAuthor
2 Jul 2019First outlineCjS77
11 Aug 2019UpdatesCjS77
15 Jun 2021Significant updatesSimianZa
11 Sep 2022Minor updateJorgeAnt
18 Jan 2023Minor updateJorgeAnt

RFC-0120/Consensus

Base Layer Consensus

status: stable

Maintainer(s): Cayle Sharrock, Stanley Bondi and SW van heerden

Licence

The 3-Clause BSD Licence.

Copyright 2019 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

The aim of this Request for Comment (RFC) is to describe the fields that a block should contain as well as all consensus rules that will determine the validity of a block.

Description

Blockchain consensus is a set of rules that a majority of nodes agree on that determines the state of the blockchain.

This RFC details the consensus rules for the Tari network.

Blocks

Every block MUST:

  • have exactly one valid block header, as per the Block Headers section

  • have exactly one coinbase transaction

  • have a total transaction weight less than the consensus maximum

  • be able to calculate matching Merkle roots (kernel_mr, output_mr, witness_mr, and input_mr)

  • each transaction input MUST:

  • each transaction output MUST:

    • be of an allowed transaction output version
    • have a unique domain separated hash (version || features || commitment || script || covenant || encrypted_values) with the domain (transaction_output)
    • have a unique commitment in the current UTXO set
    • be in a canonical order (see Transaction ordering)
    • have a valid range proof
    • have a valid metadata signature
    • contain only allowed opcodes in the script
  • each transaction kernel MUST

    • have a valid kernel excess signature
    • have a unique excess
  • have a valid total script offset, \( \gamma \), see script-offset.

  • the number of BURNED outputs MUST equal the number of BURNED_KERNEL kernels exactly,

  • the commitment values of each burnt output MUST match the commitment value of each corresponding BURNED_KERNEL exactly.

  • the transaction commitments and kernels MUST balance, as follows:

    $$ \begin{align} &\sum_i\mathrm{Cout_{i}} - \sum_j\mathrm{Cin_{j}} + \text{fees} \cdot H \stackrel{?}{=} \sum_k\mathrm{K_k} + \text{offset} \\ & \text{for each output}, i, \\ & \text{for each input}, j, \\ & \text{for each kernel excess}, k \\ & \text{and }\textit{offset }\text{is the total kernel offset} \\ \end{align} \tag{1} $$

If a block does not conform to the above, the block SHOULD be discarded and MAY ban the peer that sent it.

Coinbase

A coinbase transaction contained in a block MUST:

  • be the only transaction in the block with the coinbase flag
  • consist of exactly one output and one kernel (no input)
  • have a valid kernel signature
  • have a value exactly equal to the emission at the block height it was minted (see emission schedule) plus the total transaction fees within the block
  • have a lock-height as per consensus
  • can not have a offset except 0
  • can not have a script offset except 0

A coinbase transaction contained in a block CAN:

Block Headers

Every block header MUST contain the following fields:

  • version;
  • height;
  • prev_hash;
  • timestamp;
  • output_mr;
  • output_mmr_size;
  • input_mr;
  • witness_mr;
  • kernel_mr;
  • kernel_mmr_size;
  • total_kernel_offset;
  • script_kernel_offset;
  • nonce;
  • pow.

The block header MUST conform to the following:

  • The nonce and PoW must be valid for the block header.
  • The [achieved difficulty] MUST be greater than or equal to the target difficulty.
  • The FTL and MTP rules, detailed below.
  • The block hash must not appear in the bad block list.

The Merkle roots are validated as part of the full block validation, detailed in Blocks.

If the block header does not conform to any of the above, the block SHOULD be rejected and MAY ban the peer that sent it.

Version

This is the version currently running on the chain.

The version MUST conform to the following:

  • It is represented as an unsigned 16-bit integer.
  • Version numbers MUST be incremented whenever there is a change in the blockchain schema or validation rules starting from 0.
  • The version must be one of the allowed versions for the consensus rules at this block's height.

Height

A counter indicating how many blocks have passed since the genesis block (inclusive).

The height MUST conform to the following:

  • Represented as an unsigned 64-bit integer.
  • The height MUST be exactly one more than the block referenced in the prev_hash block header field.
  • The genesis block MUST have a height of 0.

Prev_hash

This is the hash of the previous block's header.

The prev_hash MUST conform to the following:

  • represented as an array of unsigned 8-bit integers (bytes) in little-endian format.
  • MUST be a hash of the entire contents of the previous block's header using the domain (block_header).

Timestamp

This is the timestamp at which the block was mined.

The timestamp MUST conform to the following:

  • Must be transmitted as UNIX timestamp.
  • MUST be less than FTL.
  • MUST be higher than the MTP.

Output_mr

The output_mr MUST be calculated as follows: Hash (TXO MMR root || Hash(spent TXO bitmap)).

The TXO MMR root is the MMR root that commits to every transaction output that has ever existed since the genesis block.

The spent TXO bitmap is a compact serialized roaring bitmap containing all the output MMR leaf indexes of all the outputs that have ever been spent.

The output_mr MUST conform to the following:

  • Represented as an array of unsigned 8-bit integers (bytes) in little-endian format.
  • The hashing function used MUST be blake2b with a 256-bit digest.

Output_mmr_size

This is the total size of the leaves in the output Merkle mountain range.

The Output_mmr_size MUST conform to the following:

  • Represented as a single unsigned 64-bit integer.

Input_mr

This is the Merkle root of all the inputs in the block, which consists of the hashed inputs. It is used to prove that all inputs are correct and not changed after mining. This MUST be constructed by adding, in order, the hash of every input contained in the block.

The input_mr MUST conform to the following:

  • Represented as an array of unsigned 8-bit integers (bytes) in little-endian format.
  • The hashing function must be blake2b with a 256-bit digest.

Witness_mr

This is the Merkle root of the output witness data, specifically all created outputsโ€™ range proofs and metadata signatures. This MUST be constructed by Hash ( RangeProof || metadata commitment signature), in order, for every output contained in the block.

The witness_mr MUST conform to the following:

  • Represented as an array of unsigned 8-bit integers (bytes) in little-endian format.
  • The hashing function used must be blake2b with a 256-bit digest.

Kernel_mr

This is the Merkle root of the kernels.

The kernel_mr MUST conform to the following:

  • Must be transmitted as an array of unsigned 8-bit integers (bytes) in little-endian format.
  • The hashing function used must be blake2b with a 256-bit digest.

Kernel_mmr_size

This is the total size of the leaves in the kernel Merkle mountain range.

The Kernel_mmr_size MUST conform to the following:

  • Represented as a single unsigned 64-bit integer.

Total_kernel_offset

This is the total summed offset of all the transactions in this block.

The total_kernel_offset MUST conform to the following:

  • Must be transmitted as an array of unsigned 8-bit integers (bytes) in little-endian format

Total_script_offset

This is the total summed script offset of all the transactions in this block.

The total_script_offset MUST conform to the following:

  • Must be transmitted as an array of unsigned 8-bit integers (bytes) in little-endian format

Nonce

This is the nonce used in solving the Proof of Work.

The nonce MUST conform to the following:

  • MUST be transmitted as an unsigned 64-bit integer;
  • for RandomX blocks, thus MUST be 0

PoW

This is the Proof of Work algorithm used to solve the Proof of Work. This is used in conjunction with the Nonce.

The [PoW] MUST contain the following:

  • pow_algo as an enum (0 for RandomX, 1 for Sha3x).
  • pow_data for RandomX blocks as an array of unsigned 8-bit integers (bytes) in little-endian format, containing the RandomX merge-mining Proof-of-Work data.
    • the RandomX seed, stored as randomx_key within the RandomX block, must have not been first seen in a block with confirmations more than max_randomx_seed_height.
  • pow_data for Sha3x blocks MUST be empty.

Difficulty Calculation

The target difficulty represents how difficult it is to mine a given block. This difficulty is not fixed and needs to constantly adjust to changing network hash rates.

The difficulty adjustment MUST be calculated using a linear-weighted moving average (LWMA) algorithm (2) $$ \newcommand{\solvetime}{ \mathrm{ST_i} } \newcommand{\solvetimemax}{ \mathrm{ST_{max}} } $$

SymbolValueDescription
N90Target difficulty block window
TSHA3x: 300 RandomX: 200Target block time in seconds. The value used depends on the PoW algorithm being used.
\( \solvetimemax \)SHA3x: 1800 RandomX: 1200Maximum solve time. This is six times the target time of the current PoW algorithm.
\( \solvetime \)variableThe timestamp difference in seconds between block i and i - 1 where \( 1 \le \solvetime \le \solvetimemax \)
\( \mathrm{D_{avg}} \)variableThe average difficulty of the last N blocks

$$ \begin{align} & \textit{weighted_solve_time} = \sum\limits_{i=1}^N(\solvetime*i) \\ & \textit{weighted_target_time} = (\sum\limits_{i=1}^Ni) * \mathrm{T} \\ & \textit{difficulty} = \mathrm{D_{avg}} * \frac{\textit{weighted_target_time}}{\textit{weighted_solve_time}}\\ \end{align} \tag{2} $$

It is important to note that the two proof of work algorithms are calculated independently. i.e., if the current block uses SHA3x proof of work, the block window and solve times only include SHA3x blocks and vice versa.

FTL

The Future Time Limit. This is how far into the future a time is accepted as a valid time. Any time that is more than the FTL is rejected until such a time that it is not more than the FTL. The FTL is calculated as (T*N)/20 with T and N defined as: T: Target time - This is the ideal time that should pass between blocks that have been mined. N: Block window - This is the number of blocks used when calculating difficulty adjustments.

MTP

The Median Time Passed (MTP) is the lower bound calculated by taking the median average timestamp of the last N blocks. Any block with a timestamp that is less than MTP will be rejected.

Total accumulated proof of work

This is defined as the total accumulated proof of work done on the blockchain. Tari uses two independent proof of work algorithms rated at different difficulties. To compare them, we simply multiply them together into one number: $$ \begin{align} \textit{accumulated_randomx_difficulty} * \textit{accumulated_sha3x_difficulty} \end{align} \tag{3} $$ This value is used to compare chain tips to determine the strongest chain.

Transaction Ordering

The order in which transaction inputs, outputs, and kernels are added to the Merkle mountain range completely changes the final Merkle root. Input, output, and kernel ordering within a block is, therefore, part of the consensus.

The block MUST be transmitted in canonical ordering. The advantage of this approach is that sorting does not need to be done by the whole network, and verification of sorting is exceptionally cheap.

  • Transaction outputs are sorted lexicographically by the byte representation of their Pedersen commitment i.e. ( \(k \cdot G + v \cdot H\) ).
  • Transaction kernels are sorted lexicographically by the excess signature byte representation.
  • Transaction inputs are sorted lexicographically by the hash of the output that is spent by the input.

Change Log

DateChangeAuthor
11 Oct 2022First stableSWvHeerden
13 Mar 2023Add mention of coinbase extraSWvHeerden
05 Jun 2023Add coinbase excess ruleSWvHeerden
01 Aug 2023Add Randomx rule, fix Sha and Monero namesSWvHeerden

RFC-0131/Mining

Full-node Mining on Tari Base Layer

status: stable

Maintainer(s): Hansie Odendaal

Licence

The 3-Clause BSD Licence.

Copyright 2020 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

This document describes the final proof-of-work strategy proposal for Tari main net.

This RFC replaces and deprecates RFC-0130: Mining

Description

The following proposal draws from many of the key points of debate from the Tari community on the topic of Tariโ€™s main chain proof of work strategy. The early working assumption was that Tari would be 100% merged mined by Monero.

Having a single, merge mined Proof of Work (PoW) algorithm would be nice, but the risks of hash rate attacks are real and meaningful. Double-spends and altering history can happen with >50% hash power, while selfish mining and eclipse attacks can happen with >33% hash power for a poorly connected attacker and >25% for a well-connected attacker (see Merged Mining: Analysis of Effects and Implications). Any non-merge mined PoW algorithm that is currently employed is even more vulnerable, especially if one can simply buy hash rate on platforms like NiceHash.

Hybrid mining is a strategy that apportions blocks across multiple PoW algorithms. If the hybrid algorithms are independent, then one can get at most x% of the total hash rate, where x is the fraction of blocks apportioned to that algorithm. As a result, the threat of a double-spend or selfish mining attack is mitigated, and in some cases eliminated.

This proposal puts forward Hybrid mining as the Tari PoW algorithm.

The choice of algorithms

In hybrid mining, "independence" of algorithms is key. If the same mining hardware can be used on multiple PoW algorithms in the hybrid mining scheme, you may as well not bother with hybrid mining because miners can simply switch between them.

In practice, no set of algorithms is genuinely independent. The best we can do is try to choose algorithms that work best on CPUs, GPUs, and ASICs. In truth, the distinction between GPUs and ASICs is only a matter of time. Any "GPU-friendly" algorithm is ASIC-friendly, too; it's just a case of whether the capital outlay for fabricating them is worth it, and this will eventually become true for any algorithm that supplies PoW for a growing market cap.

With this in mind, we should only choose one GPU/ASIC algorithm and one for CPUs.

An excellent technical choice would be merge mining with Monero using RandomX as a CPU-only algorithm and SHA3, also known as Keccak , for a GPU/ASIC-friendly algorithm. Using a custom configuration of such a simple and well-understood algorithm means there is a low likelihood of unforeseen optimizations that will give a single miner a considerable advantage. It also means that it stands a good chance of being "commoditized" when ASICs are eventually manufactured. This would mean that SHA3 ASICs are widely available from multiple suppliers.

The block distribution

A 50/50 split in hash rate among algorithms minimises the chance of hash rate attacks. However, sufficient buy-in is required, especially with regard to merge mining RandomX with Monero. To make it worthwhile for a Monero pool operator to merge mine Tari, but still guard against hash rate attacks and to be inclusive of independent Tari supporters and enthusiasts, a 60/40 split is employed in favour of merge mining RandomX with Monero.

The difficulty adjustment strategy

The choice of difficulty adjustment algorithm is important. In typical hybrid mining strategies, each algorithm operates completely independently with a scaled target block time. Tari testnet has been running very successfully using the Linear Weighted Moving Average (LWMA) from Bitcoin & Zcash Clones version 2018-11-27. This LWMA difficulty adjustment algorithm has also been tested in simulations, and it proved to be a good choice in the multi-PoW scene as well.

Final proposal, hybrid mining details

Tari's proof-of-work mining algorithm is summarized below:

  • Two mining algorithms, with an average combined target block time of 120 s, to match Monero's block interval.
  • A log-weighted moving average difficulty adjustment algorithm using a window of 90 blocks.

Tari mining hash

First, the block header is hashed with the 256-bit Blake2b hashing algorithm using domain-separated hashing, using the domain com.tari.base_layer.core.blocks. The fields are hashed in the order:

  • version
  • block height
  • previous header hash
  • timestamp
  • input Merkle root
  • output Merkle root
  • output Merkle mountain range size
  • witness Merkle root
  • kernel Merkle root
  • kernel Merkle mountain range size
  • total kernel offset
  • total script offset

This hash is used in both the SHA-3 and RandomX proof-of-work algorithms. The header version for the Tari Genesis block is 1.

RandomX

Monero blocks that are merge-mining Tari MUST include the Tari mining hash in the extra field of the Monero coinbase transaction.

Tari also imposes the following consensus rules:

  • The seed_hash MUST only be used for 3000 blocks, after which a block MUST be discarded if it's used again.
  • The little-endian difficulty MUST be equal to or greater than the target for that block as determined by the LWMA for Tari.
  • The LWMA MUST use a target time of 200 seconds.
  • MUST set the header field PoW:pow_algo as 0 for a Monero block
  • MUST encode the following data into the Pow:Pow_data field:
    • Monero BlockHeader,
    • RandomX VM key,
    • Monero transaction count,
    • Monero merkle root,
    • Monero coinbase merkle proof and,
    • Monero coinbase transaction

Sha-3x

Tari's independent proof-of-work algorithm is very straightforward.

Calculate the triple hash of the following input data:

  • Nonce (8 bytes)
  • Tari mining hash (32 bytes)
  • PoW record (for Sha-3x, this is always a single byte of value 1)

That is, the nonce in little-endian format, mining hash and the PoW record are chained together and hashed by the Keccak Sha3-256 algorithm. The result is hashed again, and this result is hashed a third time. The result of the third hash is compared to the target value of the current block difficulty.

If the entire 64-bit nonce space is exhausted without finding a valid block, the mining algorithm must request a new Tari mining hash from the Tari base node. The simplest way to do this is to update the timestamp field in the block header, keeping everything else constant.

Tari imposes the following consensus rules:

  • The Big endian difficulty MUST be equal or greater than the target difficulty for that block as determined by the LWMA for Tari. The difficulty and target are related by the equation difficulty = (2^256 - 1) / target.
  • MUST set the header field PoW:pow_algo as 1 for a Sha block.
  • The PoW:pow_data field is empty
  • The LWMA MUST use a target time of 300 seconds.

A triple hash is selected to keep the requirements on hardware miners (FPGAs, ASICs) fairly low. But we also want to avoid making the proof-of-work immediately "NiceHashable". There are several coins that already use a single or double SHA3 hash, and we'd like to avoid having that hashrate immediately deployable against Tari.

Historical note: In general, little-endian representations for integers are used in Tari. There are a few places where big-endian representations are used, in the PoW hash and in some merge-mined fields in particular. The latter is required to be compatible with the Monero specification. But we also use the big-endian representation for the block hash due to a historical convention. The Bitcoin white paper describes the block hash target as containing "a certain number of leading zeros" (paraphrased). This is obviously a big-endian representation. If we used little-endian, our block hashes would have trailing zeroes. So we use the big-endian form to satisfy the expected in block explorers and such that block hashes should always start with a series of zeroes.

Stabilisation note

This RFC is stable as of PR#4862

Change Log

DateChangeAuthor
2022-11-25Update mining hash decsriptionCjS77
2022-10-26Finalise SHA-3 algorithmCjS77
2022-10-11First outlineSWvHeerden

RFC-0132/MergeMiningMonero

Tari protocol for Merge Mining with Monero

status: stable

Maintainer(s): Stanley Bondi

Licence

The 3-Clause BSD Licence.

Copyright 2020 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

This document describes the specific protocol Tari uses to merge mine with Monero.

Introduction

Tari employs a hybrid mining strategy, accepting 2 mining algorithms whose difficulties are independent of each other as discussed in RFC-0131_Mining.html. This RFC details a protocol to enable Tari to accept Monero proof of work, enabling participating miners a chance to produce a valid block for either or both chain without additional mining effort.

The protocol must enable a Tari base node to make the following assertions:

REQ 1. The achieved mining difficulty exceeds target difficulty as dictated by Tari consensus,

REQ 2. The Monero block was constructed after the current Tari tip block. This is to prevent a miner from submitting blocks from the parent chain that satisfy the auxiliary chain's difficulty without doing new work.

It's worth noting that a Tari base node never has to contact or download data from Monero to make these assertions.

Merge Mining on Tari

A new Tari block template is obtained from a Tari base node by calling the get_new_block_template gRPC method, setting Monero as the chosen PoW algorithm. The Monero algorithm must be selected so that the correct mining difficulty for the Monero algorithm is returned. Remember, that Monero and SHA difficulties are independent (See RFC-0131_Mining.html). Next, a coinbase transaction is requested from a Tari Wallet for a give height by calling the get_coinbase gRPC function.

Next, the coinbase transaction is added to the new block template and passed back to the base node for the new MMR roots to be calculated. Furthermore, the base node constructs a Blake256 hash of some of the Tari header fields. We'll call this hash the merge mining hash \( h_m \) that commits to the following header fields in order: version, height,prev_hash,timestamp,input_mr, output_mr,output_mmr_size,kernel_mr, kernel_mmr_size,total_kernel_offset,total_script_offset. Note, this hash does not include the pow and nonce fields, as these fields are set as part of mining.

To have the chance of mining a Monero block as well as a Tari block, we must obtain a new valid monero block template, by calling get_block_template. This returns a blocktemplate_blob, that is, a serialized Monero block containing the Monero block header, coinbase and a list of hashes referencing the transactions included in the block. Additionally, a blockhashing_blob is a fixed size blob containing serialized_monero_header, merkle_tree_root and txn_count concatenated together. The merkle_tree_root is a merkle root of the coinbase + the transaction hashes contained in the block.

pub struct Block {
    /// The block header
    pub header: BlockHeader,
    /// Coinbase transaction a.k.a miner transaction
    pub miner_tx: Transaction,
    /// References to the transactions included in this block
    pub tx_hashes: Vec<hash::Hash>,
}

fig 1. The Monero block struct

Next, modify the Monero block template by including the merge mining hash \( h_m \) in the extra fields of the coinbase transaction. Monero has a merge mining subfield to accommodate this data. Importantly, the extra field data part of the coinbase transaction hash and therefore the merkle_tree_root, the blockhashing_blob must be reconstructed. A rust port of Monero's tree hash algorithm is needed to achieve this. The coinbase hash MUST be the first element to be hashed when constructing the merkle_tree_root. This satisfies REQ 2, proving that the proof-of-work was performed for the Tari block.

The block may now be mined. Once a solution is found that satisfies the Tari difficulty, the miner must include enough data to allow the Tari blockchain to assert REQ 1 and REQ 2. Concretely, A miner must serialize MoneroPowData using Monero consensus encoding and add it to the pow_data field in the Tari header.

pub struct MoneroPowData {
    /// Monero header fields
    header: MoneroBlockHeader,
    /// randomX vm key
    randomx_key: FixedByteArray, // Fixed 64 bytes
    /// transaction count
    transaction_count: u16,
    /// transaction root
    merkle_root: MoneroHash,
    /// Coinbase merkle proof hashes
    coinbase_merkle_proof: MerkleProof,
    /// Coinbase tx from Monero
    coinbase_tx: MoneroTransaction,
}

fig 2. Monero PoW data struct serialized in Tari blocks

pub struct MerkleProof {
   branch: Vec<Hash>,
   depth: u16,
   path_bitmap: u32,
}

fig 3. Merkle proof struct

A verifier may now check that the coinbase_tx contains the merge mining hash \( h_m \), and validate the coinbase_merkle_proof against the transaction_root. The coinbase_merkle_proof contains the minimal proof required to construct the transaction_root.

For example, a proof for a merkle tree of 4 hashes will require 2 hashes (h_1, h_23) of 32 bytes each, 4 bytes for the path bitmap and 2 bytes for the depth.

           Root*
         /      \
       h_c1*     h_23
      /    \       
     h_c*     h_1
 
 * Not included in proof

Serialisation

For Monero proof-of-work, Monero consensus encoding MUST be used to serialize the MoneroPowData struct. Given the same inputs, this encoding will byte-for-byte the same. The encoding uses VarInt for all integer types, allowing byte-savings, in particular for fields that typically contain small values. Importantly, extra bytes that a miner could tack onto the end of the pow_data field are expressly disallowed.

Merge Mining Proxy

The Tari merge mining proxy proxies the Monero daemon RPC interface. It behaves as a middleware that implements the merge mining protocol detailed above. This allows existing Monero miners to merge mine with Tari without having to make changes to mining software.

The proxy must be configured to connect to a monerod instance, a Tari base node, and a Tari console wallet. Most requests are forwarded "as is" to monerod, however some are intercepted and augmented before being returned to the miner.

get_block_template

Once monerod has provided the block template response, the proxy retrieves a Tari block template and coinbase, and assembles the Tari block. The merge mining hash \( h_m \) is generated and added to the Monero coinbase. The modified blockhashing_blob and blocktemplate_blob are returned to the miner. The difficulty is set to min(monero_difficulty, tari_difficulty) so that the miner submits the found block at either chain's difficulty. The Tari block template is cached for later submission.

submit_block

The miner submits a solved Monero block (at a difficulty of min(monero_difficulty, tari_difficulty)) to the proxy. The cached Tari block is retrieved, enriched with the MoneroPowData struct and submitted to the Tari base node.

Change Log

DateChangeAuthor
26 Oct 2022Stablise RFCCjS77
21 Oct 2022Update fieldsCifko

RFC-0140/SyncAndSeeding

Synchronizing the Blockchain: Archival and Pruned Modes

status: stable

Maintainer(s): S W van Heerden

Licence

The 3-Clause BSD Licence.

Copyright 2022 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

This Request for Comment (RFC) describes the syncing and pruning process.

Descriptions

Syncing

When a new node comes online, loses connection or encounters a chain reorganization that is longer than it can tolerate, it must enter syncing mode. This will allow it to recover its state to the newest up-to-date state. Syncing can be divided into two SynchronizationStrategys: complete sync and horizon sync. Complete sync means that the node communicates with an archive node to get the complete history of every single block from genesis block. Horizon Sync involves the node getting every block from its pruning horizon to current head, as well as every block header up to the genesis block.

To determine if the node needs to synchronise, the node will monitor the broadcasted chain_metadata messages provided by its neighbours. The fields in the chain_metadata messages MUST be:

FieldDescription
height_of_longest_chain64-bit unsigned
best_blockhash of the tip block (32-bit)
pruning_horizon64-bit unsigned
pruned_height64-bit unsigned
accumulated_difficulty128-bit unsigned
timestamp64-bit unsigned

Complete Sync

Complete sync is only available from archival nodes, as these will be the only nodes that will be able to supply the complete history required to sync every block with every transaction from genesis block up onto current head.

Complete Sync Process

Once the base node has determined that it is lagging behind the network tip it will start to synchronise with the peer it determines to have all the data required to synchronise.

The syncing process MUST be done in the following steps:

  1. Set SynchronizationState to header_sync.
  2. Sync all missing headers from the genesis block to the current chain tip. The initial header sync allows the node to confirm that the syncing peer does indeed have a fully intact chain from which to sync that adheres to this node's consensus rules and has a valid proof-of-work that is higher than any competing chains.
  3. Set SynchronizationState to block_sync.
  4. Start downloading blocks from sync peer starting with the oldest block in our database. A fresh node will start from the genesis block.
  5. Download all block up to current head, validating and adding the blocks to the local chain storage as we go.
  6. Once all blocks have been downloaded up and including the current network tip set the SynchronizationState to listening.

After this process, the node will be in sync, and will be able to process blocks and transactions normally as they arrive.

Horizon Sync Process

The horizon sync process MUST be done in the following steps:

  1. Set SynchronizationState to header_sync.
  2. Sync all missing headers from the genesis block to the current chain tip. The initial header sync allows the node to confirm that the syncing peer does indeed have a fully intact chain from which to sync that adheres to this nodes consensus rules and has a valid proof-of-work that is higher than any competing chains.
  3. Set SynchronizationState to horizon_sync.
  4. Download all kernels from the current network tip back to this node's pruning horizon.
  5. Validate kernel MMR root against headers.
  6. Download all utxo's from the current network tip back to this node's pruning horizon.
  7. Validate outputs and utxo MMR.
  8. Validate the chain balances with the expected total emission that the final sync height.
  9. Once all kernels and utxos have been downloaded from the network tip back to this node's pruning horizon set the SynchronizationState to block_sync. This hands over further syncing to the standard sync protocol which should return to the listening state if no further data has been received from peers.

After this process, the node will be in sync, and will be able to process blocks and transactions normally as they arrive.

Keeping in Sync

The node that is in the listening state SHOULD periodically test a subset of its peers with ping messages to ensure that they are alive. When a node sends a ping message, it MUST include the all the chain_metadata fields. The receiving node MUST reply with a pong message, which should also include its version of the chain_metadata information.

When a node receives pong replies from the current ping round, or the timeout expires, the collected chain_metadata replies will be examined to determine what the current best chain is, i.e. the chain with the most accumulated work. If the best chain is longer than out chain data the node will set SynchronizationState to header_sync and catch up with the network.

Chain Forks

Chain forks occur in all decentralized proof-of-work blockchains. When the local node is in the listening state it will detect that it has fallen behind other nodes in the network. It will then perform a header sync and during the header sync process will be able to detect that a chain fork has occurred. The header sync process will then determine which chain is the correct chain with the highest accumulated work. If required this node will switch the best chain and proceed to sync the new blocks required to catch up to the correct chain. This process is called a chain reorganization or reorg.

Pruning

In Mimblewimble, the state can be completely verified using the current UTXO set (which contains the output commitments and range proofs), the set of excess signatures (contained in the transaction kernels) and the PoW. The full block and transaction history is not required. This allows base layer nodes to remove old spent inputs from the blockchain storage.

Pruning is only for the benefit of the local Base Node, as it reduces the local blockchain size. Pruning only happens after the block is older than the pruning horizon height. A Base Node will either run in archival mode or pruned mode. If the Base Node is running in archive mode, it MUST NOT prune.

When running in pruning mode, Base Nodes SHOULD remove all spent outputs that are older than the pruning horizon in their current stored UTXO set when a new block is received from another Base Node.

Change Log

DateChangeAuthor
07 Feb 2019First draftSWvheerden
13 Jul 2021Update to better reflect the current implementationphilipr-za
30 Sep 2022Rename title for claritystringhandler
10 Nov 2022Minor update to reflect implementationmrnaveira
10 Oct 2023Minor update to reflect prune mode pruning intervalSWvheerden

RFC-0150/Wallets

Base Layer Wallet Module

status: stable

Maintainer(s): Cayle Sharrock

Licence

The 3-Clause BSD Licence.

Copyright 2019 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

The aim of this Request for Comment (RFC) is to propose the functionality and techniques required by the Base Layer Tari wallet module. The module exposes the core wallet functionality on which user-facing wallet applications may be built.

This RFC is derived from a proposal first made in this issue.

Description

Key Responsibilities

The wallet software is responsible for constructing and negotiating transactions for transferring and receiving Tari coins on the base layer. It should also provide functionality to generate, store and recover a master seed key and derived cryptographic keypairs that can be used for Base Layer addresses and signing of transactions.

Functional Details

A detailed description of the required functionality of the Tari software wallet is provided in three parts:

  • basic transaction functionality,
  • key management features, and
  • the different methods for recovering the wallet state of the Tari software wallet.

Basic Transaction Functionality

  • Wallet MUST be able to send and receive Tari coins using Mimblewimble transactions.
  • Wallet SHOULD be able to establish a connection with other user wallets to interactively negotiate:
    • construction of the transaction,
    • signing of multi-signature transactions.
  • Wallet SHOULD be implemented as a library or Application Programming Interface (API) so that Graphical User Interface (GUI) or Command Line Interface (CLI) applications can be developed on top of it.
  • Wallet MUST be able to establish connection to the base node, submit transactions and monitor the Tari blockchain.
  • Wallet SHOULD maintain an internal ledger to keep track of the Tari coin balance.
  • Wallet MAY offer transaction fee estimation, taking into account:
    • transaction byte size
    • network congestion
    • desired transaction priority
  • Wallet SHOULD be able to monitor and present states (Spent, Unspent or Unconfirmed) of previously submitted transactions, by querying information from the connected base node.
  • Wallet SHOULD present the total Spent, Unspent or Unconfirmed transactions in a summarized form.
  • Wallet SHOULD be able to update its software to patch potential security vulnerabilities. Automatic updating SHOULD be enabled by default, but users can decide to opt out.
  • Wallet SHOULD feature a caching mechanism for querying operations to reduce bandwidth consumption.

Key Management Features

  • Wallet MUST be able to generate a master seed key for the wallet by using at least one of the following methods:
    • input from the user (e.g. when restoring a wallet or in testing),
    • user-defined set of mnemonic word sequences using known word lists,
    • cryptographically secure random number generator.
  • Wallet SHOULD be able to generate derived, transactional, cryptographic keypairs from the master seed key using deterministic keypair generation.
  • Wallet SHOULD store wallet state using a password or passphrase encrypted persistent key-value database.
  • Wallet SHOULD provide the ability to store backup of the wallet state to a single encrypted file to simplify wallet recovery and reconstruction at a later stage.
  • Wallet MAY provide the ability to export the master seed key or the wallet state as a printable paper wallet, using coded markers.

State Recovery

  • Wallet MUST be able to reconstruct the wallet state from a manually entered master seed key.
  • Wallet MUST have a mechanism to systematically scan through the Tari blockchain and mempool for Unspent and Unconfirmed transactions, using keys derived from the master key.
  • The master seed key SHOULD be derivable from a set of mnemonic word sequences using known word lists.
  • Wallet MAY enable the reconstruction of the master seed key by scanning a coded marker of a paper wallet.

State Recovery: Process Overview

If the wallet database has been lost, corrupted or otherwise damaged, the outputs contained within (UTXOs) can still be recovered from the Tari blockchain, given you provide the valid recovery keys. When the wallet is first initialized in recovery mode, it attempts to synchronize with available base nodes, pulling blocks, attempting to recognize outputs attributed to that particular wallet.

If one can successfully decrypt the encrypted value, then the UTXO is successfully recognized. The next step is to attempt the mask (blinding factor) recovery by rewinding the range proof. All recognized and verified outputs are stored in the newly initialized, local wallet database, available for further spending.

The recovery of simple and stealth one-sided outputs is a bit more complex as we first have to recognize the output by its script pattern, before we can try to decrypt the encrypted value.

An output is recognized if it matches either of the following input script patterns:

  • The standard output is the simplest, having a single Nop instruction.
  • The simple one-sided is matched by the [Opcode::PushPubKey(scanned_pk)] so if the scanned_pk matches the key derived from the recovery phrase - it's recognized,
  • The stealth one-sided is similar to its simple counterpart with only the script pattern being different [Opcode::PushPubKey(nonce), Opcode::Drop, Opcode::PushPubKey(scanned_pk)], matching by the last provided Opcode::PushPubKey(scanned_pk) instruction.

Change Log

DateChangeAuthor
26 Oct 2022Stabilized RFCCjS77
14 Nov 2022Added table of contents, recovery process overview and a few minor adjustmentsagubarev & hansieodendaal

RFC-0152/EmojiId

Emoji Id specification

status: stable

Maintainer(s):Cayle Sharrock

Licence

The 3-Clause BSD Licence.

Copyright 2022. The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community regarding the technological merits of the potential system outlined herein.

Goals

This document describes the specification for Emoji Ids. Emoji Ids are encoded node ids used for humans to verify peer node addresses easily and for machines to verify that the address is being used in the correct context.

None

Description

Tari Communication Nodes are identified on the network via their Node ID; which in turn are derived from the node's public key. Both the node id and public key are simple large integer numbers.

The most common practice for human beings to copy large numbers in cryptocurrency software is scanning a QR code or copying and pasting a value from one application to another. These numbers are typically encoded using hexadecimal or Base58 encoding. The user will then typically scan (parts) of the string by eye to ensure that the value was transferred correctly.

For Tari, we propose encoding values, the node ID in particular and masking the network identifier, for Tari, using emojis. The advantages of this approach are:

  • Emoji are more easily identifiable; and, if selected carefully, less prone to identification errors (e.g., mistaking an O for a 0).
  • The alphabet can be considerably larger than hexadecimal (16) or Base58 (58), resulting in shorter character sequences in the encoding.
  • Should be be able to detect if the address used belongs to the correct network.

The specification

Emoji map

An emoji alphabet of 256 characters is selected. Each emoji is assigned a unique index from 0 to 255 inclusive. The list of selected emojis is:

๐Ÿฆ‹๐Ÿ“Ÿ๐ŸŒˆ๐ŸŒŠ๐ŸŽฏ๐Ÿ‹๐ŸŒ™๐Ÿค”๐ŸŒ•โญ๐ŸŽ‹๐ŸŒฐ๐ŸŒด๐ŸŒต๐ŸŒฒ๐ŸŒธ
๐ŸŒน๐ŸŒป๐ŸŒฝ๐Ÿ€๐Ÿ๐Ÿ„๐Ÿฅ‘๐Ÿ†๐Ÿ‡๐Ÿˆ๐Ÿ‰๐ŸŠ๐Ÿ‹๐ŸŒ๐Ÿ๐ŸŽ
๐Ÿ๐Ÿ‘๐Ÿ’๐Ÿ“๐Ÿ”๐Ÿ•๐Ÿ—๐Ÿš๐Ÿž๐ŸŸ๐Ÿฅ๐Ÿฃ๐Ÿฆ๐Ÿฉ๐Ÿช๐Ÿซ
๐Ÿฌ๐Ÿญ๐Ÿฏ๐Ÿฅ๐Ÿณ๐Ÿฅ„๐Ÿต๐Ÿถ๐Ÿท๐Ÿธ๐Ÿพ๐Ÿบ๐Ÿผ๐ŸŽ€๐ŸŽ๐ŸŽ‚
๐ŸŽƒ๐Ÿค–๐ŸŽˆ๐ŸŽ‰๐ŸŽ’๐ŸŽ“๐ŸŽ ๐ŸŽก๐ŸŽข๐ŸŽฃ๐ŸŽค๐ŸŽฅ๐ŸŽง๐ŸŽจ๐ŸŽฉ๐ŸŽช
๐ŸŽฌ๐ŸŽญ๐ŸŽฎ๐ŸŽฐ๐ŸŽฑ๐ŸŽฒ๐ŸŽณ๐ŸŽต๐ŸŽท๐ŸŽธ๐ŸŽน๐ŸŽบ๐ŸŽป๐ŸŽผ๐ŸŽฝ๐ŸŽพ
๐ŸŽฟ๐Ÿ€๐Ÿ๐Ÿ†๐Ÿˆโšฝ๐Ÿ ๐Ÿฅ๐Ÿฆ๐Ÿญ๐Ÿฐ๐Ÿ€๐Ÿ‰๐ŸŠ๐ŸŒ๐Ÿ
๐Ÿฆ๐Ÿ๐Ÿ‘๐Ÿ”๐Ÿ™ˆ๐Ÿ—๐Ÿ˜๐Ÿ™๐Ÿš๐Ÿ›๐Ÿœ๐Ÿ๐Ÿž๐Ÿข๐Ÿฃ๐Ÿจ
๐Ÿฆ€๐Ÿช๐Ÿฌ๐Ÿญ๐Ÿฎ๐Ÿฏ๐Ÿฐ๐Ÿฆ†๐Ÿฆ‚๐Ÿด๐Ÿต๐Ÿถ๐Ÿท๐Ÿธ๐Ÿบ๐Ÿป
๐Ÿผ๐Ÿฝ๐Ÿพ๐Ÿ‘€๐Ÿ‘…๐Ÿ‘‘๐Ÿ‘’๐Ÿงข๐Ÿ’…๐Ÿ‘•๐Ÿ‘–๐Ÿ‘—๐Ÿ‘˜๐Ÿ‘™๐Ÿ’ƒ๐Ÿ‘›
๐Ÿ‘ž๐Ÿ‘Ÿ๐Ÿ‘ ๐ŸฅŠ๐Ÿ‘ข๐Ÿ‘ฃ๐Ÿคก๐Ÿ‘ป๐Ÿ‘ฝ๐Ÿ‘พ๐Ÿค ๐Ÿ‘ƒ๐Ÿ’„๐Ÿ’ˆ๐Ÿ’‰๐Ÿ’Š
๐Ÿ’‹๐Ÿ‘‚๐Ÿ’๐Ÿ’Ž๐Ÿ’๐Ÿ’”๐Ÿ”’๐Ÿงฉ๐Ÿ’ก๐Ÿ’ฃ๐Ÿ’ค๐Ÿ’ฆ๐Ÿ’จ๐Ÿ’ฉโž•๐Ÿ’ฏ
๐Ÿ’ฐ๐Ÿ’ณ๐Ÿ’ต๐Ÿ’บ๐Ÿ’ป๐Ÿ’ผ๐Ÿ“ˆ๐Ÿ“œ๐Ÿ“Œ๐Ÿ“Ž๐Ÿ“–๐Ÿ“ฟ๐Ÿ“กโฐ๐Ÿ“ฑ๐Ÿ“ท
๐Ÿ”‹๐Ÿ”Œ๐Ÿšฐ๐Ÿ”‘๐Ÿ””๐Ÿ”ฅ๐Ÿ”ฆ๐Ÿ”ง๐Ÿ”จ๐Ÿ”ฉ๐Ÿ”ช๐Ÿ”ซ๐Ÿ”ฌ๐Ÿ”ญ๐Ÿ”ฎ๐Ÿ”ฑ
๐Ÿ—ฝ๐Ÿ˜‚๐Ÿ˜‡๐Ÿ˜ˆ๐Ÿค‘๐Ÿ˜๐Ÿ˜Ž๐Ÿ˜ฑ๐Ÿ˜ท๐Ÿคข๐Ÿ‘๐Ÿ‘ถ๐Ÿš€๐Ÿš๐Ÿš‚๐Ÿšš
๐Ÿš‘๐Ÿš’๐Ÿš“๐Ÿ›ต๐Ÿš—๐Ÿšœ๐Ÿšข๐Ÿšฆ๐Ÿšง๐Ÿšจ๐Ÿšช๐Ÿšซ๐Ÿšฒ๐Ÿšฝ๐Ÿšฟ๐Ÿงฒ

The emoji have been selected such that:

  • Similar-looking emoji are excluded from the map. For example, neither ๐Ÿ˜ or ๐Ÿ˜„ should be included. Similarly, the Irish and Cรดte d'Ivoire flags look very similar, and both should be excluded.
  • Modified emoji (skin tones, gender modifiers) are excluded. Only the "base" emoji are considered.

The selection of an alphabet with 256 symbols means there is a direct mapping between bytes and emoji.

Encoding

The emoji ID is calculated from a node public key B (serialized as 32 bytes) and a network identifier N (serialized as 8 bits) as follows:

  • Use the DammSum algorithm with k = 8 and m = 32 to compute an 8-bit checksum C using B as input.
  • Compute the masked checksum C' = C XOR N.
  • Encode B into an emoji string using the emoji map.
  • Encode C' into an emoji character using the emoji map.
  • Concatenate B and C' as the emoji ID.

The result is 33 emoji characters.

Decoding

The node public key is obtained from an emoji ID and a network identifier N (serialized to 8 bits) as follows:

  • Assert that the emoji ID contains exactly 33 valid emoji characters from the emoji alphabet. If not, return an error.
  • Decode the emoji ID as an emoji string by mapping each emoji character to a byte value using the emoji map, producing 33 bytes. Let B be the first 32 bytes and C' be the last byte.
  • Compute the unmasked checksum C = C' XOR N.
  • Use the DammSum validation algorithm on B to assert that C is the correct checksum. If not, return an error.
  • Attempt to deserialize B as a public key. If this fails, return an error. If it succeeds, return the public key.

Checksum effectiveness

It is important to note that masking the checksum reduces its effectiveness. Namely, if an emoji ID is presented with a different network identifier, and if there is a transmission error, it is possible for the result to decode in a seemingly valid way with a valid checksum after unmasking. If both conditions occur randomly, the likelihood of this occurring is n / 256 for n possible network identifiers.

Since emoji ID will typically be copied digitally and therefore not particularly subject to transmission errors, so it seems unlikely for these conditions to coincide in practice.

Change Log

DateChangeAuthor
2022-11-10Initial stableSWvHeerden
2022-11-11Algorithm improvementsAaronFeickert

RFC-0160/BlockSerialization

Tari Block Binary Serialization

status: stable

Maintainer(s): Stanley Bondi

Licence

The 3-Clause BSD Licence.

Copyright 2021 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

The aim of this Request for Comment (RFC) is to specify the binary serialization of:

  1. a mined Tari block
  2. a Tari block mining template

This is to facilitate interoperability of mining software and hardware.

Specification

By reviewing the block and mining template fields below, we have the following underlying data types for serialization:

  1. bool
  2. u8
  3. u16
  4. u64
  5. i64
  6. array of type [u8; n]
  7. Vec<T> where T is u8, enum or array

For 1. to 5. and all numbers, Base 128 Varint encoding MUST be used.

From the Protocol Buffers documentation:

Varints are a method of serializing integers using one or more bytes. Smaller numbers take a smaller number of bytes. Each byte in a varint, except the last byte, has the most significant bit (msb) set โ€“ this indicates that there are further bytes to come. The lower 7 bits of each byte are used to store the two's complement representation of the number in groups of 7 bits, least significant group first.

For 6. to 7., the dynamically sized array and Vec type, the encoded array MUST be preceded by a number indicating the length of the array. This length MUST also be encoded as a varint. By prepending the length of the array, the decoder knows how many elements to decode as part of the sequence.

Block field ordering

Using this varint encoding, all fields of the complete block MUST be encoded in the following order:

  1. Version
  2. Height
  3. Previous block hash
  4. Timestamp
  5. Output Merkle root
  6. Witness Merkle root
  7. Output Merkle mountain range size
  8. Kernel Merkle root
  9. Kernel Merkle mountain range size
  10. Input Merkle root
  11. Total kernel offset
  12. Total script offset
  13. Nonce
  14. Proof of work algorithm
  15. Proof of work supplemental data
  16. Transaction inputs - for each input:
    • Version
    • Spent output - for each output:
      • Version
      • Features
        • Version
        • Maturity
        • Output type
        • Sidechain features
        • Metadata
      • Commitment
      • Script
      • Sender offset public key
      • Covenant
      • Encrypted value
      • Minimum value promise
    • Input data (vector of Stack items)
    • Script signature
  17. Transaction outputs - for each output:
    • Version
    • Features
      • Version
      • Maturity
      • Output type
      • Sidechain features
      • Metadata
    • Commitment
    • Range proof
    • Script
    • Sender offset public key
    • Metadata signature
    • Covenant
    • Encrypted value
    • Minimum value promise
  18. Transaction kernels - for each kernel:
    • Version
    • Features
    • Fee
    • Lock height
    • Excess
    • Excess signature public nonce
    • Excess signature
    • Burn commitment

Mining template field ordering

The new block template is provided to miners to complete. Its fields MUST also be encoded using varints, in the following order:

  1. Version
  2. Height
  3. Previous block hash
  4. Total kernel offset
  5. Total script offset
  6. Proof of work algorithm
  7. Proof of work supplemental data
  8. Transaction inputs - for each input:
    • Version
    • Spent output - for each output:
      • Version
      • Features
        • Version
        • Maturity
        • Output type
        • Sidechain features
        • Metadata
      • Commitment
      • Script
      • Sender offset public key
      • Covenant
      • Encrypted value
      • Minimum value promise
    • Input data (vector of Stack items)
    • Script signature
  9. Transaction outputs - for each output:
    • Version
    • Features
      • Version
      • Maturity
      • Output type
      • Sidechain features
      • Metadata
    • Commitment
    • Range proof
    • Script
    • Sender offset public key
    • Metadata signature
    • Covenant
    • Encrypted value
    • Minimum value promise
  10. Transaction kernels - for each kernel:
    • Version
    • Features
    • Fee
    • Lock height
    • Excess
    • Excess signature public nonce
    • Excess signature
    • Burn commitment
  11. Target difficulty
  12. Reward
  13. Total fees

Value encryption

The value of the value commitment MUST be encrypted using ChaCha20Poly1305 Authenticated Encryption with Additional Data (AEAD). The AEAD key MUST be 32 bytes in size and produced using a domain separated hash of the private key and commitment.

Hash domains

To use a single hash function for producing a sampling of multiple independent hash functions, it's common to employ domain separation. Tari uses the hashing API within the tari codebase to achieve proper hash domain separation.

The following functional areas MUST each use a separate hash domain that is unique in the tari codebase:

  • kernel Merkle Mointain Range;
  • witness MMR;
  • output MMR;
  • input MMR;
  • value encryption.

To achieve interoperability with other blockchains like Bitcoin and Monero, for example an atomic swap, TariScript MUST NOT make use of hash domains.

Tari Block and Mining Template - Data Types

A Tari block is composed of the block header and aggregate body.

Here we describe the respective Rust types of these fields in the tari codebase, and their underlying data types:

Block Header

FieldAbstract TypeData TypeDescription
Versionu16u16The Tari protocol version number, used for soft/hard forks
Heightu64u64Height of this block since the genesis block
Previous Block HashBlockHash[u8;32]Hash of the previous block in the chain
TimestampEpochTimeu64Timestamp at which the block was built (number of seconds since Unix epoch)
Output Merkle RootBlockHash[u8;32]Merkle Root of the unspent transaction ouputs
Witness Merkle RootBlockHash[u8;32]MMR root of the witness proofs
Output MMR Sizeu64u64The size (number of leaves) of the output and range proof MMRs at the time of this header
Kernel Merkle RootBlockHash[u8;32]MMR root of the transaction kernels
Kernel MMR Sizeu64u64Number of leaves in the kernel MMR
Input Merkle RootBlockHash[u8;32]Merkle Root of the transaction inputs in this block
Total Kernel OffsetBlindingFactor[u8;32]Sum of kernel offsets for all transaction kernels in this block
Total Script OffsetBlindingFactor[u8;32]Sum of script offsets for all transaction kernels in this block
Nonceu64u64Nonce increment used to mine this block
PowProofOfWorkSee Proof Of WorkProof of Work information

[u8;32] indicates an array of 32 unsigned 8-bit integers

Proof Of Work

FieldAbstract TypeData TypeDescription
Proof of Work AlgorithmPowAlgorithmu8The algorithm used to mine this block ((Monero or SHA3))
Proof of Work DataVec<u8>u8Supplemental proof of work data. For example for Sha3, this would be empty (only the block header is required), but for Monero merge mining we need the Monero block header and RandomX seed hash

Block Body

FieldAbstract TypeData TypeDescription
SortedboolboolTrue if sorted
Transaction InputsVec<TransactionInput>TransactionInputList of inputs spent
Transaction OutputsVec<TransactionOutput>TransactionOutputList of outputs produced
Transaction KernelsVec<TransactionKernel>TransactionKernelKernels contain the excesses and their signatures for the transactions

A further breakdown of the body fields is described below:

TransactionInput

FieldAbstract TypeData TypeDescription
VersionTransactionInputVersionu16The features of the output being spent. We will check maturity for all outputs.
Spent OutputSpentOutputSee SpentOutputEither the hash of TransactionOutput that this Input is spending or its data
Input DataExecutionStackVec<u8>The script input data, maximum size is 512
Script SignatureCommitmentAndPublicKeySignatureSee CommitmentAndPublicKeySignatureA signature signing the script and all other transaction input metadata with the script private key
SpentOutput
FieldAbstract TypeData TypeDescription
OutputHashHashOutput[u8;32]The features of the output being spent. We will check maturity for all outputs.
VersionTransactionOutputVersionu8The TransactionOutput version
FeaturesOutputFeaturesSee OutputFeaturesOptions for the output's structure or use
CommitmentPedersenCommitment[u8;32]The commitment referencing the output being spent.
ScriptTariScriptVec<u8>The serialised script, maximum size is 512
Sender Offset Public KeyCommitmentAndPublicKeySignature[u8;32]The Tari script sender offset public key
CovenantVec<CovenantToken>See CovenantTokenA future-based contract detailing input and output metadata
Encrypted ValueEncryptedValue[u8;24]The encrypted value of the value commitment
Minimum Value PromiseMicroTariu64 (See Value encryption)The minimum value promise embedded in the range proof
CommitmentAndPublicKeySignature
FieldAbstract TypeData TypeDescription
Public NoncePedersenCommitment[u8;32]The public (Pedersen) commitment nonce created with the two random nonces
uSecretKey[u8;32]The first publicly known private key of the signature signing with the value
vSecretKey[u8;32]The second publicly known private key of the signature signing with the blinding factor
Public KeyPedersenCommitment[u8;32]The public nonce of the Schnorr signature
aSecretKey[u8;32]The publicly known private key of the Schnorr signature

Find out more about CommitmentAndPublicKey signatures:

OutputFeatures
FieldAbstract TypeData TypeDescription
VersionOutputFeaturesVersionu8The OutputFeatures version
Output TypeOutputTypeu8The type of output
Maturityu64u64The block height at which the output can be spent
MetadataVec<u8>u8The block height at which the output can be spent
Side-chain FeaturesSideChainFeaturesnoneNot implemented
CovenantToken
FieldAbstract TypeData TypeDescription
FilterCovenantFilterSee CovenantFilterThe covenant filter
ArgCovenantArgSee CovenantArgThe covenant argument(s)
CovenantFilter
FieldAbstract TypeData TypeDescription
IdentityIdentityFilternoneThe Identity
AndAndFilternoneAnd operation
OrOrFilternoneOr operation
XorXorFilternoneXor operation
NotNotFilternoneNot operation
Output Hash EqualOutputHashEqFilternoneOutput hash to be equal to
Fields PreservedFieldsPreservedFilternoneFields to be preserved
Field EqualFieldEqFilternoneField to be equal to
Fields Hashed EqualFieldsHashedEqFilternoneFields hashed to be equal to
Absolute HeightAbsoluteHeightFilternoneAbsolute height
CovenantArg
FieldAbstract TypeData TypeDescription
HashFixedHash[u8;32]Future hash value
PublicKeyPublicKey[u8;32]Future public key
CommitmentPedersenCommitment[u8;32]Future commitment referencing the output being spent.
Tari ScriptTariScriptVec<u8>Future serialised script, maximum size is 512
CovenantVec<CovenantToken>See CovenantTokenFuture covenant
Output TypeOutputTypeu8Future type of output
UintUintu64Future value
Output FieldOutputFieldSee OutputFieldFuture set of output fields
OutputFieldsVec<OutputField>See OutputFieldFuture set of output fields
BytesVec<u8>u8Future raw data
OutputField
FieldAbstract TypeData TypeDescription
Commitmentu8u8Commitment byte code
Scriptu8u8Script byte code
Sender Offset Public Keyu8u8SenderOffsetPublicKey byte code
Covenantu8u8Covenant byte code
Featuresu8u8Features byte code
Features Output Typeu8u8FeaturesOutputType byte code
Features Maturityu8u8FeaturesMaturity byte code
Features Metadatau8u8FeaturesMetadata byte code
Features Side Chain Featuresu8u8FeaturesSideChainFeatures byte code

TransactionOutput

FieldAbstract TypeData TypeDescription
VersionTransactionOutputVersionu8The TransactionOutput version
FeaturesOutputFeaturesSee OutputFeaturesOptions for the output's structure or use
CommitmentPedersenCommitment[u8;32]The homomorphic commitment representing the output amount
ProofRangeProofVec<u8>A proof that the commitment is in the right range
ScriptTariScriptVec<u8>The script that will be executed when spending this output
Sender Offset Public KeyPublicKey[u8;32]The Tari script sender offset public key
Metadata SignatureCommitmentAndPublicKeySignatureSee CommitmentAndPublicKeySignatureA signature signing all transaction output metadata with the script offset private key and spending key
CovenantVec<CovenantToken>See CovenantTokenA future-based contract detailing input and output metadata
Encrypted ValueEncryptedValue[u8;24]The encrypted value of the value commitment
Minimum Value PromiseMicroTariu64 (See Value encryption)The minimum value promise embedded in the range proof

TransactionKernel

FieldAbstract TypeData TypeDescription
VersionTransactionKernelVersionu8The TransactionKernel version
FeaturesKernelFeaturesu8Options for a kernel's structure or use
FeeMicroTariu64Fee originally included in the transaction this proof is for.
Lock Heightu64u64This kernel is not valid earlier than this height. The max maturity of all inputs to this transaction
ExcessPedersenCommitment[u8;32]Remainder of the sum of all transaction commitments (minus an offset). If the transaction is well-formed, amounts plus fee will sum to zero, and the excess is a valid public key
Excess SignatureRistrettoSchnorrSee RistrettoSchnorrAn aggregated signature of the metadata in this kernel, signed by the individual excess values and the offset excess of the sender
Burn CommitmentPedersenCommitment[u8;32]This is an optional field that must be set if the transaction contains a burned output

RistrettoSchnorr

FieldAbstract TypeData TypeDescription
Public noncePublicKey[u8;32]The public nonce of the Schnorr signature
SignatureSecretKey[u8;32]The signature of the Schnorr signature

New Block Template

The new block template is used in constructing a new partial block, allowing a miner to add the coinbase UTXO and as a final step for the Base node to add the MMR roots to the header.

FieldAbstract TypeData TypeDescription
HeaderNewBlockHeaderTemplateSee New Block Header TemplateThe Tari protocol version number, used for soft/hard forks
BodyAggregateBodySee Block BodyHeight of this block since the genesis block
Target DifficultyDifficultyu64The minimum difficulty required to satisfy the Proof of Work for the block
RewardMicroTariu64The value of the emission for the coinbase output for the block
Total FeesMicroTariu64The sum of all transaction fees in this block

New Block Header Template

FieldAbstract TypeData TypeDescription
Versionu16u16The Tari protocol version number, used for soft/hard forks
Heightu64u64Height of this block since the genesis block
Previous HashBlockHash[u8;32]Hash of the previous block in the chain
Total Kernel OffsetBlindingFactor[u8;32]Total accumulated sum of kernel offsets since genesis block. We can derive the kernel offset sum for this block from the total kernel offset of the previous block header.
Total Script OffsetBlindingFactor[u8;32]Sum of script offsets for all transaction kernels in this block
PowProofOfWorkSee Proof Of WorkProof of Work information

RFC-0170/NetworkCommunicationProtocol

The Tari Communication Network and Network Communication Protocol

status: stable

Maintainer(s): Stringhandler

License

The 3-Clause BSD License.

Copyright 2018 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

The purpose of this document and its content is for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community regarding the technological merits of the potential system outlined herein.

Goals

This document will introduce the Tari communication network and the communication protocol used to select, establish and maintain connections between peers on the network. Communication Nodes and Communication Clients will be introduced and their required functionality will be proposed.

Description

Assumptions

  • A communication channel can be established between two peers once their online communication addresses are known to each other.

Abstract

The backbone of the Tari communication network consists of a large number of nodes that maintain peer connections between each other. These nodes forward and propagate encrypted and unencrypted data messages through the network such as joining requests, discovery requests, transactions and completed blocks. Network clients, not responsible for maintaining the network, are able to create ad hoc connections with nodes on the network to perform joining and discovery requests. The majority of communication between clients and nodes will be performed using direct Peer-to-peer (P2P) communication once the discovery process was used to obtain the online communication addresses of peers. Where possible the efficient Kademlia based directed forwarding of encrypted data messages can be used to perform quick node discovery and joining of clients and nodes on the Tari communication network. Where messages are of importance to a wide variety of entities on the network, Gossip protocol based message propagation can be performed to distribute the message to the entire network.

Overview

The Tari communication network is a variant of a Kademlia network that allows for fast discovery of nodes, with an added ability to perform Gossip protocol based broadcasting of data messages to the entire network. The majority of the communication required on the Base Layer and Digital Asset Network (DAN) will be performed via direct P2P communication between known clients and nodes. Alternatively, the Tari communication network can be used for broadcasting joining requests, discovery requests and propagating data messages such as completed blocks, transactions and data messages that are of interest to a large part of the Tari communication network.

The Tari communication network consists of a number of different entities that need to communicate in a distributed and ad-hoc manner. The primary entities that need to communicate are Validator Nodes (VN), Base Nodes (BN), and Wallets (W). Here are some examples of different communication tasks that need to be performed by these entities on the Tari Communication network:

  • Base Nodes on the Base Layer need to propagate completed blocks and transactions to other Base Nodes using Gossip protocol based broadcasting.
  • Wallets need to communicate and negotiate with other Wallets to create transactions. They also need the ability to submit transactions to the mempool of Base Nodes.
  • Validator Nodes need to communicate with other Validator Nodes to perform consensus. Note that in future, the Validator Nodes may run on a different network.

Here is an overview communication matrix that show which source entities SHOULD initiate communication with destination entities on the Tari Communication network:

Destination (across)
Source (down)
Validator NodeBase NodeWallet
Validator NodeYesNoNo
Base NodeNoYesNo
WalletNoYesYes

Communication Nodes and Communication Clients

To simplify the description of the Tari communication network, the different entities with similar behaviour were grouped into two groups: Communication Nodes and Communication Clients.

  • Validator Nodes and Base Nodes are Communication Nodes (CN).
  • Wallets are Communication Clients (CC).

CNs form the core communication infrastructure of the Tari communication network and are responsible for maintaining the Tari communication network by receiving, forwarding and distributing joining requests, discovery requests, data messages and routing information. CCs are different from CNs in that they do not maintain the network and they are not responsible for propagating any joining requests, discovery requests, data messages and routing information. They do make use of the network to submit their own joining requests and perform discovery request of other specific CNs and CCs when they need to communicate with them. Once a CC has discovered the CC or CN they want to communicate with, they will establish a direct P2P channel with them. The Tari communication network is unaware of this direct P2P communication once discovery is completed.

The different entity types MUST be grouped into the different communication node types as follows:

Entity TypeCommunication Node Type
Validator NodeCommunication Node
Base NodeCommunication Node
WalletCommunication Client

Unique identification of Communication Nodes and Communication Clients

In the Tari communication network, each CN or CC makes use of a node ID to determine their position in the network. This node ID can be derived from the CNs or CCs identification public key. The method used to obtain a node ID will either enhance or limit the trustworthiness of that entity when propagating messages through them on the Tari communication network.

The similarity or distance between different node IDs can be calculated by performing the Hamming distance between the bits of the two node ID numbers. The Hamming distance can be implemented as an Exclusive OR (XOR) between the bits of the numbers and the summation of the resulting true bits. CCs and/or CNs that have similar node IDs, that produce a small Hamming distance, are located in similar regions of the Tari communication network. This does not mean that their geographic locations are near each other, but rather that their location in the network is similar. A thresholding scheme can be applied to the Hamming distance to ensure that only neighboring CNs with similar node IDs are allowed to share and propagate specific information. As an example, only routing table information that contains similar node IDs to the requesting CCs or CNs node ID should be shared with them. Limiting the sharing of routing table information makes it more difficult to map the entire Tari communication network.

Note that Mining Workers are excluded from the Tari communication network. A Mining Server will have a local or remote connection with a Base Node. They do not need to make use of the communication network and they are not responsible for propagating any messages on the network. The parent Base Node will perform any communication tasks on the Tari communication network on their behalf.

Online Communication Address, Peer Address and Routing Table

Each CC and CN on the Tari communication network will have identification cryptographic keys, a node ID and an online communication address. The online communication address SHOULD be either an IPv4, IPv6, Or Tor (Base32) address and can be stored using the network address type as follows:

DescriptionData typeComments
address typeuint4Specify if IPv4/IPv6/Tor
addresschar arrayIPv4, IPv6, Tor (Base32) address
portuint16port number

Tari uses the Multiaddr format for addresses.

A Tor address can be used when anonymity is important for a CC or CN. The IPv4 and IPv6 address types do not provide any privacy features but do provide increased bandwidth.

Each CC or CN has a local lookup table that contains the online communication addresses of all CCs and CNs on the Tari communication network known to that CC or CN. When a CC or CN wants to join the Tari communication network, the online communication address of at least one other CN that is part of the network needs to be known. The online communication address of the initial CN can either be manually provided or a bootstrapped list of "reliable" and persistent CNs can be provided with the Validator Node, Base Node or Wallet software. The new CC or CN can then request additional peer contact information of other CNs from the initial peers to extend their own routing table.

The routing table consists of a list of peer addresses that link node IDs, public identification keys and online communication addresses of each known CC and CN.

The Peer Address stored in the routing table MAY be implemented as follows:

DescriptionData typeComments
network addressnetwork_addressThe online communication address of the CC or CN
node_IDnode_IDRegistration Assigned for VN, Self selected for BN, W and TW
public_keypublic_keyThe public key of the identification cryptographic key of the CC or CN
node_typenode_typeVN, BN, W or TW
linked asset IDslist of asset IDsAsset IDs can be used as an address on Tari network similar to a node ID
last_connectiontimestampTime of last successful connection with peer
update_timestamptimestampA timestamp for the last peer address update

When a new CC or CN wants to join the Tari communication network they need to submit a joining request to the rest of the network. The joining request contains the peer address of the new CC or CN. Each CN that receives the joining request can decide if they want to add the new CCs or CNs contact information to their local routing table. When a CN, that received the joining request, has a similar node ID to the new CC or CN then that node must add the peer address to their routing table. All CNs with similar node IDs to the new CC or CN should have a copy of the new peer address in their routing tables.

To limit potential attacks, only one registration for a specific node type with the same online communication address can be stored in the routing table of a CN. This restriction will limit Bad Actors from spinning up multiple CNs on a single computer.

Joining the Network using a Joining Request

A new CC or CN needs to register their peer address on the Tari communication network. This is achieved by submitting a network joining request to a subset of CNs selected from the routing table of the new CC or CN. These peers will forward the joining request, with the peer address, to the rest of the network until CNs with similar node IDs have been reached. CNs with similar node IDs will then add the new peer address of the new node to their routing table, allowing for fast discovery of the new CC or CN.

Other CCs and CNs will then be able to retrieve the new CCs or CNs peer address by submitting discovery requests. Once the peer address of the desired CC or CN has been discovered then a direct P2P communication channel can be established between the two parties for any future communication. After discovery, the rest of the Tari communication network will be unaware of any further communication between the two parties.

Sending Data Messages and Discovery Requests

The majority of all communication on the Tari communication network will be performed using direct P2P channels established between different CCs and CNs once they are aware of the peer addresses of each other that contain their online communication addresses. Message propagation on the network will typically consist only of joining and discovery requests where a CC or CN wants to join the network or retrieve the peer address of another CC or CN so that a direct P2P channel can be established.

Messages can be transmitted in this network in either an unencrypted or encrypted form. Typically messages that have been sent in unencrypted form are of interest to a number of CNs on the network and should be propagated so that every CN that is interested in that data message obtains a copy. Block and Transaction propagation are examples of data messages where multiple entities on the Tari communication network are interested in that data message; this requires propagation through the entire Tari communication network in unencrypted form.

Encrypted data messages make use of the source and destinations identification cryptographic keys to construct a shared secret with which the message can be encoded and decoded. This ensures that only the two parties are able to decode the data message as it is propagated through the communication network. This mechanism can be used to perform private discovery requests, where the online communication address of the source node is encrypted and propagated through the network until it reached the destination node. Private discovery requests can only be performed if both parties are online at the same time. Encryption of the data message ensures that only the destination node is able to view the online address of the source node as the data message moves through the network. Once the destination node receives and decrypts the data message, that node is then able to establish a P2P communication channel with the source node for any further communication.

Propagation of completely private discovery request, hidden as an encrypted data message, can be performed as a broadcast through the entire network using the Gossip protocol. Propagation of public discovery requests can be performed using more efficient directed propagation using the Kademlia protocol. As encrypted message with visible destinations tend to not be of interest to the rest of the network, directed propagation using the Kademlia protocol to forward these messages to the correct parties are preferred. Privacy of a CCs online address, whom is sending a transaction to a Base Node, may be enhanced if the transaction is encrypted and sent to a Base Node with a node ID that is not the closest to the CCs node ID. This should prevent linking a transaction to the originating online address.

This same encryption technique can be used to send encrypted messages to a single node or a group of nodes, where the group of nodes have shared identification keys. A Validation Committee is an example of a group of CNs that have shared identification keys for the committee. The shared identification keys ensure that all members of that committee are able to receive and decrypt data messages that were sent to the committee.

Maintaining connections with peers

CCs and CNs establish and maintain connections with peers differently. CCs only create a few short-lived ad hoc channels and CNs create and maintain long-lived channels with a number of peers.

If a CC is unaware of a destination CNs or CCs online communication address then the address first needs to be obtained using a discovery request. When a CC already knows the communication address of the CC or CN that he wants to communicate with, then a direct P2P channel can be established between the two peers for the duration of the communication task. The communication channel can then be closed as soon as the communication task has been completed.

CNs consisting of VNs and BNs typically attempt to maintain communication channels with a large number of peers. The distribution of peers (VNs vs BNs) that a single CN keeps communication channels open with can change depending on the type of node. A CN that is also a BN should maintain more peer connections with other BNs, but should also have some connections with other VNs.

A CN that is also a VN should maintain more peer connections with other VNs, but also have some connections with BNs. CNs that are part of Validator Node committees should attempt to maintain permanent connections with the other members of the committee to ensure that quick consensus can be achieved.

To maintain connections with peers, the following process can be performed. Discover peers using discovery requests, and add their details to the local routing table. The CN can decide how the peer connections should be selected from the routing table by either:

  • manually selecting a subset,
  • automatically selecting a random subset or
  • selecting a subset of neighbouring nodes with similar node IDs.

Functionality Required of Communication Nodes

  • It MUST select a cryptographic key pair used for identification on the Tari Communication network.
  • A CN MAY request the peer addresses of CNs with similar node IDs from other CNs to extend their local routing table.
  • If a CN is a BN, then a node ID MUST be derived from the nodes identification public key.
  • A new CN MUST submit a joining request to the Tari communication network so that the nodes peer address can be added to the routing table of neighbouring peers in the network.
  • If a CN receives a new joining request with a similar node ID (within a network selected threshold), then the peer address specified in the joining request MUST be added to its local routing table.
  • When a CN receives an encrypted message, the node MUST attempt to open the message. It MUST authenticate the encryption before trying to decrypt it.
  • When a CN receives an encrypted message that the node is unable to open, and the destination node ID is known then the CN MUST forward it to all connected peers that have node IDs that are closer to the destination.
  • When a CN receives an encrypted message that the node is unable to open and the destination node is unknown then the CN MUST forward the message to all connected peers.
  • A CN MUST have the ability to verify the content of unencrypted messages to limit the propagation of spam messages.
  • If an unencrypted message is received by the CN with a unspecified destination node ID, then the node MUST verify the content of the message and forward the message to all connected peers.
  • If an unencrypted message is received by the CN with an specified destination node ID, then the node MUST verify the content of the message and forward the message to all connected peers that have closer node IDs.
  • A CN MUST have the ability to select a set of peer connections from its routing table.
  • Connections with the selected set of peers MUST be maintained by the CN.
  • A CN MUST have a mechanism to construct encrypted and unencrypted joining requests, discovery requests or data messages.
  • A CN MUST construct and provide a list of peer addresses from its routing table that is similar to a requested node ID so that other CCs and CNs can extend their routing tables.
  • A CN MUST keep its routing table up to date by removing unreachable peer addresses and adding newly received addresses.
  • It MUST have a mechanism to determine if a node ID was obtained through registration or was derived from an identification public key.
  • A CN MUST calculate the similarity between different node IDs by calculating the Hamming distance between the bits of the two node ID numbers.

Functionality Required of Communication Clients

  • It MUST select a cryptographic key pair used for identification on the Tari Communication network.
  • It MUST have a mechanism to derive a node ID from the self-selected identification public key.
  • A CC must have the ability to construct a peer address that links its identification public key, node ID and an online communication address.
  • A new CC MUST broadcast a joining request with its peer address to the Tari communication network so that CNs with similar node IDs can add the peer address of the new CC to their routing tables.
  • A CC MAY request the peer addresses of CNs with similar node IDs from other CNs to extend their local routing table.
  • A CC MUST have a mechanism to construct encrypted and unencrypted joining and discovery requests.
  • A CC MUST maintain a small persistent routing table of Tari Communication network peers with which ad hoc connections can be established.
  • As the CC becomes aware of other CNs and CCs on the communication network, the CC SHOULD extend its local routing table by including the newly discovered CCs or CNs contact information.
  • Peers from the CCs routing table that have been unreachable for a number of attempts SHOULD be removed from the its routing table.
  • A CC MUST calculate the similarity between different node IDs by calculating the Hamming distance between the bits of the two node ID numbers.

Change Log

DateChangeAuthor
11 Nov 2022Update, removed registration of Validator NodesStringhandler

RFC-0171/MessageSerialization

Message Serialization

status: stable

Maintainer(s): Cayle Sharrock Stanley Bondi

Licence

The 3-Clause BSD Licence.

Copyright 2019 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

The aim of this Request for Comment (RFC) is to describe the message serialization formats for message payloads used in the Tari network.

RFC-0710: Tari Communication Network and Network Communication Protocol

Description

One way of interpreting the Tari network is that it is a large peer-to-peer messaging application. The entities chatting on the network include:

  • Wallets
  • Base nodes
  • Validator nodes
  • Other client applications (e.g. chat)

The types of messages that these entities send might include:

  • Text messages
  • Transaction messages
  • Block propagation messages
  • Asset creation instructions
  • Asset state change instructions
  • State Checkpoint messages

For successful communication to occur, the following needs to happen:

  • The message is translated from its memory storage format into a standard payload format that will be transported over the wire.
  • The communication module wraps the payload into a message format, which may entail any/all of
    • adding a message header to describe the type of payload;
    • encrypting the message;
    • signing the message;
    • adding destination/recipient metadata.
  • The communication module then sends the message over the wire.
  • The recipient receives the message and unwraps it, possibly performing any/all of the following:
    • decryption;
    • verifying signatures;
    • extracting the payload;
    • passing the serialized payload to modules that are interested in that particular message type.
  • The message is deserialized into the correct data structure for use by the receiving software

This document only covers the first and last steps, i.e. serializing data from in-memory objects to a format that can be transmitted over the wire. The other steps are handled by the Tari communication protocol.

In addition to machine-to-machine communication, we also standardize on human-to-machine communication. Use cases for this include:

  • Handcrafting instructions or transactions. The ideal format here is a very human-readable format.
  • Copying transactions or instructions from cold wallets. The ideal format here is a compact but easy-to-copy format.
  • Peer-to-peer text messaging. This is just a special case of what has already been described, with the message structure containing a unicode message_text field.

When sending a message from a human to the network, the following happens:

  • The message is deserialized into the native structure.
  • Additional validation can be performed.
  • The usual machine-to-machine process is followed, as described above.

Binary Serialization Formats

The ideal properties for binary serialization formats are:

  • widely used across multiple platforms and languages, but with excellent Rust support;
  • compact binary representation; and
  • serialization "Just Works"(TM) with little or no additional coding overhead.

Several candidates fulfill these properties to some degree.

bincode

  • Pros:
    • Fast and compact
    • Serde support
  • Cons:
    • (Almost) any type changes result in incompatibilities
    • language support outside of rust is limited

Message Pack

  • Pros:
    • Compact
    • Fast
    • Multiple language support
    • Good Rust/Serde support
    • Native byte encoding (compared to JSON)
  • Cons:
    • No metadata support
    • Self-describing overhead

MessagePack has almost the exact same characteristics as JSON, without the syntactical overhead. It is also self-describing, which can be a pro and a con. Like JSON, field names are encoded as strings which adds significant overhead over p2p comms. However, when used in conjunction with msgpack-schemas this overhead is removed and binary representations become characteristically similar to protobuf.

Protobuf

Protobuf is a widely-used binary serialization format that was developed by Google and has excellent Rust support. The Protobuf byte format encodes tag numbers as varints that map to a known schema fields.

  • Pros:
    • Compact
    • Fast
    • Multiple language support
    • Good Rust/Serde support
    • Some schema changes are backward compatible
  • Cons
    • Schema must be defined for each message type
    • Does not fit into the serde ecosystem, meaning it is hard to swap out later.

In the latest protoV3 spec, all fields are optional. which forces the implementation to check for the presence of required data and allows for. This means that you can change your schema significantly and there is a

It's fairly easy to reason about backwards-compatibility for schema changes once you understand protobuf encoding. Essentially, since all fields in protov3 are optional, a message will usually be able to successfully decode even if message tags are added/changed/removed. It therefore depends on the application whether changes are backward-compatible.

Generally, the following rules apply:

  • you should not change existing field types to a non-compatible type. For example, changing a uint32 to a uint64 is fine, but changing a uint32 to a string is not.
  • you should not change existing tag numbers should not be changed, field names may change as needed since they are not included in the byte format.
  • you may delete optional or repeated fields
  • you may add new optional or repeated fields as long as you use a new field number

Cap'n Proto

Similar to Protobuf, but claims to be much faster. Rust is supported.

Hand-rolled Serialization

Hintjens recommends using hand-rolled serialization for bulk messaging. While Pieter usually offers sage advice, I'm going to argue against using custom serializers at this stage for the following reasons:

  • We're unlikely to improve hugely over existing serialization formats.
  • Since Serde does 95% of our work for us, there's a significant development overhead (and new bugs) involved with a hand-rolled solution.
  • We'd have to write de/serializers for every language that wants Tari bindings; whereas every major language has a protobuf implementation.

Tari message formats

Wire message format

The decision was taken to use Protobuf encoding for messages on the Tari peer-to-peer wire protocol, as it ticks these boxes:

  • it is possible to modify message schemas in future versions without breaking network communication with previous versions of node software,
  • de/encoding is compact and fast, and
  • it has great Rust support through the prost crate.

Other serialization formats

For human-readable formats, it makes little sense to deviate from JSON. For copy-paste semantics, the extra compression that Base64 offers over raw hex or Base58 makes it attractive.

The standard binary representation used in databases (e.g. blockchain storage, wallets) will make use of bincode. In these cases, a straightforward #[derive(Deserialize, Serialize)] is all that is required to implement de/encoding for the data structure.

However, other structures might need fine-tuning, or hand-written serialization procedures. To capture both use cases, it is proposed that a MessageFormat trait be defined:

#![allow(unused)]
fn main() {
pub trait MessageFormat: Sized {
    fn to_binary(&self) -> Result<Vec<u8>, MessageFormatError>;
    fn to_json(&self) -> Result<String, MessageFormatError>;
    fn to_base64(&self) -> Result<String, MessageFormatError>;

    fn from_binary(msg: &[u8]) -> Result<Self, MessageFormatError>;
    fn from_json(msg: &str) -> Result<Self, MessageFormatError>;
    fn from_base64(msg: &str) -> Result<Self, MessageFormatError>;
}
}

This trait will have default implementations to cover most use cases (e.g. a simple call through to serde_json). Serde also offers significant ability to tweak how a given struct will be serialized through the use of attributes.

Change Log

DateChangeAuthor
29 Mar 2019First draftCjS77
24 Jul 2019Technical editinganselld
13 Oct 2022Updatesdbondi
26 Oct 2022Stabilise RFCCjS77

RFC-0172/PeerToPeerMessaging

Peer to Peer Messaging Protocol

status: stable

Maintainer(s): Stanley Bondi, Cayle Sharrock, Stringhandler

Licence

The 3-Clause BSD Licence.

Copyright 2019 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

The aim of this Request for Comment (RFC) is to describe the peer-to-peer messaging protocol for communication nodes and communication clients on the Tari network.

Description

Assumptions

Broad Requirements

Tari network peer communication must facilitate secure, private and efficient communication between peers. Broadly, a communication node or communication client MUST be capable of:

  • bidirectional communication between multiple connected peers;
  • encrypted, authenticated over-the-wire communication;
  • understanding and constructing Tari messages;
  • gracefully reestablishing dropped connections; and (optionally)
  • either:
    • communicating to a SOCKS5 proxy (e.g. connections over Tor).
    • or have a static public IPv4 address.

Additionally, communication nodes MUST be capable of performing the following tasks:

  • maintaining a list of known and available peers in the form of a peer list;
  • forwarding directed messages to neighbouring peers; and
  • broadcasting messages to neighbouring peers.

Overall Architectural Design

The Tari communication layer has a modular design to allow for the various communicating nodes and clients to use the same infrastructure code.

Peer connection state is monitored by the ConnectionManager component. The ConnectionManager emits events to allow other components to subscribe to connection state changes.

  • ConnectionManager - manages peer connections and connection state monitoring.
  • PeerConnection - manages the sending and receiving of messages for a single peer connection.
  • NetAddress - multiaddr describing the public address and transport for a peer-to-peer connection.
  • Messaging Protocol - defines the Tari wire message format and message types.
  • Connection Multiplexer - allows multiple substreams to be established over a single transport-level connection.

NetAddress

A NetAddress is a publicly-accessible address for a peer. A peer may have one or more NetAddresses.

A good NetAddress format should:

  • have an efficient binary representation;
  • have a human-readable representation with a simple syntax;
  • support many transport protocols; and
  • be self-describing

For these reasons, we select the multiaddr format for all peer-to-peer addresses.

For example,

  • /ip4/123.123.123.123/tcp/12345 - IPv4 address with TCP transport on port 12345.
  • /onion3/abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrst:12345 - Tor onion address

Supported Transports

The following transports are supported by the Tari communication layer:

  • TCP/IP - A publicly accessible IPv4 address.
  • SOCKS5 - Allows a SOCKS5 proxy to be configured for inbound and outbound connections.
  • Tor - A specialisation of the SOCKS5 transport that facilitates connections over the Tor network.

Establishing a Connection

Every participating communication node SHOULD listen on at least one of the supported transports, accessible from a public address, to allow remote peers to establish peer connections.

A peer wishing to establish a peer connection should attempt a connection to one of the remote peer's public NetAddresses using the transport described in the NetAddress.

The peer that is initiating the outbound connection is referred to as the initiator and the peer that accepts the inbound connection is referred to as the responder for the remainder of this section.

We describe the following socket upgrade procedures for an encrypted peer-to-peer connection on the Tari network.

  1. The Wire Mode Byte

The wire mode indicates the intention of the initiator. It is up to the application domain to dictate what byte is acceptable however a common configuration is to use the wire mode to indicate which network (mainnet, testnet etc) the initiator is attempting to use allowing the responder to accept/reject the connection early on in the connection procedure.

The initiator MUST send a single byte indicating the wire mode within WIRE_MODE_TIMEOUT. The responder SHOULD to accept or reject (close) the connection based on the initiator's wire mode byte. The responder SHOULD reject (close) the connection if no byte is received within WIRE_MODE_TIMEOUT.

Once the byte is sent, the initiator may immediately proceed to the next procedure.

  1. The Noise Protocol

The Noise Protocol framework is a set of related crypto protocols that support mutual authentication and ephemeral encryption key exchange amongst other features.

We list the following characteristics and requirements for encrypted peer-to-peer connections on the Tari network:

  • Mutual authentication of the initiator and responder;
  • the responder need not know the initiator's public identity prior to the connection;
  • the public identity of each participant is hidden to any observer;
  • forward secrecy; and,
  • for efficiency, has a minimal round trip time.

For these reasons we select the single round-trip Noise_IX_25519_ChaChaPoly_BLAKE2b protocol, that is, the Noise IX handshake pattern using Curve25519 for ephemeral and static identities, ChaChaPoly encryption and a HKDF using BLAKE2b.

If successful, an authenticated encrypted socket connection is established between the peers.

  1. Identity Exchange

At this point the initiator and responder are aware of each other's public identity keys, however, some additional information is required to fully "introduce" the participants to each other.

Both the initiator and responder simultaneously transmit a message containing their up-to-date public addresses, the peer feature flags, protocols supported by the peer, a timestamp of the last time these details changed and a signature that signs the public addresses, feature flags and update timestamp. Peers may store and share these details with other peers, who can check the authenticity of the provided information by verifying the signature.

  1. Multiplexing

Now that these procedures are complete, we have an active PeerConnection. There is no explicit protocol message required to initiate multiplexing as both sides implicitly agree to send yamux protocol messages. Light-weight Yamux substreams are opened lazily as/when required by the application.

Multiplexing allows a single socket connection to be used simultaneously by multiple components as if each had their own dedicated channel, in a very similar way that your browser can perform many HTTP2 requests over a single server connection. In yamux, these dedicated channels are called substreams. The details of the yamux protocol are out of scope for this RFC.

Protocol Negotiation

To begin any protocol, an initiator MUST open a new yamux substream and begin protocol negotiation. Protocol negotiation ensures that both sides of the exchange are speaking the same language. Protocols are identified by a unique string identifier given by the author of the protocol. A protocol name can technically be any string, but it is defined in the Tari protocol as t/{protocol-ident}/{version}, where t is short for Tari, for example the messaging protocol is t/msg/0.1.

To begin, the initiator MUST send a protocol negotiation message consisting of 1 length byte, 1 bitflag byte and the protocol identifier string. The length byte MUST be equal to the length of the protocol identifier.

The bitflags are defined as follows:

  • 0x01 - OPTIMISTIC
  • 0x02 - TERMINATE
  • 0x04 - PROTOCOL_NOT_SUPPORTED (response)
  • 0x08 - 0x128 - Future use (ignored)

If the OPTIMISTIC flag is set, the initiator considers the negotiation complete as it is optimistic that the responder supports it. It can assume this because the peer gave a list of supported protocols in the Identity Exchange procedure. If the responder does not support the protocol, it can simply close the substream.

In general, peers will use OPTIMISTIC negatation and never wait for a response, as they have a full list of supported protocols. However, if the initiator wishes to negotiate a protocol not in the protocol list, it may leave the OPTIMISTIC flag unset in the initial message.

If the responder supports the protocol, it SHOULD respond with the name of the supported protocol and all flags unset and immediately proceed with the agreed upon protocol. If not, it SHOULD respond with the PROTOCOL_NOT_SUPPORTED flag set and an empty protocol name and wait for more messages. The initiator MAY send another protocol negotiation message or close the substream.

A responder MAY set the TERMINATE flag at any time and close the substream. In practise, this is used to indicate to the initiator that it has exceeded the maximum number of protocol negotiation queries (5) and should give up.

Peer

A single peer that can communicate on the Tari network.

Fields include:

  • public_key - The Ristretto public key identity of the peer.
  • addresses - a list of NetAddresses associated with the peer, perhaps accompanied by some bookkeeping metadata, such as preferred address;
  • peer_features - bitflags with the following flags
    • MESSAGE_PROPAGATION = 0x01 - peer is able to propagate/route messages
    • DHT_STORE_FORWARD = 0x02 - peer provides message storage and can respond to SafRequestMessages
    • A COMMUNICATION_NODE is defined as 0x03 (MESSAGE_PROPAGATION | DHT_STORE_FORWARD)
    • A COMMUNICATION_CLIENT is defined as 0x00
  • last_seen - a timestamp of the last time a message has been sent/received from this peer;
  • banned_until - an optional timestamp indicating the peer is banned;
  • offline_at - an optional timestamp indicating at which time a peer was marked as offline due to multiple failed attempts to contact the peer.

A peer may also contain reputation metrics (e.g. rejected_message_count, avg_latency) to be used to decide if a peer should be banned. This mechanism is yet to be decided.

PeerManager

The PeerManager is responsible for managing the list of peers with which the node has previously interacted. This list is called a routing table and is made up of Peers.

The PeerManager can

  • add a peer to the routing table;
  • search for a peer given a node ID, public key or NetAddress;
  • delete a peer from the list;
  • persist the peer list using a storage backend;
  • restore the peer list from the storage backend;
  • maintain lightweight views of peers, using a filter criterion, e.g. a list of peers that have been banned, i.e. a denylist; and
  • prune the routing table based on a filter criterion, e.g. last date seen.

General-purpose Messaging Protocol

The messaging protocol is a simple fire-and-forget protocol where arbitrary messages can be sent between peers. If Alice wants to send a message to Bob, she will open a new yamux substream and negotiate the t/msg/0.1 protocol. If Bob wants to send a message to Alice, he will do the same. This means that two substreams (one per direction) are open for bi-directional message sending as required.

Message frames are length delimited (see Tokio's LengthDelimitedCodec). At this level, no structure apart from the length-delimited framing is imposed on the message protocol allowing that to be fully determined by domain-level components.

Tari DHT and Base-Layer Messaging Protocol

The following illustrates the structure of a Tari message:

+----------------------------------------+
|              DhtEnvelope               |
|  +----------------------------------+  |
|  |             DhtHeader            |  |
|  +----------------------------------+  |
|  +----------------------------------+  |
|  |         EnvelopeBody             |  |
|  | (multipart, optionally encrypted)|  |
|  | +------------------------------+ |  |
|  | |   +-----------------------+  | |  |
|  | |   |    1. MessageHeader   |  | |  |
|  | |   +-----------------------+  | |  |
|  | |   +-----------------------+  | |  |
|  | |   |    2. MessageBody     |  | |  |
|  | |   +-----------------------+  | |  |
|  | +------------------------------+ |  |
|  +----------------------------------+  |
+----------------------------------------+

Each Tari message is wrapped in a DhtEnvelope which contains a DhtHeader and an EnvelopeBody.

The DhtHeader is a protobuf message with these fields:

  • version: u32

    The major message header version. A peer MAY discard the message if the version is not supported.

  • message_signature: 64 bytes

    The raw representation of a Schnorr signature committing to:

    • sender public key
    • signature public nonce
    • and Blake2b hash of:
      • "comms.dht.v1.message_signature"
      • version
      • destination
      • msg_type
      • flags
      • expiry
      • ephemeral_public_key
      • body

    This is required if the ENCRYPTED flag is set.

  • ephemeral_public_key: 32 bytes

    Ephemeral public key component of the ECDH shared key. MUST be specified if the ENCRYPTED flag is set.

  • dht_message_type: i32

    Enumeration of the type of message.

    • None = 0 - Domain-level message
    • Join = 1 - Join/Announce
    • Discovery = 2 - Discovery request
    • DiscoveryResponse = 3 - Response to a discovery request
    • SafRequestMessages = 20 - Request stored messages from a node
    • SafStoredMessages = 21 - Stored messages response
  • flags: u32 - bitflags 0x01 - ENCRYPTED

  • message_tag: u64 - Message trace ID. This can be omitted or any value and is used for debug tracing.

  • expires: Option<prost_types::Timestamp>

    Expiry timestamp for the message, if any. If specified any peer receiving the message after this time MAY discard it.

  • destination: Option<dht_header::Destination>

    Enumeration of the message destination:

    • UNKNOWN = 0 - the destination was not specified, this indicates that the message is destined for the receiver.
    • PUBLICKEY(XXXXX) = 1 - destination is the specified public key. This MUST be provided when the ENCRYPTED flag is set.
Inbound Message Validation

The following validation rules MUST be applied to all incoming messages:

  • If ENCRYPTED is set
    • The destination MUST be PUBLICKEY(XXXXX)
    • The ephemeral_public_key MUST be specified
    • The message_signature MUST be non-empty
    • If able to decrypt the message signature:
      • the signature MUST be valid
      • the destination public key MUST match the local public key
  • If the ENCRYPTED flag is not set, indicating a cleartext message
    • The message_signature MAY be specified. If it is, it MUST be valid.
    • Other fields relating to encryption e.g. ephemeral_public_key MAY be set but SHOULD be ignored.

If any of these rules fail the message SHOULD be discarded.

Outbound Messaging

The protocol provides for the following outbound message broadcast strategies:

  • Direct(Identity) - Send the message directly to the destination peer.
  • Flood(exclude) - Send to all connected peers excluding exclude peers. If no peers are connected, no messages are sent.
  • Random(n, exclude) - Send to a random set of peers of size n that are Communication Nodes, excluding exclude peers.
  • ClosestNodes({node_id, exclude, connected_only}) - Send to all n nearest Communication Nodes to the given node_id.
  • DirectOrClosestNodes({node_id, exclude, connected_only}) - Send directly to destination if connected but otherwise send to all n nearest Communication Nodes
  • Broadcast(excludes)- Send to a random set of connected peers, excluding excludes peers. The number of peers selected at most equal to propagation_factor.
  • SelectedPeers(peers) - Send to the specified peers.
  • Propagate(NodeDestination, Vec<NodeId>) - Propagate to a set of connected peers closer to the destination. The number of peers selected at most equal to propagation_factor.

A peer's node_id is defined as the 13-byte variable-length Blake2b hash of the public key. To determine if a peer identity is "closer" to another peer we compare the XOR distance between peers as proposed by the kademlia paper.

DHT Messages
  • Join

Announces a peer's availability to the network. A routing node SHOULD propagate this message closer to the destination. As it travels through the network, the peer information is stored in the peer list. Peers close to the newly joined node MAY attempt to dial the node on receipt of this message.

  • Discovery

An encrypted discovery request containing the sender's contact details. A routing node SHOULD propagate this message closer to the destination. The destination peer will attempt to contact the sender and send a DiscoveryResponse message to reciprocate with its peer information.

  • DiscoveryResponse

Sent in response to a Discovery message.

  • SafRequestMessages / SafStoredMessages

Request and response messages for stored messages destined for the requester.

EnvelopeBody

The EnvelopeBody is the "payload" of the message and consists of an arbitrary number of ordered opaque BLOBs. It may be encrypted for a particular destination. The contents of these BLOBs are decided by domain-level requirements.

The Tari protocol inserts a MessageHeader at index 0 and MessageBody at index 1.

A zero-sized encoding of EnvelopeBody is permitted as that is a valid proto3 encoding. When applying message encryption, the body MUST be padded and, therefore, a message SHOULD be discarded if the encoded EnvelopeBody is zero-sized.

MessageHeader

Every Tari message MUST have a payload header containing the following fields at index 0 in the EnvelopeBody:

NameTypeDescription
message_typeu8An enumeration of the message type of the body. Refer to message types below.
nonceu32The optional message nonce.

MessageTypes are represented as an unsigned eight-bit integer denoting the expected contents of the MessageBody.

CategoryNameValueDescription
NetworkPingPong1A PongPong message.
BlockchainNewTransaction65Transaction submitted by a wallet or propagated by a base node.
BlockchainNewBlock66Block propagated by a base node.
WalletSenderPartialTransaction67A partial MimbleWimble transaction submitted by a sender wallet to the receiver.
WalletReceiverPartialTransactionReply68Reply to SenderPartialTransaction submitted by a receiver wallet to the sender.
BlockchainBaseNodeRequest69Base node request message.
BlockchainBaseNodeResponse70Base node response in reply to a BaseNodeRequest message.
BlockchainMempoolRequest71Base node mempool request message.
BlockchainMempoolResponse72Base node response in reply to a MempoolRequest message.
WalletTransactionFinalized73Finalized transaction message sent by a sender to receiver wallet.
WalletTransactionCancelled74A courtesy message sent by a wallet to inform the other that the transaction is cancelled.

All other message types are reserved for future use.

Message Encryption

Encrypted messages may be routed across the Tari network such that only the destination node is able to decipher the contents of the message. An encrypted message reveals to recipient but keeps the sender and contents private.

To route an encrypted message, the following requirements MUST be met:

  • The destination public key MUST be specified.
  • The message_signature MUST be non-empty and SHOULD be encrypted.
  • The ephemeral_public_key MUST be a valid Ristretto public key.
  • The EnvelopeBody MUST be non-empty, as message padding (described below) is required.
  • If the message expiry is specified, a routing node MAY discard the message if the expiry time has passed.

A message is encrypted using the following procedure:

  • The DhtEnvelopBody is containing the MessageHeader and MessageBody is serialized using [protobuf].
  • A CSRNG is used to generate the cipher nonce and this is prepended onto the message.
  • The plaintext message is padded with '0x00' to a multiple of 6000 bytes.
  • The message encryption key is generated as follows:
    • Key material dh_key is generated by Diffie-Hellman of the recipient public key and the ephemeral private key.
    • The final message key is constructed: message_key = Blake2b("comms.dht.v1.key_message" || dh_key) to produce a 32-byte key.
  • The message Schnorr signature is generated as follows:
    • A domain-separated Blake2b hash is generated with the challenge message_challenge = Blake2b("comms.dht.v1.challenge" || protocol_version || destination || dht_message_type || le_bytes(flags) || expiry || ephemeral_public_key || message_body).
    • The signer signs the hashed challenge "comms.dht.v1.message_signature" || signer_public_key || public_nonce || message_challenge with the sender secret key.
    • The signature is serialized and encrypted using the [ChaCha20Poly1305] AEAD cipher and the same dh_key constructed earlier.
      • The final signature key is constructed: Blake2b("comms.dht.v1.key_signature" || dh_key) to produce a 32-byte key.
  • The DhtEnvelopBody is encrypted using the [ChaCha20Poly1305] AEAD cipher and message_key.
  • The final message is a DhtEnvelope containing the plaintext DhtHeader and encrypted DhtEnvelopeBody.
Message Routing

On receipt of a valid message with destination set to PUBLIC_KEY(xxxx), a node SHOULD forward a message either directly to the peer, if able, or closer to the peer as per the XOR metric. If the message is invalid, the node SHOULD discard it.

Store and Forward

Sometimes it may be desirable for messages to be sent without a destination node/client being online. This is especially important for a modern chat/messaging application as well as interactive Mimblewimble transactions.

Each communication node SHOULD allocate some disk space for storage of messages for offline recipients. A sender sends a message destined for a particular public identity to its closest peers, which forward the message to their closest peers, and so on. A peer is considered close enough by finding the farthest peer from the n closest online and available peers to the storage node and comparing that to the XOR distance of the message destination.

Eventually, the message will reach nodes that either know the destination or are very close to the destination. These nodes SHOULD store the message in some pending message storage for the destination. The maximum number of buckets and the size of each bucket SHOULD be sufficiently large as to be unlikely to overflow, but not so large as to approach disk space problems. Individual messages should be small and responsibilities for storage spread over the entire network.

On receipt of a valid message with destination set to PUBLIC_KEY(xxxx), and if the peer is sufficiently close to the destination, a node SHOULD store the message for a time and return it later to the peer in response to a SafRequestMessages message.

If the [DhtEnvelopeBody] is encrypted, the type and contents of the message remain private.

Message Deduplication

A peer propagating/routing a message may receive the same message after propagation from another peer as there is no way for routing node to know which peers have seen the message before. To prevent infinite message propagation, message contents should be hashed and stored in a dedup cache. On receiving a message, if the message hash is found, the message SHOULD be discarded and not propagated further.

Change Log

DateChangeAuthor
13 Jun 2022Moved from tari reposdbondi
9 Nov 2022Removed I2P and ZeroMQstringhandler
17 Jan 2023Implementation parity updatessdbondi
25 Jan 2023Clarify empty body rulessdbondi

RFC-0173/Versioning

Versioning

status: stable

Maintainer(s): Stringhandler

Licence

The 3-Clause BSD Licence.

Copyright 2021 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

The aim of this Request for Comment (RFC) is to describe the various types of versioning that nodes on the Tari network will use during interaction with other nodes.

Description

In a decentralized system the set of nodes on the network will run a variety of software versions as time goes on. Some of these versions will be compatible and others not. For example, if a crucial consensus change is added during a hard fork event. Furthermore, there will be multiple networks running Tari code, i.e. Mainnet vs Testnet. Versioning refers to the strategies we will use for nodes to determine if they can communicate.

Tari will contain three different versioning schemes:

  1. WireMode is the first byte a peer sends when connecting to another peer, used to identify the network and/or protocol bytes that follow
  2. P2P message versions that will accompany every P2P message,
  3. Consensus rules versions that will be exchanged on connection and are included in each block header.

WireMode byte

In the Bitcoin P2P protocol messages are preceded by 4 magic values or bytes. These values are used to delimit when a new message starts in a byte stream and also are used to indicate which of the Bitcoin networks the node is speaking on, such as TestNet or Mainnet.

Tari message packets are encapsulated using the Noise protocol, so we do not need the delimiting functionality of these bytes. Once we have a Noise socket we are able to send/receive bytes as with any other socket, but those bytes are encrypted over the wire. Using that socket we send yamux packets and messaging/rpc messages are length-delimited.

Tari includes a single WireMode byte at the beginning of every connection session. This byte indicates which network a node is communicating on, so that if the counterparty is on a different network it can reject this connection cheaply without having to perform any further operations, like completing the Noise protocol handshake.

The following is the current mapping of the WireMode byte:

   pub enum Network {
    MainNet = 0x00,
    LocalNet = 0x10,
    Ridcully = 0x21,
    Stibbons = 0x22,
    Weatherwax = 0xa3,
    Igor = 0x24,
    Dibbler = 0x25,
    Esmeralda = 0x26,
}

// As well as the special wiremode for local liveness checks
const LIVENESS_WIRE_MODE: u8 = 0xa6;

P2P message version

Peer to Peer messages on the Tari network are encapsulated into message envelopes. The body of message envelopes are defined, serialized and deserialized using Protobuf. These messages will only be updated by adding new fields to the Protobuf definitions, never removing fields. This is done in order to preserve backwards compatibility where newer nodes can still communicate with older nodes.

The P2P messaging protocol will see many changes in its lifetime. Some will be minor changes that are fully backwards compatible and some changes will be breaking where older nodes will not be able to communicate with newer nodes. In order to document these two classes of changes each P2P message header will contain a version field that will use a two-part semantic versioning scheme with the format of major.minor integer versions. The minor version will be incremented whenever there is any change. The major version be incremented when there is a breaking change made to the P2P protocol. Each integer can be stored separately.

Consensus version

The final aspect of the Tari system that will be versioned are the Consensus rules. These rules will change as the network matures. Changes to consensus rules can be achieved using either a Soft fork or Hard fork. Soft forks are where new consensus rules are added that older nodes will see as valid (thus backwards compatible) but newer nodes will reject blocks from older nodes that are not aware of the new consensus rules. A hard fork means that the new consensus rules are not backwards compatible and so only nodes that know about the new rules will be able to produce and validate new transactions and blocks.

The consensus version will be used by a node to determine if it can interact with another node successfully or not. A list of fork versions will be maintained within the code. When a connection is started with a new node the two nodes will exchange Version messages detailing the consensus version they are each running and the block height at which they are currently operating. Both nodes will need to reply with a Version Acknowledge message to confirm that they are compatible with the counterparty's version. It is possible for a newer node to downgrade its protocol to speak to an older node so this must be decided during this handshake process. Only once the acknowledgments have been exchanged can further messages be exchanged by the parties. This is the method currently employed on the Bitcoin network

For example, if we have two nodes, Node A and Node B, where Node A is ahead of Node B in version and block height. During the handshake Node B will not recognize Node A's version but should wait for Node A to reject or confirm the connection because Node A could potentially downgrade their version to match Node B's. Node A will speak to Node B if and only if Node A recognizes Node B's version and Node B's block height is in the correct range for its advertised version according to Node A's fork version list.

Tari Block Headers contain a version field which will be used to indicate the version of consensus rules that are used in the construction and validation of this block. Consensus rules versions will only consist of breaking changes and as such will be represented with a single incremented integer. This coupled with the internal list of fork versions, that includes the height at which they came into effect, will be used to validate whether the consensus rules specified in the header are valid for that block's height.

Change Log

DateChangeAuthor
26 Oct 2022Stabilise RFCCjS77

RFC-0174/Chat Metadata

Versioning

status: draft

Maintainer(s): brianp

Licence

The 3-Clause BSD Licence.

Copyright 2023 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

The aim of this Request for Comment (RFC) is to describe the various types of versioning that nodes on the Tari network will use during interaction with other nodes.

Description

The chat protocol incorporates metadata within messages to support various message types, such as Replies, TokenRequests, Gifs, and Links. This metadata-driven approach enhances the technical flexibility of chat, allowing for the structured handling of distinct content and interactions within the messaging framework.

Chat metadata is transmitted simply as a length checked byte array allowing for flexbility of future content. Content is unvalidated in the protocol and requires clients to ensure standardized formatting for specific types.

Types & Formats

Reply:

MetadataType int: 0
Data format: String message_id A reply metadata should be a single String element matching the UUID of the message being replied to.
Example: 06703dbfeabc43b98ceff16de2e104bc

TokenRequest:

MetadataType int: 1
Data format: double micro_minotari_amount
A double (reaL) number representing the request amount in MicroMinoTari.
Example: 14000.87

Gif:

MetadataType int: 2
Data format: String giphy_url
A url to a giphy hosted image
Example: https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExNjN0dmR2aWNjbTluNGZ3ZHlubHNqajIwcmlqazdtYXExNWp4aG94NSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/VIPdgcooFJHtC/giphy.gif

MetadataType int: 3
Data format: String uri
A uri in string format
Example: https://www.tari.com/

Change Log

DateChangeAuthor
21 Dec 2023Proposal draftbrianp

RFC-0181/BulletproofsPlus

Bulletproofs+ range proving

status: stable

Maintainer(s): Aaron Feickert

Licence

The 3-Clause BSD Licence.

Copyright 2022 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

This Request for Comment (RFC) describes Tari-specific implementation details for Bulletproofs+ range proving and verifying, in addition to giving an outline of comparative performance.

Introduction

The Tari implementation of the Bulletproofs+ range proving system makes several changes and optimizations that we describe here. In particular, it supports the following useful features:

  • Commitments can be extended; that is, they can commit to a value using multiple masks.
  • Range proofs can be aggregated; that is, a single range proof can assert valid range for multiple commitments in an efficient way.
  • A set of arbitrary range proofs can be verified in a batch; that is, the verifier can check the validity of all proofs in the set at once in an efficient way.
  • The prover can assert a nonzero minimum value bound to a commitment.
  • The prover can delegate to certain verifiers the ability to recover the masks used for the extended commitment in a non-aggregated proof.

The Bulletproofs+ preprint does not address extended commitments, as it only defines its range proving system using Pedersen commitments. However, a later preprint for Zarcanum updates the algorithms and security proofs to accommodate one additional mask, and the reasoning extends generally. Aggregation of range assertions using Pedersen commitments is described in the Bulletproofs+ preprint, and the Zarcanum preprint describes the corresponding changes for extended commitments. Batch verification is described only informally in the Bulletproofs+ preprint, and in an incomplete fashion. Minimum value assertion is not addressed in the preprint. An approach to mask and value recovery was used by Grin for the Bulletproofs range proving system, implemented as described by the deprecated RFC-0180, and can be modified to support Bulletproofs+ range proofs with extended commitments.

Notation

To reduce confusion in our description and more closely match implementation libraries, we use additive notation and uppercase letters for group elements, and otherwise generally assume notation from the preprints. Denote the commitment value generator by $G_c$ and the commitment mask generator vector by $\vec{H}_c$. Because the preprint uses the notation $A$ differently in the weighted inner product and range proving protocols, we rename it to $A'$ in the weighted inner product protocol.

We assume that a range proof aggregates $m$ range assertions, each of which is to an $n$-bit range. We further assume that each value commitment uses $p$ masks; that is, a standard Pedersen commitment would have $p = 1$.

A specific definition of note relates to that of the vector $\vec{d}$ introduced in the preprint. This vector is defined as \[ \vec{d} = \sum_{j=0}^{m-1} z^{2(j+1)} \vec{d}_j \tag{1} \] where each \[ \vec{d}_j = (\underbrace{0,\ldots,0}_{jn}, \vec{2}^n, \underbrace{0,\ldots,0}_{(m-j-1)n}) \tag{2} \] contains only powers of two. In particular, this means we can express individual elements of $\vec{d}$ as $d_{jn+i} = z^{2(j+1)} 2^i$ for $0 \leq i < n$ and $0 \leq j < m$.

Finally, we note one additional unfortunate notation change that applies to the implementation. Both the Bulletproofs+ and Zarcanum preprints use $G$ as the commitment value generator, and either $H$ or $\vec{H}_c$ (in our notation) for masking. However, in the Tari protocol (as in other similar protocols), this notation is switched! This is because of how generators are defined and used elsewhere in the protocol and elliptic curve library implementation. The only resulting change is a switch in generator notation in the Tari implementation: $H$ for the value component generator, and $\vec{G}_c$ (in our notation) for masking.

Minimum value assertion

We first briefly note how to achieve minimum value assertion. Let $0 \leq v_{\text{min}} \leq v \leq v_{\text{max}}$, where $v_{\text{min}}$ is a minimum value specified by the prover, $v$ is the value bound to the prover's commitment $V$, and $v_{\text{max}}$ is a globally-fixed maximum value. When generating the proof, the prover uses $v - v_{\text{min}}$ as the value witness, and additionally binds $v_{\text{min}}$ into the Fiat-Shamir transcript. When verifying the proof, the verifier uses $V - v_{\text{min}} G_c$ as the commitment (but binds the original commitment $V$ in the transcript). This asserts that $v_{\text{min}} \leq v \leq v_{\text{min}} + v_{\text{max}}$. While this approach modifies the upper bound allowed for value binding, it does not pose problems in practice, as the intent of range proving is to ensure no overflow when performing balance computations elsewhere in the Tari protocol.

Extended commitments and aggregation

We now describe how to reduce verification of a single aggregated range proof using extended commitments to a single multiscalar multiplication operation. A partial approach is described in the Bulletproofs+ preprint. The single multiscalar multiplication used to verify an aggregated range proof (given in Section 6.1 of the Bulletproofs+ preprint) can be written more explicitly in our case by accounting for the extra steps used to support extended commitments, and by noting that the $P$ input term to the weighted inner product argument (given in Figure 1 of the Bulletproofs+ preprint and Figure D.1 of the Zarcanum preprint) is replaced by the term $\widehat{A}$ defined in the overall range proving protocol (given in Figure 3 of the Bulletproofs+ preprint and Figure D.3 of the Zarcanum preprint).

Suppose we index the inner product generator vectors $\vec{G}$ and $\vec{H}$ using $i$, the inner product recursion generator vectors $\vec{L}$ and $\vec{R}$ using $j$, the aggregated commitment vector $\vec{V}$ by $k$, and the extended commitment mask generator vector $\vec{H}_c$ by $l$. We assume indexing starts at zero unless otherwise noted. Single aggregated proof verification reduces (by suitable modification of the equation given in Section 6.1 of the Bulletproofs+ preprint) to checking that the following equation holds: \[ \sum_{i=0}^{mn-1} (r'es_i) G_i + \sum_{i=0}^{mn-1} (s'es_i') H_i + \sum_{l=0}^{p-1} \delta_l' H_{c,l} = e^2 \widehat{A} + \sum_{j=0}^{\operatorname{lg}(mn)-1} (e^2e_j^2) L_j + \sum_{j=0}^{\operatorname{lg}(mn)-1} (e^2e_j^{-2}) R_j + e A' + B \] But we also have (from suitable modification of the definition given in Figure 3 of the Bulletproofs+ preprint) that \[ \widehat{A} = A - \sum_{i=0}^{mn-1} z G_i + \sum_{i=0}^{mn-1} (z + d_iy^{mn-i}) H_i + x G_c + y^{mn+1}\sum_{k=0}^{m-1} z^{2(k+1)} (V_k - v_{\text{min},k} G_c) \] defined by the range proving system outside of the inner product argument. Here \[ \begin{align*} x &= \langle \vec{1}^{mn}, \overrightarrow{y}^{mn} \rangle z - \langle \vec{1}^{mn}, \vec{d} \rangle y^{mn+1}z - \langle \vec{1}^{mn}, \overrightarrow{y}^{mn} \rangle z^2 \\ &= z\sum_{i=1}^{mn} y^i - y^{mn+1}z\sum_{i=0}^{mn-1}d_i - z^2\sum_{i=1}^{mn} y^i \tag{3} \end{align*} \] is a scalar defined entirely in terms of constants and challenge values from the proof. Grouping terms, we find that a single aggregated range proof can be verified by checking that the following equation holds: \[ \begin{multline*} \sum_{i=0}^{mn-1} (r'es_i + e^2z) G_i + \sum_{i=0}^{mn-1} (s'es_i' - e^2(z + d_iy^{mn-i})) H_i + \left( r'ys' - e^2x + e^2y^{mn+1}\sum_{k=0}^{m-1} z^{2(k+1)}v_{\text{min},k} \right) G_c \\ + \sum_{l=0}^{p-1} \delta_l' H_{c,i} - \sum_{k=0}^{m-1} (y^{mn+1}z^{2(k+1)}e^2) V_k - e^2 A - \sum_{j=0}^{\operatorname{lg}(mn)-1} (e^2e_j^2) L_j - \sum_{j=0}^{\operatorname{lg}(mn)-1} (e^2e_j^{-2}) R_j - e A' - B = 0 \tag{4} \end{multline*} \]

Batch verification

To verify a batch of proofs, we apply a separate random multiplicative scalar weight $w \neq 0$ to each proof's verification equation, form a linear combination of these equations, and group like terms. Because each equation receives a separate random weight, successful evaluation of the resulting linear combination means that each constituent equation holds with high probability, and therefore that all proofs in the set are valid. If the linear combination evaluation fails, at least one included proof is invalid. The verifier must then test each proof in turn, or use a more efficient approach like binary search to identify each failure. This follows the general approach informally discussed in Section 6.1 of the Bulletproofs+ preprint.

The reason for this rather convoluted algebra is twofold. First, grouping like terms means that each unique generator used across a batch is only evaluated in the resulting multiscalar multiplication once; since the generators $\vec{G}, \vec{H}, G_c, \vec{H}_c$ are globally fixed, this provides significant efficiency improvement. Second, the use of algorithms (like those of Straus and Pippenger and others) to evaluate the multiscalar multiplication scale slightly sublinearly, such that it is generally beneficial to minimize the number of multiscalar multiplications for a given set of generators. This means our approach to batch verification is effectively optimal.

Designated mask recovery

It is possible for the prover to perform careful modifications to a non-aggregated range proof in order to allow a designated verifier to recover the masks used in the corresponding extended commitment. The construction we describe here does not affect the verification process for non-designated verifiers. Note that this construction requires a non-aggregated proof that contains a range assertion for only a single commitment. Unlike the approach used initially in RFC-0180, it is not possible to embed additional data (like the commitment value) into a Bulletproofs+ range proof.

The general approach is that the prover and designated verifier share a common nonce seed. The prover uses this value to determinstically derive and replace certain nonces used in the proof. During the verification process, the designated verifier performs the same deterministic derivation and is able to extract the commitment masks from the proof. Because the resulting proof is still special honest-verifier zero knowledge, as long as the nonce seed is sampled uniformly at random, a non-designated verifier is not able to gain any information about the masks.

After sampling a nonce seed, the prover passes it through an appropriate set of domain-separated hash functions with scalar output to generate the following nonces used in the proof: \[ \{\eta_l\}, \{\delta_l\}, \{\alpha_l\}, \{d_{L,j,l}\}, \{d_{R,j,l}\} \] Here, as before, $0 \leq l < p$ is indexed over the number of masks used in the extended commitment, and $0 \leq j < \operatorname{lg}(mn)$ is indexed over the weighted inner product argument rounds. Let $\{\gamma_l\}$ be the masks used in the non-aggregated proof.

By doing this, the prover effectively defines the proof element set $\{\delta_l'\}$ as follows: \[ \delta_l' = \eta_l + \delta_le + e^2 \left( \alpha_l + \gamma_ly^{n+1}z^2 + \sum_{j=0}^{\operatorname{lg}(mn)-1}(e_j^2d_{L,j,l} + e_j^{-2}d_{R,j,l}) \right) \]

When verifying the proof, the designated verifier uses the nonce seed to perform the same nonce derivation as the prover. It then computes the mask set $\{\gamma_l\}$ as follows: \[ \gamma_l = \left( (\delta_l' - \eta_l - \delta_le)e^{-2} - \alpha_l - \sum_{j=0}^{\operatorname{lg}(mn)-1}(e_j^2d_{L,j,l} + e_j^{-2}d_{R,j,l}) \right) y^{-(n+1)}z^{-2} \] The recovered masks must then be checked against the extended commitment once the value is separately communicated to the verifier. Otherwise, if the verifier uses a different nonce seed than the prover did (or if the prover otherwise did not derive the nonces using a nonce seed at all), it will recover incorrect masks. If the verifier is able to construct the extended commitment from the value and recovered masks, the recovery succeeds; otherwise, the recovery fails.

Sum optimization

From Equation (3), the verifier must compute $\sum_i d_i$. Because the vector $\vec{d}$ contains $mn$ elements by Equations (1) and (2), computing the sum naively is a slow process. The implementation takes advantage of the fact that this sum can be expressed in terms of a partial sum of a geometric series to compute it much more efficiently; we describe this here.

We first recall the following identity for the partial sum of a geometric series for $r \neq 0$: \[ \sum_{k=0}^{n-1} r^k = \frac{1 - r^n}{1 - r} \]

Next, we note that from Equation (2), we have \[ \sum_{i=0}^{mn-1} (d_j)_i = \sum_{k=0}^{n-1} 2^k \] for all $0 \leq j < m$.

Given these facts, we can express the required sum of the elements of $\vec{d}$ as follows: \[ \begin{align*} \langle \vec{1}^{mn}, \vec{d} \rangle &= \sum_{i=0}^{mn-1} d_i \\ &= \sum_{i=0}^{mn-1} \left( \sum_{j=0}^{m-1} z^{2(j+1)} (d_j)_i \right) \\ &= \sum_{j=0}^{m-1} z^{2(j+1)} \left( \sum_{i=0}^{mn-1} (d_j)_i \right) \\ &= \sum_{j=0}^{m-1} z^{2(j+1)} \left( \sum_{k=0}^{n-1} 2^k \right) \\ &= (2^n - 1) \sum_{j=0}^{m-1} z^{2(j+1)} \end{align*} \] This requires a sum of only $m$ even powers of $z$, which can be computed iteratively.

Comparative performance

We now compare Bulletproofs+ performance to that of the Bulletproofs range proving system.

Size

Bulletproofs+ range proofs, like Bulletproofs, scale logarithmically in size. In each case, a proof consists of the following:

Proving systemGroup elementsScalars
Bulletproofs$2 \operatorname{lg}(nm) + 4$$5$
Bulletproofs+$2 \operatorname{lg}(nm) + 3$$p + 2$

That is, both the range bit length $n$ and aggregation factor $m$ contribute logarithmically to the proof size. In the case of the Tari-specific implementation of Bulletproofs+, the number of masks contributes linearly to the proof size.

Regardless of the bit length $n$ or aggregation factor $m$ used, a single-mask ($p = 1$) Bulletproofs+ range proof saves $1$ group element and $2$ scalars over the equivalent Bulletproofs range proof. For the Ristretto-based Tari implementation, this amounts to $96$ bytes after group element and scalar encoding.

We also note that while it is possible to encode an extra scalar of data in a Bulletproofs non-aggregated range proof (in addition to the mask) using nonce-based recovery techniques, this is not possible with Bulletproofs+ range proofs.

Verification efficiency

Like in Bulletproofs+, it is possible to reduce verification of a batch of Bulletproofs range proofs to a single multiscalar multiplication operation.

To compare this efficiency, we count unique generators used in the multiscalar multiplication operation in both cases. It is the case that scalar-only operations differ greatly between the two systems, but these operations are much faster than those involving group elements.

Let $b$ be the batch size of such a verification; that is, the number of proofs to be verified together. This means single-proof verification has $b = 1$.

Proving systemOperation size
Bulletproofs$b[2\operatorname{lg}(mn) + m + 4] + 2mn + 2$
Bulletproofs+$b[2\operatorname{lg}(mn) + m + 3] + 2mn + p + 1$

Verification in Bulletproofs+ is slightly faster than in Bulletproofs in theory.

In practice, verification of a single 64-bit range proof in the Tari Bulletproofs+ implementation is comparable to an updated fork of the Dalek Bulletproofs implementation, though verification of an aggregated proof is notably slower.

Proving efficiency

It is more challenging to compare proving efficiency, since generation of a range proof does not reduce cleanly to a single multiscalar multiplication evaluation for either Bulletproofs or Bulletproofs+ range proofs. However, we note that the overall complexity between the inner-product arguments in the proving systems is similar in terms of group operations; outside of these inner-product arguments, Bulletproofs+ requires fewer group operations. Overall efficiency is likely to depend on specific optimizations.

In practice, generation of a single 64-bit range proof in the Tari Bulletproofs+ implementation is about 10% faster than in the Dalek Bulletproofs updated fork, with similar performance for aggregated proofs.

Changelog

DateChangeAuthor
7 Dec 2022First draftAaron
13 Jan 2022Performance updatesbrianp
20 Jul 2023Sum optimizationAaron
31 Jul 2023Notation and efficiencyAaron
3 Aug 2023Proving efficiency noteAaron

RFC-0182/CommitmentSignatures

Commitment and public key signatures

status: draft

Maintainer(s): Aaron Feickert

Licence

The 3-Clause BSD Licence.

Copyright 2023 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

This Request for Comment (RFC) describes signatures relating to commitments and public keys that are useful for Tari transaction authorization.

Introduction

A commitment and public key signature (CAPK signature) is used in Tari protocols as part of transaction authorization. Given a commitment $C = aH + xG$ and public key $P = yG$, a CAPK signature asserts knowledge of the openings $(a,x)$ and $y$ in zero knowledge. Optionally, the value $a$ may be disclosed as part of the signature.

Structurally, a CAPK signature is a conjunction of Schnorr-type representation proofs for $C$ and $P$. While it is defined as an interactive protocol, the (strong) Fiat-Shamir technique is used to make it non-interactive and bind arbitrary message data, effectively transforming the proof into a signature.

Value-hiding protocol

We first describe the version of the protocol that does not reveal the commitment value. It is a sigma protocol for the following relation, where $G$ and $H$ are fixed group generators with no efficiently-computable discrete logarithm relationship: \[ \left\{ C, P ; (a, x, y) | C = aH + xG, P = yG \right\} \] The interactive form of this protocol proceeds as follows:

  • The prover samples scalar nonces $r_a, r_x, r_y$ uniformly at random.
  • The prover computes ephemeral values $C_{eph} = r_a H + r_x G$ and $P_{eph} = r_y G$, and sends these values to the verifier.
  • The verifier samples a nonzero scalar challenge $e$ uniformly at random, and sends it to the prover.
  • The prover computes $u_a = r_a + ea$ and $u_x = r_x + ex$ and $u_y = r_y + ey$, and sends these values to the verifier.
  • The verifier accepts the proof if and only if $u_a H + u_x G = C_{eph} + eC$ and $u_y G = P_{eph} + eP$.

The strong Fiat-Shamir technique can transform this into a non-interactive protocol. To do so, the prover and verifier both use a domain-separated cryptographic hash function to compute the challenge $e$, carefully binding $C$, $P$, $C_{eph}$, $P_{eph}$, and any arbitrary message data. In this non-interactive format, the public statement data is the tuple $(C, P)$ and the proof data is the tuple $(C_{eph}, P_{eph}, u_a, u_x, u_y)$.

Verification can be made more efficient by reducing to a single linear combination evaluation. To do this, the verifier samples a nonzero scalar weight $w$ uniformly at random (not using Fiat-Shamir!) and accepts the proof if and only if the following holds: \[ u_a H + (u_x + wu_y)G - C_{eph} - wP_{eph} - eC - weP = 0 \]

Security proof

We require that the (interactive) protocol be correct, special sound, and special honest-verifier zero knowledge. While the proof technique is standard, we present it here for completeness.

Correctness follows immediately by inspection.

To show the protocol is special sound, consider a rewinding argument with two distinct challenges $e \neq e'$ on the same statement $(C, P)$ and initial transcript $(C_{eph}, P_{eph})$. We must produce extracted witnesses $a, x, y$ consistent with the statement. Suppose the responses on these transcripts are $(u_a, u_x, u_y)$ and $(u_a', u_x', u_y')$, respectively. The first verification equation applied to both transcripts yields $(u_a - u_a')H + (u_x - u_x')G = (e - e')C$, from which we obtain witness extractions \[ a = \frac{u_a - u_a'}{e - e'} \] and \[ x = \frac{u_x - u_x'}{e - e'} \] such that $C = aH + xG$, as required. Similarly, the second verification equation yields $(u_y - u_y')G = (e - e')P$, so \[ y = \frac{u_y - u_y'}{e - e'} \] is the remaining extracted witness, such that $P = yG$. This shows the protocol is 2-special sound.

Finally, we show the protocol is special honest-verifier zero knowledge. This requires us to simulate, for an arbitrary statement and challenge, a transcript distributed identically to that of a real proof. Fix a statement $(C, P)$ and sample a challenge $e \neq 0$ uniformly at random. Then, sample $u_a, u_x, u_y$ uniformly at random. We then set $C_{eph} = u_a H + u_x G - eC$ and $P_{eph} = u_y G - eP$. The resulting transcript is valid by construction. Further, all proof elements are uniformly distributed at random in both the simulation and in real proofs. This shows the protocol is special honest-verifier zero knowledge.

Value-revealing protocol

We now describe a modified version of the protocol that reveals the commitment value. While this protocol can be made more efficient than we list here (discussed later), this design is intended to be more closely compatible with the value-hiding protocol for easier implementation. It is a sigma protocol for the following relation, where $G$ and $H$ are fixed group generators with no efficiently-computable discrete logarithm relationship: \[ \left\{ C, P, a ; (x, y) | C = aH + xG, P = yG \right\} \] The interactive form of this protocol proceeds as follows:

  • The prover samples scalar nonces $r_x, r_y$ uniformly at random.
  • The prover computes ephemeral values $C_{eph} = r_x G$ and $P_{eph} = r_y G$, and sends these values to the verifier.
  • The verifier samples a nonzero scalar challenge $e$ uniformly at random, and sends it to the prover.
  • The prover computes $u_a = ea$ and $u_x = r_x + ex$ and $u_y = r_y + ey$, and sends these values to the verifier.
  • The verifier accepts the proof if and only if $u_a = ea$ and $u_a H + u_x G = C_{eph} + eC$ and $u_y G = P_{eph} + eP$.

As in the value-hiding protocol, the strong Fiat-Shamir technique can transform this into a non-interactive protocol. Crucially, in this version of the protocol, the prover and verifier must also bind the value $a$ into the challenge. That is, they both use a domain-separated cryptographic hash function to compute the challenge $e$, carefully binding $C$, $P$, $a$, $C_{eph}$, $P_{eph}$, and any arbitrary message data. In this non-interactive format, the public statement data is the tuple $(C, P, a)$ and the proof data is the tuple $(C_{eph}, P_{eph}, u_a, u_x, u_y)$.

The same weighting technique as above may be used to combine the second and third verification equations here. However, the first verification equation must still be checked.

Security proof

We require that the (interactive) protocol be correct, special sound, and special honest-verifier zero knowledge. While the proof technique is standard, we present it here for completeness.

Correctness follows immediately by inspection.

To show the protocol is special sound, consider a rewinding argument with two distinct challenges $e \neq e'$ on the same statement $(C, P, a)$ and initial transcript $(C_{eph}, P_{eph})$. We must produce extracted witnesses $x, y$ consistent with the statement. Suppose the responses on these transcripts are $(u_a, u_x, u_y)$ and $(u_a', u_x', u_y')$, respectively. The first verification equation gives that $u_a = ea$ and $u_a' = e'a$. The second verification equation applied to both transcripts then yields $(e - e')aH + (u_x - u_x')G = (e - e')C$, from which we obtain witness extraction \[ x = \frac{u_x - u_x'}{e - e'} \] such that $C = aH + xG$, as required. Similarly, the third verification equation yields $(u_y - u_y')G = (e - e')P$, so \[ y = \frac{u_y - u_y'}{e - e'} \] is the remaining extracted witness, such that $P = yG$. This shows the protocol is 2-special sound.

Finally, we show the protocol is special honest-verifier zero knowledge. This requires us to simulate, for an arbitrary statement and challenge, a transcript distributed identically to that of a real proof. Fix a statement $(C, P)$ and sample a challenge $e \neq 0$ uniformly at random. Then, fix $u_a = ea$ and sample $u_x, u_y$ uniformly at random. We then set $C_{eph} = u_a H + u_x G - eC$ and $P_{eph} = u_y G - eP$. The resulting transcript is valid by construction. Further, all proof elements are either fixed by verification or uniformly distributed at random in both the simulation and in real proofs. This shows the protocol is special honest-verifier zero knowledge.

Simplification

This protocol may be simplified further. To do so, the prover does not compute $u_a$ at all, instead sending only $u_x$ and $u_y$ as its post-challenge responses. To account for this, the verifier accepts the proof if and only if $eaH + u_x G = C_{eph} + eC$ and $u_y G = P_{eph} + eP$.

However, this alters the proof format and verification, which may be undesirable for a more general implementation.

RFC-0190/Mempool

status: updated Maintainer(s): Stanley Bondi, SW van Heerden

The Mempool for Unconfirmed Transactions on the Tari Base Layer

License

The 3-Clause BSD License.

Copyright 2018 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

The purpose of this document and its content is for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community regarding the technological merits of the potential system outlined herein.

Goals

This document will introduce the Tari base layer Mempool that consists of an Unconfirmed Pool and Reorg Pool. The Mempool is used for storing and managing unconfirmed transactions.

Description

Assumptions

  • Each base node is connected to a number of peers that maintain their own copies of the Mempool.

Abstract

The Mempool is responsible for managing, verifying and maintaining all unconfirmed transactions that have not yet been included in a block and added to the Tari blockchain. It is also responsible for propagating valid transactions and sharing the Mempool state with connected peers. An overview of the required functionality for the Mempool and each of its component pools will be provided.

Overview

Every base node maintains a transaction pool called Mempool that consists of two separate pools: the Unconfirmed Pool and Reorg Pool. These two pools have different tasks and work together to form the Mempool used for maintaining unconfirmed transactions.

This is the role descriptions for each component pool:

  • Unconfirmed Pool: contains all unconfirmed transactions that have been verified, have passed all checks, that only spend valid UTXOs and don't have any time-lock restrictions.
  • Reorg Pool: stores a backup of all transactions that have recently been included into blocks, in case a blockchain reorganization occurs and these transactions have to be restored back to the Unconfirmed Pool, so that they can be included in future blocks.

Prioritizing Unconfirmed Transactions

The maximum storage capacity used for storing unconfirmed transactions by the Mempool and each of its component pools can be configured. When a new transaction is received and the storage capacity limit is reached, then transactions are prioritized by ordering their total fee per gram of all UTXOs used and transaction age, in that order. The transactions of the least priority are discarded to make room for higher priority transactions.

The transaction priority metric has the following behavior:

  • transactions with higher fee per gram SHOULD be prioritized over lower fee per gram transactions.
  • older transactions in the mempool SHOULD be prioritized over newer ones.

Memory Pool State: Syncing and Updating

On the initial startup, the complete state of the unconfirmed pool is pulled from the connected peers. Typically, Mempool doesn't persist its state, but can be configured to do so. If the state is locally present, then only the missing, unconfirmed transactions are synced from the peers, otherwise, the full state is requested. The validity and priority of transactions are computed as they are being downloaded from the connected peers. If the base node undergoes a re-org, then the missing state is again pulled from the peers.

The functional bahavior required for the Mempool's synchronization are the following:

  • All verified transactions MUST be propagated to neighboring peers.
  • Unverified or invalid transactions MUST NOT be propagated to peers.
  • Verified transactions that were discarded due to low priority levels MUST be propagated to peers.
  • Duplicate transactions MUST NOT be propagated to peers.
  • Mempool MUST have an interface, allowing peers to query and download its state, partially and in full.
  • Mempool MUST accept all transactions received from peers but MAY decide to discard low-priority transactions.
  • Mempool MUST allow wallets to track payments by monitoring that a particular transaction has been added to the Mempool.
  • Mempool MAY choose:
    • to discard its state on restart and then download the full state from its peers or
    • to store its state using persistent storage to reduce communication bandwidth required when reinitializing the pool after a restart.

Unconfirmed Pool

This Mempool component consists of transactions that have been received, verified and have passed all the checks, but not yet included in the blocks. These transactions are ready to be used to construct new blocks for the Tari blockchain.

Functional behavior required of the Unconfirmed Pool:

  • It MUST verify that incoming transactions spend only existing UTXOs.
  • It MUST ensure that incoming transactions don't have a processing time-lock or has a time-lock that has expired.
  • It MUST ensure that all time-locks of the UTXOs that will be spent by the transaction have expired.
  • Transactions that have been used to construct new blocks MUST be removed from the Unconfirmed Pool and added to the Reorg Pool.

Reorg Pool

The Reorg Pool consists of transactions that have recently been added to blocks, resulting in their removal from the Unconfirmed Pool. When a potential blockchain reorganization occurs that invalidates previously assembled blocks, the transactions used to construct these discarded blocks can be moved back into the Unconfirmed Pool. This ensures that high-priority transactions are not lost during reorganization and can be included in future blocks. The Reorg Pool is an internal, isolated Mempool component and cannot be accessed or queried from outside.

Functional behavior required of the Reorg Pool:

  • Copies of the transactions used recently in blocks MUST be stored in the Reorg Pool.
  • Transactions in the Reorg Pool MAY be discarded after a set expiration threshold has been reached.
  • When reorganization is detected, all affected transactions found in the Reorg Pool MUST be moved back to the Unconfirmed Pool and removed from the Reorg Pool.

Mempool

The Mempool is responsible for the internal transaction management and synchronization with the peers. New transactions must pass all verification steps to make it into the Unconfirmed Pool and further be propagated to peers.

Functional behavior required of the Mempool:

  • If the received transaction already exists in the Mempool, then it MUST be discarded.
  • If multiple transactions contain the same UTXO, then only the highest priority transaction MUST be kept and the rest (having the said UTXO included) MUST be discarded.
  • If the storage capacity limit is reached, then new incoming transactions SHOULD be prioritized according to a set number of rules.
  • Transactions of the least priority MUST be discarded to make room for higher priority incoming transactions.
  • Transactions with its computed priority being lower than the minimum set threshold MUST be discarded.
  • The Mempool MUST verify that incoming transactions do not have duplicate outputs.
  • The Mempool MUST verify that only matured coinbase outputs can be spent.
  • Each component pool MAY have the storage capacity configured and adjusted.
  • The memory pool SHOULD have a mechanism to estimate fee categories from the current Mempool state. For example, the transaction fee can be estimated, ensuring that new transactions will be properly prioritized, to be added into new blocks in a timely manner.

Functional behavior required for the allocation of incoming transactions in the component pools:

  • Verified transactions that have passed all checks such as spending of valid UTXOs and expired time-locks MUST be placed in the Unconfirmed Pool.
  • Incoming transactions with time-locks, prohibiting them from being included in new blocks SHOULD be discarded.
  • Newly received, verified transactions attempting to spend a UTXO that does not yet exist MUST be discarded.
  • Transactions that have been added to blocks and were removed from the Unconfirmed Pool SHOULD be added to the Reorg Pool.

Tari-specific Base Layer Extensions

This section covers RFCs that describe extensions to the Mimblewimble protocol that enable key functionality for the Tari Base layer token and Digital Asset network.

RFC-0201/TariScript

TariScript

status: stable

Maintainer(s): Cayle Sharrock, Stringhandler

Licence

The 3-Clause BSD Licence.

Copyright 2020 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

This Request for Comment (RFC) presents a proposal for introducing TariScript into the Tari base layer protocol. Tari Script aims to provide a general mechanism for enabling further extensions such as side-chains, the DAN, one-sided payments and atomic swaps.

$$ \newcommand{\script}{\alpha} % utxo script \newcommand{\input}{ \theta } \newcommand{\cat}{\Vert} \newcommand{\so}{\gamma} % script offset \newcommand{\hash}[1]{\mathrm{H}\bigl({#1}\bigr)} $$

Introduction

It is hopefully clear to anyone reading these RFCs that the ambitions of the Tari project extend beyond a Mimblewimble-clone-coin. It should also be fairly clear that vanilla Mimblewimble does not have the feature set to provide functionality such as:

  • One-sided payments
  • Multiparty side-chain peg-outs and peg-ins
  • Generalised smart contracts

Extensions to Mimblewimble have been proposed for most of these features, for example, David Burkett's one-sided payment proposal for LiteCoin (LIP-004) and this project's HTLC RFC.

Some smart contract features are possible, or partly possible in vanilla Mimblewimble using Scriptless script, such as

  • Atomic swaps
  • Hash time-locked contracts

Tari implemented a scripting language similar to Bitcoin script, called TariScript, under a single set of (relatively minor) modifications and additions to the Mimblewimble protocol, which achieved collapsing all of these use cases.

Scripting on Mimblewimble

Other than Beam, none of the existing Mimblewimble projects have employed a scripting language.

Grin styles itself as a "Minimal implementation of the Mimblewimble protocol", so one might infer that this status is unlikely to change soon.

Beam does have a smart contract protocol, which allows users to execute arbitrary code (shaders) in a sandboxed Beam VM and have the results of that code interact with transactions.

Mimblewimble coin is a fork of Grin and "considers the protocol ossified".

Litecoin has included Mimblewimble as a side-chain through MWEB. As of 2022, there appears to be no plans to include general scripting into the protocol.

Scriptless scripts

Scriptless script is a wonderfully elegant technology and the inclusion of TariScript does not preclude the use of Scriptless scripts in Tari. However, scriptless scripts have some disadvantages:

  • They are often difficult to reason about, with the result that the development of features based on scriptless scripts is essentially in the hands of a very select group of cryptographers and developers.
  • The use case set is impressive considering that the "scripts" are essentially signature wrangling, but is still somewhat limited.
  • Every feature must be written and implemented separately using the specific and specialised protocol designed for that feature. That is, it cannot be used as a dynamic scripting framework on a running blockchain.

TariScript - a brief motivation

The essential idea of TariScript is as follows:

Given a standard Tari UTXO, we add additional restrictions on whether that UTXO can be included as a valid input in a transaction.

As long as those conditions are suitably committed to, are not malleable throughout the existence of the UTXO, and one can prove that the script came from the UTXO owner, then these conditions are not that different to the requirement of having range proofs attached to UTXOs, which require that the value of Tari commitments is non-negative.

This argument is independent of the nature of the additional restrictions. Specifically, if these restrictions are manifested as a script that provides additional constraints over whether a UTXO may be spent, the same arguments apply.

This means that in a very hand-wavy sort of way, there ought to be no reason that TariScript is not workable.

Note that range proofs can be discarded after a UTXO is spent. This entails that the global security guarantees of Mimblewimble is not that every transaction in history was valid from an inflation perspective, but that the net effect of all transactions leads to zero spurious inflation. This sounds worse than it is, since locally, every individual transaction is checked for validity at the time of inclusion in the blockchain.

If it somehow happened that two illegal transactions made it into the blockchain (perhaps due to a bug), and the two cancelled each other out such that the global coin supply was still correct, one would never know this when doing a chain synchronisation in pruned mode.

But if there was a steady inflation bug due to invalid range proofs making it into the blockchain, a pruned mode sync would still detect that something was awry, because the global coin supply balance acts as another check.

With TariScript, once the script has been pruned away, and then there is a re-org to an earlier point on the chain, then there's no way to ensure that the script was honoured unless you run an archival node.

This is broadly in keeping with the Mimblewimble security guarantees that, in pruned-mode synchronisation, individual transactions are not necessarily verified during chain synchronisation.

However, the guarantee that no additional coins are created or destroyed remains intact.

Put another way, the blockchain relies on the network at the time to enforce the TariScript spending rules. This means that the scheme may be susceptible to certain horizon attacks.

Incidentally, a single honest archival node would be able to detect any fraud on the same chain and provide a simple proof that a transaction did not honour the redeem script.

Additional requirements

The assumptions that broadly equate scripting with range proofs in the above argument are:

  • The script must be committed to the blockchain.
  • The script must not be malleable in any way without invalidating the transaction. This restriction extends to all participants, including the UTXO owner.
  • We must be able to prove that the UTXO originator provides the script and no one else.
  • The scripts and their redeeming inputs must be stored on the blockchain. In particular, the input data must not be malleable.

Preventing Cut-through

A major issue with many Mimblewimble extension schemes is that miners are able to cut-through UTXOs if an output is spent in the same block it was created. This makes it so that the intervening UTXO never existed; along with any checks and balances carried in that UTXO. It's also impossible to prove without additional information that cut-through even occurred (though one may suspect, since the "one" transaction would contribute two kernels to the block).

In particular, cut-through is devastating for an idea like TariScript which relies on conditions present in the UTXO being enforced. For example, say there is a UTXO in the mempool that everyone knows the blinding factor to, but is restricted to a single public key via the TariScript. A malicious user can spend the UTXO in a zero-conf transaction, and send the cut-through transaction to the mempool. Since the miner only sees the resulting aggregate transaction, it cannot know that there was a TariScript on the removed UTXO. The solution to this problem is described later in this RFC.

In contrast, range proofs are still valid if they are cut-through, because the resulting UTXOs must have valid range proofs.

Protocol additions

Please refer to Notation, which provides important pre-knowledge for the remainder of the report.

At a high level, TariScript works as follows:

  • The spending script \((\script)\) is recorded in the transaction UTXO.
  • Although scripts are included on the UTXO, they are only executed when the UTXO is spent, and in most cases, will require additional input data to be provided at this time.
  • The script input data is recorded in the transaction inputs.
  • When validating a transaction, the script is executed using the script input data.
  • After the script \((\script)\) is executed, the execution stack must contain exactly one value that will be interpreted as the script public key \((K_{S})\).
  • The script public key and commitment must match the script signature on the input, which prevents malleability of the data in the input.
  • To prevent a script from being removed from a UTXO, a new field sender offset public key \((K_{O})\) has been added.
  • The sender offset private keys \((k_{O})\) and script private keys \((k_{S})\) are used in conjunction to create a script offset \((\so)\), which are used in the consensus balance to prevent a number of attacks.

NOTE: One can prove ownership of a UTXO by demonstrating knowledge of both the commitment blinding factor \((k\)), and the script private key \((k_{S})\) for a valid script input.

UTXO data commitments

The script, as well as other UTXO metadata, such as the output features are signed for with the sender offset private key to prevent malleability. As we will describe later, the notion of a script offset is introduced to prevent cut-through and forces the preservation of these commitments until they are recorded into the blockchain.

Transaction output

The definition of a Tari transaction output is:

pub struct TransactionOutput {
    /// The transaction output version
    version: TransactionOutputVersion,
    /// Options for an output's structure or use
    features: OutputFeatures,
    /// The homomorphic commitment representing the output amount
    commitment: Commitment,
    /// A proof that the commitment is in the right range
    proof: RangeProof,
    /// The serialised script
    script: Vec<u8>,
    /// The sender offset pubkey, K_O
    sender_offset_public_key: PublicKey
    /// UTXO signature signing the transaction output data and the homomorphic commitment with a combination 
    /// of the homomorphic commitment private values (amount and blinding factor) and the sender offset private key.
    metadata_signature: CommitmentAndPublicKeySignature,
    /// The covenant that will be executed when spending this output
    covenant: Covenant,
    /// The encrypted commitment value.
    encrypted_value: EncryptedValue,
    /// The minimum value of the commitment that is proven by the range proof
    minimum_value_promise: MicroTari,
}

The metadata signature is a CAPK signature (as described in RFC-0182) signed with the commitment value, \( v_i \), known by the sender and receiver, the spending key, \( k_i \), known by the receiver and the sender offset private key, \(k_{Oi}\), known by the sender. (Note that \( k_{Oi} \) should be treated as a nonce.) The CAPK signature is effectively an aggregated CAPK signature between the sender and receiver, and the challenge consists of all the transaction output metadata, effectively forming a contract between the sender and receiver, making all those values non-malleable and ensuring only the sender and receiver can enter into this contract.

For purposes of this RFC, we denote the metadata signature terms as follows:

  • \( R_{MRi} \) is the ephemeral commitment,
  • \( R_{MSi} \) is the ephemeral public key,
  • \( a_{MRi} \) and \( b_{MRi} \) are the first and second commitment signature scalars,
  • \( b_{MSi} \) is the public key signature scalar.

Sender:

The sender's ephemeral public key is:

$$ \begin{aligned} R_{MSi} &= r_{MSi_b} \cdot G \end{aligned} \tag{3} $$

The sender sends \( (K_{Oi}, R_{MSi}) \) along with the other partial transaction information \( (\script_i, F_i) \) to the receiver, who now has all the required information to calculate the final challenge.

Reciver:

The commitment definition is unchanged:

$$ \begin{aligned} C_i = v_i \cdot H + k_i \cdot G \end{aligned} \tag{4} $$

The receiver's ephemeral commitment is:

$$ \begin{aligned} R_{MRi} &= r_{MRi_a} \cdot H + r_{MRi_b} \cdot G \end{aligned} \tag{5} $$

The final challenge is:

$$ \begin{aligned} e &= \hash{ R_{MSi} \cat R_{MRi} \cat \script_i \cat F_i \cat K_{Oi} \cat C_i \cat \pi_i \cat \varphi_i \cat \vartheta_i } \\ \end{aligned} \tag{6} $$

The receiver can now calculate their portion of the aggregated CAPK signature as:

$$ \begin{aligned} a_{MRi} &= r_{MRi_a} + e \cdot v_{i} \\ b_{MRi} &= r_{MRi_b} + e \cdot k_i \end{aligned} \tag{7} $$

The receiver sends \( s_{MRi} = (a_{MRi}, b_{MRi}, R_{MRi} ) \) along with the other partial transaction information \( (C_i) \) to the sender.

Sender:

The sender starts by calculating the final challenge \( e \) (6) and then completes their part of the aggregated CAPK signature.

$$ \begin{aligned} b_{MSi} &= r_{MSi_b} + e \cdot k_{Oi} \end{aligned} \tag{8} $$

The final CAPK signature is combined as follows:

$$ \begin{aligned} s_{Mi} = (a_{MRi}, b_{MRi}, R_{MRi}, b_{MSi}, R_{MSi} ) \end{aligned} \tag{9} $$

Verifier:

This is verified by the following:

$$ \begin{aligned} a_{MRi} \cdot H + b_{MRi} \cdot G &\overset{?}{=} R_{MRi} + e \cdot C \\ b_{MSi} \cdot G &\overset{?}{=} R_{MSi} + e \cdot K_{Oi} \end{aligned} \tag{10} $$

Note that:

  • The UTXO has a positive value \( v \) like any normal UTXO.
  • The script and the output features can no longer be changed by the miner or any other party. This includes the sender and receiver; they would need to cooperate to enter into a new contract to change any metadata, otherwise, the metadata signature will be invalidated.
  • We provide the complete script on the output.

Transaction input

In standard Mimblewimble, an input is the same as an output sans range proof. The range proof doesn't need to be checked again when spending inputs, so it is dropped.

The definition of a Tari transaction input is:

pub struct TransactionInput {
    /// The transaction input version
    version: TransactionInputVersion,
    /// The output that will be spent that this input is referencing 
    spent_output: SpentOutput {
        /// The transaction output version
        version: TransactionOutputVersion,
        /// Options for an output's structure or use
        features: OutputFeatures,
        /// The homomorphic Pedersen commitment representing the output amount
        commitment: Commitment,
        /// The serialised script
        script: Vec<u8>,
        /// The sender offset pubkey, K_O
        sender_offset_public_key: PublicKey
        /// The covenant that will be executed when spending this output
        covenant: Covenant,
        /// The encrypted commitment value.
        encrypted_value: EncryptedValue,
        /// The minimum value of the commitment that is proven by the range proof
        minimum_value_promise: MicroTari,
    }
    /// The script input data, if any
    input_data: Vec<u8>,
    /// Signature signing the script, input data, [script public key], and the homomorphic commitment with a combination 
    /// of the homomorphic commitment private values (amount and blinding factor) and the [script private key].
    script_signature: CommitmentAndPubKeySignature,
}

The script signature is a CAPK signature using a combination of the output commitment private values \( (v_i \, , \, k_i )\) and script private key \(k_{Si}\) to prove ownership thereof. It signs the script, the script input, script public key, and the commitment.

For purposes of this RFC, we denote the script signature terms as follows:

  • \( R_{SCi} \) is the ephemeral commitment,
  • \( R_{SPi} \) is the ephemeral public key,
  • \( a_{SCi} \) and \( b_{SCi} \) are the first and second commitment signature scalars,
  • \( b_{SPi} \) is the public key signature scalar.

Sender:

The script signature is given by

$$ \begin{aligned} s_{Si} = (a_{SCi}, b_{SCi}, R_{SCi}, b_{SPi}, R_{SPi} ) \end{aligned} \tag{11} $$

where

$$ \begin{aligned} R_{SCi} &= r_{SCi_a} \cdot H + r_{SCi_b} \cdot G \\ a_{SCi} &= r_{SCi_a} + e \cdot v_i \\ b_{SCi} &= r_{SCi_b} + e \cdot k_i \\ R_{SPi} &= r_{SPi_b} \cdot G \\ b_{SPi} &= r_{SPi_b} + e \cdot k_{Si} \\ \end{aligned} \tag{12} $$

with the challenge being

$$ \begin{aligned} e &= \hash{ R_{SCi} \cat R_{SPi} \cat \alpha_i \cat \input_i \cat K_{Si} \cat C_i} \\ \end{aligned} \tag{13} $$

Verifier:

This is verified by the following:

$$ \begin{aligned} a_{SCi} \cdot H + b_{SCi} \cdot G &\overset{?}{=} R_{SCi} + e \cdot C \\ b_{SPi} \cdot G &\overset{?}{=} R_{SPi} + e \cdot K_{Si} \end{aligned} \tag{14} $$

The script public key \(K_{Si}\) needed for the script signature verification is not stored with the TransactionInput, but obtained by executing the script with the provided input data. Because this signature is signed with the script private key \(k_{Si}\), it ensures that only the owner can provide the input data \(\input_i\) to the TransactionInput.

Script Offset

For every transaction, an accompanying script offset \( \so \) needs to be provided. This is there to prove that every
script public key \( K_{Sj} \) and every sender offset public key \( K_{Oi} \) supplied with the UTXOs are the correct ones. The sender will know and provide sender offset private keys \(k_{Oi} \) and script private keys \(k_{Si} \); these are combined to create the script offset \( \so \), which is calculated as follows:

$$ \begin{aligned} \so = \sum_j\mathrm{k_{Sj}} - \sum_i\mathrm{k_{Oi}} \; \text{for each input}, j,\, \text{and each output}, i \end{aligned} \tag{15} $$

Verification of (15) will entail:

$$ \begin{aligned} \so \cdot G = \sum_j\mathrm{K_{Sj}} - \sum_i\mathrm{K_{Oi}} \; \text{for each input}, j,\, \text{and each output}, i \end{aligned} \tag{16} $$

We modify the transactions to be:

pub struct Transaction {
    
    ...
    
    /// A scalar offset that links outputs and inputs to prevent cut-through, enforcing the correct application of
    /// the output script.
    pub script_offset: BlindingFactor,
}

All script offsets (\(\so\)) from (15) contained in a block are summed together to create a total script offset (17) so that algorithm (15) still holds for a block.

$$ \begin{aligned} \so_{total} = \sum_k\mathrm{\so_{k}}\; \text{for every transaction}, k \end{aligned} \tag{17} $$

Verification of (17) will entail:

$$ \begin{aligned} \so_{total} \cdot G = \sum_j\mathrm{K_{Sj}} - \sum_i\mathrm{K_{Oi}} \; \text{for each input}, j,\, \text{and each output}, i \end{aligned} \tag{18} $$

As can be seen, all information required to verify (17) is contained in a block's inputs and outputs. One important distinction to make is that the Coinbase output in a coinbase transaction does not count toward the script offset. This is because the Coinbase UTXO already has special rules accompanying it and it has no input, thus we cannot generate a script offset \( \so \). The coinbase output can allow any script \(\script_i\) and sender offset public key \( K_{Oi} \) as long as it does not break any of the rules in RFC 120 and the script is honored at spend. If the coinbase is used as an input, it is treated exactly the same as any other input.

We modify Blockheaders to be:

pub struct BlockHeader {
    
    ...
    
    /// Sum of script offsets for all kernels in this block.
    pub total_script_offset: Scalar,
}

This notion of the script offset \(\so\) means that no third party can remove any input or output from a transaction or the block, as that will invalidate the script offset balance equation, either (16) or (18) depending on whether the scope is a transaction or block. It is important to know that this also stops cutโ€‘through so that we can verify all spent UTXO scripts. Because the script private key and
sender offset private key are not publicly known, it's impossible to create a new script offset.

Certain scripts may allow more than one valid set of input data. Users might be led to believe that this will allow a third party to change the script keypair \((k_{Si}\),\(K_{Si})\). If an attacker can change the \(K_{Si}\) keys of the input then he can take control of the \(K_{Oi}\) as well, allowing the attacker to change the metadata of the UTXO including the script. But as shown in Script offset security, this is not possible.

If equation (16) or (18) balances then we know that each included input and output in the transaction or block has its correct script public key and sender offset public key. Signatures (9) & (11) are checked independently from script offset verification (16) and (18), and looked at in isolation those could verify correctly but can still be signed by fake keys. When doing verification in (16) and (18) you know that the signatures and the message/metadata signed by the private keys can be trusted.

Consensus

TariScript does not impact the Mimblewimble balance for blocks and transactions, however, an additional consensus rule for transaction and block validation is required.

Verify that for every valid transaction or block:

  1. The metadata signature \( s_{Mi} \) is valid for every output.
  2. The script executes successfully using the given input script data.
  3. The result of the script is a valid script public key, \( K_S \).
  4. The script signature, \( s_{Si} \), is valid for every input.
  5. The script offset is valid for every transaction and block.

Preventing Cut-through with the Script Offset

Earlier, we described that cut-through must be prevented; this is achieved by the script offset. It mathematically links all inputs and outputs of all the transactions in a block and that tallied up creates the script offset. Providing the script offset requires knowledge of keys that miners do not possess; thus they are unable to produce the necessary script offset when attempting to perform cut-through on a pair of transactions.

Let's show by example how the script offset stops cut-through, where Alice spends to Bob who spends to Carol. Ignoring fees, we have:

$$ C_a \Rightarrow C_b \Rightarrow C_c $$

For these two transactions, the total script offset is calculated as follows:

$$ \begin{aligned} \so_1 = k_{Sa} - k_{Ob}\\ \so_2 = k_{Sb} - k_{Oc}\\ \end{aligned} \tag{19} $$

$$ \begin{aligned} \so_t = \so_1 + \so_2 = (k_{Sa} + k_{Sb}) - (k_{Ob} + k_{Oc})\\ \end{aligned} \tag{20} $$

In standard Mimblewimble cut-through can be applied to get:

$$ C_a \Rightarrow C_c $$

After cut-through the total script offset becomes:

$$ \begin{aligned} \so'_t = k_{Sa} - k_{Oc}\\ \end{aligned} \tag{21} $$

As we can see:

$$ \begin{aligned} \so_t\ \neq \so'_t \\ \end{aligned} \tag{22} $$

A third party cannot generate a new script offset as only the original owner can provide the script private key \(k_{Sa}\) to create a new script offset.

Script offset security

If all the inputs in a transaction or a block contain scripts such as just NOP or CompareHeight commands, then the hypothesis is that it is possible to recreate a false script offset. Let's show by example why this is not possible. In this Example we have Alice who pays Bob with no change output:

$$ C_a \Rightarrow C_b $$

Alice has an output \(C_{a}\) which contains a script that only has a NOP command in it. This means that the script \( \script_a \) will immediately exit on execution leaving the entire input data \( \input_a \)on the stack. She sends all the required information to Bob as per the standard mw transaction, who creates an output \(C_{b}\). Because of the NOP script \( \script_a \), Bob can change the script public key \( K_{Sa}\) contained in the input data. Bob can now use his own \(k'_{Sa}\) as the script private key. He replaces the sender offset public key with his own \(K'_{Ob}\) allowing him to change the script \( \script_b \) and generate a new signature as in (9). Bob can now generate a new script offset with \(\so' = k'_{Sa} - k'_{Ob} \). Up to this point, it all seems valid. No one can detect that Bob changed the script to \( \script_b \).

But what Bob also needs to do is generate the signature in (13). For this signature, Bob needs to know \(k_{Sa}, k_a, v_a\). Because Bob created a fake script private key, and there is no change in this transaction, he does know the script private key and the value. But Bob does not know the blinding factor \(k_a\) of Alice's commitment and thus cannot complete the signature in (13). Only the rightful owner of the commitment, which in Mimblewimble terms is the person who knows \( k_a, v_a\), and can generate the signature in (13).

Script lock key generation

At face value, it looks like the burden for wallets has tripled, since each UTXO owner has to remember three private keys, the spend key, \( k_i \), the sender offset key \( k_{O} \), and the script key \( k_{S} \). In practice, the script key will often be a static key associated with the user's node or wallet. Even if it is not, the script and sender offset keys can be deterministically derived from the spend key. For example, \( k_{S} \) could be \( \hash{ k_i \cat \alpha} \).

Blockchain bloat

The most obvious drawback to TariScript is the effect it has on blockchain size. UTXOs are substantially larger, with the addition of the script, metadata signature, script signature, and a public key to every output.

These can eventually be pruned but will increase storage and bandwidth requirements.

The input size of a block is much bigger than in standard Mimblewimble, whereas it would only be a commitment and output features. In Tari, each input includes a script, input_data, the script signature, and an extra public key. This could be compacted by just broadcasting input hashes along with the missing script input data and signature, instead of the full input in a transaction message, but this will still be larger than standard Mimblewimble inputs.

In Tari, every header is also bigger as it includes an extra blinding factor that cannot be pruned away.

Fodder for chain analysis

Another potential drawback of TariScript is the additional information that is handed to entities wishing to perform chain analysis. Having scripts attached to outputs will often clearly mark the purpose of that UTXO. Users may wish to re-spend outputs into vanilla, default UTXOs in a mixing transaction to disassociate Tari funds from a particular script.

Notation

Where possible, the "usual" notation is used to denote terms commonly found in cryptocurrency literature. Lowercase characters are used as private keys, while uppercase characters are used as public keys. New terms introduced by TariScript are assigned Greek lowercase letters in most cases.

SymbolDefinition
\( \script_i \)An output script for output i, serialised to binary.
\( F_i \)Output features for UTXO i.
\( f_t \)Transaction fee for transaction t.
\( (k_{Oi}, K_{Oi}) \)The private - public keypair for the UTXO sender offset key. Note that \( k_{Oi} \) should be treated as a nonce.
\( (k_{Si}, K_{Si}) \)The private - public keypair for the script key. The script, \( \script_i \) resolves to \( K_S \) after completing execution.
\( \so_t \)The script offset for transaction t, see (15)
\( C_i \)A Pedersen commitment to a value \( v_i \), see (4)
\( \input_i \)The serialised input for script \( \script_i \)
\( \pi_i \)The covenant for UTXO i.
\( \varphi_i \)The encrypted value for UTXO i.
\( \vartheta_i \)The minimum value promise for UTXO i.
\( s_{Si} \)A script signature for output \( i \), see (11 - 13). Additionally, the capital letter subscripts, C and P refer to the ephemeral commitment and ephemeral public key portions respectively (example \( s_{SCi}, s_{SPi} \)) .
\( s_{Mi} \)A metadata signature for output \( i \), see (3 - 10). Additional the capital letter subscripts, R and S refer to a UTXO receiver and sender respectively (exmple \( s_{MRi}, s_{MSi} \)) .

Credits

Thanks to David Burkett for proposing a method to prevent cut-through and willingness to discuss ideas.

Change log

DateChangeAuthor
17 Aug 2020First draftCjS77
11 Feb 2021Major updateCjS77, SWvheerden, philipr-za
26 Apr 2021Clarify one sided payment rulesSWvheerden
31 May 2021Including full script in transaction outputsphilipr-za
04 Jun 2021Remove beta range-proof calculationSWvheerden
22 Jun 2021Change script_signature type to ComSighansieodendaal
30 Jun 2021Clarify Tari Script nomenclaturehansieodendaal
06 Oct 2022Minor improvements in legibilitystringhandler
11 Nov 2022Update ComAndPubSig and move out examplesstringhandler
22 Nov 2022Added metadata_signature and script_signature mathhansieodendaal
06 Apr 2023Grammar and spelling changesSWvheerden

RFC-0202/TariScriptOpcodes

TariScript Opcodes

status: stable

Maintainer(s): Cayle Sharrock

Licence

The 3-Clause BSD Licence.

Copyright 2020 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

This Request for Comment (RFC) defines the opcodes that make up the TariScript scripting language and provides some examples and applicaitons.

Introduction

TariScript semantics

The proposal for TariScript is straightforward. It is based on Bitcoin script and inherits most of its ideas.

The main properties of TariScript are

  • The scripting language is stack-based. At redeem time, the UTXO spender must supply an input stack. The script runs by operating on the stack contents.
  • If an error occurs during execution, the script fails.
  • After the script completes, it is successful if and only if it has not aborted, and there is exactly a single element on the stack. The script fails if the stack is empty, or contains more than one element, or aborts early.
  • It is not Turing complete, so there are no loops or timing functions.
  • The opcodes enforce type safety. e.g. A public key cannot be added to an integer scalar. Errors of this kind MUST cause the script to fail. The Rust implementation of TariScript automatically applies the type safety rules.

Failure modes

Bitcoin transactions can "fail" in two main ways: Either there is a genuine error in the locking or unlocking script; or a wallet broadcasts a Non-standard transaction to a non-mining node. To be more precise, Bitcoin core nodes only accept a small subset of valid transaction scripts that are deemed Standard transactions. To succesfully produce a non-standard Bitcoin transaction, one has to submit it directly to a miner that accepts non-standard transactions.

It's interesting to note that only 0.02% of transactions mined in Bitcoin before block 550,000 were non-standard, and it appears that the vast majority of these were in fact unintentional, leading to loss of funds (see Non-standard transaction).

The present RFC proposes that TariScript not identify a subset of transactions as "standard". However, some transactions might be invalid in and of- themselves (e.g. invalid signature, invalid script), while others may be invalid because of the execution context (e.g. a lock time has not expired).

All Tari nodes MUST reject the former type of invalid transaction; and SHOULD reject the latter. In these instances, it is the wallets' responsibility to wait until transactions are valid before broadcasting them.

Rejected transactions are simply silently dropped.

This policy discourages spamming on the network and promotes responsible behaviour by wallets.

The full list of Error codes is given below.

Constraints

  • The maximum length of a script when serialised is 1,024 bytes.
  • The maximum length of a script's input is 1,024 bytes.
  • The maximum stack height is 255.

Opcode versions

Base layer core consensus constants are linked to block height for each network, be it testnet, stagenet or mainnet, and are backwards compatible, meaning a base node running updated consensus constants will also be able to validate the blockchain for the previous version up to its last effective block height.

Opcode versioning is contained within the consensus constants and is used to determine which opcodes are effective from which block height. As an example, OpcodeVersion::V0 could be effective from the genesis block, OpcodeVersion::V1 may contain two additional opcodes and could be effective from height 1234, whereas OpcodeVersion::V2 may deprecate three other opcodes and be effective from height 21743.

Opcodes

TariScript opcodes range from 0 to 255 and are represented as a single unsigned byte. The opcode set is limited to allow for the applications specified in this RFC, but can be expanded in the future.

Block height checks

All these opcodes test the current block height (or, if running the script as part of a transaction validation, the next earliest block height) against a given value.

CheckHeightVerify(height)

Pops the top of the stack as height. Compare the current block height to height.

  • Fails with IncompatibleTypes if u64 is not a valid 64-bit unsigned integer.
  • Fails with VerifyFailed if the block height < height.
CheckHeight(height)

Pops the top of the stack as height. Pushes the value of (the current tip height - height) to the stack. In other words, the top of the stack will hold the height difference between height and the current height. If the chain has progressed beyond height, the value is positive; and negative if the chain has yet to reach height.

  • Fails with IncompatibleTypes if u64 is not a valid 64-bit unsigned integer.
  • Fails with StackOverflow if the stack would exceed the max stack height.
CompareHeightVerify

Pops the top of the stack as height and compares it to the current block height.

  • Fails with InvalidInput if there is not a valid integer value on top of the stack.
  • Fails with StackUnderflow if the stack is empty.
  • Fails with VerifyFailed if the block height < height.
CompareHeight

Pops the top of the stack as height, then pushes the value of (height - the current height) to the stack. In other words, this opcode replaces the top of the stack with the difference between height and the current height.

  • Fails with InvalidInput if there is not a valid integer value on top of the stack.
  • Fails with StackUnderflow if the stack is empty.

Stack manipulation

NoOp

No op. Does nothing. Never fails.

PushZero

Pushes a zero onto the stack. This is a very common opcode and has the same effect as PushInt(0) but is more compact. PushZero can also be interpreted as PushFalse, although no such opcode exists.

  • Fails with StackOverflow if the stack would exceed the max stack height.
PushOne

Pushes a one onto the stack. This is a very common opcode and has the same effect as PushInt(1) but is more compact. PushOne can also be interpreted as PushTrue, although no such opcode exists.

  • Fails with StackOverflow if the stack would exceed the max stack height.
PushHash(HashValue)

Pushes the associated 32-byte value onto the stack.

  • Fails with IncompatibleTypes if HashValue is not a valid 32 byte sequence.
  • Fails with StackOverflow if the stack would exceed the max stack height.
PushInt(val)

Pushes the associated 64-bit signed integer (val) onto the stack.

  • Fails with IncompatibleTypes if val is not a valid 64-bit signed integer.
  • Fails with StackOverflow if the stack would exceed the max stack height.
PushPubKey(PublicKey)

Pushes the associated 32-byte value onto the stack. It will be interpreted as a public key or a commitment.

  • Fails with IncompatibleTypes if PublicKey is not a valid 32 byte RistrettoPublicKey sequence.
  • Fails with StackOverflow if the stack would exceed the max stack height.
Drop

Drops the top stack item.

  • Fails with StackUnderflow if the stack is empty.
Dup

Duplicates the top stack item.

  • Fails with StackUnderflow if the stack is empty.
  • Fails with StackOverflow if the stack would exceed the max stack height.
RevRot

Reverse rotation. The top stack item moves into 3rd place, e.g. abc => bca.

  • Fails with StackUnderflow if the stack has fewer than three items.

Math operations

GeZero

Pops the top stack element as val. If val is greater than or equal to zero, push a 1 to the stack, otherwise push 0.

  • Fails with StackUnderflow if the stack is empty.
  • Fails with InvalidInput if val is not an integer.

GtZero

Pops the top stack element as val. If val is strictly greater than zero, push a 1 to the stack, otherwise push 0.

  • Fails with StackUnderflow if the stack is empty.
  • Fails with InvalidInput if the item is not an integer.

LeZero

Pops the top stack element as val. If val is less than or equal to zero, push a 1 to the stack, otherwise push 0.

  • Fails with StackUnderflow if the stack is empty.
  • Fails with InvalidInput if the item is not an integer.

LtZero

Pops the top stack element as val. If val is strictly less than zero, push a 1 to the stack, otherwise push 0.

  • Fails with StackUnderflow if the stack is empty.
  • Fails with InvalidInput if the items is not an integer.
Add

Pops two items from the stack and pushes their sum to the stack.

  • Fails with StackUnderflow if the stack has fewer than two items.
  • Fails with InvalidInput if the items cannot be added to each other (e.g. an integer and public key).
Sub

Pops two items from the stack and pushes the second minus the top to the stack.

  • Fails with StackUnderflow if the stack has fewer than two items.
  • Fails with InvalidInput if the items cannot be subtracted from each other (e.g. an integer and public key).
Equal

Pops the top two items from the stack, and pushes 1 to the stack if the inputs are exactly equal, 0 otherwise. A 0 is also pushed if the values cannot be compared (e.g. integer and pubkey).

  • Fails with StackUnderflow if the stack has fewer than two items.
EqualVerify

Pops the top two items from the stack, and compares their values.

  • Fails with StackUnderflow if the stack has fewer than two items.
  • Fails with VerifyFailed if the top two stack elements are not equal.

Boolean logic

Or(n)

Pops n + 1 items from the stack. If the last item matches at least one of the first n items, push 1 onto the stack, otherwise push 0 onto the stack.

  • Fails with StackUnderflow if the stack has fewer than n + 1 items.
  • Fails with InvalidInput if n is not a valid 8-bit unsigned integer.

OrVerify(n)

Pops n + 1 items from the stack. If the last item matches at least one of the first n items, continue.

  • Fails with StackUnderflow if the stack has fewer than n + 1 items.
  • Fails with VerifyFailed the last item does not match at least one of the first n items.
  • Fails with InvalidInput if n is not a valid 8-bit unsigned integer.

Cryptographic operations

HashBlake256

Pops the top element, hash it with the Blake256 hash function and push the result to the stack.

  • Fails with StackUnderflow if the stack is empty.
  • Fails with InvalidInput if the input is not a valid 32 byte hash value.
HashSha256

Pops the top element, hash it with the SHA256 hash function and push the result to the stack.

  • Fails with StackUnderflow if the stack is empty.
  • Fails with InvalidInput if the input is not a valid 32 byte hash value.
HashSha3

Pops the top element, hash it with the SHA-3 hash function and push the result to the stack.

  • Fails with StackUnderflow if the stack is empty.
  • Fails with InvalidInput if the input is not a valid 32 byte hash value.
CheckSig(Message)

Pops the public key and then the signature from the stack. If signature validation using the 32-byte message and public key succeeds , push 1 to the stack, otherwise push 0.

  • Fails with IncompatibleTypes if Message is not a valid 32-byte sequence.
  • Fails with StackUnderflow if the stack has fewer than 2 items.
  • Fails with InvalidInput if the top stack element is not a PublicKey.
  • Fails with InvalidInput if the second stack element is not a Signature.
CheckSigVerify(Message)

Identical to CheckSig, except that nothing is pushed to the stack if the signature is valid.

In addition to the failures mentioned:

  • Fails with VerifyFailed if the signature is invalid.
CheckMultiSig(m, n, Vec, Message)

Pops exactly m signatures from the stack. The multiple signature validation will not succeed if the m signatures are not unique or if Vec contains a duplicate public key. Each signature is validated using the 32-byte message and a public key that match. If signature validation for m unique signatures succeeds, push 1 to the stack, otherwise push 0.

  • Fails with IncompatibleTypes if either m or n is not a valid 8-bit unsigned integer, if Vec contains an invalid public key or if Message is not a valid 32-byte sequence.
  • Fails with ValueExceedsBounds if m == 0 or if n == 0 or if m > n or if n > MAX_MULTISIG_LIMIT (32) or if the number of public keys provided != n.
  • Fails with StackUnderflow if the stack has fewer than m items.
  • Fails with IncompatibleTypes if any of the m signatures from the stack is not a valid signature.
  • Fails with InvalidInput if each of the top m elements is not a Signature.
CheckMultiSigVerify(m, n, Vec, Message)

Identical to CheckMultiSig, except that nothing is pushed to the stack if the multiple signature validation is either valid or invalid.

In addition to the failures mentioned:

  • Fails with VerifyFailed if any signature is invalid.
CheckMultiSigVerifyAggregatePubKey(m, n, public keys, Msg)

Identical to CheckMultiSig, except that the aggregate of the public keys is pushed to the stack if multiple signature validation succeeds.

In addition to the failures mentioned:

  • Fails with VerifyFailed if any signature is invalid.
ToRistrettoPoint

Pops the top element from the stack (either a scalar or a hash), parses it canonically as a Ristretto secret key if possible, computes the corresponding Ristretto public key, and pushes this value to the stack.

  • Fails with StackUnderflow if the stack is empty.
  • Fails with IncompatibleTypes if the stack item is not either a scalar or a hash.
  • Fails with InvalidInput if the stack item cannot be canonically parsed as a Ristretto secret key.

Miscellaneous

Return

This opcode does nothing except that it always fails.

  • Fails with Return.
If-then-else

Pops the top element of the stack into pred. If pred is 1, the instructions between IfThen and Else are executed. If pred is 0, instructions are popped until Else or EndIf is encountered. If Else is encountered, instructions are executed until EndIf is reached. EndIf is a marker opcode and a no-op.

  • Fails with StackUnderflow if the stack is empty.
  • Fails with InvalidInput if pred is anything other than 0 or 1.
  • Fails with the corresponding failure code if any instruction during execution of the clause causes a failure.
Else

Marks the beginning of the Else branch.

EndIf

Marks the end of the IfThen statement.

Serialisation

TariScript and the execution stack are serialised into byte strings using a simple linear parser. Since all opcodes are a single byte, it's very easy to read and write script byte strings. If an opcode has a parameter associated with it, e.g. PushHash then it is equally known how many bytes following the opcode will contain the parameter.

The script input data is serialised in an analogous manner. The first byte in a stream indicates the type of data in the bytes that follow. The length of each type is fixed and known a priori. The next n bytes read represent the data type.

As input data elements are read in, they are pushed onto the stack. This means that the last input element will typically be operated on first!

The types of input parameters that are accepted are:

TypeRange / Value
Number64-bit signed integer
Hash32-byte hash value
Scalar32-byte scalar value
Commitment32-byte homomorphic commitment (Pedersen commitment )
PublicKey32-byte Ristretto public key
Signature64-byte Ristretto Schnorr signature (32-byte nonce + 32-byte signature)

Example scripts

Anyone can spend

The simplest script is an empty script, or a script with a single NoOp opcode. When faced with this script, the spender can supply any pubkey in her script input for which she knows the private key. The script will execute, leaving that public key as the result, and the transaction script validation will pass.

One-sided transactions

One-sided transactions lock the input to a predetermined public key provided by the recipient; essentially the same method that Bitcoin uses. The simplest form of this is to simply post the new owner's public key as the script:

PushPubkey(P_B)

To spend this output, Bob provides an empty input stack. After execution, the stack contains his public key.

An equivalent script to Bitcoin's P2PKH would be:

Dup HashBlake256 PushHash(PKH) EqualVerify

To spend this, Bob provides his public key as script input. To illustrate the execution process, we show the script running on the left, and resulting stack on the right:

Initial scriptInitial Stack
DupBob's Pubkey
HashBlake256
PushHash(PKH)
EqualVerify

Copy Bob's pubkey:

Dup
HashBlake256Bob's Pubkey
PushHash(PKH)Bob's Pubkey
EqualVerify

Hash the public key:

HashBlake256
PushHash(PKH)H(Bob's Pubkey)
EqualVerifyBob's Pubkey

Push the expected hash to the stack:

PushHash(PKH)
EqualVerifyPKH
H(Bob's Pubkey)
Bob's Pubkey

Is PKH equal to the hash of Bob's public key?

EqualVerify
Bob's Pubkey

The script has completed without errors, and Bob's public key remains on the stack.

Multiparty Time-locked contract

Alice sends some Tari to Bob. If he doesn't spend it within a certain timeframe (up till block 4000), then she is also able to spend it back to herself.

The spender provides their public key as input to the script.

Dup PushPubkey(P_b) CheckHeight(4000) GeZero IFTHEN PushPubkey(P_a) OrVerify(2) ELSE EqualVerify ENDIF

Let's run through this script assuming it's block 3990 and Bob is spending the UTXO. We'll only print the stack this time:

Initial Stack
Bob's pubkey

Dup:

Stack
Bob's pubkey
Bob's pubkey

PushPubkey(P_b):

Stack
P_b
Bob's pubkey
Bob's pubkey

CheckHeight(4000). The block height is 3990, so 3990 - 4000 is pushed to the stack:

Stack
-10
P_b
Bob's pubkey
Bob's pubkey

GeZero pushes a 1 if the top stack element is positive or zero:

Stack
0
P_b
Bob's pubkey
Bob's pubkey

IFTHEN compares the top of the stack to 1. It is not a match, so it will execute the ELSE branch:

Stack
P_b
Bob's pubkey
Bob's pubkey

EqualVerify checks that P_b is equal to Bob's pubkey:

Stack
Bob's pubkey

The ENDIF is a no-op, so the stack contains Bob's public key, meaning Bob must sign to spend this transaction.

Similarly, if it is after block 4000, say block 4005, and Alice or Bob tries to spend the UTXO, the sequence is:

Initial Stack
Alice or Bob's pubkey

Dup and PushPubkey(P_b) as before:

Stack
P_b
Alice or Bob's pubkey
Alice or Bob's pubkey

CheckHeight(4000) calculates 4005 - 4000) and pushes 5 to the stack:

Stack
5
P_b
Alice or Bob's pubkey
Alice or Bob's pubkey

GeZero pops the 5 and pushes a 1 to the stack:

Stack
1
P_b
Alice or Bob's pubkey
Alice or Bob's pubkey

The top of the stack is 1, so IFTHEN executes the first branch, PushPubkey(P_a):

Stack
P_a
P_b
Alice or Bob's pubkey
Alice or Bob's pubkey

OrVerify(2) compares the 3rd element, Alice's pubkey, with the 2 top items that were popped. There is a match, so the script continues.

Stack
Alice or Bob's pubkey

If the script executes successfully, then either Alice's or Bob's public key is left on the stack, meaning only Alice or Bob can spend the output.

Error codes

CodeDescription
ReturnThe script failed with an explicit Return
StackOverflowThe stack exceeded 255 elements during script execution
NonUnitLengthStackThe script completed execution with a stack size other than one
StackUnderflowTried to pop an element off an empty stack
IncompatibleTypesAn operand was applied to incompatible types
ValueExceedsBoundsA script opcode resulted in a value that exceeded the maximum or minimum value
InvalidOpcodeThe script encountered an invalid opcode
MissingOpcodeThe script is missing closing opcodes (Else or EndIf)
InvalidSignatureThe script contained an invalid signature
InvalidInputThe serialised stack contained invalid input
InvalidDataThe script contained invalid data
VerifyFailedA verification opcode failed, aborting the script immediately
InvalidDigestas_hash requires a Digest function that returns at least 32 bytes

Credits

Thanks to @philipr-za and @SWvheerden for their input and contributions to this RFC.

Change Log

DateChangeAuthor
17 Aug 2020First draftCjS77
11 Feb 2021Tari script proposal v3CjS77
16 Feb 2021Update TariScript OpCodesCjS77
08 Mar 2021Update RFC docsdelta1
12 Nov 2021Add CheckMultiSig/Verifydelta1
30 Jun 2021Add missing OP_CHECKMULTISIG/VERIFY and update error codessdbondi
11 Jan 2022Add ToRistrettoPoint opcode to TariScriptSWvheerden
27 Sep 2022Add aggregate signatures to transaction inputs and outputshansieodendaal
28 Sep 2022Minor update to reflect implementationsdbondi
11 Nov 2022Update for code review/audithansieodendaal
20 Nov 2023Update ToRistrettoPoint documentationAaronFeickert

RFC-0204/TariScriptExamples

$$ \newcommand{\script}{\alpha} % utxo script \newcommand{\input}{ \theta } \newcommand{\cat}{\Vert} \newcommand{\so}{\gamma} % script offset \newcommand{\hash}[1]{\mathrm{H}\bigl({#1}\bigr)} $$

TariScript Examples

status: stable

Maintainer(s): Cayle Sharrock

Licence

The 3-Clause BSD Licence.

Copyright 2020 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

TariScript Examples

Standard MW transaction

For this use case we have Alice who sends Bob some Tari. Bob's wallet is online and is able to countersign the transaction.

Alice creates a new transaction spending \( C_a \) to a new output containing the commitment \( C_b \) (ignoring fees for now).

To spend \( C_a \), she provides:

  • An input that contains \( C_a \).
  • The script input, \( \input_a \).
  • A valid script signature, \( s_{Si} \) as per (13),(14) proving that she owns the commitment \( C_a \), knows the private key, \( k_{Sa} \), corresponding to \( K_{Sa} \), the public key left on the stack after executing \( \script_a \) with \( \input_a \).
  • A sender offset public key, \( K_{Ob} \).
  • The sender portion of the public nonce, \( R_{MSi} )\, as per (10).
  • The script offset, \( \so\) with: $$ \begin{aligned} \so = k_{Sa} - k_{Ob} \end{aligned} \tag{20} $$

Alice sends the usual first round data to Bob, but now because of TariScript also includes \( K_{Ob} \) and \( R_{MSi} \). Bob can then complete his side of the transaction as per the [standard Mimblewimble protocol] providing the commitment \(C_b\), its public blinding factor, its rangeproof and the partial transaction signature. In addition, Bob also needs to provide a partial metadata signature as per (5) where he commits to all the transaction output metadata with a commitment signature. Because Alice is creating the transaction, she can suggest the script \( \script_b \) to use for Bob's output, similar to a [bitcoin transaction], but Bob can choose a different script \(\script_b\). However, in most cases the parties will agree on using something akin to a NOP script \(\script_b\). Bob has to return this consistent set of information back to Alice.

Alice verifies the information received back from Bob, check if she agrees with the script \( \script_b \) Bob signed, and calculates her portion of the metadata signature \( s_{Mb} \) with:

$$ \begin{aligned} s_{Mb} = r_{mb} + k_{Ob} \hash{ \script_b \cat F_b \cat R_{Mb} } \end{aligned} \tag{21} $$

Alice then constructs the final aggregated metadata signature \(s_{Mb}\) as per (12) and replaces Bob's partial metadata signature in Bob's TransactionOutput.

She completes the transaction as per [standard Mimblewimble protocol] and also adds the script offset \( \so \), after which she sends the final transaction to Bob and broadcasts it to the network.

Transaction validation

Base nodes validate the transaction as follows:

  • They check that the usual Mimblewimble balance holds by summing inputs and outputs and validating against the excess signature. This check does not change nor do the other validation rules, such as confirming that all inputs are in the UTXO set etc.
  • The metadata signature \(s_{Ma}\) on Bob's output,
  • The input script must execute successfully using the provided input data; and the script result must be a valid public key,
  • The script signature on Alice's input is valid by checking:

$$ \begin{aligned} a_{Sa} \cdot H + b_{Sa} \cdot G = R_{Sa} + (C_a + K_{Sa})* \hash{ R_{Sa} \cat \alpha_a \cat \input_a \cat K_{Sa} \cat C_a} \end{aligned} \tag{22} $$

  • The script offset is verified by checking that the balance holds:

$$ \begin{aligned} \so \cdot{G} = K_{Sa} - K_{Ob} \end{aligned} \tag{23} $$

Finally, when Bob spends this output, he will use \( K_{Sb} \) as his script input and sign it with his script private key \( k_{Sb} \). He will choose a new sender offset public key \( K_{Oc} \) to give to the recipient, and he will construct the script offset, \( \so_b \) as follows:

$$ \begin{aligned} \so_b = k_{Sb} - k_{Oc} \end{aligned} \tag{24} $$

One sided payment

In this example, Alice pays Bob, who is not available to countersign the transaction, so Alice initiates a one-sided payment,

$$ C_a \Rightarrow C_b $$

Once again, transaction fees are ignored to simplify the illustration.

Alice owns \( C_a \) and provides the required script to spend the UTXO as was described in the previous cases.

Alice needs a public key from Bob, \( K_{Sb} \) to complete the one-sided transaction. This key can be obtained out-of-band, and might typically be Bob's wallet public key on the Tari network.

Bob requires the value \( v_b \) and blinding factor \( k_b \) to claim his payment, but he needs to be able to claim it without asking Alice for them.

This information can be obtained by using Diffie-Hellman and Bulletproof rewinding. If the blinding factor \( k_b \) was calculated with Diffie-Hellman using the sender offset keypair, (\( k_{Ob} \),\( K_{Ob} \)) as the sender keypair and the script keypair, \( (k_{Sb} \),\( K_{Sb}) \) as the receiver keypair, the blinding factor \( k_b \) can be securely calculated without communication.

Alice uses Bob's public key to create a shared secret, \( k_b \) for the output commitment, \( C_b \), using Diffie-Hellman key exchange.

Alice calculates \( k_b \) as

$$ \begin{aligned} k_b = k_{Ob} * K_{Sb} \end{aligned} \tag{25} $$

Next Alice uses Bulletproof rewinding, see RFC 180, to encrypt the value \( v_b \) into the the Bulletproof for the commitment \( C_b \). For this she uses \( k_{rewind} = \hash{k_{b}} \) as the rewind_key and \( k_{blinding} = \hash{\hash{k_{b}}} \) as the blinding key.

Alice knows the script-redeeming private key \( k_{Sa}\) for the transaction input.

Alice will create the entire transaction, including generating a new sender offset keypair and calculating the script offset,

$$ \begin{aligned} \so = k_{Sa} - k_{Ob} \end{aligned} \tag{26} $$

She also provides a script that locks the output to Bob's public key, PushPubkey(K_Sb). This will only be spendable if the spender can provide a valid signature as input that demonstrates proof of knowledge of \( k_{Sb}\) as well as the value and blinding factor of the output \(C_b\). Although Alice knowns the value and blinding factor of the output \(C_b\) only Bob knows \( k_{Sb}\).

Any base node can now verify that the transaction is complete, verify the signature on the script, and verify the script offset.

For Bob to claim his commitment he will scan the blockchain for a known script because he knowns that the script will be PushPubkey(K_Sb). In this case, the script is analogous to an address in Bitcoin or Monero. Bob's wallet can scan the blockchain looking for scripts that he would know how to resolve.

When Bob's wallet spots a known script, he requires the blinding factor, \( k_b \) and the value \( v_b \). First he uses Diffie-Hellman to calculate \( k_b \).

Bob calculates \( k_b \) as

$$ \begin{aligned} k_b = K_{Ob} * k_{Sb} \end{aligned} \tag{27} $$

Next Bob's wallet calculates \( k_{rewind} \), using \( k_{rewind} = \hash{k_{b}}\) and (\( k_{blinding} = \hash{\hash{k_{b}}} \), using those to rewind the Bulletproof to get the value \( v_b \).

Because Bob's wallet already knowns the script private key \( k_{Sb} \), he now knows all the values required to spend the commitment \( C_b \)

For Bob's part, when he discovers one-sided payments to himself, he should spend them to new outputs using a traditional transaction to thwart any potential horizon attacks in the future.

To summarise, the information required for one-sided transactions are as follows:

Transaction inputSymbolsKnowledge
commitment\( C_a = k_a \cdot G + v \cdot H \)Alice knows the blinding factor and value.
features\( F_a \)Public
script\( \alpha_a \)Public
script input\( \input_a \)Public
script signature\( s_{Sa} \)Alice knows \( k_{Sa},\, r_{Sa} \) and \( k_{a},\, v_{a} \) of the commitment \(C_a\).
sender offset public key\( K_{Oa} \)Not used in this transaction.
Transaction outputSymbolsKnowledge
commitment\( C_b = k_b \cdot G + v \cdot H \)Alice and Bob know the blinding factor and value.
features\( F_b \)Public
script\( \script_b \)Script is public; only Bob knows the correct script input.
range proofAlice and Bob know opening parameters.
sender offset public key\( K_{Ob} \)Alice knows \( k_{Ob} \).
metadata signature\( s_{Mb} \)Alice knows \( k_{Ob} \), \( (k_{b},\, v) \) and the metadata.

HTLC-like script

In this use case we have a script that controls where it can be spent. The script is out of scope for this example, but has the following rules:

  • Alice can spend the UTXO unilaterally after block n, or
  • Alice and Bob can spend it together.

This would be typically what a lightning-type channel requires.

Alice owns the commitment \( C_a \). She and Bob work together to create \( C_s\). But we don't yet know who can spend the newly created \( C_s\) and under what conditions this will be.

$$ C_a \Rightarrow C_s \Rightarrow C_x $$

Alice owns \( C_a\), so she knows the blinding factor \( k_a\) and the correct input for the script's spending conditions. Alice also generates the sender offset keypair, \( (k_{Os}, K_{Os} )\).

Now Alice and Bob proceed with the standard transaction flow.

Alice ensures that the sender offset public key \( K_{Os}\) is part of the output metadata that contains commitment \( C_s\). Alice will fill in the script with her \( k_{Sa}\) to unlock the commitment \( C_a\). Because Alice owns \( C_a\) she needs to construct \( \so\) with:

$$ \begin{aligned} \so = k_{Sa} - k_{Os} \end{aligned} \tag{28} $$

The blinding factor, \( k_s\) can be generated using a Diffie-Hellman construction. The commitment \( C_s\) needs to be constructed with the script that Bob agrees on. Until it is mined, Alice could modify the script via double-spend and thus Bob must wait until the transaction is confirmed before accepting the conditions of the smart contract between Alice and himself.

Once the UTXO is mined, both Alice and Bob possess all the knowledge required to spend the \( C_s \) UTXO. It's only the conditions of the script that will discriminate between the two.

The spending case of either Alice or Bob claiming the commitment \( C_s\) follows the same flow described in the previous examples, with the sender proving knowledge of \( k_{Ss}\) and "unlocking" the spending script.

The case of Alice and Bob spending \( C_s \) together to a new multiparty commitment requires some elaboration.

Assume that Alice and Bob want to spend \( C_s \) co-operatively. This involves the script being executed in such a way that the resulting public key on the stack is the sum of Alice and Bob's individual script keys, \( k_{SsA} \) and \( k_{SaB} \).

The script input needs to be signed by this aggregate key, and so Alice and Bob must each supply a partial signature following the usual Schnorr aggregate mechanics, but one person needs to add in the signature of the blinding factor and value.

In an analogous fashion, Alice and Bob also generate an aggregate sender offset private key \( k_{Ox}\), each using their own \( k_{OxA} \) and \( k_{OxB}\).

To be specific, Alice calculates her portion from

$$ \begin{aligned} \so_A = k_{SsA} - k_{OxA} \end{aligned} \tag{29} $$

Bob will construct his part of the \( \so\) with:

$$ \begin{aligned} \so_B = k_{SsB} - k_{OxB} \end{aligned} \tag{30} $$

And the aggregate \( \so\) is then:

$$ \begin{aligned} \so = \so_A + \so_B \end{aligned} \tag{31} $$

Notice that in this case, both \( K_{Ss} \) and \( K_{Ox}\) are aggregate keys.

Notice also that because the script resolves to an aggregate key \( K_s\) neither Alice nor Bob can claim the commitment \( C_s\) without the other party's key. If either party tries to cheat by editing the input, the script validation will fail.

If either party tries to cheat by creating a new output, the script offset will not validate correctly as it locks the output of the transaction.

A base node validating the transaction will also not be able to tell this is an aggregate transaction as all keys are aggregated Schnorr signatures. But it will be able to validate that the script input is correctly signed, thus the output public key is correct and that the \( \so\) is correctly calculated, meaning that the commitment \( C_x\) is the correct UTXO for the transaction.

To summarise, the information required for creating a multiparty UTXO is as follows:

Transaction inputSymbolsKnowledge
commitment\( C_a = k_a \cdot G + v \cdot H \)Alice knows the blinding factor and value.
features\( F_a \)Public
script\( \alpha_a \)Public
script input\( \input_a \)Public
script signature\( s_{Sa} \)Alice knows \( k_{Sa},\, r_{Sa} \) and \( k_{a},\, v_{a} \) of the commitment \(C_a\).
sender offsetย publicย key\( K_{Oa} \)Not used in this transaction.

Transaction outputSymbolsKnowledge
commitment\( C_s = k_s \cdot G + v \cdot H \)Alice and Bob know the blinding factor and value.
features\( F_s \)Public
script\( \script_s \)Script is public; Alice and Bob only knows their part of the correct script input.
range proofAlice and Bob know opening parameters.
sender offsetย publicย key\( K_{Os} = K_{OsA} + K_{OsB}\)Alice knows \( k_{OsA} \), Bob knows \( k_{OsB} \), neither party knows \( k_{Os} \).
metadataย signature\( (a_{Ms} , b_{Ms} , R_{Ms}) \)Alice knows \( k_{OsA} \), Bob knows \( k_{OsB} \), both parties know \( (k_{s},\, v) \). Neither party knows \( k_{Os}\).

When spending the multi-party input:

Transaction inputSymbolsKnowledge
commitment\( C_s = k_s \cdot G + v_s \cdot H \)Alice and Bob know the blinding factor and value.
features\( F_s \)Public
script\( \alpha_s \)Public
script input\( \input_s \)Public
scriptย signature\( (a_{Ss} ,b_{Ss} , R_{Ss}) \)Alice knows \( (k_{SsA},\, r_{SsA}) \), Bob knows \( (k_{SsB},\, r_{SsB}) \), both parties know \( (k_{s},\, v_{s}) \), neither party knows \( k_{Ss}\).
sender offsetย publicย key\( K_{Os} \)As above, Alice and Bob each know part of the sender offset key.

Multi-party considerations

Multi-party in this context refers to n-of-n parties creating a single combined transaction output and m-of-n parties spending a single combined transaction input. We have some options to do this:

  • using the m-of-n script TariScript without sharding the spending key;
  • combination of the m-of-n script TariScript and sharding the spending key;
  • sharding the spending key combined with the NoOp script TariScript;

If the spending key \( k_i \) is sharded for any of these options the commitment definition changes to:

$$ \begin{aligned} C_i = v_i \cdot H + \sum_\psi (k_{i_\psi} \cdot G) \; \; \; \text{ for each receiver party } \psi \end{aligned} \tag{1b} $$

The sender-receiver interaction can be categorized as follows, however, for simplicity we can assume that each multi-party side will have a single party acting as the dealer:

  • Multi-party senders can create the single output and send it to a single receiver.
  • A single sender can create the single output and send it to multi-party receivers.
  • Multi-party senders can create the single output and send it to multi-party receivers.

The multi-party impact on the transaction output, transaction input and script offset is discussed below.

Multi-party transaction output

If multiple senders and receiver parties need to create an aggregate metadata_signature for a single multi-party transaction output, there are two secrets that warrant our attention; the script offset private key \( k_{Oi} \) controlled by the senders and the spending key \( k_i \) controlled by the receivers. Depending on the protocol design, one or both secrets may be sharded amongst all parties.

Sharding the script offset private key:

If the script offset private key \( k_{Oi} \) is sharded, the aggregate sender terms in (10) and (11) collected by the sender's dealer change to:

$$ \begin{aligned} R_{MSi} &= \sum_\omega (r_{{MSi_b}_\omega} \cdot G) \; \; \; \text{ for each sender party } \omega \end{aligned} \tag{10b} $$

$$ \begin{aligned} a_{MSi} &= 0 \\ b_{MSi} &= \sum_\omega (r_{{MSi_b}_\omega} + e \cdot k_{{Oi}_\omega}) \; \; \; \text{ for each sender party } \omega \end{aligned} \tag{11b} $$

Sharding the spending key:

If the spending key \( k_i \) is sharded, the receiver's dealer needs to collect shards and combine them. The aggregate receiver terms in (3) and (5) collected by the receiver's dealer change to:

$$ \begin{aligned} R_{MRi} &= r_{MRi_a} \cdot H + \sum_\psi ( r_{{MRi_b}_\psi} \cdot G ) \; \; \; \text{ for each receiver party } \psi \end{aligned} \tag{3b} $$

$$ \begin{aligned} a_{MRi} &= r_{MRi_a} + e(v_{i}) \\ b_{MRi} &= \sum_\psi ( r_{{MRi_b}_\psi} + e \cdot k_{i_\psi} ) \; \; \; \text{ for each receiver party } \psi \end{aligned} \tag{5b} $$

Multi-party transaction input

If multiple senders need to create an aggregate script_signature for a multi-party transaction input, again, there are two secrets that warrant our attention, the script private key \( k_{Si} \) and the spending key \( k_i \). Depending on the protocol design, one or both secrets may be sharded amongst all parties, with some limitations:

  • Sharding the script private key will always be applicable for an m-of-n script TariScript.
  • Sharding the script private key will never be applicable for a NoOp script TariScript.

Sharding only the script private key:

If only the script private key \( k_{Si} \) will be sharded the aggregate terms in (14) collected by the sender's dealer change to:

$$ \begin{aligned} R_{Si} &= r_{Si_a} \cdot H + \sum_\omega ( r_{{Si_b}_\omega} \cdot G ) \; \; \; \text{ for each sender party } \omega \\ a_{Si} &= r_{Si_a} + e(v_{i}) \\ b_{Si} &= \sum_\omega ( r_{{Si_b}_\omega} + e \cdot k_{{Si}_\omega} ) + e \cdot k_i \; \; \; \text{ for each sender party } \omega \\ e &= \hash{ R_{Si} \cat \alpha_i \cat \input_i \cat K_{Si} \cat C_i} \\ \end{aligned} \tag{14b} $$

Sharding the script private key and the spending key:

If both the script private key \( k_{Si} \) and the spending key \( k_i \) will be sharded the aggregate terms in (14) collected by the sender's dealer change to:

$$ \begin{aligned} R_{Si} &= r_{Si_a} \cdot H + \sum_\omega ( r_{{Si_b}_\omega} \cdot G ) \; \; \; \text{ for each sender party } \omega \\ a_{Si} &= r_{Si_a} + e(v_{i}) \\ b_{Si} &= \sum_\omega ( r_{{Si_b}_\omega} + e \cdot (k_{{Si}_\omega} + k_{i_\omega}) ) \; \; \; \text{ for each sender party } \omega \\ e &= \hash{ R_{Si} \cat \alpha_i \cat \input_i \cat K_{Si} \cat C_i} \\ \end{aligned} \tag{14c} $$

It is worth noting that m-of-n treatment of the script private key \( k_{Si} \) and the spending key \( k_i \) differs slightly. Only m of the original n parties need to create the script input, and the script will resolve to the correct \( K_{Si} \), but all n parties need to be present to recreate the original \( k_i \). This can be done with Pedersen Verifiable Secret Sharing (PVSS), similar to this example. The dealer will reconstruct the \( k_{i_j} \) shards of the missing parties and add them to their shard before combining.

Sharding only the spending key:

If only the spending key \( k_i \) will be sharded the aggregate terms in (14) collected by the sender's dealer change to:

$$ \begin{aligned} R_{Si} &= r_{Si_a} \cdot H + \sum_\omega ( r_{{Si_b}_\omega} \cdot G ) \; \; \; \text{ for each sender party } \omega \\ a_{Si} &= r_{Si_a} + e(v_{i}) \\ b_{Si} &= \sum_\omega ( r_{{Si_b}_\omega} + e \cdot k_{i_\omega} ) + e \cdot k_{Si} \; \; \; \text{ for each sender party } \omega \\ e &= \hash{ R_{Si} \cat \alpha_i \cat \input_i \cat K_{Si} \cat C_i} \\ \end{aligned} \tag{14d} $$

Multi-party script offset

For multiple senders, aggregate terms are collected by the sender's dealer and (16) changes according to which of the secrets have been sharded:

Sharding only the script private key:

$$ \begin{aligned} \so = \sum_\omega \left( \sum_j\mathrm{k_{Sj}} \right) _\omega - \sum_i\mathrm{k_{Oi}} \; \; \text{for each input}, j,\, \text{and each output}, i \; \text{ for each sender party } \omega \end{aligned} \tag{16b} $$

Sharding only the script offset private key:

$$ \begin{aligned} \so = \sum_j\mathrm{k_{Sj}} - \sum_\omega \left( \sum_i\mathrm{k_{Oi}} \right) _\omega \; \; \text{for each input}, j,\, \text{and each output}, i \; \text{ for each sender party } \omega \end{aligned} \tag{16c} $$

Sharding the script private key and the script offset private key:

$$ \begin{aligned} \so = \sum_\omega \left( \sum_j\mathrm{k_{Sj}} - \sum_i\mathrm{k_{Oi}} \right) _\omega \; \; \text{for each input}, j,\, \text{and each output}, i \; \text{ for each sender party } \omega \end{aligned} \tag{16d} $$

DateChangeAuthor
11 Nov 2022Move out of RFC 0201stringhandler

RFC-0203/Stealth addresses

Stealth addresses

status: stable

Maintainer(s): Stringhandler

Licence

The 3-Clause BSD Licence.

Copyright 2022 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

This Request for Comment (RFC) presents a design for a one-time (stealth) address protocol useful for one-sided payments to improve recipient privacy for payments on the Tari base layer.

Introduction

The Tari protocol extends the Mimblewimble protocol to include scripting in the form of TariScript. One of the first features implemented using TariScript was one-sided payments. These are payments to a recipient that do not require an interactive negotiation in the same way a standard Mimblewimble transaction does. One of the main downsides of the current implementation of one-sided payments is that the script key used is the public key of the recipient's wallet. This public key is embedded in the TariScript of the UTXO created by the sender. The issue is that it becomes very easy for a third party to scan the blockchain to look for one-sided transaction outputs being sent to a given wallet. In order to alleviate this privacy leak, this RFC proposes the use of one-time (stealth) addresses to be used as the script key when sending a one-sided payment.

Background

Stealth addresses were first proposed on the Bitcoin Talk forum by user Bytecoin. The concept was further refined by Peter Todd, using a design similar to the BIP-32 style of address generation. In this approach, the sender can use an ephemeral public key (derived from a nonce) to perform a non-interactive Diffie-Hellman exchange with the recipient's public key, and use this to derive a one-time public key to which only the recipient can derive the corresponding private key. This reduces on-chain linkability (but, importantly, does not eliminate it).

The approach was further extended in the CryptoNote whitepaper to support a dual-key design, whereby a separate scanning key is also used in the one-time address construction; this enables identification of outputs, but requires the spending key to derive the private key required to spend the output.

It is important to note that while one-time addresses are not algebraically linkable, it is possible to observe transactions that consume multiple such outputs and infer common ownership of them.

For use in Tari, single-key one-time addresses are supported.

One-time (stealth) addresses

Single-key one-time stealth addresses require only that a recipient possess a private key \( a \) and corresponding public key \( A = a \cdot G \), and distribute the public key out of band to receive one-sided payments in a non-interactive manner.

The protocol that a sender will use to make a payment to the recipient is as follows:

  1. Generate a random nonce \( r \) and use it to produce an ephemeral public key \( R = r \cdot G \).
  2. Compute a Diffie-Hellman exchange to obtain the shared secret \( c = H( r \cdot A ) \), where \( H \) is a cryptographic hash function.
  3. Include \( K_S = c \cdot G + A \) as the last public key in a one-sided payment script in a transaction.
  4. Include \( R \) in the script for use by the recipient, but DROP it so that it is not used in script execution. This changes the script for a one-sided payment from PushPubkey(K_S) to PushPubkey(R) Drop PushPubkey(K_S).

To identify one-sided payments, the recipient scans the blockchain for outputs containing a one-sided payment script. It then does the following to test for ownership:

  1. Extract the ephemeral public key \( R \) from the script.
  2. Compute a Diffie-Hellman exchange to obtain the shared secret \( c = H( a \cdot R ) \).
  3. Compute \( K_S' = c \cdot G + A \). If \( K_S' = K_S \) is included in the script, the recipient can produce the required script signature using the corresponding one-time private key \( c + a \).

Implementation notes

  • Stealth addresses were included in the Tari Console Wallet as of version v0.35.0 (2022-08-11).
  • The FFI (used in Aurora) uses stealth addresses by default as of libwallet-v0.35.0 (2022-08-11).

Change Log

DateChangeAuthor
01 Jun 2022First draftphilip-za
26 Oct 2022Stabilise RFCCjS77

RFC-0205/Hardware transactions

Hardware transactions

status: stable

Maintainer(s): SW van Heerden

Licence

The 3-Clause BSD Licence.

Copyright 2023 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

This Request for Comment (RFC) presents a design for a hardware wallet with MimbleWimble

Introduction

Hardware wallets are secure physical devices used to protect crypto assets and funds by requiring user interaction from the device. These devices are used to sign for transactions by keeping all the secrets from the transactions on the hardware device. If the host machines that run the wallets are compromised by malware the secrets from the transactions are still safe as they are not kept on the machine as with regular wallets. The devices are very low power and only feature a very limited processing power. MimbleWimble has intensive crypto operations that are in some instances very slow or not able to run on the hardware wallet at all. This RFC describes a way to get around these limitations to have fully functional secure hardware wallet integration.

Background

Vanilla Mimblewimble only has a single secret per UTXO, the blinding factor (\( k_i \) ). The Tari protocol extends the Mimblewimble protocol to include scripting in the form of TariScript. This adds a second secret per UTXO called the script_key (\( k_s \) ). In practical terms this means that while vanilla Mimblewimble only requires that a wallet wishing to spend an UTXO, prove knowledge of (\( k_i \) ) by producing the kernel signature, this is not sufficient for Tari. A Tari wallet must also prove knowledge of the script key (\( k_s \) ), by producing the script signature.

Requirements

To properly implement hardware wallets we need the following requirements to be met:

  • No UTXO can be spent without a user physically approving the transaction on the hardware wallet.
  • Users need to verify transaction properties when signing for transactions.
  • The user must be able to receive transactions without having to authorize them on the hardware wallet.
  • The user must be able to receive transactions without having the Hardware wallet attached.
  • The hardware device implementation should comply with the best practices and/or security recommendations of the provider.

Implementation

Entities

Normal transactions have only a single entity, the wallet which controls all secrets and transactions. But with hardware wallets, we need to define two distinct entities:

  • Signer: This is the entity that keeps the secrets for the transactions and approves them, aka the hardware wallet.
  • Helper: This entity is the program that helps the signer construct the transaction, send it over the network and scan the network, aka wallet.

Process Overview

By splitting the ownership of the UTXO's secrets by assigning knowledge of only the script key (\( k_s \) ) to the signer, we can lift much of the heavy cryptography like bulletproof creation to the helper device by exposing (\( k_i \) ) to it. By looking at how one-sided-stealth transactions are created, we can construct the script key in such a way that the helper can calculate the public script key, but cannot calculate the private script key.

All hardware wallet created UTXOs will contain a script PushPubkey(K_S). The key (\( k_s \) ) is created as follows: $$ \begin{align} k_S &= H(k_i) + a \\ K_S &= H(k_i) \cdot G + A \end{align} $$

The blinding factor (\( k_i \) ) is used as a random nonce when creating the script key. This means the helper can create the public key without the signer present, and the signer can then at a later stage create the private key from the nonce. The key pair (\( a, A \) ) is the master key pair from the signer. The private key (\( a \) ) is kept secret by the signer at all times.

Initialization

Adding a hardware wallet to a wallet (helper) we need to ensure that all keys are only derived from a single seed phrase provided by the hardware wallet.

Helper asks signer for master helper key (\( k_H \) ). This key is derived from the signer seed phrase.

Transaction receiving

When a transaction is received the helper constructs the new UTXO with its Rangeproof. Choosing a new ( \( k_i \) ) for the UTXO, it calculates a new \( K_S \). It attaches the script PushPubkey(K_S) to output.

Transaction sending

When the user wants to send a transaction, the helper retrieves the desired UTXO. The helper asks the signer to sign the transaction. The signer calculates \( k_s \) to sign the transaction. The signer creates a random nonce \( k_O \) to use for the script_offset. It produces the metadata signature with \( k_O \), and supplies the script_offset to the helper. The helper can attach the correct signatures to the UTXOs and ship the transaction.

Receiving normal one-sided transaction

This can be done by the helper asking the signer for a public key. And advertising this public key as the destination public key for a 1-sided transaction. The helper can scan the blockchain for this public key.

Receiving one-sided-stealth

Not yet possible with this this design.

Output recovery

When creating outputs the wallet encrypts the blinding factor \(k_i \) and value \( v \) with \( k_H \). This is encrypted using extended-nonce AEAD using a random nonce and authenticated decryption. Because the key \( k_H \) is calculated from the seed phrase of the signer, this will be the same each time. The helper can try to decrypt each scanned output, when it is successful it knows it has found its own output. The helper can validate that the commitment is correct using the blinding factor \(k_i \) and value \( v \). It can also validate (\( K_S)\) corresponds to (\( k_i, A \) )

Security

Because the script key is required for spending, it is the only key that needs to be kept secret. The following table explains what an attacker can do upon learning a key from the helper

keyWorst case scenario
\( k_H \)Can view all transactions and values made by the wallet
\( k_i \)Can try and brute force the value of the transaction

The keys ( \( a, k_S, k_O \) ) are only known to the signer, and should never leave the hardware wallet.

Change Log

DateChangeAuthor
17 May 2023First draftswvheerden

RFC-0250/Covenants

Covenants

status: stable

Maintainer(s): Stanley Bondi

Licence

The 3-Clause BSD Licence.

Copyright 2022 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

This Request for Comment (RFC) presents a proposal for introducing covenants into the Tari base layer protocol. Tari Covenants aims to provide restrictions on the future spending of subsequent transactions to enable a number of powerful use-cases, such as

  • vaults
  • side-chain checkpointing transactions,
  • commission on NFT transfers, and
  • many others not thought of here.

Introduction

The Tari protocol already provides programmable consensus, through TariScript, that restricts whether a UTXO may be included as an input to a transaction (a.k.a spent). The scope of information within TariScript is inherently limited, by the TariScript Opcodes and the input data provided by a spender. Once the requirements of the script are met, a spender may generate UTXOs of their choosing, within the constraints of MimbleWimble.

This RFC expands the capabilities of Tari protocol by adding additional requirements, called covenants that allow the owner(s) of a UTXO to control the composition of a subsequent transaction.

Covenants are not a new idea and have been proposed and implemented in various forms by others.

For example,

Covenants in MimbleWimble

In blockchains like Bitcoin, a block contains discrete transactions containing inputs and outputs. A covenant in Bitcoin would be able to interrogate those outputs belonging to the input to ensure that they adhere to rules.

In MimbleWimble, the body of a block and transaction can be expressed in an identical data structure. This is indeed the case in the Tari codebase, which defines a structure called AggregateBody containing inputs and outputs (and kernels) for transactions and blocks. This is innate to MimbleWimble, so even if we were to put a "box" around these inputs/outputs there is nothing to stop someone from including inputs and outputs from other boxes as long as balance is maintained.

This results in an interesting dilemma: how do we allow rules that dictate how future outputs look only armed with the knowledge that the rule must apply to one or more outputs?

In this RFC, we detail a covenant scheme that allows the UTXO originator to express a filter that must be satisfied for a subsequent spending transaction to be considered valid.

Assumptions

The following assumptions are made:

  1. Duplicate commitments within a block are disallowed by consensus prior to covenant execution,
  2. all outputs in the output set are valid, and
  3. all inputs are valid spends, save for covenant checks.

Protocol modifications

Modifications to the existing protocol and consensus are as follows:

  • the covenant is recorded in the transaction UTXO,
  • the covenant is committed to in the output and input hashes to prevent malleability,
  • transactions with covenants entering the mempool MUST be validated, and
  • each covenant in a block must be validated before being included in the block chain.

Transaction input and output changes

A covenant field would need to be added to the TransactionOutput and TransactionInput structs and committed to in their hashes.

Covenant definition

We define a clear notation for covenants that mirrors the miniscript project.

Execution Context and Scope

Covenants execute within a limited read-only context and scope. This is both to reduce complexity (and therefore the possibility of bugs) and maintain reasonable performance.

A covenant's context is limited to:

  • an immutable reference to the current input,
  • a vector of immutable references to outputs in the current block/transaction (called the output set),
  • the current input's mined height, and
  • the current block height.

Each output's covenant is executed with this context, filtering on the output set and returning the result. The output set given to each covenant at execution MUST be the same set for all covenants and MUST never be influenced by other covenants. The stateless and immutable nature of this scheme has the benefit of being able to execute covenants in parallel.

A covenant passes if at least one output in the set is matched. Allowing more than one output to match allows for covenants that restrict the characteristics of multiple outputs. A covenant that matches zero outputs fails which invalidates the transaction/block.

If a covenant is empty (zero bytes) the identity operation is implied and therefore, no actual execution need occur.

Argument types

enum CovenantArg {
    // byte code: 0x01
    // data size: 32 bytes
    Hash([u8; 32]),
    // byte code: 0x02
    // data size: 32 bytes
    PublicKey(RistrettoPublicKey),
    // byte code: 0x03
    // data size: 32 bytes
    Commitment(PedersonCommitment),
    // byte code: 0x04
    // data size: variable
    TariScript(TariScript),
    // byte code: 0x05
    // data size: <= 4096 bytes
    Covenant(Covenant),
    // byte cide: 0x06
    // data size: variable
    Uint(u64),
    // byte cide: 0x07
    // data size: variable
    OutputField(OutputField),
    // byte cide: 0x08
    // data size: variable
    OutputFields(OutputFields),
    // byte cide: 0x09
    // data size: variable
    Bytes(Vec<u8>),
    // byte cide: 0x0a
    // data size: 1 bytes
    OutputType(OutputType),
}
Output field tags

Fields from each output in the output set may be brought into a covenant filter. The available fields are defined as follows:

Tag NameByte CodeReturns
field::commitment0x00output.commitment
field::script0x01output.script
field::sender_offset_public_key0x02output.sender_offset_public_key
field::covenant0x03output.covenant
field::features0x04output.features
field::features_output_type0x05output.features.output_type
field::features_maturity0x06output.features.maturity
field::features_metadata0x07output.features.metadata
field::features_sidechain_features0x08output.features.sidechain_features

Each field tag returns a consensus encoded byte representation of the value contained in the field. How those bytes are interpreted depends on the covenant. For instance, filter_fields_hashed_eq will concatenate the bytes and hash the result whereas filter_field_eq will interpret the bytes as a little-endian 64-bit unsigned integer.

Set operations

identity()

The output set is returned unaltered. This rule is implicit for an empty (0 byte) covenant.

op_byte: 0x20
args: []

and(A, B)

The intersection (\(A \cap B\)) of the resulting output set for covenant rules \(A\) and \(B\).

op_byte: 0x21
args: [Covenant, Covenant]

or(A, B)

The union (\(A \cup B\)) of the resulting output set for covenant rules \(A\) and \(B\).

op_byte: 0x22
args: [Covenant, Covenant]

xor(A, B)

The symmetric difference (\(A \triangle B\)) of the resulting output set for covenant rules \(A\) and \(B\). This is, outputs that match either \(A\) or \(B\) but not both.

op_byte: 0x23
args: [Covenant, Covenant]

not(A)

Returns the compliment of A. That is, all the elements of A are removed from the resultant output set.

op_byte: 0x24
args: [Covenant]

Filters

filter_output_hash_eq(hash)

Filters for a single output that matches the hash. This filter only returns zero or one outputs.

op_byte: 0x30
args: [Hash]

filter_fields_preserved(fields)

Filter for outputs where all given fields in the input are preserved in the output.

op_byte: 0x31
args: [Fields]

filter_fields_hashed_eq(fields, hash)

op_byte: 0x32
args: [Fields, VarInt]

filter_field_eq(field, int)

Filters for outputs whose field value matches the given integer value. If the given field cannot be cast to an unsigned 64-bit integer, the transaction/block is rejected.

op_byte: 0x33
args: [Field, VarInt]

filter_absolute_height(height)

Checks the block height that the current UTXO (i.e. the current input) is greater than or equal to the HEIGHT block height. If so, the identity() is returned.

op_byte: 0x34
args: [VarInt]

Encoding / Decoding

Covenants can be encoded to/decoded from bytes as a token stream. Each token is consumed and interpreted serially before being executed.

For instance,

xor(
    filter_output_hash_eq(Hash(0e0411c70df0ea4243a363fcbf161ebe6e2c1f074faf1c6a316a386823c3753c)),
    filter_relative_height(10),
)

is represented in hex bytes as 23 30 01 a8b3f48e39449e89f7ff699b3eb2b080a2479b09a600a19d8ba48d765fe5d47d 35 07 0a. Let's unpack that as follows:

23 // xor - consume two covenant args
30 // filter_output_hash_eq - consume a hash arg
01 // 32-byte hash
a8b3f48e39449e89f7ff699b3eb2b080a2479b09a600a19d8ba48d765fe5d47d // data
// end filter_output_hash_eq
35 // 2nd covenant - filter_absolute_height
07 // varint
0A // 10
// end varint, filter_absolute_height, xor

Some functions can take any number of arguments, such as filter_fields_hashed_eq which defines the Fields type. This type is encoded first by its byte code 34 followed by a varint encoded number that indicates the number of field identifiers to consume. To mitigate misuse, the maximum allowed arguments are limited.

Covenant Validation

A covenant and therefore the block/transaction MUST be regarded as invalid if:

  1. an unrecognised bytecode is encountered
  2. the end of the byte stream is reached unexpectedly
  3. there are bytes remaining on the stream after interpreting
  4. an invalid argument type is encountered
  5. the Fields type encounters more than 9 arguments (i.e. the number of fields tags available)
  6. the depth of the calls exceeds 16.

Consensus changes

The covenant is executed once all other validations, including TariScript, are complete. This ensures that invalid transactions in a block cannot influence the results.

Considerations

Complexity

This introduces additional validation complexity. We avoid stacks, loops, and conditionals (covenants are basically one conditional), there are overheads both in terms of complexity and performance as a trade-off for the power given by covenants.

The worst case complexity for covenant validation is O(num_inputs*num_outputs), although as mentioned above validation for each input can be executed in parallel. To compensate for the additional workload the network encounters, use of covenants should incur heavily-weighted fees to discourage needlessly using them.

Cut-through

The same arguments made in the TariScript RFC for the need to prevent cut-through apply to covenants.

Chain analysis

The same arguments made in the TariScript RFC apply.

Security

As all outputs in a block are in the scope of an input to be checked, any unrelated/malicious output in a block could pass an unrelated covenant rule if given the chance. A secure covenant is one that uniquely identifies one or more outputs.

Examples

Now or never

Spend by block 10 or burn

not(filter_absolute_height(10))

Note, this covenant may be valid when submitted to the mempool, but invalid by the time it is put in a block for the miner.

Side-chain checkpointing

and(
   filter_field_eq(field::feature_flags, 16) // SIDECHAIN CHECKPOINT = 16
   filter_fields_preserved([field::features, field::covenant, field::script])
)

Restrict spending to a particular commitment if not spent by block 100

or(
   not(filter_absolute_height(100)),
   filter_fields_hashed_eq([field::commmitment], Hash(xxxx))
)

Output must preserve covenant, features and script or be burnt

xor(
    filter_fields_preserved([field::features, field::covenant, field::script]),
    and(
        filter_field_eq(field::features_flags, 128), // FLAG_BURN = 128
        filter_fields_hashed_eq([field::commitment, field::script], Hash(...)),
    ),
)

Commission for NFT transfer

// Must be different outputs
xor(
    and(
        // Relavant input fields preserved in subsequent output
        filter_fields_preserved([fields::features, fields::covenant, fields::script]),
        // The spender must obtain the covenent for the subsequent output
        filter_fields_hashed_eq([fields::covenant], Hash(xxxx)),
    ),
    // The spender must obtain and submit the output that matches this hash
    filter_output_hash_eq(Hash(xxxx)),
)

Other potential covenants

  • filter_script_eq(script)
  • filter_covenant_eq(covenant)
  • filter_script_match(<pattern>)
  • filter_covenant_match(<pattern>)

Change Log

DateChangeAuthor
17 Oct 2021First draftsbondi
08 Oct 2022Stable updatebrianp

RFC-0303/DanOverview

Digital Assets Network

status: draft

Maintainer(s): Cayle Sharrock,S W van Heerden

Licence

The 3-Clause BSD Licence.

Copyright 2022 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

The aim of this Request for Comment (RFC) is to describe the key elements of the Tari second layer, also known as the Digital Assets Network (DAN).

Description

The Tari DAN is based on a sharded BFT consensus mechanism called Cerberus.

One particular note is that Tari has chosen Hotstuff as the base BFT consensus algorithm over pBFT mentioned in the paper.

The core idea of Cerberus is that instead of dividing work up between validator nodes according to the contracts they are managing (as per Tari DANv1, Polkadot, Avalanche, etc.), Cerberus distributes nodes evenly over a set of shard addresses. Any time an instruction modifies the state of a contract, it will affect one or more shard addresses, and only those nodes that are responsible for covering those addresses will reach consensus on the correct state changes.

This means that nodes have to be prepared to execute instructions on any contract in the network. This does create a data synchronisation burden, but the added benefit of a highly scalable, decentralised DAN significantly outweighs this trade-off.

Key actors

There are several components on both the Tari Digital Assets Network (DAN) and base layer that interoperate to collectively enable scalable smart contracts on Tari.

These components include:

  • Minotari base layer - Enforces Tari monetary policy and plays the role of global registrar.
  • Templates - Reusable smart contract components.
  • Contracts - Self-contained pieces of code that describe the behaviour of a smart contract. They are compiled and executed in the Tari VM.
  • Validator Nodes - VNs validate smart contracts and earn fees for doing so.
  • Cerberus consensus engine - highly scalable, high-speed sharded BFT consensus engine.
  • Tari Virtual Machine - Runs smart contracts in a secure sandbox.
  • Tari - the token that fuels the Tari Network a.k.a DAN.

The remainder of this document describes these elements in a little more detail and how they relate to each other.

The Minotari Base Layer

Obviously, the most important role of the Minotari base layer (formerly, Tari base layer) is to issue and secure the base Tari token.

As it relates to the DAN, the base layer also serves as an immutable global registry for several key pieces of data:

  • It maintains the register of all validator nodes.
  • It provides the only means of minting more [Tari] into the DAN economy.
  • It maintains the register of all DAN contract templates.

The base layer also forces the DAN to make progress in the case of a Byzantine stoppage.

Templates

Templates are parameterised smart contracts. Templates are intended to be well-tested, secure, reusable components for building and running smart contracts on the DAN.

For example, an NFT template would allow a user to populate a few fields, such as name, number of tokens, media locations, and then launch a new NFT series without having to write any actual code.

Templates are stored and managed on the base layer. You can think of the Tari base layer as a type of git for smart contracts. Templates will also have version control features and a smooth upgrade path for existing contracts.

Contracts

Tari smart contracts are the meat of the Tari ecosystem. Usually, a smart contract will be comprised of one or more Tari templates, glue code, and initialisation code.

The contracts are always executed in the Tari Virtual machines. The input and output of every contract instruction is validated by validator nodes that reach consensus using the Cerberus consensus engine.

Validator Nodes

Validator Nodes (VNs) execute and reach consensus on DAN contract instructions. VNs must register on the base layer and lock up funds (the registration deposit) in order to participate in the DAN.

With this in place, every base node has an up-to-the-minute list of all active validator nodes and their metadata. The registration deposit also serves as a Sybil prevention mechanism.

Validator nodes are the bridge between the consensus layer and the Tari Virtual Machine (TVM).

VNs are required to re-register periodically as a proof-of-liveness mechanism.

Validator nodes must be able to

  1. interpret the instructions they receive from clients, identifying the contract code that the instruction refers,
  2. retrieve and deserialize the relevant input state,
  3. compute the output state that result from applying the contract logic to the input state, and
  4. reach consensus with its peers.

Steps 1 - 3 are carried out in the Tari Virtual Machine (TVM). Step 4 is achieved by communicating with peers via the DAN consensus layer.

Tari exists at the Validator node level, and VNs earn fees, in Tari, for each instruction -- in aggregate -- that it aids in getting finalised.

DAN consensus layer

The Cerberus BFT consensus algorithm runs on the consensus layer. This layer is completely ignorant of the semantics of DAN smart contracts.

This layer only cares that:

  • only VNs that have registered on the base layer are participating in consensus.
  • VNs are self-organising into VN committees and are carrying out the rules of Cerberus correctly.

In particular, the consensus layer has no idea whether an instruction's output is correct. If two-thirds (plus one) of the committee agree on the results, then consensus has been reached and the consensus layer is happy.

For example, if consensus decides that 2 + 2 = 5, then for the purposes of this contract, that is the case.

The Tari Virtual Machine

The TVM is a WASM-based virtual machine designed to run Tari contracts. Contracts are composed of one or more Tari templates, glue code and a state schema. Tari Labs provides a Rust implementation for TVM contracts, but in principle, other languages could implement the specification as well.

The TVM is able to

  • load a contract.
  • provide a list of methods that the contract exposes.
  • Execute calls on the contract.
  • Initiate retrieval and persistence of the state of the contract. The state itself is not stored in the VM, but by Indexers.

Tari and the turbine model

Tari is used to power the DAN's economic engine. Tari is minted via a one-way perpetual peg as described in RFC-0320. Briefly, Minotari are burnt to create Tari, which are used to pay for the execution of instructions on the DAN. A portion of instruction fees are burnt with every instruction to provide a constant source of demand for Tari in the DAN.

There is no peg out back to the base layer for Tari. The reason for this is explained in RFC-0320. Tari holders wishing to convert back to Tari will be able to perform a submarine swap with a Tari seller. We also anticipate that exchanges will list the Tari-Minotari pair to enable easy conversion of Tari back to Minotari.

Change Log

DateChangeAuthor
23 Oct 2023Thaum -> TariCjS77
1 Nov 2022High-level overviewCjS77
26 Oct 2022First outlineSWvHeerden

RFC-313/VN Registration

status: draft

Maintainer(s): stringhandler, SW van heerden and sdbondi

Licence

The 3-Clause BSD Licence.

Copyright 2022 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

The goal of this RFC is to outline the DAN validator node registration requirements and define a set of procedures that allow permissionless participation in the DAN. This includes defining the interaction between the validator and base node, new base layer validations, and validator shard key allocation.

Overview

Building on the RFC-0303, we define the mechanism that allows DAN validators to register on the DAN network.

Each validator requires a connection to a trusted base-layer node that provides a canonical view of the blockchain. The blockchain serves as a shared logical clock for the DAN.

In this RFC, we will show that the validators can leverage the strong liveness guarantees of proof-of-work while providing a scheme that mitigates the effects of weak safety guarantees (namely, reorgs) on the base layer.

Requirements and Definitions

  1. To participate in DAN BFT consensus, a validator node MUST be registered on the Layer 1 Tari blockchain.
  2. Each registration expires after a number of epochs, at which point the validator may no longer participate in DAN consensus.
  3. A validator MAY re-register before or after the expiration epoch is reached to allow continued participation in DAN consensus.
  4. A validator registration MUST be submitted as a base layer ValidatorNodeRegistration UTXO signed by the VN_Public_Key
  5. A validator MUST be assigned a deterministic but randomized VN_Shard_Key that can be verified at any epoch by other validators by inspecting the base layer.
  6. The VN_Shard_Key MUST be periodically reassigned/shuffled to prevent prolonged control over a particular shard space.
  7. A validator MUST be able to generate a Merkle proof that it is registered for the epoch that it is currently participating.
  8. The validator set for any given epoch MUST be unambiguous between validators or anyone observing the base layer chain.
  9. Base layer reorgs MUST NOT negatively affect the DAN layer.

We define the following validator variables:

SymbolNameDescription
$V_i$VN_Public_KeyThe $i$th public validator node key
$S_i$VN_Shard_KeyThe $i$th 256-bit VN shard key.
$\epsilon_i$EpochThe $i$th epoch. An epoch is EpochLength blocks.

An epoch $\epsilon$ is defined by the base layer block height $h$, where $\epsilon_i = \lfloor \frac{h}{\text{EpochLength}} \rfloor$, and spans all blocks from the start of the epoch up to but excluding the start of the next epoch.

Base layer consensus constants (all values TBD):

NameValueDescription
EpochLength60 Blocks (~2h)The number of blocks in an epoch
VNRegistrationValidityPeriod20 Epochs (~40hrs)The number of epochs that a validator node registration is valid
VNRegDepositAmountTBD TariThe minimum amount that must be spent to create a valid ValidatorNodeRegistration UTXO
VNRegLockHeight10 EpochsThe lock height that must be set on every ValidatorNodeRegistration UTXO
VNShardShuffleInterval100 EpochsThe interval that a validator node shard key is shuffled

Validator node consensus constants:

NameValueDescription
VNConfirmationPeriod1000 Blocks

Validator Registration

A validator node operator wishing to participate in DAN consensus MUST generate a Ristretto keypair <VN_Public_Key, VN_Secret_Key> that serves as a stable DAN identity and a signing key for L2 consensus messages. The VN_Public_Key MUST be registered on the base layer by submitting a ValidatorNodeRegistration UTXO which allows the validator to participate in DAN consensus for VNRegistrationValidityPeriod.

The published ValidatorNodeRegistration UTXO has these requirements:

  1. MUST contain a valid Schnorr signature that proves knowledge of the VN_Secret_Key
    • The signature challenge is defined as $e = H(P \mathbin\Vert R \mathbin\Vert m)$
    • $R$ is a public nonce, $P$ is the public VN_Public_Key
    • $m$ is the UTXO commitment
  2. the UTXO's minimum_value must be at least VNRegDepositAmount to mitigate spam/Sybil attacks,
  3. the UTXO lock-height must be set to VNRegLockHeight.
  4. A script which burns the validator node registration funds if the validator does not reclaim it for a long period after the lock height expires.
OP_PUSH_INT(N) OP_COMPARE_HEIGHT OP_LTE_ZERO OP_IF_THEN 
    NOP
OP_ELSE
    OP_RETURN
OP_END_IF

By submitting this UTXO the validator node operator is committing to providing a highly-available node from the next epoch after the validator node registration was submitted to VNRegistrationValidityPeriod epochs after that.

A validator node operator MAY re-register their VN_Public_Key before the VNRegistrationValidityPeriod epoch is reached, OPTIONALLY spending the previous ValidatorNodeRegistration UTXO. If the previous ValidatorNodeRegistration UTXO has not expired and a new ValidatorNodeRegistration UTXO is submitted, the new ValidatorNodeRegistration UTXO supersedes the previous one.

The validator node may implement auto re-registration to ensure that the validator node continues to be included in the current VN set without constant manual intervention.

A validator MAY deregister by spending their ValidatorNodeRegistration UTXO. This will remove the validator from the current VN set in the next epoch.

Base-layer consensus

The base layer performs the following additional validations for ValidatorNodeRegistration UTXOs:

  1. The VN_Registration_Signature MUST be valid for the given VN_Public_Key and challenge
  2. The minimum_value field MUST be at least VNRegDepositAmount. The existing minumum_value validation ensures the committed value is correct.

Additionally, we introduce a new block header field validator_node_mr that contains a Merkle root committing to all validator Vn_Shard_Keys in the current epoch. The validator_node_mr needs to be recalculated at every EpochSize blocks to account for departing and arriving nodes. The validator_node_mr MUST remain unchanged for blocks between epochs, that is, blocks that are not multiples of EpochSize.

A validator generates a Merkle proof that proves its VN_Shard_Key is included in the validator set for any given epoch. This proof is provided in layer 2 Quorum Certificates.

The validator_node_mr is calculated for each block as follows:

  1. if the current block height is a multiple of EpochSize
    • then fetch the VN set for the epoch
    • build a merkle tree from the VN set, each node is $H(V_i \mathbin\Vert S_i)$
  2. otherwise, fetch the previous block's validator_node_mr and return it

Epoch transitions

The DAN BFT consensus protocol relies on a shared and consistent "source of truth" from the base layer chain that defines the current epoch and validator set as well as templates.

As briefly mentioned in the overview, any PoW base layer chain is prone to reorgs. The question arises, how do we achieve a shared, consistent view of the chain when the data can disappear from underneath you?

We define a VNConfirmationPeriod as is a network-wide constant that specifies the number confirmations (blocks) that a block must have before a validator will recognise it as final. The chosen value for VNConfirmationPeriod must be large enough to make reorgs beyond that point practically impossible. Validators simply ignore base layer reorgs with a depth less than VNConfirmationPeriod deep as the data they have extracted is still valid. This means that the point of finality is not always VNConfirmationPeriod blocks away from the tip and is non-decreasing/monotonic which effectively negates reorg "noise" from the chain tip.

However, this does not address base layer latency delays where a single huge block or multi-block reorgs may take seconds to be received and processed by all base nodes. Moreover, the validators may poll the base layer only every few seconds, further increasing the latency for validators to become aware of the state. This means a single strict validator epoch change-over point will almost always cause liveness failures at and after the epoch transition.

To address this, we define a VNEpochGracePeriod where both the previous and current epoch are accepted. This value, in blocks, must allow enough time for all validators to become aware of the base layer state for the epoch.

To illustrate, consider the following view of a base layer chain. We mark 3 views of the chain.

                                      (a) (b) (c)                
                                       |   |   |                                         {noisy}
------ | --x------------ | --x------------ | --x------------ | --x------------ | --- .... ------> tip
  |  ฯต10       |   |    ฯต11       |      ฯต12       |       ฯต13               ฯต14
 V_1          V_2 V_3   V_4      V_5              V_6                      
Key:
x - Epoch transition point
ฯตn - Epoch n
V_n - Validator registrations 

Point (a)

  • the validator node set is incomplete for epoch 12.
  • the active epoch is 11.
  • the validator MUST reject instructions for epoch 12.

Point (b) - the start of epoch 12:

  • the validator node set is final for epoch 12.
  • the active epoch remains at 11. This is because validators may not have reached epoch 12/point (b) and therefore will only accept epoch 11.
  • a validator MUST accept instructions from epoch 11 and 12.
  • a validator may receive a leader proposal for epoch 11 and 12, however a well-behaved validator MUST only vote for one of these proposals.

Point (c) - the transition point for epoch 12:

  • the active epoch is now 12
  • the validator MUST reject instructions from 11.
  • it is assumed at this point that almost all (at least $2f + 1$) nodes will accept epoch 12

Validator Node Set Definition

The function $\text{get_vn_set}(\epsilon_\text{start}, \epsilon_\text{end}) \rightarrow \vec{S}$ that returns an ordered vector $\vec{S}$ of VN_Shard_Keys that are registered for the epoch $\epsilon_n$. The validator node set is ordered by VN_Shard_Key.

Data Indexes

The following additional indexes are recommended to allow efficient retrieval of the VN set, shard key mappings and to produce a valid validator_node_mr:

  • $I_\text{primary} = \{ (h_i, V_i, C_i) \rightarrow (V_i, S_i) \}$
  • $I_\text{shard} = \{ (V_i, h_i, C_i) \rightarrow S_i \}$
  • $C_i$ is the $i$th UTXO commitment

Database index $I_\text{primary}$ that maintains a mapping from the next epoch after the registration to all the <VN_Public_Key, VN_Shard_Key> tuples for all ValidatorNodeRegistration UTXOs. This allows efficient retrieval from a particular height onwards, optionally for a particular validator node public key.

The index entry is not removed whenever the ValidatorNodeRegistration UTXO is spent or expires. This is to allow state to be rewound for reorgs. If a validator adds two or more validator registration UTXOS in the same block, the index will order them by commitment, that is, the same canonical ordering as the Tari block body. The get_vn_set function MUST return the last registration public key and shard key tuple only, according to this canonical ordering.

Algorithm

The function $\text{get_vn_set}$ is defined as follows:

  1. Iterate on index $I_\text{primary}$, starting from where the key is between (inclusive) the equivalent block height for $\epsilon_\text{start}$ and $\epsilon_\text{end}$:
    • Add to the set, and
    • if the validator node public key is already in the set, remove the previous entry.

For this example, we say that there have been no registrations prior to V_1; we define VNRegistrationValidityPeriod = 2 epochs.

                                    (a)            (b)            (c)
                                     |              |              |                      {noisy}
------ | --x------------ | --x------------ | --x------------ | --x------------ | --- .... ------> tip
  |   ฯต10       |   |    ฯต11       |      ฯต12  |    |       ฯต13               ฯต14
 V_1           V_2 V_3  V_4       V_5         V_2  V_6         
 
Key:
x - Epoch transition point
ฯตn - Epoch n
V_n - Validator Node Registration UTXO n
  • Point (a) $\text{get_vn_set}(\epsilon_11) -> [V_1, V_2, V_3]$
  • Point (b):
    • In: $[V_2, V_3, V_4, V_5]$, out: $[V_1]$
    • $\text{get_vn_set}(\epsilon_12) -> [V_2, V_3, V_4, V_5]$
  • Point (c):
    • In: $[V_6]$, out: $[]$
    • $\text{get_vn_set}(\epsilon_13) -> [V_2, V_4, V_5, V_6]$

Shard Key and Shuffling

The VN_Shard_Key is a deterministic 256-bit random number that is assigned to a validator node by the base layer for a given epoch, and maps onto the 256-bit shard space.

The DAN network needs to agree on and maintain a mapping between each participant's VN_Public_Key and the corresponding VN_Shard_Key for the current epoch.

Over time, an adversary may gain excessive control over a particular shard space. To mitigate this, we introduce a shuffling mechanism that periodically and randomly reassigns VN_Shard_Keys within the network.

We define the function $\text{generate_shard_key}(V_n, \eta) \rightarrow S$ that generates the VN_Shard_Key from the inputs. $S = H_\text{shard}(V_n \mathbin\Vert \eta)$ where $H_\text{shard}$ is a domain-separated Blake256 hash function, $V_n$ is the public VN_Public_Key and $\eta$ is some entropy.

And we define the function $\text{derive_shard_key}(S_{n-1}, V_n, \epsilon_n, \hat{B}) \rightarrow S$ that deterministically derives the VN_Shard_Key for epoch $\epsilon_n$ from the public VN_Public_Key $V_n$, $\hat{B}$ the block hash at height $\epsilon_n * \text{EpochSize} - 1$ (the block before the epoch block).

The function $\text{derive_shard_key}$ is defined as follows:

  1. Given:
    • $S_{n-1}$ the previous VN_Shard_Key
    • $V$ the VN_Public_Key
    • $\epsilon_n$ the epoch number to generate a VN_Shard_Key for
    • $\hat{B}$ the previous block hash
  2. If the previous shard key $S_{n-1}$ is not null,
    • Check $(V + \epsilon_n) \bmod \text{ShufflePeriod} == 0$.
    • If true, generate a new shard key using $\text{generate_shard_key}(V, \hat{B})$.
    • If false, return $S_{n-1}$.
  3. If the previous shard key $S_{n-1}$ is null,
    • generate a new shard key using $\text{generate_shard_key}(V, \hat{B})$.

Only a random fraction of validators will be re-assigned shard keys per epoch and that fraction will not be shuffled again for VNShardShuffleInterval epochs. Although the exact number of validators that shuffle per epoch varies, on average the VNShardShuffleInterval should aim to shuffle around 5% of the network at every epoch. This is to ensure that the number of shuffling validators is much less than $\frac{1}{3}$ of the validator set, as this could break liveness and safety guarantees.

The prev_shard_key is the last VN_Shard_Key that was assigned to the validator node within the VNRegistrationValidityPeriod. Should VNRegistrationValidityPeriod elapse without a renewed registration, a new VN_Shard_Key is assigned. This means that a validator may be assigned a different VN_Shard_Key after each VNRegistrationValidityPeriod, sooner than VNShardShuffleInterval. The validator has nothing to gain from this, on the contrary, they will have to re-sync their state and spend time not participating in the network, losing out on fees.

Change Log

DateChangeAuthor
12 Oct 2022First outlineSWvHeerden
08 Nov 2022Updatessdbondi
18 Nov 2022Implementation updatessdbondi

RFC-0320/TurbineModel

The DAN peg-in mechanism, or Turbine model

status: draft

Maintainer(s): Cayle Sharrock

Licence

The 3-Clause BSD Licence.

Copyright 2022 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

Tari is used to power the DAN's economic engine.

This RFC describes the motivation and mechanism of the Minotari to DAN peg-in mechanism.

Description

Side-chains are related to their parent chains via a pegging mechanism. In general, peg-in transactions (transferring value to the side-chain) are straightforward. One locks value up on the parent chain, which can be referenced by the side-chains. However, the reverse transaction is fraught with difficulty, since the parent chain must know almost nothing about the transaction particulars of the side chain. We know this is the case, otherwise the entire side-chain + parent-chain system are forced to work in lock-step and the two chains are really just one larger, more complicated chain.

Peg outs are particularly difficult if the participants change on the side-chain (as opposed to say, payment channels, like the Lightning Network, where the same parties peg-in and -out).

There are several proposals to develop a reliable two-way peg, including space-chains, drive-chains and federated side-chains, like elements. All of them have particular trade-offs and difficulties.

For Tari, we propose a slightly different approach: A one-way peg with persistent 2nd-layer burn.

Because the operating principle is quite similar to that of a gas turbine, we call this approach the turbine model. Fuel (Minotari) is fed into the turbine, which is burnt (also burnt :)) which produces a hot, motive gas (Tari) that drives the engine (the DAN). The exhaust gas is ejected from the rear of the turbine (a portion of the instruction fees are burnt).

An aside - the monetary policy trilemma

The monetary policy trilemma states that you cannot control all 3 of these things simultaneously:

  1. exchange rate
  2. monetary policy (i.e. minting and burning to control supply)
  3. flow of capital (money leaving or entering the system)

Let's briefly consider the trilemma from the point of view of the DAN.

Monetary policy

We are effectively forced to control monetary policy. Or put another way, we can't let people freely mint their own Tari. Unfortunately, even though we have "chosen" to control supply, it's difficult for us to control the Tari supply in practice. In the physical world, the money supply is typically managed by a central bank, with the emphasis on "central".

The designers of a decentralised monetary system have precious few levers available to control supply and no simple ones.

Capital flow

Allowing free capital flow would require a reliable and efficient peg-out mechanism from the DAN back to the base layer. However, I argue that this is an Achilles heel. Any peg-out system you can devise that is coupled with a burn-type peg-in mechanism is an existential threat to the base layer.

Why? Because the peg-out necessarily requires creation of coins on the base layer by trusting some mechanism external to it. Note that we're not bringing UTXOs that were pegged-in back into circulation (in this case they would not be burned, merely locked-up as in a traditional peg). Therefore, the base layer accounting simply has to accept these mints as valid.

Consequently, any bug whatsoever on the DAN related to the minting process' authenticity could lead to undetectable inflation on the base layer.

A central axiom of side-chain design, if there is such a thing, is that the side-chain should pose zero risk to the security of the base layer. For this reason, the burn mechanism effectively excludes the possibility of peg ins.

So essentially, we cannot allow the free flow of capital either.

Exchange rate

Since we have already picked two legs of the trilemma, we cannot do anything about the third, and must allow the change rate to float.

The turbine model

This is actually not as terrible as it sounds. The trilemma doesn't force you to sit at the vertex of the monetary policy triangle. If you allow partial freedoms in supply and capital flow, then the exchange rate will move, but will tend to remain range-bound.

Although capital flow here is not free, it's not completely restricted either:

  1. Peg-ins are completely unrestricted.
  2. Submarine-swaps allow people to remove money from the system on the micro-level (albeit not on the macro level).

And the money supply can be tuned, if not controlled. This all leads to the proposal of a new mechanism, the turbine model:

turbine

The DAN Tari supply is increased by user peg-in deposits, and any other mechanism that we may want to enforce, such as asset issuer financing.

To prevent the eventual collapse of the Tari price to zero, there must be an exhaust mechanism that continually removes Tari from the system..

The simplest exhaust mechanism is to simply burn a fraction of Tari fees from every transaction! These can be very low. Presumably, over the long-run the burn rate should approximately match the Minotari blockchain tail emission.

The exhaust places a permanent upward pressure on the Tari exchange rate; but it will never exceed 1:1 with Minotari, since any premium will be immediately arbitraged away. This is because anyone can always burn as much Minotari as they wish and mint Tari at a 1:1 ratio on the DAN and then sell them for a risk-free profit. This action will increase the supply of Tari and drive the price back down to parity.

If the exhaust is temporarily insufficient to hold the peg, the Tari price will drop below 1 XTR. This will immediately shut off deposits because submarine swaps will a be cheaper route to obtaining Tari than burning Minotari (which are always 1:1). Since the exhausts upward price pressure is a constant force, the Tari price will eventually approach 1:1 again.

Over time, we expect this mechanism to provide a somewhat stable peg between Tari and Minotari with the Tari price occasionally dropping below parity and possibly remaining there for some time. As secondary markets for the Tari-Minotari pair matures, this event will immediately create a bid on Tari, since -- in the absence of catastrophic failure -- speculators know that the Tari price will eventually return to parity, causing upward pressure to come into play quickly and efficiently.

Change Log

DateChangeAuthor
23 Nov 2023Thaum -> TariCjS77
1 Nov 2022First draftCjS77

RFC-0350/TariVM

The Tari Virtual Machine

status: draft

Maintainer(s): Cayle Sharrock

Licence

The 3-Clause BSD Licence.

Copyright 2023 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

This RFC describes the design goals and rationale for the Tari Virtual Machine (TVM).

Description

The Consensus Layer for the Tari network, described in RFC-0305, is responsible for distributed and trust-minimised decision-making in the Tari digital assets network (DAN).

The consensus layer is blind to any notions of smart contracts, NFTs or stablecoins. It merely enforces that decision made by honest nodes are propagated to the rest of the network.

Business logic is encapsulated in the Tari Logic layer.

The relationship between the logic and the consensus layers is best illustrated by the transaction flow, from client to resolution. This flow is diagrammed in Figure 1.

The client creates a transaction using a transaction manifest. The manifest collates every contract, function call and log entry that the client wants to execute into a single bundle. The manifest is then submitted to an indexer that will collect all the input state required for the transaction pre-emptively. This is a deviation from the Cerberus and Chainspace papers and allows the indexer to perform a dry-run of the transaction before submitting it to a validator node.

The advantage of this is that the client can be notified of any errors before submitting the transaction to the network and incurring fees.

On the other hand, the indexer commits to the given set of inputs when submitting the transaction to the validator node. If the indexer is mistaken, or lagging in its updates, it is possible that the transaction will be aborted due to attempting to use expired inputs.

However, this is not expected to happen often, and assuming everything is in order, the validator node will confirm the correct version of all the input states by collecting all local and foreign substates involved in the transaction.

If these are satisfactory and match what was provided by the indexer, the validator node will pass the transaction manifest data along with the input state to the Tari Engine for execution.

The Tari Engine will determine which WASM modules are required to perform this work, load them into a WASM runtime, and execute the functions. The Tari engine makes use of several services to help it in this task, including the template manager, the Tari SDK, and Tari runtime wrapper.

The execution result (whether successful or not) is returned to the validator node, which then compares the results with its peers via Hotstuff consensus to achieve agreement on the final result. If consensus is achieved, the affected substates are updated (see RFC-330) and the result is relayed to the indexer who passes it on to the client.

A key point here is that the consensus layer delegates all business logic to the logic layer. However, only the consensus layer has the ability to make changes to the state of the network, after reaching consensus.

flowchart LR
    Cl((Client)) -------> |tx
    'Transfer JPG to Bob'| Ix1
    Ix1 -.-> |returns result| Cl
    
    subgraph Indexer
        TM --> Ix1([Indexer])
        Ix1 <-.-> |reads| SS1[(Substates)]
        Ix1 <-.-> |requests| VN1[[VNC]]    
        Ix1 <-.-> |dry run| VM1[[TariVM]]
    end
    Ix1 -->|submits with\nlocked inputs| C
    
 
    subgraph Logic Layer
      T[Template manager]
      W[Wasm compiler]
      ABI[Contract interface]
      E[Tari engine]
      RT[WASM Runtime]
      SDK[Tari SDK]
      E <--> RT
    end

    C --> |calls| E
    E --> |returns new substates| C
    subgraph Consensus Layer
        C[Validator node]
        C <-.-> |reads/writes| SS[(Local substates)] 
        C <-.-> |foreign state\nrequests| VNC[[VNC]]
        EM[Epoch management]
        VNCm[Validator committee\n management]
        HS[Cerberus-Hotstuff]
    end

The Tari Logic layer comprises several submodules:

  • The Tari engine. The Tari engine is responsible for the transaction execution process and fee disbursements.
  • The Template manager. The template manager polls the Minotari base layer looking for template registration transactions.
  • The Tari runtime. The Tari runtime wraps a WASM virtual machine that executes the compiled contract code. The runtime is able to calculate the total compute requirements for every instruction, which determines the transaction fee.
  • The contract interface (ABI). This is a list of functions, their arguments and return values that a particular contract is able to execute. The ABI is generated when the contract template is compiled.
  • The transaction manifest. This is a high-level set of instructions that a client application generates to achieve some user goal.

The Tari engine

The Tari engine is responsible for the transaction execution process and fee disbursements. It communicates with other actors in the Tari network via JSON-RPC. Transactions can be submitted to be executed via the submit_transaction procedure call.

A transaction contains the following information:

  • A list of input substate that will be downed (spent).
  • A list of input substates that are used as references, but their state is not altered.
  • The list of instructions to execute (call method, claim funds, emit logs etc.).
  • Signatures
  • Network metadata

Transaction execution proceeds via the following high-level flow:

  • The engine determines the total fee for the transaction by charging for every operation executed within the WASM runtime, as per the fee schedule.
  • The engine initializes a WASM runtime, which executes the instructions embedded in the transaction. For each instruction, a [template provider] will attempt to provide the WASM, or other compatible binary, to execute the instruction with the given input parameters. See also the base node scanner. For now, only WASM modules are supported, but in future, additional runtimes could be supported by the Tari Engine, including Zero-Knowledge contracts, or the EVM.
  • The result of execution -- the execution status, and the set of outputs -- is passed back to the consensus layer. Note that outside of the vanishingly small chance of an output substate collision, even a failed execution attempt is a 'positive' (i.e. COMMITted) result in terms of consensus, as long as the super-majority of nodes agree that "failure" is the consensus result!

Template manager

The Tari engine maintains a service that scans the Minotari chain every few minutes that among other things, looks for template registration transactions.

When a new template is registered, the template manager will locate the registered WASM module, validate it, and store it in the local database.

The template manager abstracts away issues such as template versioning, whether the module is stored in IPFS, or a centralised repository. It can also request binaries from a peer to reduce the load on external services.

In future, the template manager might also be able to retrieve source code, and make use of reproducible builds to compile audited source code and add it to the local WASM repository.

The Tari runtime

The Tari runtime is a wrapper that provides common functionality for executing arbitrary smart contracts in WASM
modules. Functionality includes:

  • calling a function,
  • calling a method,
  • emitting a log entry,
  • pushing an object into the workspace.

In combination with a contract's ABI, the runtime is able to execute almost any contract code.

Tari uses the wasmer runtime to actually load and execute Web Assembly inside a secure, sandboxed environment.

The contract interface (ABI)

Rust is strongly-typed, yet we need to be able to call an unlimited variety of functions and methods from arbitrary contracts in a unified, consistent way. This is where the ABI comes in. It defines all the public methods and their arguments that a contract exposes.

The ABI is generated when a contract template is compiled from Rust source.

The transaction manifest

When a client wants to interact with the DAN, it is often the case that she wants to invoke multiple functions across multiple contracts simultaneously. For example, Alice may want to buy a monkey NFT from Bob. Her transaction might lock funds in a cryptographic escrow (in the Tari contract) until she has proof that the NFT has landed in her NFT account (which is in a different contract).

The transaction manifest collects all the information necessary to achieve this goal, including the contract(s) function(s) to call, their arguments, fee information and all the necessary signature and witness data to authorise the transaction. The transaction manifest is compiled into an abstract syntax tree (AST) that can be consumed by a validator node.

Internally, A manifest is simply a list of manifest intents that are bundled together into an atomic whole.

An intent is typically one of the following:

  • A template invocation or component invocation, indicating that the user wants to execute a function on a contract, supplying the necessary input arguments.
  • A log entry, providing the log level and message.

Why Web Assembly?

The Smart contract execution environment is incredibly hostile. The runtime is effectively tasked to run arbitrary code on a global system that has potentially billions of dollars of value at stake.

It is therefore critical that the execution environment does not affect state in the broader network that it is not entitled to, but also, the code cannot be allowed to jailbreak the execution environment of the validator node itself and wreak havoc on the host system.

Thus, the runtime environment should

  • be strictly sandboxed,
  • support multiple concurrent VM without being a resource hog,
  • have no access to the host system internals, including disk storage, camera, microphones, etc.
  • be ephemeral and support rapid cold starts.

Given these onerous requirements, rather than reinvent the wheel, we considered several existing solutions, including

  • WebAssembly (WASM)
  • Extended Berkeley Packet Filter (eBPF)
  • Full virtualisation, such as KVM or VmWare

Full virtualisation was quickly discarded as being too heavy-weight. Validator nodes will be required to swap out contracts constantly, and so cold-starting a runtime environment must be as lightweight and as fast as possible.

Surprisingly, there were very few options remaining, with eBPF and Wasm being the most mature and closest to our needs. In fact both runtimes are used in smart-contract execution environments today. WASM is used in Stellar, Near, Cosmos, Polkadot, Radix and a host of others. eBPF is used in Solana.

We chose WASM for the following reasons:

  • WASM is a mature, well-supported standard. It is supported by the major browsers, which means that trillion-dollar companies like Google, Microsoft and Apple have a vested interest in its success.
  • WASM was designed from the ground-up to be a highly performant, lightweight, sandboxed runtime.
  • There is fantastic tooling to convert Rust, C, Go, and [insert favorite language here] into WASM.
  • WASM can run almost anywhere, but especially in the browser. Thus, the path to running Dapps safely in the browser is likely much easier.

On the other hand, eBPF was originally design as a packet-filter to protect networks from malicious data packets. It has since been extended to support a full-blown virtual machine, but it still feels a little like a square peg being banged into a round hole.

Ultimately, it was a fairly straightforward decision to go forward with WASM.

Independently, Stellar reached the same conclusion, for much the same reasons.

In contrast, while Polkadot does use WASM, there is an active discussion to replace Wasm with RISC-V. This VM is being designed explicitly for smart-contract environments and is worth watching.

For the time-being, WebAssembly is a pretty clear winner.

Change Log

DateChangeAuthor
20 Dec 2023First draftCjS77

RFC-0305/Consensus

The Tari Network Consensus Layer

status: draft

Maintainer(s): Cayle Sharrock,stringhandler

Licence

The 3-Clause BSD Licence.

Copyright 2022 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

This Request for Comment (RFC) describe the consensus mechanism known as Cerberus as it is implemented in Tari. Tari implements the Cerberus variant known as Pessimistic Cerberus, for the most part, with Hotstuff BFT replacing pBFT as described in the Cerberus paper.

This RFC serves to document any deviations from the academic paper as well as finer-grained details of the implementation.

Introduction

The Tari DAN is based on a sharded BFT consensus mechanism called Cerberus.

One particular note is that Tari has chosen Hotstuff as the base BFT consensus algorithm over pBFT mentioned in the paper.

The core idea of Cerberus is that instead of dividing work up between validator nodes according to the contracts they are managing (as per Tari DANv1, Polkadot, Avalanche, etc.), Cerberus distributes nodes evenly over a set of state slots. Any time an instruction modifies the state of a contract, it will affect one or more state slot, and only those nodes that are responsible for covering those addresses will reach consensus on the correct state changes.

This means that nodes have to be prepared to execute instructions on any contract in the network. This does create a data synchronisation burden, but the added benefit of a highly scalable, decentralised DAN significantly outweighs this trade-off.

The consensus layer is logic agnostic

The first key point to make about the Cerberus layer is that it is logic agnostic. The consensus layer does not know anything about Tari, about digital assets, or smart contracts. It has one job:

Ensure that a super-majority of participating nodes agree on the state transition for every Tari transaction.

Defining the consensus layer in this way allows us to separate the concerns of the consensus layer from the concerns of the smart contract layer. This is important because it reduces the attack surface of the consensus layer, and allows us to develop the consensus layer in isolation from the smart contract, or "business logic" layer.

To be clear, if 67% percent of nodes decide that $1 + 1 = 3$ then that is the truth as far as the consensus layer is concerned.

This job can be subdivided into several smaller, co-ordinated tasks:

  1. Deterministic distribution of validator nodes across the state space, to form validator committees.
  2. Periodically re-distributing validator nodes across the state space to reduce the likelihood and opportunity for collusion.
  3. Efficient transmission of consensus messages to the rest of the network.
  4. Identifying and removing malicious nodes from the network.
  5. Correct identification of nodes participating in cross-shard consensus.
  6. Requesting and responding to state requests from other nodes.
  7. Reaching consensus on the state transition for a given transaction.
  8. Effective leader rollover in the case of a faulty leader.
  9. Guaranteeing liveness in the face of a Byzantine stoppage.

Distribution of validator nodes

Validator node selection and distribution is described in RFC-314. RFC-314 also covers the periodic re-distribution of validator nodes across the state space.

Efficient transmission of consensus messages to the rest of the network

The Tari communications layer is used to transmit consensus messages to the rest of the network. The Comms layer is described in RFC-170 and related sub-RFCs.

TODO:
  • Describe differences in configuration between the Tari and Minotari networks.
  • Describe how VNC members find each other and how they keep in touch.
  • Describe how banning or other sanctioning behaviour works.
  • How client messages are propagated and routed to the correct nodes in the network.
  • How consensus messages are communicated across the network.

Identifying and removing malicious nodes from the network

In the current proposal, malicious nodes are not actively removed from the network. Instead, they can be banned by peers, as described above, and then de-registered as validator nodes at an epoch transition.

This is still an indirect punishment, since a substantial deposit is required to register as a validator node. After de-registration, the deposit is locked up for a significant period (3-6 months). Therefore, a serial offender running bad validator nodes will incur a significant opportunity cost over time.

However, the community is open to other proposals, both game-theoretic and technical, for dealing with malicious nodes.

Many proof-of-stake systems utilise "slashing" to punish non-cooperative nodes. Slashing mechanisms sound good at first, but in fact, there are many edge cases that can result in honest-but-poorly-configured nodes being punished. We are somewhat sceptical that slashing will achieve their intended goals.

Slashing introduces significant additional complexity,
including the need for additional tuning parameters, the need for 'watchtowers' to police the VN set's behaviour (which is a centralising force), the need for trustless fraud-proofs (a non-trivial problem), and the fact that software bugs don't follow the rules of economic game-theory (in other words, they're not rational).

Furthermore, slashing is less relevant in a BFT process where safety and liveness is guaranteed as long as 67% of the committee is honest. The motivation for punishing malicious nodes in Tari is essentially two-fold:

  • to reduce the chance that a critical mass of 1/3 malicious nodes accumulate on the network.
  • to deter nodes from colluding to try and achieve 33% (to break liveness) or 67% (to break safety).

One alternative to slashing os to make all VN deposits non-refundable. Therefore, a malicious node will implicitly have their deposit slashed once they are banned. Banning can also be made temporary, depending on the offense. Honest nodes will need to run for a period of time before they become profitable, akin to an apprenticeship, or 'paying your dues'. VN fees would be increased to compensate for this mechanism.

Overall, this strategy is very similar to slashing, but is simpler to implement and police.

Another option is to make use of the auditability and fraud-proof properties of Cerberus (See Section V.B of the Chainspace paper). This would allow retroactive punitive actions against malicious nodes, and in particular, colluding nodes that act together to subvert an entire validator node committee. This is an avenue worth exploring, since it's quite clear from the experience of incumbent proof-of-stake networks, controlling hundreds of billions of dollars of value, that essentially all slashing events are due to configuration errors or intentional bugs, rather than intentional attempts to bring the network down.

Identification of nodes participating in cross-shard consensus.

Every validator node is registered on the base layer. Therefore, anyone with a synchronised Minotari node will be in possession of the current set of validator nodes running the Tari network.

The rules for assigning a given validator node (with its public key) to a Tari shard are deterministic and described in RFC-314.

It therefore follows that every validator node must also run a Minotari node (or connect to one that they trust). This will provide all the information that they need to determine which VNs are part of every committee and therefore which nodes to contact when participating in cross-shard consensus.

Requesting and responding to state requests from other nodes.

State requests come from two primary sources:

  1. Other validator nodes requesting state that they need to process an instruction. They will typically request this state from peers in the braided consensus group as part of a consensus round, although there are opportunities to optimise this process through caching and pre-fetching via an Indexer.
  2. Clients (wallets, dApp users etc.) will usually request state from an Indexer that is following the history of a set of contracts on interest. Indexers are a trusted party. Users wanting to operate in a trustless environment will need to run their own indexer. Indexers are described in RFC-331.

Reaching consensus on the state transition for a given transaction.

Tari uses Cerberus in conjunction with HotStuff BFT to achieve consensus on substate transitions. This process is described in detail in RFC-330.

Effective leader rollover in the case of a faulty leader.

Leader rollover is also covered in RFC-330.

Guaranteeing liveness in the face of a Byzantine stoppage.

The final design for liveness guarantees is still under active discussion.

A liveness break will only occur if at least a third of nodes in a single VNC are actively or passively colluding to prevent consensus being reached. Successive leader rollovers will have failed to resolve the issue, and the transaction will become stuck.

Eventually, the entire network will stop functioning even though the network is sharded, because probabilistically, every contract will eventually produce a state change that required the Byzantine committee to be part of the consensus.

Therefore, it's critical that liveness can be forced relatively quickly and efficiently.

The basic strategy is that enough nodes vote to force an epoch change. Nodes need to provide proof of recent activity in order to participate in the new epoch. Nodes that cannot provide proof will be banned and de-registered as validator nodes.

The epoch change causes a validator node shuffle, and any remaining nodes that may have been preparing to collude will be assigned new shards.

Change Log

DateChangeAuthor
16 Dec 2023Second draftCjS77
30 Oct 2023First draftCjS77

RFC-314/VNC Selection

Validator node committee selection

status: out of date

Maintainer(s): stringhandler and SW van heerden

Licence

The 3-Clause BSD Licence.

Copyright 2022 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

The goal of this RFC is to describe the process for allocating Validator Nodes (VNs) to Validator Node Committees (VNCs)

Intro

Validator nodes will have to group themselves into Validator Node Committees (VNC) for transactions/shard processing. Committees will be formed for each new transaction/shard processing. This committee needs to be determined pseudo-randomly, and we use the VN_Key as this changes periodically and is pseudorandom. Each VN will be allocated a unique and individual shard space to serve in as a VNC member.

Requirements

In order to ensure that VNCs are selected securely and can operate successfully, we pin down the following requirements:

  1. A percentage of the nodes must change to a new shard space every epoch (e.g. exactly 25%)
  2. A VN must not be able to determine its own shard space location ahead of time, i.e. the VN only knows it's new location once the block with the new epoch is mined.
  3. It must be easy, e.g. ๐“ž(log n), to calculate the VN set. (You should have to replay all shuffles to calculate the current VN set).
  4. A VN must be able to be part of a committee for a certain period to allow time to sync state, before being penalised (if applicable) for not participating in consensus.
  5. Open Question: Should there be a limit (e.g. 10%) in the number of nodes that can join per epoch

VN-key expiry

Each new VN will get a VN-key on registration. This key will expire on some pseudorandom height. This is calculated as follows:

$$ \begin{aligned} \text{Expire height} = \text{(Current block height)} + \text{(min expire height)} + \text{new VN-pubkey } MOD \text{ ( max expire height)} \end{aligned} $$

Every time a new VN-Key is assigned a new expiry date is calculated. Because the [miner]s calculate the VN-key, they also calculate the new VN-key every time it expires.

Process of choosing committees when an instruction needs to be processed

For each instruction, the substates involved in this instruction MUST be known before they can be processed. For each involved substate, the address of the substate is mapped to a shard. For each shard, the VN committee is constructed from COMMITTEE_SIZE/2 VNs to the left and right of the shard, in the [VNKey Merkle tree].

Thus, a single instruction will have a maximum N * COMMITTEE_SIZE validator nodes processing it, when N is the number of involved substates.

Committee Creation

This section is out of date

Because we have the base layer where each VN needs to publish a registration transaction, we can get base_node and miners to keep track of all active VNs. We represent all VNs in a (balanced) Merkle tree with the VN_keys as leaves. We declare a constant COMMITTEE_SIZE which can be changed in the consensus constants.

For the sake of simplicity, COMMITTEE_SIZE MUST be an even positive integer.

As per the Cerberus algorithm, validator nodes are responsible for managing sub-states, rather than contract semantics. A given instruction may involve dozens of sub-states, meaning that there are potentially dozens of non-overlapping committees that are required to reach a braided consensus.

Each committee is determined independently. For each sub-state, a committee is formed by taking the first COMMITTEE_SIZE / 2 VN_keys to the left and COMMITTEE_SIZE / 2 VN_Keys to the right of the sub-state's shard address in the merkle tree.

This makes it very easy to determine what shard space a VN needs to serve, which states the VN needs to sync from peers, and who has them. Because the second layer has a delayed view of the network. VNs can also know beforehand and prep to ensure they are ready when new block heights appear.

An important edge case here that is implied but not listed, is that if the whole network is less or equal to the COMMITTEE_SIZE, then the whole network participates in the VNC.

Committee proofs

We construct the balanced Merkle tree from all active VN registration transactions and prune away all inactive ones. Because the base nodes have to keep track of the entire unspent UTXO set, it becomes easy for them to track and validate all active VN registration UTXOs. This means we can keep base nodes responsible for validating and constructing the Merkle tree. We commit this Merkle tree per block as a Merkle root inside the block's header as the validator_node_set_root.

This Merkle root in the header always lags by one block, meaning that the Merkle root is for the state of the VN's before the start of the block it's mined in. When a VN registers, that new VN key will only appear inside the Merkle tree in the next block. The reason for this is that header_hash is used in the the calculation process of the VN_key.

Option 2 for Committee proofs

The VN_key needs some random entropy that's not minable to ensure that a VN cannot choose its VN_key. This random entropy is currently the block hash of the block the VN registration was mined in. Hence the reason for the Merkle root lagging by one block. If we change this entropy value be another source of randomness, such as the utxo_merkle_root, then we don't have to do the lag by 1, and it can all be calculated in the single block.

Leader selection

The VNC leader should not be decided beforehand and must be chosen pseudorandomly only when a tx is published. We also need to select an order of leaders in the case, the first leader is offline/and or not responsive.

$$ \begin{aligned} vn_position_hash(vn, tx) = Hash(vn.signing_key || hash(tx)) \end{aligned} $$

This will give a sortable list we use to order the VNs for leader selection.

Change Log

DateChangeAuthor
11 Oct 2022First outlineSWvHeerden

RFC-0321/ProcessingForeignProposals

Processing Foreign Proposals

status: draft

Maintainer(s): stringhandler

Licence

The 3-Clause BSD Licence.

Copyright 2022 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

This RFC describes the process of distributing and processing foreign proposals in the Tari DAN Cerberus Model

Across the entire network transactions must be processed or time out. When a transaction is started on a shard, it locks up substates, preventing other transactions from completing. Therefore if a transaction is started on a shard, it should complete or be aborted in a timely manner to release the resources.

None

Glossary

  • Block - A second layer block, consisting of ordered commands
  • Command - Command can either be Prepare, LocalPrepared, Accept, and moves a transaction into that state.

Description

To solve the above problems, we'll use reliable broadcast between shards and process foreign evidence in order.

In a local shard committee, the proposed block must include a reliable broadcast counter for each other shard. If the proposal includes transactions that involve other shards, this counter must be incremented. At the beginning of each epoch, all reliable broadcast counters must be reset.

When the proposed block becomes committed locally (i.e. it has a chain of 3 QCs validating it), the block must be broadcast to each involved shard that was incremented, along with evidence of being committeed (The chain of QCs must be included).

To ensure this, f+1 nodes in the local committee will forward this committed block to each relevant committee, along with a 3 chain of QC's proving it was committeed.

As a local committee member, when I receive a foreign proposal, if it is valid I will queue up a special command ForeignProposal(number, QC_Hash) that I must propose when I am next leader (if it has not been proposed already). I also should request all transaction hashes that I have not seen from involved_shards for each transaction in the proposal, and add them to my mempool for execution.

When processing transactions from a foreign, there are two methodologies we can try.

  1. Strict ordering
  2. Relaxed ordering

Strict ordering

In strict ordering, before transactions in the N+1th foreign proposal for a shard, all transactions in the Nth foreign proposal for that shard must be sequenced into the local chain as either a ABORT(reason = Timeout) or a LOCALPREPARE(TxId, ForeignShardId). This means that if a transaction is going to timeout, it will hold up all transactions in future proposals. While timeouts are expected to be rare when at least one honest node is able to provide the transaction, this approach could lead to really long finalization times, slowing down all cross shard transactions. In addition, there may be potential for deadlocks, where state is locked for a long time while transactions wait to timeout.

Relaxed ordering

In relaxed ordering, transactions from foreign proposals can be processed in any order, but transactions must still timeout if they are not processed after a certain number of blocks from the FOREIGN_PROPOSAL command. This could lead to some strange behaviour where a transaction can be aborted due to double spends, even though the double spend happens much later in one shard. This however could happen even with strict ordering if the transactions arrive at different times.

Given the above, we shall use relaxed ordering unless future development reveals other problems.

NOTE: ForeignProposal commands can be proposed in between a previous ForeignProposal and LocalPrepare/Timeout commands, but commands from the ForeignProposal must only be proposed after all transactions in the first ForeignProposal have been sequenced. The TIMEOUT_TIME block is counted from the height where the ForeignProposal is sequenced.

ForeignProposal commands must appear in strict ascending order in the blockchain, but do not have to be in sequential blocks. In other words, for shard s, the block containing ForeignProposal(s, 1) must have a height lower than ForeignProposal(s, 2). Also, if a chain contains ForeignProposal(s, 1) and ForeignProposal(s, 3), then it must also contain ForeignProposal(s, 2).

If a node receives a foreign proposal (not the command), and it has not received the previous foreign proposal, then it should ask the committee to provide it to them.

Change Log

DateChangeAuthor
17 Nov 2023First draftstringhandler

RFC-0325/DanEpochManagement

Epochs and time management

status: draft

Maintainer(s): SW van Heerden

Licence

The 3-Clause BSD Licence.

Copyright 2019 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

The aim of this Request for Comment (RFC) is to describe the role of Epochs and time management on the DAN.

Motivation

For stability and security in the VNCs we have the following requirements:

  • We need to know who the valid and active VNs are.
  • VNCs need to be periodically shuffled to prevent shard targeting attacks.
  • VNC members cannot be swapped on a whim and need to be stable to allow members to vote on and process instructions.
  • Chain re-organisations on the Minotari chain should not have an effect on the VNC distribution.
  • When swapping VNs from one [shard] to another, the VN has enough time to sync the required state before the swapping takes effect.
  • When swapping VNs from one [shard] to another, the VNC must retain enough members so as to keep functioning.

DAN Lag

Because the Minotari chain influences the DAN layer and is used as a timing mechanism for the DAN, we need to ensure that the DAN has a stable view of the Minotari.

The Minotari is built on Proof-of-Work and this means that chain might undergo re-organisation. We need to ensure that re-orgs do cause a VNC reshuffle. We introduce a concept called DAN Lag which is an offset between when a VNC change is recorded on the Minotari chain and when it takes effect.

For example, if we define DAN Lag as 720 blocks, or a day. Then when a VN registers and that transaction is mined in the Minotari at height 1000 then only at height 1720 will the DAN Layer recognise the VN as being registered.

This DAN Lag allows the Minotari chain to have small re-orgs of less than the DAN Lag without it having any effect on the DAN Layer. An added benefit of this is that the DAN Layer knows all changes in advance of when it will happen, so when a VN is swapped to a new shard space it will give it the DAN Lag period to sync the required state.

DAN Grace Time

Minotari nodes are decentralized. Thus, we don't have a single point of view of the state of the chain.

The DAN operates in ms timeframes, while the Minotari chain runs at the minute timeframe, averaging 2 minutes per block. These blocks also take a few seconds to process and propagate through the network.

This means that for some VNs the Minotari block height might be 999, while for others it might be 1000. The practical problem for this is that we cannot base consensus decisions on the block height if the VNs cannot come to some consensus as to what the Minotari height is.

We define DAN Grace Time or DGT as the number of blocks in the past or future of the VN's own current block height, that it will accept. This is very similar to the FTL concept concerning the timestamp in the block header.

In example of this would be, that we have VN_1 whose base node is on height 999 and VN_2 whose base node is on height 1000. We have set the DGT as 1. An instruction has specified that from height 1000 the VNC that must process it contain both VN_1 and VN_2, but before that it is only VN_1. From this it can be seen that VN_1 thinks only it must be in the VNC but VN_2 thinks they both need to be. But using the DGT of 1 block, VN_1 will accept VN_2 as part of the VNC because it is withing the DGT period and it assumes that its own base node is simply lagging 1 block.

The DGT has the additional benefit of allowing a VN to finish processing an instruction after a block height has been reached.

With the example mentioned above if VN_1 has to stop processing an instruction at height 1000, but it started the instruction at height 999. When height 1000 comes along it can still continue to process the instruction till height 1001.

VN Epoch

We define VN Epoch as the time period a VN must serve in a single [shard] before being moved to a different [shard]. The purpose of this shuffling is to prevent having a single VNC cover the same shard for an extended period of time and thus reduce the risk of VN collusion.

The shuffling algorithm can be linked to the VN registration hash. This provides a random and uniformly distributed seed that allows us to shuffle the VNs around the shard space in a deterministic way. The algorithm can also be constructed such that a minority of nodes are shuffled at every epoch, while still maintaining equal-sized VNCs.

Summary

VNs must re-register periodically. This is to ensure that the VNs are still active and to prevent a VN from registering once and then never being removed from the VN registry. A new shard key is generated each time that a VN re-registers.

(Open question: Is the re-registration fee cheaper than the initial registration fee? Is it zero?)

VNs that miss their re-registration deadline will automatically be de-registered. (Open question: Is the registration deposit lost in this case?) Therefore it is always possible to maintain a list of recently active VNs.

The DAN Lag gives VNs enough time to sync their new shard's state.

VN are shuffled periodically, but a minority of nodes are shuffled every epoch.

Tuning the values

We need to ensure that all the consensus values defined in the RFC needs to be tuned with the following in mind:

DAN Lag

The DAN Lag must be long enough that small frequent re-orgs dont have an effect on the DAN layer. This must also be long enough to give any new VN ample time to download any required state for [shard] it will cover.

DAN Grace Time

This must be only a block or two. This is just to handle the edge case of what happens if a VN's node has not yet seen a new height, or the height changes while processing an instruction

VN Epoch

This must be long enough so that VNs don't flood the network with sync requests and are able to spend most of their time processing instructions. It must clearly be longer than the DAN Lag.

It must be short enough that we don't allow VNs to collude and carry out sharding attacks.

The epoch must also be short enough that we can effectively remove inactive VNs from the VN registry.

Change Log

DateChangeAuthor
19 Oct 2022First outlineSWvHeerden

RFC-0330/Cerberus

The Tari Cerberus-Hotsuff Consensus Algorithm

status: draft

Maintainer(s): Cayle Sharrock,stringhandler

Licence

The 3-Clause BSD Licence.

Copyright 2022 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

This Request for Comment (RFC) describe the consensus mechanism known as Cerberus as it is implemented in Tari. Tari implements the Cerberus variant known as Optimistic Cerberus, for the most part, with Hotstuff BFT replacing pBFT as described in the Cerberus paper.

This RFC serves to document any deviations from the academic paper as well as finer-grained details of the implementation.

Introduction

The Tari DAN is based on a sharded BFT consensus mechanism called Cerberus.

One particular note is that Tari has chosen Hotstuff as the base BFT consensus algorithm over pBFT mentioned in the paper.

The core idea of Cerberus is that instead of dividing work up between validator nodes according to the contracts they are managing (as per Tari DANv1, Polkadot, Avalanche, etc.), Cerberus distributes nodes evenly over a set of state slots, called substates. Any time an instruction modifies the state of a contract, it will affect one or more substates, and only those nodes that are responsible for covering those addresses will reach consensus on the correct state changes.

This means that nodes have to be prepared to execute instructions on any contract in the network. This does create a data synchronisation burden, but the added benefit of a highly scalable, decentralised DAN significantly outweighs this trade-off.

Shards, substates and state addresses

The central idea of Cerberus is that all possible state objects are assigned a unique address, deterministically. Know the provenance of the state, know the address 1. The state space is incredibly large, with 2^256 possible substate addresses; which is way more than the number of atoms in our galaxy. The chance of any two pieces of state ever trying to occupy the same substate is vanishingly small.

The state space is also evenly divided into contiguous sections, called shards. Each shard covers a set of non-overlapping substate addresses and the full set of shards covers the entire state space.

Each validator node registered on the base layer is randomly assigned a shard to cover. The number of shards depends on the total number of validator nodes in the network.

Collectively, all the nodes covering the same shard are known as a validator (node) committee (VNC).

Broadly speaking, the shard-assignment algorithm will try to arrange things in a way that every shard has the same number of validator nodes covering it.

The number of nodes in a VNC is set system-wide. The final number has not been determined yet, but it will be a value, 3n+1, where n is an integer between 8 and 33, giving a committee size of between 25 and 100 nodes.

As nodes continue to join the network, the target committee size stays fixed, whereas the shard size will shrink. This is what will allow the Tari network to scale to achieve thousands of transactions per second.

Every substate slot can only be used once. The substate lifecycle is

  • Empty. No state has ever been stored in this slot.
  • Up. A transaction output has resulted in some object being stored in this substate slot.
  • Down. The state in this slot has changed. We mark the substate as 'down' to indicate that the state is no longer valid. Once a substate is down, it can never be used again 2.
1

This is a simplification to convey the general idea. The address derivation procedure is explained in full below.

2

It's possible that substates could be reset, decades in the future, if substate address collisions become a risk. For now, we treat all down substates as permanently unusable.

Braided consensus

A question that naturally arises whenever sharded distributed networks are discussed is, what happens when cross-shard communication happens. With Cerberus, the procedure is that affected shards come together to form a temporary Hotstuff consensus group, and reach agreement on the correct outcome of the instruction.

A correct outcome is one of:

  • Abort: The instruction was invalid, and any state changes are rolled back such that the instruction never happened.
  • Commit: All input substates for the instruction will be set to Down, and at least one new substate will be marked to Up (from Empty).

Achieving this outcome entails a fairly complicated dance between the participating nodes3:

  • When nodes receive an instruction that affects contract state, the nodes determine the input substates that will be consumed in the instruction. This substates MUST currently all be in an Up state. If any input state is Empty, or Down, the nodes can immediately vote Abort on the instruction.
  • Assuming all input states are valid, nodes will then pledge these substates, effectively marking them as pending Down.
  • Then we have cross-shard exchange. Every leader for the round will forward the instruction and the pledged states to all other nodes in the wider consensus group.
  • Nodes wait until they have received the transaction and pledges from all the other committee leaders. Otherwise they time-out, and ???.
  • Once this is complete, and all pledges have been received, nodes decide within their local committee whether to Commit or Abort. This procedure proceeds via Hotstuff consensus rules and takes several rounds of communication between the local leader and the committee members.
3

For a more formal treatment, refer to Pessimistic-Cerberus in the Cerberus paper.

Cerberus consensus - a diagram

Transaction processing for Pessimistic Cerberus follows the following broad algorithm:

  1. A client broadcasts a transaction to multiple validator nodes, ensuring that at least one node from every shard that covers the inputs for the transaction receives the transaction. In practice, this can be achieved by communicating with a single node, and the node shoulders the responsibility of broadcasting the transaction to the rest of the network, including to every node in the node's local VNC.
  2. When a transaction is received by a validator node, it checks to see if at least one input for the transaction is in the node's shard space. If not, it may ignore the transaction. Otherwise, it forwards the message to the current round leader.
  3. Soon, every round leader for the shards that contain affected inputs will have received the transaction.
  4. The affected shards then begin the local consensus phase. This consists of a full Hotstuff consensus chain with the leader proposing a new block containing the transaction, and the committee members voting on the block.
  5. At the same time, all local inputs (the subset of transaction inputs covered by the local shard) are marked as Pledged. If any input is already marked as Pledged, the transaction immediately resolves as Abort. This step prevents double-spending of inputs across concurrent transactions in separate shards. Note that if a double-spend is attempted by submitting the two transactions to different shards, then both transactions will be aborted, since Cerberus does not have a way to determine which transaction was 'first'.
  6. If every input is in a single shard, then the local consensus is sufficient to finalise the outcome of the transaction (proceeding to execution phase as described below), and the result can be broadcast to the client.
  7. Otherwise, the leader of the round broadcasts the transaction, some metadata, and the local input state to the other shards leaders involved in the transaction. Notice that up until this point, execution of the transaction is impossible in a multi-shard transaction because no node has all the input state it needs to run the transaction instruction. The local consensus phase is solely to determine the validity of the transaction from an input and double-spend perspective.
  8. When every shard leader has received a message from every other shard leader participating the transaction, and none of the messages received was Abort, then the shards can begin the global consensus phase.
  9. Round leaders transmit the received messages to the rest of their VNC.
  10. Each VN checks that the state received from each foreign shard corresponds to the inputs in the transaction. If not, the VN can immediately vote Abort.
  11. At this point, the shard leaders have all the state they need to execute the transaction. Execution is handed off to the TariVM which returns a new set of state objects as output. It is important to note that if a transaction execution returns an error (because someone tried to spend more than they have, for example), then this _does not lead to an Abort decision!`
  12. Each shard executes the transaction independently, and another Hotstuff consensus chain is produced to achieve consensus on the resulting output set. If the transaction is Abort, then all pledged inputs are rolled back. Otherwise, the decision is Commit, and the pledged inputs are marked as Down. Any output objects that belong in the current shard can be marked as Up, and the transaction result is broadcast to the client as well as other shards that need to mark new substates as Up as a result of the transaction output.

A mermaid flow diagram of the above process is shown below:


flowchart TD
    A[Client] --> B([Broadcast transaction])
    B --> C{Any tx inputs in my shard?}
    C --> |Yes| D[Forward to leader]
    C --> |No| E[Ignore transaction]
    D -.-> L
    
    L[Leader] ==> BL([ Broadcast message to VNC ])
    BL -.-> |PREPARE| LN[VNC nodes]
    subgraph Local_Consensus
        LN --> G{Any inputs already pledged?}    
        G --> |Yes. 
        Vote PREPARED_ABORT | LC
        
        G --> |No| I[Pledge inputs]
        I --> |Vote PREPARED_COMMIT| LC[[Consensus on pledges]]
        LC --> LCD{Consensus on PREPARED?}
        LCD --> |No| LocalStall[Local liveness break!]
    end
        
    
    LCD --> |Yes| Pledge[Pledge inputs]
    Pledge --> SS{ Single shard tx? }
    SS --> |Yes| EX1[[Execute transaction]]
    EX1 --> SSC[[Consensus on result]]
    SSC --> GCD
    
    ssCommit --> |No| Abort[Abort!] 
    ssCommit --> |Yes| createSubstates[[Substate creation]]
    
    SS -.-> |No| BLC([ Send LOCAL_PREPARED_*
     & local state to other Leaders ])
    BLC -.-> Leaders
    Leaders -.-> |Forward to VNC| Fwd[Validator node]
    
    subgraph Global_consensus
        Fwd --> wait{Are ANY messages 
        LOCAL_PREPARED_ABORT or
        timed out? }
        wait --> |Yes
        Vote SOMEPREPARED_ABORT| globalConsensus
        wait --> |No| EX[[Execute transaction]]
        
        EX --> |Vote ACCEPT_*| globalConsensus[[Intershard Consensus on result]]
    end
    globalConsensus --> GCD{Consensus on ACCEPT_*?}
    GCD --> |No.| Stall[Abandon block!]
    GCD--> |Yes| ssCommit{Decision == ACCEPT_COMMIT?}

The process above describes PCerberus in general, with some modifications from Chainspace. There are a few details that need some additional explanation. In particular, this includes substate address derivation and state synchronisation.

Substate address derivation

A substate contains two pieces of information:

  • The value of the substate object,
  • The version number of the substate object.

The substate address is the universal location of the substate in the 256-bit state space. Most substate addresses are derived from a hash of their id, the provenance of which depends on the substate value type, and their version.

The substate value depends on the type of data the value represents. It is exactly one of the following:

  • Component - A component is an instantiation of a contract template.
  • Resource - A resource represents a token. Tokens can be fungible, non-fungible, or confidential. The Resource substate does not store the tokens themselves, but serves as the global identifier for the resource. The tokens themselves are kept in Vaults, or NonFungible substates.
  • Vault - Resources are stored in Vaults. Vaults provide generalised functionality for depositing and withdrawing their resources from the vaults into Buckets.
  • NonFungible - A substate representing a singular non-fungible item. Non-fungible items are always associated with their associated non-fungible Resource.
  • NonFungibleIndex - A substate that holds a reference to another substate.
  • UnclaimedConfidentialOutput - A substate representing funds that were burnt on the Minotari layer and are yet to be claimed in the Tari network.
  • TransactionReceipt - A substate recording the result of a transaction.
  • FeeClaim - To prevent a proliferation of dust-like value transfers for every transaction due to fees, a fee claim is generated instead that allows VNs to aggregate fees and claim them in a single batched transaction at a later time. Fee claims remain in the up state forever to prevent double claims.

Substate ids are domain-separated hashes of their identifying data, which depends on substate type as follows:

  • Component - Component addresses are derived from the hash of the component's contract template id and a component id. The component id is typically a hash of the origin transaction's hash and a counter.
  • Resource - Generally a unique, random 256-bit integer, derived from the hash of the transaction hash and a counter. Some ids for special resources are hard-coded.
  • Vault - Vault ids are a unique, random 256-bit integer, derived from the hash of the transaction hash and a counter.
  • NonFungible - The id of a non-fungible item is derived from the item's resource address, and its id. The id depends on the specifics of the NFT, and could be an integer, a string, a hash, or a uuid.
  • NonFungibleIndex - Non-fungible index ids are derived from the resource they are pointing to and an index offset.
  • UnclaimedConfidentialOutput - The UCO id is derived from the burn commitment on the Minotari layer.
  • TransactionReceipt - The id of a transaction receipt is the hash of the associated transaction.
  • FeeClaim - The id of a fee claim is derived from the epoch number and the validator's public key.

State synchronisation

Change Log

DateChangeAuthor
17 Dec 2023Second draftCjS77
30 Oct 2023First draftCjS77

RFC-0304/Consensus

The Tari Network Consensus Layer

status: raw

Maintainer(s): stringhandler, Stanley Bondi

Licence

The 3-Clause BSD Licence.

Copyright 2022 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

This Request for Comment (RFC) describes operation of Tari network Indexers. Indexers are a key actor in providing rapid, up-to-date and accurate information about the state of Tari contracts to client applications.

Introduction

Since the Tari network is designed to scale to hundreds of thousands of contracts, and millions of transactions per hours, having a global state tracking system, like Etherscan, is neither feasible nor advisable. Instead, client applications (wallets, ticket apps, exchange front-ends etc) will run an Indexer that follows the state of a finite set of contracts of interest all across the shard-space.

A note on nomenclature: This RFC is primarily focused on the role of indexer_lib in the DAN source code.

The indexer application has additional functionality (such as a copy of the Tari engine for executing dry runs of transactions) that is outside the domain of what the indexer is responsible for: maintaining a database of contract state, and delivering that state to client applications.

You can think of Validator nodes as staying in position and managing the state of a fixed set of addresses, possibly having to operate instructions for thousands of different contracts, while Indexers are constantly hopping around the shard-space, following the progress of a fixed set of contracts wherever their state goes.

Figure 1 illustrates this dynamic:

Figure 1: Validator and Indexer

The indexer library work via three inter-oparting modules: substate scanning, substate decoding, and the substate cache.

Substate scanning

Given a substate address, the indexer will obtain the state for that substate, if it exists, using the following algorithm:

  1. If the substate is in the cache, retrieve the cached value. Query the network for the next version, and if it does not exist, return the cached version.
  2. If the cache misses, or the network indicates that the cache is stale because the next version does exist then:
    1. If the next substate version's state is UP, update the cache and return the substate.
    2. Otherwise, if the next substate version's state is DOWN, query the network until the current version is found.
    3. If the cache misses, and the network indicates that the substate does not exist, then the substate address is not valid.
  3. After retrieving the latest version from the network, update the cache before returning the substate.

Substate decoding

The substate decoding module decodes the state at a given substate address, and collects any referenced substate addresses contained within. This is done recursively until all substates have been decoded.

This is the primary mechanism for discovering the state of a contract, and allowing it to solely track updates to the state of contracts the indexer is interested in. This is a key aspect of Tari's scalability. Without this, we would fall back into the trap of having a global state machine, which axiomatically, does not scale.

Substate cache

The substate cache reduces the amount of network traffic by storing the state of substates of interest. When a request for a substate is made, the cache is checked first. If the substate is not in the cache, the network is queried for the substate, potentially making many queries to find the latest version of the substate.

If the substate is in the cache, the cache may be checked to see if the substate is stale. Sometimes (e.g. for transaction dry runs), we may simply be optimistic and accept the cached value as the latest version.

For consensus, the approach is more conservative and the network is consulted to verify the freshness of the cached value.

The current implementation of the substate cache uses a local file-based cache and is highly performant and designed with concurrency in mind.

Change Log

DateChangeAuthor
20 Dec 2023First draftCjS77
23 Oct 2023Placeholder textCjS77

Proposals

RFC documents in this section are not part of the Tari specification, and for this reason are not placed in the preceding sections. However, they describe useful applications of the Tari protocol that may be built on top of the core technology.

RFCs in this section will remain in draft status until they are implemented in some fashion in a wallet, standalone application, or in the RPC protocols, at which point they can be marked as stable.

If the underlying protocol changes to the point that an application is no longer implementable in its current state, it must be updated to outdated or else deprecated and moved to the Deprecated RFCs chapter.

RFC-0123/ReplayAttacks

Mitigating One-sided payment replay attacks

status: draft

Maintainer(s): Cayle Sharrock and S W van heerden

Licence

The 3-Clause BSD Licence.

Copyright 2021 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

The aim of this Request for Comment (RFC) is to describe ways we can block replay attacks related to one-sided payments using TariScript.

Replay attack

Replay attacks are "replaying" old messages to deceive the receiver about the message's authenticity. With TariScript, a vulnerability exists where a replay attack can occur under certain conditions, even with the current consensus rules.

For this attack to work, we need Alice and Charlie to collude to steal some of Bob's funds:

  • Alice sends a one-sided transaction to Bob.
  • Bob spends this UTXO to Charlie.
    • Bob has to spend this and only this UTXO alone to Charlie with zero change.
  • Alice sends a new one-sided transaction to Bob, creating the exact same output as before
  • Alice shares the Blinding factor of the UTXO with Charlie
  • Charlie can now claim this UTXO by replaying his old transaction
    • Charlie has the signatures to spend the scripts, sign for the changes, etc.
    • Because the previous transaction contains no other inputs, Charlie only has to provide signatures for this one UTXO.
    • Because there is no change UTXO, Charlie has the keys for all the outputs in the transactions and can thus add another transaction or input /output to make sure the kernel excess signature is unique.

This does not work if Bob includes another UTXO in the transaction to Charlie due to the script offset. Although Charlie has the blinding factor, for the one UTXO, he does not have the script offset. Charlie can create a new kernel signature unique for blockchain consensus with the blinding factor. Still, because the script offset needs to balance as well, and he does not know the private keys for this, he needs to use this as is, meaning he needs to use an exact copy of the transaction. If the transaction includes any UTXO that he does not know the blinding factor of, he cannot create a new kernel excess signature. Meaning it won't pass consensus rules.

Solutions

This is a very niche attack that will only be useful under certain circumstances, but never less still needs to be addressed.

Sign with chain information

If we require as part of the script signature challenge that we sign the mined block height of that UTXO, it will ensure that Charlie cannot replay the signatures that Bob provided on the Input to spend the output, as each duplicate commitment will have its own block height. This is ensured as we currently have a limit that a commitment must be unique in the unspent set.

Advantages

  • Does not require any more on-chain information

Disadvantages

  • Reorged transactions cannot be put back in if the inputs are now spent at different heights

Enforce global commitment uniqueness

Alice cannot send the same one-sided UTXO to Bob if we require the commitment to be globally unique. This does mean that pruned nodes needs to track the spent TXO set's commitment and the UTXO set.

advantages

  • Safely reorg transactions

disadvantages

  • Pruned node needs to save extra data about the spent set.
  • Syncing pruned nodes need to provide extra info to ensure that the downloaded list of commitments is correct
    • Without requiring extra information in the header, pruned nodes need to download the entire TXO set and compare this to the output_mmr root.

Change Log

DateChangeAuthor
2022-10-19Minor editorial changesCjS77
2022-10-10First outlineSWvheerden

RFC-0141/SparseMerkleTrees

Sparse Merkle Trees: A mutable data structure for TXO commitments

status: draft

Maintainer(s): CjS77

Licence

The 3-Clause BSD Licence.

Copyright 2023 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

This Request for Comment (RFC) proposes replacing the current Mutable Merkle Mountain Range (MMMR) data structure used for tracking the commitment to the UTXO set, with a Sparse Merkle tree (SMT).

Description

Sparse Merkle trees

A sparse Merkle tree (SMT) is a Merkle-type structure, except the contained data is indexed, and each datapoint is placed at the leaf that corresponds to that datapointโ€™s index. Empty nodes are represented by a predefined "null" value.

Since every empty node has the same hash, and the tree is sparse, it is possible to prune the tree in a way such that only the non-zero leaf nodes and placeholders marking the empty sub-trees are stored. Therefore, SMTs are relatively compact.

A major feature of SMTs is that they are truly mutable. The current UTXO Merkle root in Tari is calculated using a Merkle Mountain Range (MMR). This has some drawbacks:

  1. MMRs are immutable data structures, and therefore as a workaround (some would say, hack), a bitmap is appended to the MMR to mark the spent outputs. In Tari's implementation, a roaring bitmap is used, which takes advantage of compression, but even so, it is still fairly large and will grow indefinitely.
  2. The Merkle tree must keep a record of all TXOs forever, and mark them as they are spent. The blockchain cannot prune STXOs from the set.
  3. The root is path-dependent. Let's say that the UTXO merkle root currently has a value R1. When you add a UTXO to the set, to giving a new Merkle root R2, say, and then immediately remove the UTXO, the Merkle root will now be some R3 and not R1 as you might expect. This path dependence also extends to the order of adding UTXOs. Adding UTXO A then B yields a different root to B then A.

SMTs are true mutable data structures and do not have these drawbacks.

  1. No tracking bitmap is needed. When a UTXO is spent, it can be deleted from the tree.
  2. It is possible to prune STXOs from the UTXO set to calculate the Merkle root.
  3. Adding and removing UTXOs in any order will always yield the same Merkle root. Adding and then deleting a UTXO from the set will result in the same Merkle root as before the UTXO was added.

Inclusion and exclusion proofs

Inclusion proofs for the current MMMR structure are possible but clunky, since the entire bitmap state must be included with the Merkle tree proof.

Exclusion proofs are not possible in the current MMMR implementation, unless an output happens to be a spent output. In this "STXO proof", the form of the proof is identical to the inclusion proof, with the verifier checking that the bit corresponding to the TXO is set, rather than unset.

SMTs support inclusion and exclusion proofs, and they are both succinct, O(log n), representations of the tree.

Space savings

In terms of space, the SMT is more efficient than MMMRs and the advantage grows with time.

For an SMT, to calculate the root of the UTXO set, all you need is the UTXO set itself, assuming the commitment is used as the tree index.

Consider some representative numbers:

Let's assume there are 1,000,000 UTXOs, with another 2,000,000 UTXOs having being spent over the lifetime of the project. A busy blockchain might achieve this level of traffic in a few days.

If each commitment-UTXO hash pair is 64 bytes, you need serialize 64MB to recreate the Merkle root for the SMT.

For the MMMR, even though you only need the UTXO hash, you need all 3,000,000 values (96MB) plus approximately 1MB for every million hashes in a bitmap to indicate which hashes have been deleted (3 MB) for a total of 99MB.

This only gets worse with time. Over a period of a year, a busy blockchain might have 100,000,000 spent transaction outputs. However, the UTXO set will grow far more slowly, and perhaps only 10x in size to 10 million outputs.

The SMT requires serialising 640MB of data to recreate the root, whereas the MMMR now requires 3.6GB of data.

Implementation

The proposed implementation assumes that a key-value store for the data exists. The Merkle tree is only concerned with the index and the value hash, as opposed to the value itself.

When constructing a new tree, a hashing algorithm is specified. As indicated, the "values" provided to the tree must already be a hash, and should have been generated from a different hashing algorithm to the one driving the tree, in order to prevent second pre-image attacks.

To insert a new leaf, the key is used to derive a path through the tree. Starting with the most significant bit, you move down the left branch if the bit is zero, or take the right branch if the bit is equal to one. Once a terminal node is reached, the node is replaced with a new sub-tree with the existing terminal node and the new leaf node forming the children of the last branch node in the sub-tree. The depth of the sub-tree is determined by the number of matching bits of the respective keys of the two nodes.

To delete a node, the procedure above is reversed. This entails that a significant portion of the tree may be pruned when deleting a node in a highly sparse region of the tree.

The null hashes representing the empty sub-trees are treated identically to the leaf nodes. Thus branch hashes are calculated in the usual way, inter alia, H_branch = H(Branch marker, H_left, H_right), irrespective of whether the left or right nodes are empty or not.

Domain separation SHOULD be used to distinguish branch nodes from leaf nodes. This also mitigates second pre-image attacks if the advice above is not followed and the values are hashed with the same algorithm as the tree.

For leaf node hashes, the key MUST be included in the hash. This prevents leaf node spoofing of the pruned tree. Therefore, leaf node hashes are of the form H_leaf = H(Leaf marker, H_key, H_value).

A proof of concept implementation has been written and submitted for review in PR #5457. The examples that follow assume this implementation.

Example

Let's create a SMT with four nodes.

If we insert the nodes at

  • A: 01001111 (79 in decimal)
  • B: 01011111 (95 in decimal)
  • C: 11100000 (224 in decimal)
  • D: 11110000 (240 in decimal)

you will notice that the first two diverge at the fourth bit, while the first and last pairs differ at the first bit. This results in a SMT that looks like this:

            โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”
      โ”Œโ”€โ”€โ”€โ”€โ”€โ”ค root โ”œโ”€โ”€โ”€โ”€โ”€โ”
      โ”‚     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ”‚
     โ”Œโ”ดโ”0               โ”Œโ”ดโ”1
  โ”Œโ”€โ”€โ”ค โ”œโ”€โ”€โ”          โ”Œโ”€โ”€โ”ค โ”œโ”€โ”€โ”€โ”
  โ”‚  โ””โ”€โ”˜  โ”‚          โ”‚  โ””โ”€โ”˜   โ”‚
 โ”Œโ”ดโ”00   โ”Œโ”ดโ”01      โ”Œโ”ดโ”10    โ”Œโ”ดโ”11
 โ”‚0โ”‚  โ”Œโ”€โ”€โ”ค โ”œโ”€โ”€โ”     โ”‚0โ”‚    โ”Œโ”€โ”ค โ”œโ”€โ”
 โ””โ”€โ”˜  โ”‚  โ””โ”€โ”˜  โ”‚     โ””โ”€โ”˜    โ”‚ โ””โ”€โ”˜ โ”‚
     โ”Œโ”ดโ”010  โ”Œโ”ดโ”011     110โ”Œโ”ดโ”   โ”Œโ”ดโ”111
   โ”Œโ”€โ”ค โ”œโ”€โ”   โ”‚0โ”‚          โ”‚0โ”‚ โ”Œโ”€โ”ค โ”œโ”€โ”
   โ”‚ โ””โ”€โ”˜ โ”‚   โ””โ”€โ”˜          โ””โ”€โ”˜ โ”‚ โ””โ”€โ”˜ โ”‚
  โ”Œโ”ดโ”   โ”Œโ”ดโ”                  โ”Œโ”ดโ”   โ”Œโ”ดโ”
  โ”‚Aโ”‚   โ”‚Bโ”‚                  โ”‚Dโ”‚   โ”‚Cโ”‚
  โ””โ”€โ”˜   โ””โ”€โ”˜                  โ””โ”€โ”˜   โ””โ”€โ”˜

Figure 1: An example sparse Merkle tree.

The merkle root is calculated by hashing nodes in the familiar way.

Of note is that when we delete all the nodes, the SMT hash is 000..., as expected. The MMMR will never have a hash that's the same after adding, and then deleting the same node because of the bitmap tracking deleted entries.

So even though the SMT is slower, it is still fast enough. The bandwidth savings are substantial and the privacy benefits are significant.

Benchmarks

Details

The SMT implementation is faster than MutableMMR up to around 10,000 nodes, and then the average depth starts to affect it. By 1,000,000 nodes, it is significantly slower than MMR.

This makes sense since MMR is basically O(1) for inserts, while SMT is O(log(n)).

A rudimentary benchmark test yielded the following results

Starting: SMT: Inserting 1000000 keys
Finished: SMT: Inserting 1000000 keys - 1.921310493s
Starting: SMT: Calculating root hash
Tree size: 1000000. Root hash: 3e42ca40df366db52464c19b6ba71428976a56d7b120bc3c882fc29bf05dc1d7
Finished: SMT: Calculating root hash - 644.226062ms
Starting: SMT: Deleting 500000 keys
Finished: SMT: Deleting 500000 keys - 863.873761ms
Starting: SMT: Calculating root hash
Tree size: 500000. Root hash: 2a7b51f114a17c229f1067feb4ba5b6aad975689160a5eab0d90f89a3bcf09f8
Finished: SMT: Calculating root hash - 207.30907ms
Starting: SMT: Deleting another 500000 keys
Finished: SMT: Deleting another 500000 keys - 850.606501ms
Starting: SMT: Calculating root hash
Tree size: 0. Root hash: 0000000000000000000000000000000000000000000000000000000000000000
Finished: SMT: Calculating root hash - 3.892ยตs
Starting: MMR: Inserting 1000000 keys
Finished: MMR: Inserting 1000000 keys - 741.641704ms
Starting: SMT: Calculating root hash
Tree size: 1000000. Root hash: da6135ccaabf146024cae1b0e7ad6ba7e9dad79724fb9199b721d4cd243ba999
Finished: SMT: Calculating root hash - 8.649ยตs
Starting: MMR: Deleting 500000 keys
Finished: MMR: Deleting 500000 keys - 6.525858ms
Starting: SMT: Calculating root hash
Tree size: 500000. Root hash: fd60e168f27acba374109de9b8231e7252f0cfdf385f87dbfd92873d4956c995
Finished: SMT: Calculating root hash - 50.276ยตs
Starting: MMR: Deleting another 500000 keys
Finished: MMR: Deleting another 500000 keys - 6.862469ms
Starting: SMT: Calculating root hash
Tree size: 0. Root hash: 5d70f3177a0b46ea1b853c58d5e3f7d6e78cbc4149d71592bb6cea63d50ed96c
Finished: SMT: Calculating root hash - 93.618ยตs

The SMT is taking 1.92s to insert 1 mil nodes, or 1.9us per node on average on a 2018 Intel i9 Macbook Pro.

This is still sufficiently fast for our purposes and the benefits of having a truly mutable data structure, succinct inclusion and exclusion proofs, and significant serialisation savings far outweigh the performance costs.

Specification

Define the following constants:

NameTypeValue
EMPTY_NODE_HASHbytes[0; 32]
LEAF_PREFIXbytesb"V"
BRANCH_PREFIXbytesb"B"
KEY_LENGTH_BYTESinteger32

Node types

Each Node in the tree is either, Empty, a Leaf, or a Branch. Every node in the tree is associated with a hash function, H that has a digest output length of KEY_LENGTH_BYTES.

Leaf nodes store the key and value in their data property. Leaf nodes are immutable, and MAY cache the hash value for efficiency.

Branch nodes contain two Node instances, referring to the left and right child nodes respectively. Branch nodes MAY store additional data, such as the height of the node in the tree, the key prefix and the node's hash value, for performance and efficiency purposes.

Default empty nodes always have a constant hash, EMPTY_NODE_HASH.

In summary, the nodes are defined as:

pub struct LeafNode<H> {
    key: NodeKey,
    hash: NodeHash,
    value: ValueHash,
    hash_type: PhantomData<H>,
}

pub struct EmptyNode {}

impl EmptyNode {
    pub fn hash(&self) -> &'static NodeHash {
        &EMPTY_NODE_HASH
    }
}

pub struct BranchNode<H> {
    // The height of the branch. It is also the number of bits that all keys below this branch share.
    height: usize,
    // Only the first `height` bits of the key are relevant for this branch.
    key: NodeKey,
    hash: NodeHash,
    // Flag to indicate that the tree hash changed somewhere below this branch. and that the hash should be
    // recalculated.
    is_hash_stale: bool,
    left: Box<Node<H>>,
    right: Box<Node<H>>,
    hash_type: PhantomData<H>,
}

Leaf node values

This specification outsources hashing the value data to an external service. 'Value' data in terms of the specification refers to the hash of the value data. The hashing algorithm used to hash the data SHOULD be different from H, to prevent second preimage attacks.

The digest length of the value hashes does not need to be KEY_LENGTH_BYTES, but it MUST be a constant predefined length.

Node hashes

Empty nodes

As described above, empty nodes always return EMPTY_NODE_HASH as the hash value.

Leaf nodes

The definition of a leaf node's hash is

    H::digest(LEAF_PREFIX || KEY(32-bytes) || VALUE_HASH)

The key MUST be included in the hash. Imagine every leaf node has the same value. If the key was not hashed, there would be many different tree structures that would yield the same tree root. Specifically, any tree could replace a leaf node with a different leaf node with the same key prefix corresponding to the height of the original leaf node without changing the root hash.

Branch nodes

The definition of a branch node's hash is

    H::digest(BRANCH_PREFIX || height || key_prefix || left_child_hash || right_child_hash)

where

  • height is the height of the branch in the tree, where the root node is height 0.
  • key_prefix is the common prefix that the key of every descendent node of this branch will begin with. key_prefix is KEY_LENGTH_BYTES long, and every bit after the prefix MUST be set to zero. This means that key prefixes are not unique. For example, every key prefix for the left-most path down the tree will always have a prefix of [0; 32]. The height parameter helps disambiguate this.
  • left_child_hash and right_child_hash are the hashes of the left and right child noes respectively, and have length KEY_LENGTH_BYTES.

Tree structure

The Merkle tree is built on top of an underlying dataset consisting of a set of (key, value) tuples. The key fixes the position of each dataset element in the tree: starting from the root, each digit in the binary expansion indicates whether we should follow the left child (next digit is 0) or the right child (next digit is 1), see Figure 1. The length of the key (in bytes) is a fixed constant of the tree, KEY_LENGTH_BYTES, larger than 0.

Rather than explicitly creating a full tree, we simulate it by inserting only non-zero leaves into the tree whenever a new key-value pair is added to the dataset, using the two optimizations:

  1. Each subtree with exactly one non-empty leaf is replaced by the leaf itself.
  2. Each subtree containing only empty nodes is replaced by a constant node with hash value equal to EMPTY_HASH.

Root Hash Calculation

The Merkle root of a dataset is computed as follows:

  1. The Merkle root of an empty dataset is set to the constant value EMPTY_HASH.
  2. The Merkle root of a dataset with a single element is set to the leaf hash of that element.
  3. Otherwise, the Merkle root is the hash of the branch node occupying the root position. The child hashes are calculated recursively using the definitions above.

Adding or updating a key-value pair

Adding a new node or updating an existing one follows the same logic. Therefore, a single function, upsert is defined. The borrow semantics of Rust requires a slightly different approach to managing the tree than one might take in other languages (e.g. LIP39).

    pub fn upsert(&mut self, key: NodeKey, value: ValueHash) -> Result<UpdateResult, SMTError> {
        let new_leaf = LeafNode::new(key, value);
        if self.is_empty() {
            self.root = Node::Leaf(new_leaf);
            return Ok(UpdateResult::Inserted);
        } else if self.root.is_leaf() {
            return self.upsert_root(new_leaf);
        }
        // Traverse the tree until we find either an empty node or a leaf node.
        let mut terminal_branch = self.find_terminal_branch(new_leaf.key())?;
        let result = terminal_branch.insert_or_update_leaf(new_leaf)?;
        Ok(result)
    }
    
    /// Look at the height-th most significant bit and returns Left of it is a zero and Right if it is a one 
    fn traverse_direction(height: usize, child: &NodeKey) -> TraverseDirection {...}
    
    // Finds the branch node above the terminal node. The case of an empty or leaf root node must be handled elsewhere 
    fn find_terminal_branch(&mut self, child_key: &NodeKey) -> Result<TerminalBranch<'_, H>, SMTError> {
        let mut parent_node = &mut self.root;
        let mut empty_siblings = Vec::new();
        if !parent_node.is_branch() {
            return Err(SMTError::UnexpectedNodeType);
        }
        let mut done = false;
        let mut traverse_dir = TraverseDirection::Left;
        while !done {
            let branch = parent_node.as_branch_mut().unwrap();
            traverse_dir = traverse_direction(branch.height(), child_key)?;
            let next = match traverse_dir {
                TraverseDirection::Left => {
                    empty_siblings.push(branch.right().is_empty());
                    branch.left()
                },
                TraverseDirection::Right => {
                    empty_siblings.push(branch.left().is_empty());
                    branch.right()
                },
            };
            if next.is_branch() {
                parent_node = match traverse_dir {
                    TraverseDirection::Left => parent_node.as_branch_mut().unwrap().left_mut(),
                    TraverseDirection::Right => parent_node.as_branch_mut().unwrap().right_mut(),
                };
            } else {
                done = true;
            }
        }
        let terminal = TerminalBranch {
            parent: parent_node,
            direction: traverse_dir,
            empty_siblings,
        };
        Ok(terminal)
    }
struct TerminalBranch<'a, H> {
    parent: &'a mut Node<H>,
    direction: TraverseDirection,
    empty_siblings: Vec<bool>,
}

impl<'a, H: Digest<OutputSize = U32>> TerminalBranch<'a, H> {
    /// Returns the terminal node of the branch
    pub fn terminal(&self) -> &Node<H> {
        let branch = self.parent.as_branch().unwrap();
        branch.child(self.direction)
    }

    // When inserting a new leaf node, there might be a slew of branch nodes to create depending on where the keys
    // of the existing leaf and new leaf node diverge. E.g. if a leaf node of key `1101` is being inserted into a
    // tree with a single leaf node of key `1100` then we must create branches at `1...`, `11..`, and `110.` with
    // the leaf nodes `1100` and `1101` being the left and right branches at height 4 respectively.
    //
    // This function handles this case, as well the simple update case, and the simple insert case, where the target
    // node is empty.
    fn insert_or_update_leaf(&mut self, leaf: LeafNode<H>) -> Result<UpdateResult, SMTError> {
        let branch = self.parent.as_branch_mut().ok_or(SMTError::UnexpectedNodeType)?;
        let height = branch.height();
        let terminal = branch.child_mut(self.direction);
        match terminal {
            Empty(_) => {
                let _ = [Set terminal to the new leaf (Insert)]
                Ok(UpdateResult::Inserted)
            },
            Leaf(old_leaf) if old_leaf.key() == leaf.key() => {
                let old_value = [Replace of leaf with new leaf (Update)]
                Ok(UpdateResult::Updated(old_value))
            },
            Leaf(_) => {
                let branch = // Create a new sub-tree with the old and new leaf being children of a branch at the height
                             // of the common key-prefixes
                Ok(UpdateResult::Inserted)
            },
            _ => unreachable!(),
        }
    }

Removing a Leaf Node

A certain key-value pair can be removed from the tree by deleting the corresponding leaf node and rearranging the affected nodes in the tree. The following protocol can be used to remove a key k from the tree.

    /// Attempts to delete the value at the location `key`. If the tree contains the key, the deleted value hash is
    /// returned. Otherwise, `KeyNotFound` is returned.
    pub fn delete(&mut self, key: &NodeKey) -> Result<DeleteResult, SMTError> {
        if self.is_empty() {
            return Ok(DeleteResult::KeyNotFound);
        }
        if self.root.is_leaf() {
            return self.delete_root(key);
        }
        let mut path = self.find_terminal_branch(key)?;
        let result = match path.classify_deletion(key)? {
            PathClassifier::KeyDoesNotExist => DeleteResult::KeyNotFound,
            PathClassifier::TerminalBranch => {
                let deleted = // prune tree placing sibling at correct place upstream
                DeleteResult::Deleted(deleted)
            },
            PathClassifier::NonTerminalBranch => {
                let deleted_hash = path.delete()?;
                DeleteResult::Deleted(deleted_hash)
            },
        };
        Ok(result)
    }

Proof Construction

Proofs are constructed in a straightforward manner. Unlike other SMT implementations (e.g. LIP39), if a sibling node is empty, then EMPTY_NODE_HASH is included as the sibling hash, rather than building a bitmap of non-empty siblings.

Both inclusion and exclusion proofs use a common algorithm, build_proof_candidate for traversing the tree to the desired proof key,
collecting hashes of every sibling node. The terminal node for where the proof key should reside is also noted:

pub struct ExclusionProof<H> {
    siblings: Vec<NodeHash>,
    // The terminal node of the tree proof, or `None` if the the node is `Empty`.
    leaf: Option<LeafNode<H>>,
    phantom: std::marker::PhantomData<H>,
}

impl<H: Digest<OutputSize = U32>> SparseMerkleTree<H> {
    /// Construct the data structures needed to generate the Merkle proofs. Although this function returns a struct
    /// of type `ExclusionProof` it is not really a valid (exclusion) proof. The constructors do additional
    /// validation before passing the structure on. For this reason, this method is `private` outside of the module.
    pub(crate) fn build_proof_candidate(&self, key: &NodeKey) -> Result<ExclusionProof<H>, SMTError> {
        let mut siblings = Vec::new();
        let mut current_node = &self.root;
        while current_node.is_branch() {
            let branch = current_node.as_branch().unwrap();
            let dir = traverse_direction(branch.height(), key)?;
            current_node = match dir {
                TraverseDirection::Left => {
                    siblings.push(branch.right().hash().clone());
                    branch.left()
                },
                TraverseDirection::Right => {
                    siblings.push(branch.left().hash().clone());
                    branch.right()
                },
            };
        }
        let leaf = current_node.as_leaf().cloned();
        let candidate = ExclusionProof::new(siblings, leaf);
        Ok(candidate)
    }
}

Inclusion proof

An inclusion proof is valid if the terminal node found in build_proof_candidate matches the key and value provided in the proof request. Equivalently, the leaf node's hash must match the hash of a new leaf node generated with the key and value given in the proof request.

The final proof consists of the vector of sibling hashes.

pub struct InclusionProof<H> {
    siblings: Vec<NodeHash>,
    phantom: std::marker::PhantomData<H>,
}

impl<H: Digest<OutputSize = U32>> InclusionProof<H> {
    /// Generates an inclusion proof for the given key and value hash from the given tree. If the key does not exist in
    /// tree, or the key does exist, but the value hash does not match, then `from_tree` will return a
    /// `NonViableProof` error.
    pub fn from_tree(tree: &SparseMerkleTree<H>, key: &NodeKey, value_hash: &ValueHash) -> Result<Self, SMTError> {
        let proof = tree.build_proof_candidate(key)?;
        match proof.leaf {
            Some(leaf) => {
                let node_hash = LeafNode::<H>::hash_value(key, value_hash);
                if leaf.hash() != &node_hash {
                    return Err(SMTError::NonViableProof);
                }
            },
            None => return Err(SMTError::NonViableProof),
        }
        Ok(Self::new(proof.siblings))
    }
}

Exclusion proof

An exclusion proof request only requires a key value. A proof is valid if the leaf node returned by build_proof_candidate does not have the same key as the proof request.

The proof consists of the sibling hashes and a copy of the terminal leaf node.

impl<H: Digest<OutputSize = U32>> ExclusionProof<H> {
    /// Generates an exclusion proof for the given key from the given tree. If the key exists in the tree then
    /// `from_tree` will return a `NonViableProof` error.
    pub fn from_tree(tree: &SparseMerkleTree<H>, key: &NodeKey) -> Result<Self, SMTError> {
        let proof = tree.build_proof_candidate(key)?;
        // If the keys match, then we cannot provide an exclusion proof, since the key *is* in the tree
        if let Some(leaf) = &proof.leaf {
            if leaf.key() == key {
                return Err(SMTError::NonViableProof);
            }
        }
        Ok(proof)
    }

Proof Verification

To check an exclusion proof, the Verifier calls the ExclusionProof::validate(&self, keys, root) function. This function is not a method of the tree, and can be run just by holding the Merkle root.

The function reconstructs the tree using the expected key and places the leaf node provided in the proof at the terminal position. It then calculates the root hash.

Validation succeeds if the calculated root hash matches the given root hash, and the leaf node is empty, or the existing leaf node has a different key to the expected key.

    pub fn validate(&self, expected_key: &NodeKey, expected_root: &NodeHash) -> bool {
        let leaf_hash = match &self.leaf {
            Some(leaf) => leaf.hash().clone(),
            None => (EmptyNode {}).hash().clone(),
        };
        let root = self.calculate_root_hash(expected_key, leaf_hash);
        // For exclusion proof, roots must match AND existing leaf must be empty, or keys must not match
        root == *expected_root &&
            match &self.leaf {
                Some(leaf) => leaf.key() != expected_key,
                None => true,
            }
    }

Verifying inclusion proofs is similar, except that the terminal leaf node will be constructed from the key and value hash provided by the verifier.

    pub fn validate(&self, expected_key: &NodeKey, expected_value: &ValueHash, expected_root: &NodeHash) -> bool {
        // calculate expected leaf node hash
        let leaf_hash = LeafNode::<H>::hash_value(expected_key, expected_value);
        let calculated_root = self.calculate_root_hash(expected_key, leaf_hash);
        calculated_root == *expected_root
    }

Backwards Compatibility

If the UTXO Merkle root is replaced by a sparse Merkle tree, this change would require a hard fork, since it fundamentally alters how the UTXO Merkle root is calculated.

References

  1. Dahlberg et. al., "Efficient Sparse Merkle Trees", SMT
  2. A. Ricottone, "LIP-0039: Introduce sparse Merkle trees", LIP39

Change Log

DateChangeAuthor
10 Jul 2023First draftCjS77

RFC-0153/StagedWalletSecurity

Staged Wallet Security

status: stable

Maintainer(s): Cayle Sharrock

Licence

The 3-Clause BSD Licence.

Copyright 2021 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

This Request for Comment (RFC) aims to describe Tari's ergonomic approach to securing funds in a hot wallet. The focus is on mobile wallets, but the strategy described here is equally applicable to console or desktop wallets.

Description

Rationale

A major UX hurdle when users first interact with a crypto wallet is the friction they experience with the first user experience.

A common theme: I want to play with some new wallet X that I saw advertised somewhere, so I download it and run it. But first I get several screens that

  • ask me to review my seed phrase,
  • ask me to write down my seed phrase,
  • prevent typical "skip this" tricks like taking a screenshot,
  • ask to confirm if I've written down my seed phrase,
  • force me to write a test, either by supplying a random sample of my seed phrase, or by getting me to type in the whole thing.

After all this, I play with the wallet a bit, and then typically, I uninstall it.

The goal of this RFC is to get the user playing with the wallet as quickly as possible. Without sacrificing security whatsoever.

A staged approach

This RFC proposes a smart, staged approach to wallet security. One that maximises user experience without compromising safety.

Each step enforces more stringent security protocols on the user than the previous step.

The user moves from one step to another based on criteria that

  1. the user configures based on her preferences, or
  2. uses sane predefined defaults.

The criteria are generally based on the value of the wallet balance.

Once a user moves to a stage, the wallet does not move to a lower stage if the requirements for the stage are no longer met.

Users may also jump to any more advanced stage from their wallet settings / configuration at any time.

Stage zero - zero balance

When the user has a zero balance, there's no risk in letting them skip securing their wallet.

Therefore, Tari wallets SHOULD just skip the whole seed phrase ritual and let the user jump right into the action.

Stage 1a - a reminder to write down your seed phrase

Once the user's balance exceeds the MINIMUM_STAGE_ONE_BALANCE, they will be prompted to review and write down their seed phrase. The MINIMUM_STAGE_ONE_BALANCE is any non-zero balance by default.

After the transaction that causes the balance to exceed MINIMUM_STAGE_ONE_BALANCE is confirmed, the user is presented with a friendly message:

You now have _real_ money in your wallet. If you accidentally delete your wallet app or lose
your device, your funds are lost, and there is no way to recover them unless you have safely kept a copy of your
`seed phrase` safe somewhere. Click 'Ok' to review and save the phrase now, or 'Do it later' to do it at a more
convenient time.

If the user elects not to save the phrase, the message pops up again periodically. Once per day, or when the balance increases -- whichever is less frequent -- is sufficient without being too intrusive.

Stage 1b - simple wallet backups

Users are used to storing their data in the cloud. Although this practice is frowned upon by crypto purists, for small balances (the type you often keep in a hot wallet), using secure cloud storage for wallet backups is a fair compromise between keeping the keys safe from attackers and protecting users from themselves.

The simple wallet backup saves the spending keys and values of the user's wallet to a personal cloud space (e.g. Google Drive, Apple iCloud, Dropbox).

This solution does not require any additional input from the user besides providing authorisation to store in the cloud. This can be done using the standard APIs and Authentication flows that each cloud provider publishes for their platform.

In particular, we do not ask for a password to encrypt the commitment data. The consequence is that anyone who gains access to this data -- by stealing the user's cloud credentials -- could steal the user's funds.

Therefore, the threshold for moving from this stage to Stage 2, STAGE_TWO_THRESHOLD_BALANCE is relatively low; somewhere in the region of \$10 to \$50.

The seed phrase MUST NOT be stored on the cloud in Stage 1b. Doing so would result in all future funds of the user being lost if the backup were ever compromised. Since the backup is unencrypted in Stage 1b, we store the minimum amount of data needed to recover the funds and limit the potential loss of funds in case of a breach to just that found in the commitments in the backup, which should not be more than \$50.

Therefore, stage 1b backups are really just exporting and importing of UTXO data. The consequence of this is that restoring from a 1b backup does not restore the emoji id. On the other hand, you can easily import into any other wallet (e.g. into another wallet on another device owned by the same user).

Backups MUST be authorised by the user when the first cloud backup is made and SHOULD be automatically updated after each transaction is confirmed.

Wallet authors MAY choose to exclude Stage 1b from the staged security protocol.

As usual, the user MUST be able to configure STAGE_TWO_THRESHOLD_BALANCE to suit their particular needs.

When this threshold is reached, the user SHOULD be prompted with a call to back-up their data:

Your wallet now holds a fair amount of value, which you probably don't want to lose. Unlike a physical wallet's 
notes, it's possible to make a copy of your wallet's contents that you can use to recover your funds in case you 
lose them.
 
You should think about making a copy of your wallet's coins, which we will keep up date date after every transaction.

If you have a copy of your seed phrase saved somewhere safe, and don't want to back up your coins into the cloud, you 
can skip this step.

The user can pick between backup my coins or I have my seed phrase. Skip backups for now.

If the user chooses to skip the backup, do not prompt them for stage 1b backups again.

Stage 2 - full wallet backups

Once a user has a significant balance (over STAGE_TWO_THRESHOLD_BALANCE), Stage 2 is active. Stage 2 entails a full, encrypted backup of the user's wallet to the cloud. The user needs to provide a password to perform and secure the encryption.

This makes the user's fund safer while at rest in the cloud. It also introduces an additional point of failure: the user can forget their wallet's encryption password.

Stage 1b and 2 are similar in functionality but different in scope (Stage 2 allows us to store all the wallet metadata, rather than just the commitments). For this reason, Stage 1b is optional.

Backups MUST be authorised by the user when the first cloud backup is made and SHOULD be automatically updated after each transaction.

When migrating from Stage 1 to Stage 2, the Stage 1b backups SHOULD be deleted.

Once the stage 2 threshold is reached, the user is again presented with a call-to-action:

                   ๐Ÿ˜Ž๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ˜Ž
It's awesome that you're using [this Tari wallet] so much!
You're a high-roller, so it's time to add another layer of security 
to your wallet backups.

Your funds will be safer if we encrypt your cloud backups with an 
additional password of your own choosing. You need to make sure that 
this password is very string and that YOU DO NOT FORGET it!

(Whatever happens, you can always restore from your seed phrase, so 
make sure you're still keeping this safe and secreted away).

As an added benefit, password-encrypted backups allow us to backup your 
transaction history as well!            

The user can choose to Upgrade my backups, do this later, check my backup settings.

The latter option takes the user to the wallet settings, where they can configure the various thresholds and manually decide which backup strategy they want to pursue.

They should be prompted periodically (once per day is sufficient) if they choose to defer the upgrade.

Stage 3 - Sweep to cold wallet

Pending hardware wallet support

Above a given limit -- user-defined, or the default MAX_HOT_WALLET_BALANCE, the user should be prompted to transfer funds into a cold wallet. The amount to sweep can be calculated as MAX_HOT_WALLET_BALANCE - SAFE_HOT_WALLET_BALANCE.

If the user ignores the prompt, they SHOULD be reminded one week later. From the second prompt onward, users SHOULD be given an option to re-configure the values for MAX_HOT_WALLET_BALANCE and SAFE_HOT_WALLET_BALANCE.

Assuming one-sided payments are live, the user SHOULD be able to configure a COLD_WALLET_ADDRESS in the wallet.

For security reasons, a user SHOULD be asked for their 2FA confirmation, if it is configured, before broadcasting the sweep transaction to the blockchain.

A suitable prompt for the user once the MAX_HOT_WALLET_BALANCE threshold is reached is:

๐Ÿ‹๐Ÿ‹๐Ÿ‹๐Ÿ‹๐Ÿ‹๐Ÿ‹๐Ÿ‹๐Ÿ‹  โ—โ—โ— Whale Alert โ—โ—โ—  ๐Ÿ‹๐Ÿ‹๐Ÿ‹๐Ÿ‹๐Ÿ‹๐Ÿ‹๐Ÿ‹๐Ÿ‹
Your mobile wallet holds way more funds than would normally be 
considered safe to walk around with in your pocket.

You should really consider sweeping most of your balance into a 
cold- or hardware wallet.   

Possible actions from this prompt include:

  • Set up my cold wallet address, if the cold wallet address has not been configured.
  • Transfer funds to my cold wallet, if the cold wallet has been set up. The usual transaction confirmation flow SHOULD be followed here, and a stealth one-sided payment SHOULD be the default transaction type.
  • I'll deal with this myself, which takes the user to their backup settings.

To avoid spamming the user, do not fire this prompt more than once every three days.

Security hygiene

  • From stage 1 onwards Users should be asked periodically whether they still have their seed phrase written down. Once every two months is sufficient.

Change Log

DateChangeAuthor
2022-11-10Initial stableAdrian Truszczyล„ski
2022-12-12Update user messagesCjS77
2022-12-20Fix typoCjS77

RFC-0154/DeepLinksConvention

status: stable

Maintainer(s): Adrian Truszczyล„ski

Licence

The 3-Clause BSD Licence.

Copyright 2022 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community regarding the technological merits of the potential system outlined herein.

Goals

The aim of this Request for Comment (RFC) is to specify the deep links structure used in the Tari Aurora project. The primary motivation is to create a simple, human-readable, and scalable way to structure deep links used by the Tari Aurora clients.

Description

Deep links are the URIs with hierarchical components sequence. We can use this sequence to pass and handle data in a standardized and predictable way. To do that, we need to pass three components to the target client: the scheme, command, and data.

Scheme

The scheme is used to address the client, which will handle the command and data components. In the Tari Aurora project, we're using the tari scheme to open the wallet app and execute the command.

Command

The command is a path string used to pass information about the action that should be performed by the client. The handler uses this command to determine how to deserialize the data, before passing it to the command function. To support multiple networks the first path component should be the name of corresponding network. Before proceeding with the command, the client should first check that the active account is pointed to the correct network, or show an error if there is a mismatch.

For simple actions, the command can be defined as a single phrase. For more complex actions, the command should be defined as a multi-path where the second path component should be the name of the action group.

Examples:

Simple Actions:

mainnet/user_profile
testnet/login

Complex Actions:

mainnet/payments/send
testnet/payments/request

Data

The data component is an optional string of key-value pairs used by the parser/decoder to deserialize the data, which the client passes to the command function. The data component should be formatted in the same way as a URL query. The sub-component should have a ? prefix, the key-value pairs should be separated by &, and every key should be separated from the value by = character.

The Structure

Combining all three components, they will form a deep link with a structure presented below:

{scheme}://{network_name}/{command}?{data}

Examples:

tari://mainnet/profile
tari://mainnet/profile/username
tari://testnet/payments/send?amount=1.23&pubKey=01234556789abcde
tari://nextnet/contacts/list[0][alias]=MrTari&list[0][hex]=01234556789abcde&list[1][alias]=AdamSmith&list[1][hex]=edcba9876543210
  • {network_name}/transactions/send

The data contains transaction information used in the send tokens process.

Value NameValue TypeNote
tariAddressStringReceiver's Tari Address
amountUInt64?The amount in micro Tari
noteString?Note passed with transaction
  • {network_name}/base_nodes/add

The data contains a custom base node configuration. This deep link adds a new base node configuration to the pool and switches to the added base node.

Value NameValue TypeNote
nameStringThe name of the base node
peerStringBase node's public link and onion address combined together
  • {network_name}/contacts

List of contacts. This deep link is used to share multiple Tari contacts with another user.

Value NameValue TypeNote
listArray<Contact>List of Contact objects

Contact object

Value NameValue TypeNote
aliasString?Contact's name/alias
hexStringContact's Tari Address
  • {network_name}/profile

User's profile card. It is used to initiate the transaction with a predefined receiver address and presented alias.

Value NameValue TypeNote
aliasStringUser's name/alias
hexStringUser's Tari Address

Change Log

DateChangeAuthor
19 Apr 2023First stable, replace public key with tari addressSWvHeerden
06 Jun 2023Added descriptions for /contacts and /profile deep linksAdrian Truszczyล„ski

RFC-0230/Time-related Transactions

status: stable

Maintainer(s): S W van Heerden

Licence

The 3-Clause BSD Licence.

Copyright 2019 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

The aim of this Request for Comment (RFC) is to describe a few extensions to Mimblewimble to allow time-related transactions.

Description

Time-locked UTXOs

Time-locked Unspent Transaction Outputs (UTXOs) can be accomplished by adding a feature flag to a UTXO and a lock height, also referred to as the output's maturity. This allows a consensus limit on after which height the output can be spent.

This requires that users constructing a transaction:

  • MUST include a feature flag of their UTXO; and
  • MUST include a lock height in their UTXO.

This adds the following requirement for a base node:

  • A base node MUST NOT allow a UTXO to be spent if the current head has not already exceeded the UTXO's lock height.

This also adds the following requirement for a base node:

  • A base node MUST reject any block that contains a UTXO with a lock height not already past the current head.

Time-locked Contracts

In standard Mimblewimble, time-locked contracts can be accomplished by modifying the kernel of each transaction to include a lock height. This limits how early in the blockchain lifetime the specific transaction can be included in a block. This approach is used in a traditional Mimblewimble construction that does not implement any kind of scripting. This has two disadvantages. Firstly, the spending condition is very primitive and cannot be linked to other conditions. Secondly, it bloats the kernel, which is a component of the transaction that cannot be pruned.

However, with TariScript it becomes possible to express spending conditions like a time-lock as part of a UTXO's script. The CheckHeightVerify(height) TariScript Op code allows a time-lock check to be incorporated into a script. The following is a simple example of a plain time-lock script that prevents an output from being spent before the chain reaches height 4000:

CheckHeightVerify(4000)

Hashed Time-locked Contract

Hashed time-locked contracts (HTLC) are a way of reserving funds that can only be spent if a hash pre-image can be provided or if a specified amount of time has passed. The hash pre-image is a secret that can be revealed under the right conditions to enable spending of the UTXO before the time-lock is reached. The secret can be directly exchanged between the parties or revealed to the other party by spending an output that makes use of an adaptor signature.

HTLCs enable a number of interesting transaction constructions. For example, Atomic Swaps and Payment Channels like those in the Lightning Network.

The following is an example of an HTLC script. In this script, Alice sends some Tari to Bob that he can spend using the private key of P_b if he can provide the pre-image to the SHA256 hash output (HASH256{pre_image}) specified in the script by Alice. If Bob has not spent this UTXO before the chain reaches height 5000 then Alice will be able to spend the output using the private key of P_a.

HashSha256
PushHash(HASH256{pre_image})
Equal
IFTHEN
   PushPubkey(P_b)
ELSE
   CheckHeightVerify(5000)
   PushPubkey(P_a)
ENDIF

A more detailed analysis of the execution of this kind of script can be found at Time-locked Contact

Change Log

DateChangeAuthor
14 Apr 2019First draftSWvheerden
19 Dec 2021TariScript updatesphilipr
31 Oct 2022Stable updatebrianp

RFC-0240/Atomic Swap

status: draft

Maintainer(s): S W van Heerden

Licence

The 3-Clause BSD Licence.

Copyright 2021 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

This Request for Comment (RFC) aims to describe how Atomic swaps will be created between two parties on different blockchains.

$$ \newcommand{\preimage}{\phi} % pre image \newcommand{\hash}[1]{\mathrm{H}\bigl({#1}\bigr)} $$

Description

Atomic swaps are atomic transactions that allow users to exchange different crypto assets and or coins without using a central exchange and or trusting each other. Trading coins or assets this way makes it much more private and secure to do and swap because no third party is required to be secure. Atomic swaps work on the principle of using Hashed Time Lock Contracts(HTLC). In short, it requires some hash pre-image to unlock the contract, or the time-lock can be used to reclaim the funds.

In a cross-chain Atomic swap, both users lock up the funds to be exchanged on their respective chains in an HTLC-type contract. However, the two contractsโ€™ pre-image, or spending secret, is the same, but only one party knows the correct pre-image. When the first HTLC contract is spent, this publicly reveals the pre-image for the other party to spend the second HTLC. If the first HTLC is never spent, the second transaction's time-lock will allow the user to respend the funds back to themselves after the time lock has passed.

BTC - XTR AtomicSwap

Overview

BTC uses a scripting language for smart contracts on transactions which enables atomic swaps on the BTC chain. Traditionally Mimblewimble coins do not implement scripts, which makes Atomic swaps harder to implement but not impossible. Grin has implemented atomic swaps using a version of a 2-of-2 multi-signature transaction mimblewimble atomic swaps. Fortunately, Tari does have scripting with TariScript, which works a lot like BTC scripts, making the implementation simpler. Because of the scripting similarities, the scripts to both HTLCs will look very similar, and we only need to ensure that we use the same hash function in both.

To do an Atomic swap from BTC to XTR, we need four wallets, two BTC wallets, and two XTR wallets, one wallet per person, per coin.

As an example, Alice wants to trade some of her XTR for Bob's BTC. Alice and Bob need to agree on an amount of XTR and BTC to swap. Once an agreement is reached, the swap is executed in the following steps:

  • Alice chooses a set of random bytes, \( \preimage \), as the pre-image and hashes it with SHA256. She then sends

  • the hash of the pre-image, \( \hash{\preimage} \), to Bob along with her BTC address.

  • Bob sends her a public version of his script key, \( K_{Sb} \), for use in the XTR transaction, which we can refer to as Bob's script address.

  • Alice creates a one-sided XTR transaction with an HTLC contract requiring \( \preimage \) as the input, which will either payout to Bob's script address or her script address, \( K_{Sa} \), after a particular "time" has elapsed (block height has been reached).

  • Bob waits for this transaction to be mined. When it is mined, he verifies that the UTXO spending script expects a comparison of \( \hash{\preimage} \) as the first instruction, and that his public script key, \( K_{Sb} \), will be the final value remaining after executing the script. He has the private script key, \( k_{Sb} \), to enable him to produce a signature to claim the funds if he can get hold of the expected pre-image input value, \( \preimage \). He also verifies that the UTXO has a sufficiently long time-lock to give him time to claim the transaction.

  • Upon verification, Bob creates a Segwit HTLC BTC transaction with the same \( \hash{\preimage} \), which will spend

  • to Alice's BTC address she gave him. It is essential to note that the time lock for this HTLC has to expire before

  • the time lock of the XTR HTLC that Alice created.

  • Alice checks the Bitcoin blockchain, and upon seeing that the transaction is mined, she claims the transaction, but,

  • for her to do so, she has to make public what \( \preimage \) is as she has to use it as the witness of the claiming transaction.

  • Bob sees that his BTC is spent, and looks at the witness to get \( \preimage \). Bob can then use \( \preimage \) to claim the XTR transaction.

BTC - HTLC script

Here is the required BTC script that Bob publishes:

	OP_IF
	   OP_SHA256 <HASH256{pre_image}> OP_EQUALVERIFY
		<Alice BTC address> OP_CHECKSIG
	OP_ELSE
      <relative locktime>
      OP_CHECKSEQUENCEVERIFY
      OP_DROP
      <Bob BTC address> OP_CHECKSIG
   OP_ENDIF

relative locktime is a time sequence in which Alice chooses to lock up the funds to give Bob time to claim this.

XTR - HTLC script

Here is the required XTR script that Alice publishes:

   HashSha256 PushHash(HASH256{pre_image}) Equal
   IFTHEN
      PushPubkey(K_{Sb})
	ELSE
      CheckHeightVerify(height)
      PushPubkey(K_{Sa})
   ENDIF

(\( K_{Sb} \)) is the public key of the script key pair that Bob chooses to claim this transaction if Alice backs out. height is an absolute block height that Bob chooses to lock up the funds to give Alice time to claim the funds.

XTR - XMR swap

The Tari - Monero atomic swap involved a bit more detail than just a simple script and is explained in RFC-0241: XTR - XMR swap

Notation

Where possible, the "usual" notation is used to denote terms commonly found in cryptocurrency literature. Lower case characters are used as private keys, while uppercase characters are used as public keys. New terms introduced here are assigned greek lowercase letters in most cases. Some terms used here are noted down in TariScript.

NameSymbolDefinition
Pre-image\( \preimage \)The random byte data used for the pre-image of the hash

|

Change Log

DateChangeAuthor
17 Oct 2022First outlineSWvHeerden

RFC-0241/XMR Atomic Swap

status: draft

Maintainer(s): S W van Heerden

Licence

The 3-Clause BSD Licence.

Copyright 2021 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

This Request for Comment (RFC) aims to describe how an Atomic swap between Tari and Monero will be created.

$$ \newcommand{\script}{\alpha} % utxo script \newcommand{\input}{ \theta } \newcommand{\cat}{\Vert} \newcommand{\so}{\gamma} % script offset \newcommand{\hash}[1]{\mathrm{H}\bigl({#1}\bigr)} $$

Comments

Any comments, changes or questions to this PR can be made in one of the following ways:

Description

Doing atomic swaps with Monero is more complicated and requires a cryptographic dance to complete as Monero does not implement any form of HTLC's or the like. This means that when doing an atomic swap with Monero, most of the logic will have to be implemented on the Tari side. Atomic swaps between Monero and Bitcoin have been implemented by the Farcaster project and the Comit team. Due to the how TariScript works, we have a few advantages over Bitcoin script regarding adaptor signatures, as the script key was explicitly designed with scriptless scripts in mind.

Method

The primary, happy path outline of a Tari - Monero atomic swap is described here, and more detail will follow. We assume that Alice wants to trade her XTR for Bob's XMR.

  • Negotiation - Both parties negotiate the value and other details of the Monero and Tari UTXO's.
  • Commitment - Both parties commit to the keys, nonces, inputs, and outputs to use for the transaction.
  • XTR payment - Alice makes the XTR payment to a UTXO containing a "special" script described below.
  • XMR Payment - The Monero payment is made to a multiparty scriptless script UTXO.
  • Claim XTR - Bob redeems the XTR, and in doing so, reveals the XMR private key to Alice only.
  • Claim XMR - Alice may claim the XMR using the revealed key.

Please take note of the notation used in TariScript and specifically notation used on the signatures on the transaction inputs and on the signatures on the transaction outputs. We will note other notations in the Notation section.

TL;DR

The scheme revolves around Alice, who wants to exchange her Tari for Bob's Monero. Because they don't trust each other, they have to commit some information to do the exchange. And if something goes wrong here, we want to ensure that we can refund both parties either in Monero or Tari.

How this works is that Alice and Bob create a shared output on both chains. The Monero output is a simple aggregate key to unlock the UTXO, while multiple keys are needed to unlock the Tari UTXO. An aggregate key locks this Monero UTXO that neither Alice nor Bob knows, but they both know half of the key. The current Tari block height determines the unlocking key for the Tari UTXO.

The process is started by Alice and Bob exchanging and committing to some information. Alice is the first to publish a transaction, which creates the Tari UTXO. If Bob is happy that the Tari UTXO has been mined and verifies all the information, he will publish a transaction to create the Monero UTXO.

The TariScript script on the UTXO ensures that they will have to reveal their portion of the Monero key when either Alice or Bob spends this. This disclosure allows the other party to claim the Monero by being the only one to own the complete Monero aggregate key.

We can visualize the happy path flow with the image below. swap flow

The script will ensure that at any point in time, at least someone can claim the Tari UTXO, and if that person does so, the other party can claim the Monero UTXO by looking at the spending data. It has two lock heights, determining who can claim the Tari UTXO if the happy path fails. Before the first lock height, only Bob can claim the Tari; we call this the swap transaction.

If Bob disappears after Alice has posted the Tari UTXO, Alice can claim the Tari after the first lock height and before the second lock height; we call this the refund transaction. It ensures that Alice can reclaim her Tari if Bob disappears, and if Bob reappears, he can reclaim his Monero.

That leaves us with the scenario where Alice disappears after Bob posts the Monero transaction, in which case we need to protect Bob. After the second lock height, only Bob can claim the Tari; we call this the lapse transaction. The lapse transaction will reveal Bob's Monero key so that if Alice reappears, she can claim the Monero.

The image below details the time flow of the Tari transactions spending the Tari UTXO. swap flow

Heights, Security, and other considerations

We need to consider a few things for this to be secure, as there are possible scenarios that can reduce the security in the atomic swap.

When looking at the two lock heights, the first lock height should be sufficiently large enough to give ample time for Alice to post the Tari UTXO transaction and for it to be mined with a safe number of confirmations, and for Bob to post the Monero transaction and for it to be mined with a safe number of confirmations. The second lock height should give ample time for Alice after the first lock height to re-claim her Tari. Larger heights here might make refunds slower, but it should be safer in giving more time to finalize this.

Allowing both to claim the Tari after the second lock height is, on face value, a safer option. This can be done by enabling either party to claim the script with the lapse transaction. The counterparty can then claim the Monero. However, this will open up an attack vector to enable either party to claim the Monero while claiming the Tari. Either party could trivially pull off such a scheme by performing a front-running attack and having a bit of luck. The counterparty monitors all broadcast transactions to base nodes. Upon identifying the lapse transaction, they do two things; in quick succession, broadcast their lapse transaction and the transaction to claim the Monero, both with sufficiently high fees. Base nodes will prefer to mine transactions with the higher fees, and thus the counterparty can walk away with both the Tari and the Monero.

It is also possible to prevent the transaction from being mined after being submitted to the mempool. This can be caused by a combination of a too busy network, not enough fees, or a too-small period in the time locks. When one of these atomic swap transactions gets published to a mempool, we effectively already have all the details exposed. For the atomic swaps, it means we already revealed part of the Monero key, although the actual Tari transaction has not been mined. But this is true for any HTLC or like script on any blockchain. But in the odd chance that this does happen whereby the fees are too little and time locks not enough, it should be possible to do a child-pays-for-parent transaction to bump up the fees on the transaction to get it mined and confirmed.

Key construction

Using multi-signatures with Schnorr signatures, we need to ensure that the keys are constructed so that key cancellation attacks are not possible. To do this, we create new keys from the chosen public keys \(K_a'\) and \(K_b'\)

$$ \begin{aligned} K_a &= \hash{\hash{K_a' \cat K_b'} \cat K_a' } * K_a' \\ k_a &= \hash{\hash{K_a' \cat K_b'} \cat K_a' } * k_a' \\ K_b &= \hash{\hash{K_a' \cat K_b'} \cat K_b' } * K_b' \\ k_b &= \hash{\hash{K_a' \cat K_b'} \cat K_b' } * k_b' \\ \end{aligned} \tag{1} $$

Key equivalence

Monero uses Ed25519, while Tari uses Ristretto as its curve of choice. While Ristretto and Ed25519 both works on Curve25519 and the Edward points are the same, they differ when the points are encoded. In practice, this means the following:

$$ \begin{aligned} Xm &= x \cdot G_m \\ X &= x \cdot G \\ X &\neq X_m \\ \end{aligned} \tag{2} $$

To use public keys across different implementations or curves, we must prove that the same private key created the "different" public keys. Because we exchange encoded points, we need some way of proving they are the same. Jepsen designed a proof to prove Discrete Logarithm Equality (DLEQ) between two generator groups. But the proof here is not created for Elliptic Curve Cryptography (ECC) but can be adapted to ECC by:

$$ \begin{aligned} e &= \hash{K_1 \cat K_2 \cat R_{1} \cat R_{2}} \\ s &= r + e(k) \\ R_{1} &= r \cdot G \\ R_{2} &= r \cdot G_m \\ K_{1} &= k \cdot G \\ K_{2} &= k \cdot G_m \\ \end{aligned} \tag{3} $$

The verification is then: $$ \begin{aligned} e &= \hash{K_1 \cat K_2 \cat R_{1} \cat R_{2}} \\ s \cdot G &= R_{1} + e(K_1) \\ s \cdot G_m &= R_{2} + e(K_2) \\ \end{aligned} \tag{4} $$

But this method has a problem with ECC keys as they are always used as \(mod(n)\) where n is the group size. With ECC, we cannot always use the method; it can only be used when the two curves are of the same group size \(n\), or \(s < n\). If this is not the case, a bit comparison needs to be created to prove this. But luckily, we use Ristretto and Ed25519, both on Curve25519, because itโ€™s the same curve, same generator point, and just different encodings. We can use this proof to know even though the encoded public keys do not match, they still share a private key

Key security

The risk of publicly exposing part of the Monero private key is still secure because of how ECC works. We can add two secret keys together and share the public version of both. And at the same time, we know that no one can calculate the secret key with just one part.

$$ \begin{aligned} (k_a + k_b) \cdot G &= k_a \cdot G + k_b \cdot G\\ (k_a + k_b) \cdot G &= K_a + K_b \\ (k_a + k_b) \cdot G &= K \\ \end{aligned} \tag{5} $$

We know that \(K\), \(K_a\), \(K_b\) are public. While \(k\), \(k_a\), \(k_b\) are all private.

But if we expose \(k_b\), we can try to do the following: $$ \begin{aligned} (k_a + k_b) \cdot G &= K_a + K_b\\ k_a \cdot G &= (K_a + K_b - k_b \cdot G) \\ k_a \cdot G &= K_a \\ \end{aligned} \tag{6} $$

However, this is the Elliptic-Curve Discrete Logarithm Problem, and there is no easy solution to solve this on current computer hardware. Thus this is still secure even though we leaked part of the secret key \(k\).

Method 1

Detail

This method relies purely on TariScript to enforce the exposure of the private Monero aggregate keys. Based on Point Time Lock Contracts, the script forces the spending party to supply their Monero private key part as input data to the script, evaluated via the operation ToRistrettoPoint. This TariScript operation will publicly reveal part of the aggregated Monero private key, but this is still secure: see Key security.

The simplicity of this method lies therein that the spending party creates all transactions on their own. Bob requires a pre-image from Alice to complete the swap transaction; Alice needs to verify that Bob published the Monero transaction and that everything is complete as they have agreed. If she is happy, she will provide Bob with the pre-image to claim the Tari UTXO.

TariScript

The Script used for the Tari UTXO is as follows:

   ToRistrettoPoint
   CheckHeight(height_1)
   LtZero
   IFTHEN
      PushPubkey(X_b)
      EqualVerify
      HashSha256 
      PushHash(HASH256{pre_image})
      EqualVerify
      PushPubkey(K_{Sb})
   Else
      CheckHeight(height_2)
      LtZero
      IFTHEN
         PushPubkey(X_a)
         EqualVerify
         PushPubkey(K_{Sa})
      Else
         PushPubkey(X_b)
         EqualVerify
         PushPubkey(K_{Sb})
      ENDIF
   ENDIF

Before height_1, Bob can claim the Tari UTXO by supplying pre_image and his private Monero key part x_b. After height_1 but before height_2, Alice can claim the Tari UTXO by supplying her private Monero key part x_a. After height_2, Bob can claim the Tari UTXO by providing his private Monero key part x_b.

Negotiation

Alice and Bob have to negotiate the exchange rate and the amount exchanged in the atomic swap. They also need to decide how the two UTXO's will look on the blockchain. To accomplish this, the following needs to be finalized:

  • Amount of Tari to swap for the amount of Monero
  • Monero public key parts \(Xm_a\), \(Xm_b\) ,and its aggregate form \(Xm\)
  • DLEQ proof of \(Xm_a\) and \(X_a\)
  • Tari script key parts \(K_{Sa}\), \(K_{Sb}\)
  • The TariScript to be used in the Tari UTXO
  • The blinding factor \(k_i\) for the Tari UTXO, which can be a Diffie-Hellman between their Tari addresses.

Key selection

Using (1), we create the Monero keys as they are multi-party aggregate keys. The Monero key parts for Alice and Bob is constructed as follows:

$$ \begin{aligned} Xm_a' &= xm_a' \cdot G_m \\ Xm_b' &= xm_b' \cdot G_m \\ xm_a &= \hash{\hash{Xm_a' \cat Xm_b'} \cat Xm_a' } * xm_a' \\ xm_b &= \hash{\hash{Xm_a' \cat Xm_b'} \cat Xm_b' } * xm_b' \\ xm_a &= x_a \\ xm_b &= x_b \\ Xm_a &= \hash{\hash{Xm_a' \cat Xm_b'} \cat Xm_a' } * Xm_a' \\ Xm_b &= \hash{\hash{Xm_a' \cat Xm_b'} \cat Xm_b' } * Xm_b' \\ xm &= xm_a + xm_b + k_i \\ Xm &= Xm_a + Xm_b + k_i \cdot G_m\\ x &= x_a + x_b + k_i \\ X &= X_a + X_b + k_i \cdot G\\ \end{aligned} \tag{7} $$

Commitment phase

This phase allows Alice and Bob to commit to using their keys.

Starting values

Alice needs to provide Bob with the following:

  • Script public key: \( K_{Sa}\)
  • Monero public key \( Xm_a'\) with Ristretto encoding

Bob needs to provide Alice with the following:

  • Script public key: \( K_{Sb}\)
  • Monero public key \( Xm_b'\) with Ristretto encoding

Using the above equations in (7), Alice and Bob can calculate \(Xm\), \(Xm_a\), \(Xm_b\)

DLEQ proof

Alice needs to provide Bob with:

  • Monero public key \(X_a\) encoding on Ristretto
  • DLEQ proof for \(Xm_a\) and \(X_a\): \((R_{ZTa}, R_{ZMa}, s_{Za})\)

$$ \begin{aligned} e &= \hash{X_a \cat Xm_a \cat R_{ZTa} \cat R_{ZMa}} \\ s_{Za} &= r + e(x_a) \\ R_{ZTa} &= r \cdot G \\ R_{ZMa} &= r \cdot G_m \\ \end{aligned} \tag{8} $$

Bob needs to provide Alice with:

  • Monero public key \(X_b\) encoding on Ristretto
  • DLEQ proof for \(Xm_b\) and \(X_b\): \((R_{ZTb}, R_{ZMb}, s_{Zb})\)

$$ \begin{aligned} e &= \hash{X_b \cat Xm_b \cat R_{ZTb} \cat R_{ZMb}} \\ s_{Za} &= r + e(x_b) \\ R_{ZTa} &= r \cdot G \\ R_{ZMa} &= r \cdot G_m \\ \end{aligned} \tag{9} $$

Alice needs to verify Bob's DLEQ proof with:

$$ \begin{aligned} e &= \hash{X_b \cat Xm_b \cat R_{ZTb} \cat R_{ZMb}} \\ s_{Zb} \cdot G &= R_{ZTb} + e(X_b) \\ s_{Zb} \cdot G_m &= R_{ZMb} + e(Xm_b) \\ \end{aligned} \tag{10} $$

Bob needs to verify Alice's DLEQ proof with:

$$ \begin{aligned} e &= \hash{X_a \cat Xm_a \cat R_{ZTa} \cat R_{ZMa}} \\ s_{Za} \cdot G &= R_{ZTa} + e(X_a) \\ s_{Za} \cdot G_m &= R_{ZMa} + e(Xm_a) \\ \end{aligned} \tag{11} $$

XTR payment

Alice will construct the Tari UTXO with the correct script and publish the containing transaction to the blockchain, knowing that she can reclaim her Tari if Bob vanishes or tries to break the agreement. This is done with standard Mimblewimble rules and signatures.

XMR payment

When Bob sees that the Tari UTXO that Alice created is mined on the Tari blockchain with the correct script, Bob can publish the Monero transaction containing the Monero UTXO with the aggregate key \(Xm = Xm_a + Xm_b + k_i \cdot G_m \).

Claim XTR

When Alice sees that the Monero UTXO that Bob created is mined on the Monero blockchain containing the correct aggregate key \(Xm\), she can provide Bob with the required pre_image to spend the Tari UTXO. She does not have the missing key \(xm_b \) to claim the Monero yet, but it will be revealed when Bob claims the Tari.

Bob can now supply the pre_image and his Monero private key as transaction input to unlock the script.

Claim XMR

Alice can now see that Bob spent the Tari UTXO, and by examining the input_data required to satisfy the script, she can learn Bob's secret Monero key. Although this private key \( xm_b \) is now public knowledge, her part of the Monero spend key is still private, and thus only she knows the complete Monero spend key. She can use this knowledge to claim the Monero UTXO.

The refund

If something goes wrong and Bob never publishes the Monero or disappears, Alice needs to wait for the lock height height_1 to pass. This will allow her to reclaim her Tari, but in doing so, she needs to publish her Monero secret key as input to the script to unlock the Tari. When Bob comes back online, he can use this public knowledge to reclaim his Monero, as only he knows both parts of the Monero UTXO spend key.

The lapse transaction

If something goes wrong and Alice never gives Bob the required pre_image, Bob needs to wait for the lock height height_2 to pass. This will allow him to claim the Tari he wanted all along, but in doing so, he needs to publish his Monero secret key as input to the script to unlock the Tari. When Alice comes back online, she can use this public knowledge to claim the Monero she wanted all along as only she now knows both parts of the Monero UTXO spend key.

Method 2

Detail

This method is based on work by the Farcaster project and the Comit team. It utilizes adapter signatures and multi-party commitment signatures to ensure that the spending party leaks their private Monero key part. Because all keys are aggregates keys, we need to ensure that the refund and lapse transactions are negotiated and signed before Alice publishes the Tari UTXO. This will allow either Alice or Bob to claim the refund and lapse transactions, respectively, without the other party being online.

TariScript

The Script used for the Tari UTXO is as follows:

   CheckHeight(height_1)
   LtZero
   IFTHEN
      PushPubkey(K_{Ss})
   Else
      CheckHeight(height_2)
      LtZero
      IFTHEN
         PushPubkey(K_{Sr})
      Else
         PushPubkey(K_{Sl})
      ENDIF
   ENDIF

Before height_1, Bob can claim the Tari UTXO if Alice gives him the correct signature to complete the transaction. After height_1 but before height_2, Alice can claim the Tari UTXO. After height_2, Bob can claim the Tari UTXO.

Negotiation

Alice and Bob have to negotiate the exchange rate and the amount exchanged in the atomic swap. They also need to decide how the two UTXO's will look on the blockchain. To accomplish this, the following needs to be finalized:

  • Amount of Tari to swap for the amount of Monero
  • Monero public key parts \(Xm_a\), \(Xm_b\), and its aggregate form \(X\)
  • Tari script key parts \(K_{Ssa}\), \(K_{Ssb}\), and its aggregate form \(K_{Ss}\) for the swap transaction
  • Tari script key parts \(K_{Sra}\), \(K_{Srb}\), and its aggregate form \(K_{Sr}\) for the refund transaction
  • Tari script key parts \(K_{Sla}\), \(K_{Slb}\), and its aggregate form \(K_{Sl}\) for the lapse transaction
  • Tari sender offset key parts \(K_{Osa}\), \(K_{Osb}\), and its aggregate form \(K_{Os}\) for the swap transaction
  • Tari sender offset key parts \(K_{Ora}\), \(K_{Orb}\), and its aggregate form \(K_{Or}\) for the refund transaction
  • Tari sender offset key parts \(K_{Ola}\), \(K_{Olb}\), and its aggregate form \(K_{Ol}\) for the lapse transaction
  • All of the nonces used in the script signature creation and Metadata signature for the swap, refund, and lapse transactions
  • The script offset used in both the swap, refund, and lapse transactions
  • The TariScript to be used in the Tari UTXO
  • The blinding factor \(k_i\) for the Tari UTXO, which can be a Diffie-Hellman between their addresses.

Key selection

Using (1), we create the Monero keys as they are multi-party aggregate keys.

The script key parts for Alice and Bob is constructed as follows:

$$ \begin{aligned} k_{Ssa} &= \hash{\hash{K_{Ssa}' \cat K_{Ssb}'} \cat K_{Ssa}' } * k_{Ssa}' \\ k_{Ssb} &= \hash{\hash{K_{Ssa}' \cat K_{Ssb}'} \cat K_{Ssb}' } * k_{Ssb}' \\ k_{Ss} &= k_{Ssa} + k_{sb} \\ k_{Sra} &= \hash{\hash{K_{Sra}' \cat K_{Srb}'} \cat K_{Sra}' } * k_{Sra}' \\ k_{Srb} &= \hash{\hash{K_{Sra}' \cat K_{Srb}'} \cat K_{Srb}' } * k_{Srb}' \\ k_{Sr} &= k_{Sra} + k_{Srb} \\ k_{Sla} &= \hash{\hash{K_{Sla}' \cat K_{Slb}'} \cat K_{Sla}' } * k_{Sla}' \\ k_{Slb} &= \hash{\hash{K_{Sla}' \cat K_{Slb}'} \cat K_{Slb}' } * k_{Slb}' \\ k_{Sl} &= k_{Sla} + k_{Slb} \\ \end{aligned} \tag{12} $$

The sender offset key parts for Alice and Bob is constructed as follows:

$$ \begin{aligned} k_{Osa} &= \hash{\hash{K_{Osa}' \cat K_{Osb}'} \cat K_{Osa}' } * k_{Osa}' \\ k_{Osb} &= \hash{\hash{K_{Osa}' \cat K_{Osb}'} \cat K_{Osb}' } * k_{Osb}' \\ k_{Os} &= k_{Osa} + k_{Ssb} \\ k_{Ora} &= \hash{\hash{K_{Ora}' \cat K_{Orb}'} \cat K_{Ora}' } * k_{Ora}' \\ k_{Orb} &= \hash{\hash{K_{Ora}' \cat K_{Orb}'} \cat K_{Orb}' } * k_{Orb}' \\ k_{Or} &= k_{Ora} + k_{Srb} \\ k_{Ola} &= \hash{\hash{K_{Ola}' \cat K_{Olb}'} \cat K_{Ola}' } * k_{Ola}' \\ k_{Olb} &= \hash{\hash{K_{Ola}' \cat K_{Olb}'} \cat K_{Olb}' } * k_{Olb}' \\ k_{Ol} &= k_{Ola} + k_{Slb} \\ \end{aligned} \tag{13} $$

The Monero key parts for Alice and Bob is constructed as follows:

$$ \begin{aligned} xm_a' &= x_a' \\ xm_b' &= x_b' \\ Xm_a' &= x_a' \cdot G_m \\ Xm_b' &= x_b' \cdot G_m \\ X_a' &= x_a' \cdot G \\ X_b' &= x_b' \cdot G \\ x_a &= \hash{\hash{X_a' \cat X_b'} \cat X_a' } * x_a' \\ x_b &= \hash{\hash{X_a' \cat X_b'} \cat X_b' } * x_b' \\ x &= x_a + x_b + k_i \\ Xm &= Xm_a + Xm_b + k_i \cdot G_m \\ X &= X_a + X_b + k_i \cdot G\\ \end{aligned} \tag{14} $$

Commitment phase

Similar to method 1, this phase allows Alice and Bob to commit to using their keys and requires more than one round to complete. Some of the information that needs to be committed depends on previous knowledge.

Starting values

Alice needs to provide Bob with the following:

  • Output commitment \(C_r\) of the refund transaction's output
  • Output features \( F_r\) of the refund transaction's output
  • Output script \( \script_r\) of the refund transaction's output
  • Output commitment \(C_l\) of the lapse transaction's output
  • Output features \( F_l\) of the lapse transaction's output
  • Output script \( \script_l\) of the lapse transaction's output
  • Public keys: \( K_{Ssa}'\), \( K_{Sra}'\), \( K_{Sla}'\), \( K_{Osa}'\), \( K_{Ora}'\), \( K_{Ola}'\), \( X_a'\)
  • Nonces: \( R_{Ssa}\), \( R_{Sra}\), \( R_{Sla}\), \( R_{Msa}\), \( R_{Mra}\), \( R_{Mla}\)

Bob needs to provide Alice with the following:

  • Output commitment \(C_s\) of the swap transaction's output
  • Output features \( F_s\) of the swap transaction's output
  • Output script \( \script_s\) of the swap transaction's output
  • Public keys: \( K_{Ssb}'\), \( K_{Srb}'\), \( K_{Slb}'\), \( K_{Osb}'\), \( K_{Orb}'\), \( K_{Olb}'\), \( X_b'\)
  • Nonces: \( R_{Ssb}\), \( R_{Srb}\), \( R_{Slb}\), \( R_{Msb}\), \( R_{Mrb}\), \( R_{Mlb}\)

After Alice and Bob have exchanged the variables, they start trading calculated values.

Construct adaptor signatures and DLEQ proofs

Alice needs to provide Bob with the following values:

  • Adaptor signature part \(b_{Sra}'\) for \(b_{Sra}\)
  • Signature part \(a_{Sra}\)
  • Monero public key \(X_a\) encoded with Ristretto
  • Monero public key \(Xm_a\) encoded with ed25519
  • DLEQ proof for \(Xm_a\) and \(X_a\): \((R_{ZTa}, R_{ZMa}, s_{Za})\)

Alice constructs the adaptor signature of the input script signature parts for the refund transaction ( \(a_{Sra}\) and \(b_{Sra}'\) ) with:

$$ \begin{aligned} a_{Sra} &= r_{Sra_a} + e_r(v_{i}) \\ b_{Sra}' &= r_{Sra_b} + e_r(k_{Sra}+k_i) \\ e_r &= \hash{ (R_{Sr} + (X_a)) \cat \alpha_r \cat \input_r \cat (K_{Sra} + K_{Srb}) \cat C_i} \\ R_{Sr} &= r_{Sra_a} \cdot H + r_{Sra_b} \cdot G + R_{Srb} \\ X_a &= x_a \cdot G \\ \end{aligned} \tag{15} $$

Alice constructs the DLEQ proof:

$$ \begin{aligned} e &= \hash{X_a \cat Xm_a \cat R_{ZTa} \cat R_{ZMa}} \\ s_{Za} &= r + e(x_a) \\ R_{ZTa} &= r \cdot G \\ R_{ZMa} &= r \cdot G_m \\ \end{aligned} \tag{16} $$

Bob needs to provide Alice with the following values:

  • Adaptor signature part \(b_{Ssb}'\) for \(b_{Ssb}\)
  • Signature part \(a_{Ssb}\)
  • Adaptor signature part \(b_{Slb}'\) for \(b_{Slb}\)
  • Signature part \(a_{Slb}\)
  • Monero public key \(X_b\) encoded with Ristretto
  • Monero public key \(Xm_b\) encoded with ed25519
  • DLEQ proof for \(Xm_b\) and \(X_b\): \((R_{ZTb}, R_{ZMb}, s_{Zb})\)

Bob constructs the adaptor signatures of the input script signature parts for the swap transaction ( \(a_{Ssb}\), \(b_{Ssb}'\) ) and the lapse transaction ( \(a_{Slb}\) and \(b_{Slb}'\) ) with

$$ \begin{aligned} a_{Ssb} &= r_{Ssb_a} + e_s(v_{i}) \\ b_{Ssb}' &= r_{Ssb_b} + e_s(k_{Ssb}+k_i) \\ e_s &= \hash{ (R_{Sr} + (X_b)) \cat \alpha_i \cat \input_i \cat (K_{Ssa} + K_{Ssb}) \cat C_i} \\ R_{Ss} &= r_{Ssb_a} \cdot H + r_{Ssb_b} \cdot G + R_{Ssa} \\ a_{Slb} &= r_{Slb_a} + e_l(v_{i}) \\ b_{Slb}' &= r_{Slb_b} + e_l(k_{Slb}+k_i) \\ e_l &= \hash{ (R_{Sl} + (X_b)) \cat \alpha_i \cat \input_i \cat (K_{Sla} + K_{Slb}) \cat C_i} \\ R_{Sl} &= r_{Slb_a} \cdot H + r_{Slb_b} \cdot G + R_{Sla} \\ X_b &= x_b \cdot G \\ \end{aligned} \tag{17} $$

Bob constructs the DLEQ proof:

$$ \begin{aligned} e &= \hash{X_b \cat Xm_b \cat R_{ZTb} \cat R_{ZMb}} \\ s_{Zb} &= r + e(x_b) \\ R_{ZTb} &= r \cdot G \\ R_{ZMb} &= r \cdot G_m \\ \end{aligned} \tag{18} $$

Verify adaptor signatures and DLEQ proofs

Alice needs to verify Bob's adaptor signatures with:

$$ \begin{aligned} a_{Ssb} \cdot H + b_{Ssb}' \cdot G &= R_{Ssb} + (C_i+K_{Ssb})*e_s \\ a_{Slb} \cdot H + b_{Slb}' \cdot G &= R_{Slb} + (C_i+K_{Slb})*e_l \\ \end{aligned} \tag{19} $$

Alice needs to verify Bob's Monero public keys using the zero-knowledge proof:

$$ \begin{aligned} e &= \hash{X_b \cat Xm_b \cat R_{ZTb} \cat R_{ZMb}} \\ s_{Zb} \cdot G &= R_{ZTb} + e(X_b) \\ s_{Zb} \cdot G_m &= R_{ZMb} + e(Xm_b) \\ \end{aligned} \tag{20} $$

Bob needs to verify Alice's adaptor signature with:

$$ \begin{aligned} a_{Sra} \cdot H + b_{Sra}' \cdot G &= R_{Sra} + (C_i+K_{Sra})*e_r \\ \end{aligned} \tag{21} $$

Bob needs to verify Alice's Monero public keys using the zero-knowledge proof:

$$ \begin{aligned} e &= \hash{X_a \cat XM_a \cat R_{ZTa} \cat R_{ZMa}} \\ s_{Za} \cdot G &= R_{ZTa} + e(X_a) \\ s_{Za} \cdot G_m &= R_{ZMa} + e(Xm_a) \\ \end{aligned} \tag{22} $$

Swap out refund and lapse transactions

If Alice and Bob are happy with the verification, they need to swap out refund and lapse transactions.

Alice needs to provide Bob with the following:

Alice constructs for the lapse transaction signatures. $$ \begin{aligned} a_{Sla} &= r_{Sla_a} + e_l(v_{i}) \\ b_{Sla} &= r_{Sla_b} + e_l(k_{Sla}) \\ e_l &= \hash{ (R_{Sl} + (X_b)) \cat \alpha_i \cat \input_i \cat (K_{Sla} + K_{Slb}) \cat C_i} \\ R_{Sl} &= r_{Sla_a} \cdot H + r_{Sla_b} \cdot G + R_{Slb}\\ b_{Mla} &= r_{Mla_b} + e(k_{Ola}) \\ R_{Mla} &= b_{Mla} \cdot G \\ e &= \hash{ (R_{Mla} + R_{Mlb}) \cat \script_l \cat F_l \cat (K_{Ola} + K_{Olb}) \cat C_l} \\ \so_{la} &= k_{Sla} - k_{Ola} \\ \end{aligned} \tag{23} $$

Bob needs to provide Alice with the following:

Bob constructs for the refund transaction signatures. $$ \begin{aligned} a_{Srb} &= r_{Srb_a} + e_r(v_{i}) \\ b_{Srb} &= r_{Srb_b} + e_r(k_{Srb}) \\ e_r &= \hash{ (R_{Sr} + (X_a)) \cat \alpha_i \cat \input_i \cat (K_{Sra} + K_{Srb}) \cat C_i} \\ R_{Sl} &= r_{Srb_a} \cdot H + r_{Srb_b} \cdot G + R_{Sra}\\ b_{Mrb} &= r_{Mrb_b} + e(k_{Orb}) \\ R_{Mrb} &= b_{Mrb} \cdot G \\ e &= \hash{ (R_{Mra} + R_{Mrb}) \cat \script_r \cat F_r \cat (K_{Ora} + K_{Orb}) \cat C_r} \\ \so_{rb} &= k_{Srb} - k_{Orb} \\ \end{aligned} \tag{24} $$

Although the script validation on output \(C_i\) will not pass due to the lock height, both Alice and Bob need to verify that the total aggregated signatures and script offset for the refund and lapse transaction are valid should they need to publish them at a future date without the other partyโ€™s presence.

XTR payment

If Alice and Bob are happy with all the committed values, Alice will construct the Tari UTXO with the correct script and publish the containing transaction to the blockchain. Because Bob already gave her the required signatures for his part of the refund transaction, Alice can easily compute the required aggregated signatures by adding the parts together. She has all the knowledge to spend this after the lock expires.

XMR Payment

When Bob sees that the Tari UTXO that Alice created is mined on the Tari blockchain with the correct script, he can go ahead and publish the Monero UTXO with the aggregate key \(Xm = Xm_a + Xm_b \).

Claim XTR

When Alice sees that the Monero UTXO that Bob created is mined on the Monero blockchain containing the correct aggregate key \(Xm\), she can provide Bob with the following allowing him to spend the Tari UTXO:

  • Script signature for the swap transaction \((a_{Ssa}\, b_{Ssa}), R_{Ssa}\)
  • Metadata signature for swap transaction \((b_{Msa}, R_{Msa})\)
  • Script offset for swap transaction \( \so_{sa} \)

She does not have the missing key \(x_b \) to claim the Monero yet, but it will be revealed when Bob claims the Tari.

Alice constructs for the swap transaction. $$ \begin{aligned} a_{Ssa} &= r_{Ssa_a} + e_s(v_{i}) \\ b_{Ssa} &= r_{Ssa_b} + e_s(k_{Ssa}) \\ e_s &= \hash{ (R_{Ss} + (X_b)) \cat \alpha_i \cat \input_i \cat (K_{Ssa} + K_{Ssb}) \cat C_i} \\ R_{Ss} &= r_{Ssa_a} \cdot H + r_{Ssa_b} \cdot G + R_{Ssb}\\ b_{Msa} &= r_{Msa_b} + e(k_{Osa}) \\ R_{Msa} &= b_{Msa} \cdot G \\ e &= \hash{ (R_{Msa} + R_{Msb}) \cat \script_s \cat F_s \cat (K_{Osa} + K_{Osb}) \cat C_s} \\ \so_{sa} &= k_{Ssa} - k_{Osa} \\ \end{aligned} \tag{25} $$

Bob constructs the swap transaction. $$ \begin{aligned} a_{Ssb} &= r_{Ssb_a} + e_s(v_{i}) \\ b_{Ssb} &= r_{Ssb_b} + x_b + e_s(k_{Ssb} + k_i) \\ e_s &= \hash{ (R_{Ss} + (X_b)) \cat \alpha_i \cat \input_i \cat (K_{Ssa} + K_{Ssb}) \cat C_i} \\ a_{Ss} &= a_{Ssa} + a_{Ssb} \\ b_{Ss} &= b_{Ssa} + b_{Ssb} \\ R_{Ss} &= r_{Ssa_b} \cdot H + r_{Ssb_b} \cdot G + R_{Ssa}\\ a_{Msb} &= r_{Msb_a} + e(v_{s}) \\ b_{Msb} &= r_{Msb_b} + e(k_{Osb}+k_s) \\ R_{Msb} &= a_{Msb} \cdot H + b_{Msb} \cdot G \\ e &= \hash{ (R_{Msa} + R_{Msb}) \cat \script_s \cat F_s \cat (K_{Osa} + K_{Osb}) \cat C_s} \\ R_{Ms} &= R_{Msa} + R_{Msb} \\ \so_{sb} &= k_{Ssb} - k_{Osb} \\ \so_{s} &= \so_{sa} +\so_{sb} \\ \end{aligned} \tag{26} $$

Bob's transaction now has all the required signatures to complete the transaction. He will then publish the transaction.

Claim XMR

Because Bob has now published the transaction on the Tari blockchain, Alice can calculate the missing Monero key \(x_b\) as follows:

$$ \begin{aligned} b_{Ss} &= b_{Ssa} + b_{Ssb} \\ b_{Ss} - b_{Ssa} &= b_{Ssb} \\ b_{Ssb} &= r_{Ssb_b} + x_b + e_s(k_{Ssb} + k_i) \\ b_{Ssb} - b_{Ssb}' &= r_{Ssb_b} + x_b + e_s(k_{Ssb} + k_i) -(r_{Ssb_b} + e_s(k_{Ssb}+k_i))\\ b_{Ssb} - b_{Ssb}' &= x_b \\ \end{aligned} \tag{27} $$

With \(x_b\) in hand, she can calculate \(X = x_a + x_b\), and with this, she claims the Monero.

The refund

If something goes wrong and Bob never publishes the Monero, Alice needs to wait for the lock height height_1 to pass. This will allow her to create the refund transaction to reclaim her Tari.

Alice constructs the refund transaction with

$$ \begin{aligned} a_{Sra} &= r_{Sra_a} + e_s(v_{i}) \\ b_{Sra} &= r_{Sra_b} + x_a + e_s(k_{Sra} + k_i) \\ e_r &= \hash{ (R_{Sr} + (X_a)) \cat \alpha_i \cat \input_i \cat (K_{Sra} + K_{Srb}) \cat C_i} \\ a_{Sr} &= a_{Sra} + a_{Srb} \\ b_{Sr} &= b_{Sra} + b_{Srb} \\ R_{Sr} &= r_{Sra_a} \cdot H + r_{Sra_b} \cdot G + R_{Srb}\\ a_{Mra} &= r_{Mra_a} + e(v_{r}) \\ b_{Mra} &= r_{Mra_b} + e(k_{Ora}+k_r) \\ R_{Mra} &= a_{Mra} \cdot H + b_{Mra} \cdot G \\ e &= \hash{ (R_{Mra} + R_{Mrb}) \cat \script_s \cat F_s \cat (K_{Ora} + K_{Orb}) \cat C_r} \\ R_{Mr} &= R_{Mra} + R_{Mrb} \\ \so_{ra} &= k_{Sra} - k_{Ora} \\ \so_{r} &= \so_{ra} +\so_{rb} \\ \end{aligned} \tag{28} $$

This allows Alice to claim back her Tari, but it also exposes her Monero key \(x_a\) This means if Bob did publish the Monero UTXO, he could calculate \(X\) using: $$ \begin{aligned} b_{Sr} &= b_{Sra} + b_{Srb} \\ b_{Sr} - b_{Sra} &= b_{Sra} \\ b_{Sra} &= r_{Sra_b} + x_a + e_r(k_{Sra} + k_i) \\ b_{Sra} - b_{Sra}' &= r_{Sra_b} + x_a + e_r(k_{Sra} + k_i) -(r_{Sra_b} + e_r(k_{Sra}+k_i))\\ b_{Sra} - b_{Sra}' &= x_a \\ \end{aligned} \tag{29} $$

The lapse transaction

If something goes wrong and Alice never publishes her refund transaction, Bob needs to wait for the lock height height_2 to pass. This will allow him to create the lapse transaction to claim the Tari.

Bob constructs the lapse transaction with $$ \begin{aligned} a_{Slb} &= r_{Slb_a} + e_l(v_{i}) \\ b_{Slb} &= r_{Slb_b} + x_b + e_l(k_{Slb} + k_i) \\ e_l &= \hash{ (R_{Sl} + (X_b)) \cat \alpha_i \cat \input_i \cat (K_{Sla} + K_{Slb}) \cat C_i} \\ a_{Sl} &= a_{Sla} + a_{Slb} \\ b_{Sl} &= b_{Sla} + b_{Slb} \\ R_{Sl} &= r_{Slb_a} \cdot H + r_{Slb_b} \cdot G + R_{Sla}\\ a_{Mlb} &= r_{Mlb_a} + e(v_{l}) \\ b_{Mlb} &= r_{Mlb_b} + e(k_{Olb}+k_l) \\ R_{Mlb} &= a_{Mlb} \cdot H + b_{Mlb} \cdot G \\ e &= \hash{ (R_{Mla} + R_{Mlb}) \cat \script_l \cat F_l \cat (K_{Ola} + K_{Olb}) \cat C_l} \\ R_{Ml} &= R_{Mla} + R_{Mlb} \\ \so_{lb} &= k_{Slb} - k_{Olb} \\ \so_{r} &= \so_{la} +\so_{lb} \\ \end{aligned} \tag{30} $$

This allows Bob to claim the Tari he originally wanted, but it also exposes his Monero key \(x_b\) This means if Alice ever comes back online, she can calculate \(X\) and claim the Monero she wanted all along using: $$ \begin{aligned} b_{Sl} &= b_{Slb} + b_{Slb} \\ b_{Sl} - b_{Slb} &= b_{Slb} \\ b_{Slb} &= r_{Slb_b} + x_a + e_r(k_{Slb} + k_i) \\ b_{Slb} - b_{Slb}' &= r_{Slb_b} + x_b + e_r(k_{Slb} + k_i) -(r_{Slb_b} + e_r(k_{Slb}+k_i))\\ b_{Slb} - b_{Slb}' &= x_b \\ \end{aligned} \tag{31} $$

Notation

Where possible, the "usual" notation is used to denote terms commonly found in cryptocurrency literature. Lower case characters are used as private keys, while uppercase characters are used as public keys. New terms introduced here are assigned greek lowercase letters in most cases. Some terms used here are noted down in TariScript.

NameSymbolDefinition
subscript s\( _s \)The swap transaction
subscript r\( _r \)The refund transaction
subscript l\( _l \)The lapse transaction
subscript a\( _a \)Belongs to Alice
subscript b\( _b \)Belongs to Bob
Monero key\( X \)Aggregate Monero public key encoded with Ristretto
Alice's Monero key\( X_a \)Alice's partial Monero public key encoded with Ristretto
Bob's Monero key\( X_b \)Bob's partial Monero public key encoded with Ristretto
Monero key\( Xm \)Aggregate Monero public key encoded with Ed25519
Alice's Monero key\( Xm_a \)Alice's partial Monero public key encoded with Ed25519
Bob's Monero key\( Xm_b \)Bob's partial Monero public key encoded with Ed25519
Script key\( K_s \)The script key of the utxo
Alice's Script key\( K_sa \)Alice's partial script key
Bob's Script key\( K_sb \)Bob's partial script key
Alice's adaptor signature\( b'_{Sa} \)Alice's adaptor signature for the signature \( b_{Sa} \) of the script_signature of the utxo
Bob's adaptor signature\( b'_{Sb} \)Bob's adaptor signature for the \( b_{Sb} \) of the script_signature of the utxo
Ristretto G generator\(k \cdot G \)Value k over Curve25519 G generator encoded with Ristretto
Ristretto H generator\(k \cdot H \)Value k over Tari H generator encoded with Ristretto
ed25519 G generator\(k \cdot G_m \)Value k over Curve25519 G generator encoded with Ed25519

RFC-310/Submarine Swap

status: draft

Maintainer(s): S W van Heerden

Licence

The 3-Clause BSD Licence.

Copyright 2021 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

This Request for Comment (RFC) aims to describe how an Atomic swap between Tari and Minotari will be created.

$$ \newcommand{\script}{\alpha} % utxo script \newcommand{\input}{ \theta } \newcommand{\cat}{\Vert} \newcommand{\so}{\gamma} % script offset \newcommand{\hash}[1]{\mathrm{H}\bigl({#1}\bigr)} $$

Comments

Any comments, changes or questions to this PR can be made in one of the following ways:

Description

To be able to exchange Tari and Minotari without the use of some centralized exchange service, we need to do Submarine swaps or Atomic swaps between the two. We want to keep Tari as bare bones as possible with if possible just a commitment and perhaps a range proof, this means that we will not have access to smart contract features typically required for doing submarine swaps. This does not mean it is not possible to do atomic swaps with non-smart contract coins, look at RFC-0241: AtomicSwap XMR to see how this is done with Minotari and Monero.

Method

The primary, happy path outline of a Tari - Minotari submarine swap is described here, and more detail will follow. We assume that Alice wants to trade her Minotari for Bob's Tari.

  • Negotiation - Both parties negotiate the value and other details of the Tari and Minotari commitments.
  • Commitment - Both parties commit to the keys, nonces, inputs, and outputs to use for the transaction.
  • Minotari payment - Alice makes the Minotari payment to a UTXO containing a "special" script described below.
  • Tari Payment - The Tari payment is made to a multiparty scriptless script commitment.
  • Claim Minotari - Bob redeems the Minotari, and in doing so, reveals the Tari private key to Alice only.
  • Claim Tari - Alice may claim the Tari using the revealed key.

Please take note of the notation used in TariScript and specifically notation used on the signatures on the transaction inputs and on the signatures on the transaction outputs. We will note other notations in the Notation section.

TL;DR

The scheme revolves around Alice, who wants to exchange her Minotari for Bob's Tari. Because they don't trust each other, they have to commit some information to do the exchange. And if something goes wrong here, we want to ensure that we can refund both parties either in Tari or Minotari.

How this works is that Alice and Bob create a shared output on both chains. The Tari output is a simple aggregate key to unlock the commitment, while multiple keys are needed to unlock the Minotari UTXO. An aggregate key locks this Tari commitment that neither Alice nor Bob knows, but they both know half of the key. The current Minotari block height determines the unlocking key for the Minotari UTXO.

The process is started by Alice and Bob exchanging and committing to some information. Alice is the first to publish a transaction, which creates the Minotari UTXO. If Bob is happy that the Minotari UTXO has been mined and verifies all the information, he will publish a transaction to create the Tari commitment.

The TariScript script on the UTXO ensures that they will have to reveal their portion of the Tari key when either Alice or Bob spends this. This disclosure allows the other party to claim the Tari by being the only one to own the complete Tari aggregate key.

The script will ensure that at any point in time, at least someone can claim the Minotari UTXO, and if that person does so, the other party can claim the Tari commitment by looking at the spending data. It has two lock heights, determining who can claim the Minotari UTXO if the happy path fails. Before the first lock height, only Bob can claim the Tari; we call this the swap transaction.

If Bob disappears after Alice has posted the Minotari UTXO, Alice can claim the Minotari after the first lock height and before the second lock height; we call this the refund transaction. It ensures that Alice can reclaim her Minotari if Bob disappears, and if Bob reappears, he can reclaim his Tari.

That leaves us with the scenario where Alice disappears after Bob posts the Tari transaction, in which case we need to protect Bob. After the second lock height, only Bob can claim the Minotari; we call this the lapse transaction. The lapse transaction will reveal Bob's Tari key so that if Alice reappears, she can claim the Tari.

Heights, Security, and other considerations

We need to consider a few things for this to be secure, as there are possible scenarios that can reduce the security in the atomic swap.

When looking at the two lock heights, the first lock height should be sufficiently large enough to give ample time for Alice to post the Tari UTXO transaction and for it to be mined with a safe number of confirmations, and for Bob to post the Tari transaction and for it to be mined with a safe number of confirmations. The second lock height should give ample time for Alice after the first lock height to re-claim her Minotari. Larger heights here might make refunds slower, but it should be safer in giving more time to finalize this.

Allowing both to claim the Minotari after the second lock height is, on face value, a safer option. This can be done by enabling either party to claim the script with the lapse transaction. The counterparty can then claim the Tari. However, this will open up an attack vector to enable either party to claim the Tari while claiming the Minotari. Either party could trivially pull off such a scheme by performing a front-running attack and having a bit of luck. The counterparty monitors all broadcast transactions to base nodes. Upon identifying the lapse transaction, they do two things; in quick succession, broadcast their lapse transaction and the transaction to claim the Tari, both with sufficiently high fees. Base nodes will prefer to mine transactions with the higher fees, and thus the counterparty can walk away with both the Minotari and the Tari.

It is also possible to prevent the transaction from being mined after being submitted to the mempool. This can be caused by a combination of a too busy network, not enough fees, or a too-small period in the time locks. When one of these atomic swap transactions gets published to a mempool, we effectively already have all the details exposed. For the atomic swaps, it means we already revealed part of the Tari key, although the actual Minotari transaction has not been mined. But this is true for any HTLC or like script on any blockchain. But in the odd chance that this does happen whereby the fees are too little and time locks not enough, it should be possible to do a child-pays-for-parent transaction to bump up the fees on the transaction to get it mined and confirmed.

Key construction

Using multi-signatures with Schnorr signatures, we need to ensure that the keys are constructed so that key cancellation attacks are not possible. To do this, we create new keys from the chosen public keys \(K_a'\) and \(K_b'\)

$$ \begin{aligned} K_a &= \hash{\hash{K_a' \cat K_b'} \cat K_a' } * K_a' \\ k_a &= \hash{\hash{K_a' \cat K_b'} \cat K_a' } * k_a' \\ K_b &= \hash{\hash{K_a' \cat K_b'} \cat K_b' } * K_b' \\ k_b &= \hash{\hash{K_a' \cat K_b'} \cat K_b' } * k_b' \\ \end{aligned} \tag{1} $$

Key security

The risk of publicly exposing part of the Tari private key is still secure because of how ECC works. We can add two secret keys together and share the public version of both. And at the same time, we know that no one can calculate the secret key with just one part.

$$ \begin{aligned} (k_a + k_b) \cdot G &= k_a \cdot G + k_b \cdot G\\ (k_a + k_b) \cdot G &= K_a + K_b \\ (k_a + k_b) \cdot G &= K \\ \end{aligned} \tag{5} $$

We know that \(K\), \(K_a\), \(K_b\) are public. While \(k\), \(k_a\), \(k_b\) are all private.

But if we expose \(k_b\), we can try to do the following: $$ \begin{aligned} (k_a + k_b) \cdot G &= K_a + K_b\\ k_a \cdot G &= (K_a + K_b - k_b \cdot G) \\ k_a \cdot G &= K_a \\ \end{aligned} \tag{6} $$

However, this is the Elliptic-Curve Discrete Logarithm Problem, and there is no easy solution to solve this on current computer hardware. Thus this is still secure even though we leaked part of the secret key \(k\).

Method

Detail

We rely purely on TariScript to enforce the exposure of the private Tari aggregate keys. Based on Point Time Lock Contracts, the script forces the spending party to supply their Tari private key part as input data to the script, evaluated via the operation ToRistrettoPoint. This TariScript operation will publicly reveal part of the aggregated Tari private key, but this is still secure: see Key security.

The simplicity of this method lies therein that the spending party creates all transactions on their own. Bob requires a pre-image from Alice to complete the swap transaction; Alice needs to verify that Bob published the Tari transaction and that everything is complete as they have agreed. If she is happy, she will provide Bob with the pre-image to claim the Tari UTXO.

TariScript

The Script used for the Tari UTXO is as follows:

   ToRistrettoPoint
   CheckHeight(height_1)
   LtZero
   IFTHEN
      PushPubkey(X_b)
      EqualVerify
      HashSha256 
      PushHash(HASH256{pre_image})
      EqualVerify
      PushPubkey(K_{Sb})
   Else
      CheckHeight(height_2)
      LtZero
      IFTHEN
         PushPubkey(X_a)
         EqualVerify
         PushPubkey(K_{Sa})
      Else
         PushPubkey(X_b)
         EqualVerify
         PushPubkey(K_{Sb})
      ENDIF
   ENDIF

Before height_1, Bob can claim the Minotari UTXO by supplying pre_image and his private Tari key part x_b. After height_1 but before height_2, Alice can claim the Minotari UTXO by supplying her private Tari key part x_a. After height_2, Bob can claim the Minotari UTXO by providing his private Tari key part x_b.

Negotiation

Alice and Bob have to negotiate the exchange rate and the amount exchanged in the atomic swap. They also need to decide how the two UTXO's will look on the blockchain. To accomplish this, the following needs to be finalized:

  • Amount of Minotari to swap for the amount of Tari
  • Tari public key parts \(X_a\), \(X_b\) ,and its aggregate form \(X\)
  • Minotari script key parts \(K_{Sa}\), \(K_{Sb}\)
  • The TariScript to be used in the Minotari UTXO
  • The blinding factor \(k_i\) for the Minotari UTXO, which can be a Diffie-Hellman between their Tari network addresses.

Key selection

Using (1), we create the Tari keys as they are multi-party aggregate keys. The Tari key parts for Alice and Bob is constructed as follows:

$$ \begin{aligned} X_a' &= x_a' \cdot G \\ X_b' &= x_b' \cdot G \\ x_a &= \hash{\hash{X_a' \cat X_b'} \cat X_a' } * x_a' \\ x_b &= \hash{\hash{X_a' \cat X_b'} \cat X_b' } * x_b' \\ x_a &= x_a \\ x_b &= x_b \\ X_a &= \hash{\hash{X_a' \cat X_b'} \cat X_a' } * X_a' \\ X_b &= \hash{\hash{X_a' \cat X_b'} \cat X_b' } * X_b' \\ x &= x_a + x_b + k_i \\ X &= X_a + X_b + k_i \cdot G_m\\ x &= x_a + x_b + k_i \\ X &= X_a + X_b + k_i \cdot G\\ \end{aligned} \tag{7} $$

Commitment phase

This phase allows Alice and Bob to commit to using their keys.

Starting values

Alice needs to provide Bob with the following:

  • Script public key: \( K_{Sa}\)
  • Tari public key \( X_a'\)

Bob needs to provide Alice with the following:

  • Script public key: \( K_{Sb}\)
  • Tari public key \( X_b'\)

Using the above equations in (7), Alice and Bob can calculate \(X\), \(X_a\), \(X_b\)

Minotari payment

Alice will construct the Minotari UTXO with the correct script and publish the containing transaction to the blockchain, knowing that she can reclaim her Minotari if Bob vanishes or tries to break the agreement. This is done with standard Mimblewimble rules and signatures.

Tari payment

When Bob sees that the Minotari UTXO that Alice created is mined on the Minotari blockchain with the correct script, Bob can publish the Tari transaction containing the Tari commitment with the aggregate key \(X = X_a + X_b + k_i \cdot G \).

Claim Minotari

When Alice sees that the Tari commitment that Bob created is confirmed on the second layer containing the correct aggregate key \(X\), she can provide Bob with the required pre_image to spend the Minotari UTXO. She does not have the missing key \(x_b \) to claim the Tari yet, but it will be revealed when Bob claims the Minotari.

Bob can now supply the pre_image and his Tari private key as transaction input to unlock the script.

Claim Tari

Alice can now see that Bob spent the Minotari UTXO, and by examining the input_data required to satisfy the script, she can learn Bob's secret Tari key. Although this private key \( x_b \) is now public knowledge, her part of the Tari spend key is still private, and thus only she knows the complete Tari spend key. She can use this knowledge to claim the Tari commitment.

The refund

If something goes wrong and Bob never publishes the Tari or disappears, Alice needs to wait for the lock height height_1 to pass. This will allow her to reclaim her Minotari, but in doing so, she needs to publish her Tari secret key as input to the script to unlock the Minotari. When Bob comes back online, he can use this public knowledge to reclaim his Tari, as only he knows both parts of the Tari commitment spend key.

The lapse transaction

If something goes wrong and Alice never gives Bob the required pre_image, Bob needs to wait for the lock height height_2 to pass. This will allow him to claim the Minotari he wanted all along, but in doing so, he needs to publish his Tari secret key as input to the script to unlock the Minotari. When Alice comes back online, she can use this public knowledge to claim the Tari she wanted all along as only she now knows both parts of the Tari commitment spend key.

Notation

Where possible, the "usual" notation is used to denote terms commonly found in cryptocurrency literature. Lower case characters are used as private keys, while uppercase characters are used as public keys. New terms introduced here are assigned greek lowercase letters in most cases. Some terms used here are noted down in TariScript.

NameSymbolDefinition
subscript s\( _s \)The swap transaction
subscript r\( _r \)The refund transaction
subscript l\( _l \)The lapse transaction
subscript a\( _a \)Belongs to Alice
subscript b\( _b \)Belongs to Bob
Tari key\( X \)Aggregate Tari public key
Alice's Tari key\( X_a \)Alice's partial Tari public key
Bob's Tari key\( X_b \)Bob's partial Tari public key
Script key\( K_s \)The script key of the utxo
Alice's Script key\( K_sa \)Alice's partial script key
Bob's Script key\( K_sb \)Bob's partial script key
Alice's adaptor signature\( b'_{Sa} \)Alice's adaptor signature for the signature \( b_{Sa} \) of the script_signature of the utxo
Bob's adaptor signature\( b'_{Sb} \)Bob's adaptor signature for the \( b_{Sb} \) of the script_signature of the utxo
Ristretto G generator\(k \cdot G \)Value k over Curve25519 G generator encoded with Ristretto

Change Log

DateChangeAuthor
23 Oct 2023Thaum -> TariCjS77
15 Nov 2022First outlineSWvHeerden

RFC-0388 Bearer Tokens

A Scheme for Granting the Bearer Permissions on Second Layer Assets

status: raw

Maintainer(s): @mikethetike

Licence

The 3-Clause BSD Licence.

Copyright 2022 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community regarding the technological merits of the potential system outlined herein.

Goals

Many dapps and web3 enabled applications require the ability to spend or interact with the assets that a user owns on their behalf. Ethereum and many other blockchains achieve this in a stateful manner by invoking methods such as 'approve' and 'approve_all' in ERC20 and ERC721. Being stateful, the user must spend fees in order to add or revoke permissions. When fees are high, or due to simplistic user interfaces, a user will often grant a much higher level of permission than is required. For example, the user may approve a service to transfer all of their funds, or all of their NFTs.

The scheme proposed by this RFC is a stateless token that the bearer can use to invoke methods on the DAN layer.

Description

This scheme is inspired by Macaroons in that it allows the bearer of a token to delegate a more restrictive token to another bearer.

Structure

Auth Token

FieldTypeDescription
root_noncebytes(32) (optional)a base ID used to revoke permissions(see below)
expires_at_heightu64 (optional)the sidechain height at which this token expires. Timestamps are not reliable in blockchains, so height is used in this case
granted_topubkeythe public key of the grantee. Any bearer using this token will need to prove knowledge of the private key
scopesstring[]A list of scopes granted to this scope
caveatsCaveatExpression[]An ordered list of caveats
based_onToken (optional)If this token is derived from another token, it should be present here
issuerPubKeythe issuer of this token
issuer_sigSignaturea signature signed by issuer of the challenge Hash(root_nonce + granted_to + scopes + caveats + expires_at_height + based_on + issuer )

Caveat Expression

Caveat Expression = <Field> <operator> <Argument>

Field: An arbitrary string that will be interpreted by the code Operator: OneOf("eq", "le", "lt","ge", "gt") Argument: A constant value, to be interpreted by the code

Examples

amount lt 1000
token_id eq 4759

Delegation

A bearer of a token may grant another identity a more specific token, provided that the scopes and caveats are more restrictive.

Validation

The root_nonce, if specified must match the root nonce on record for the issuer.

Open question: should root nonce just be a special caveat?

Before starting execution of the instruction, the list of auth tokens must be validated to ensure that each token is more restrictive than the last.

When executing instructions, caveats MUST be checked if they are relevant to the resources being acted upon. For example, if a function requires a scope transfer, the most specific AuthToken must have that scope present.

Revoking Tokens

Each contract SHOULD store a root nonce for each identity in the contract. To revoke a set of tokens, an identity owner may change their root nonce. This will revoke all tokens based on this root.

Example 1: Invoking an instruction

In this example, Alice wants to allow Bob to spend 100 of a fungible asset called WARI from her account.

Let's assume the transfer function looks like this:

fn transfer(amount: u64, from: PublicKey, to: PublicKey) 
{
   requires_scope("transfer", from);
   // ...
}

Alice creates a token:

/* bob's token */
{
   "scopes": ["transfer"],
   "granted_to": "<Bob's Public Key>",
   "caveats": [
      "amount le 100"
   ],
   "issuer": "<Alice's Public Key>",
   "issuer_sig": "<sig>"
}

Note: The root_nonce and expires_at_height are missing here, but it would have been better for Alice to include these. Also, this token allows Bob to spend up to 100 at a time, but does not restrict Bob from using this token again

Bob can now create the instruction and submit it to the validator node. The validator node checks that the instruction signature matches the public key in granted_to and also checks that the amount parameter is less than or equal to 100. Finally, the validator node checks that the scope includes 'transfer', and Alice's public key (specified in from) matches the auth token's issuer signature.

Example 2: Delegation

Let's continue the example, but in this case Bob wants to allow Carol to spend the funds.

In this case, Bob creates a token for Carol with the following:

/* carol's token */
{
   "scopes": ["transfer"],
   "granted_to": "<Carol's Public Key",
   "caveats": [
      "amount le 90"
   ],
   "issuer": "<Bob's Public Key>",
   "issuer_sig": "<sig>",
   "based_on": {
      /* bob's token */
   }
}

Carol can now create a set of instructions, attach the token and sign it with her public key.

{
   "instructions": [
      {
         "method": "transfer",
         "amount": "80",
         "from": "<alice's pub key>",
         "to": "<carol's pub key>"
      },
      {
         "method": "transfer",
         "amount": "10",
         "from": "<alice's pub key>",
         "to": "<bob's pub key>"
      }
   ],
   "authority": {
      "bearer": {/* carol's token */},
      "sig": "<sig with carol's pub key>"
   }
}

When the validator node receives a set of instructions with this token, it must process each token recursively, with respect to the token it is based on.

RFC-8001/MultiPartyTransactions

status: draft

Maintainer(s): SW van heerden

License

The 3-Clause BSD License.

Copyright 2019 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

The purpose of this document and its content is for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community regarding the technological merits of the potential system outlined herein.

Goals

This document describes a few extension to MimbleWimble to allow multi-party UTXOs.

Description

Multi Party UTXO

Normal MimbleWimble does not have the concept of a multisig UTXO. The UTXO is a commitment C(v,r) = rยทG + vยทH with the value blinded. However, the blinding factor r can be composed of multiple blinding factors where r = r1 + r2 + ... + rn, as Pedersen commitments are linear.

The output commitment can then be constructed as C(v,r) = r1ยทG + r2ยทG + ... + rnยทG + vยทH = (r1 + r2 + ... + rn)ยทG + vยทH. This can be exploited for multiple users where each participant has their own ri and keeps their private blinding factor hidden and only provides their public blinding factor.

The base layer is oblivious as to how the commitment and related signature were constructed. To open such commitments (in order to spend it) only the n-of-n blinding factor r is required, and not the original aggregated signature that was used to sign the transaction. The parties that wants to open the commitment needs to collaborate to produce the n-of-n blinding factor r.

RFC-8002/TransactionProtocol

Transaction Protocol

status: draft

Maintainer(s): Cayle Sharrock

Licence

The 3-Clause BSD Licence.

Copyright 2019 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

This Request for Comment (RFC) describes the transaction protocol for peer-to-peer Tari payments using the Mimblewimble protocol. It also considers some attacks that may be launched against the protocol and offers some discussion around those attacks and potential alternatives to the protocol.

The goal is to describe a transaction protocol that:

  • permits multiple recipients;
  • preserves privacy regarding how many parties are involved in the transaction; and
  • is secure against all reasonable attacks.

Description

The Tari base layer is built using Mimblewimble, which requires that all parties involved in a Tari transfer must interact to construct a valid Mimblewimble transaction.

A valid transaction involves:

  • a set of one or more inputs being spent by the Sender;
  • a set of zero or more outputs being sent to the Sender;
  • a set of recipients, each of whom MUST construct exactly one output; and
  • a set of partial Schnorr signatures which, when aggregated, validates the transaction construction and indicates every party's satisfaction with the terms.

The Issue with Multiple Recipients

Each party involved in a Tari transaction must produce a partial signature, signing the same challenge. This challenge is defined as

$$ e = H(\Sigma R_i \Vert \Sigma P_i \Vert m) $$

where \(R_i\) are public nonces generated by each party for this signature; \(P_i\) are the public Spending Keys; and m is the additional metadata for the transaction. \(\Sigma P_i\) is the value of the (pre-offset) excess that is stored in the transaction kernel.

Notice that every signing party needs to know the sum of all the nonces and public spending keys. This suggests that every party knows how many parties are involved in the transaction, which is not an ideal privacy scenario. It would be preferable if a secure scheme could be found where each recipient interacts only with the sender and does not need to calculate these sums themselves.

Unfortunately, as discussed below, it seems that using any known scheme, it's not possible to satisfy this privacy goal while achieving the desired security level.

The Issue with Multiple Senders

To increase privacy, the public excess values are offset by a constant random value. The choice of this value, as well as fee selection, can only be set once per transaction. The privilege of selecting these values is generally bestowed on the sender, since the sender pays the fee. Allowing multiple sending parties (or equivalently, allowing recipients to provide inputs) would require a negotiation round to set the fee and offset before the transaction could be constructed. This is a complication we don't want to deal with, and so all schemes presented here allow exactly one sender.

Two-party Transactions

Two-party transactions are fairly straightforward and are described in detail by Tari Labs University (TLU). (Refer to Mimblewimble Transaction.)

It is proposed that Tari implement this single-round two-party transaction scheme as a special case to support both online two-party transactions as well as "offline" transactions such as via email, text message and carrier pigeon.

Multiple-recipient Transaction Scheme

sequenceDiagram participant Sender participant Receivers # Sender Initializes the transaction activate Sender Sender-->>Sender: initialize deactivate Sender # Sender transmits initial data to all receivers activate Sender Sender-->>+Receivers: [tx_id, amt_i,H(Rs||Xs),m] note left of Sender: CollectingCommitments note right of Receivers: SendingCommitment Receivers-->>-Sender: [tx_id, H(Ri||Pi)] deactivate Sender # Receiving Outputs activate Sender Sender-->>+Receivers: [tx_id, [Hashed commitments]] note left of Sender: CollectingPubKeys note right of Receivers: SendingOutput Receivers-->>-Sender: [tx_id, Pi, Ri, C_i, RP_i] deactivate Sender alt inflation_error()? Sender--XReceivers: [tx-id, failed] end # Signature collection activate Sender Sender-->>+Receivers: [tx_id, [Rs, Ri] [Xs, Pi]] note left of Sender: CollectingSignatures note right of Receivers: Signing Receivers-->>Receivers: validate nonces, pubkeys and pubkey sum alt invalid Receivers--XSender: failed end Receivers-->>Receivers: Sign Receivers-->>-Sender: [tx_id, s_i] deactivate Sender # Sender sends final notification of result note left of Sender: Finalizing alt is_valid() Sender-->>Receivers: [tx_id, OK] else invalid Sender--XReceivers: [tx_id, Failed] end

** Legend **

SymbolMeaning
tx_idTransaction identifier
amt_iAmount sent to i-th recipient
Rs, RiPublic nonce
Xs, PiPublic excess/key
mMessage metadata
C_iCommitment
RP_iRange proof
[..]Vector of data

Transaction ID

The scheme above makes use of a tx_id field in every peer-to-peer message. Since all messages are stateless and asynchronous, peers need some way of figuring out which message refers to which transaction. The transaction ID fulfils this role.

The ID does not appear on the blockchain in any manner; is purely used to disambiguate Tari transaction messages and can be discarded after the transaction is broadcast to the network.

The tx_id is unique for every receiver so that any observers of the communication will not be able to group receivers together (however, the communication should be over secure channels in general).

The format of the transaction ID is a four-byte, little-endian integer (u64) and is calculated as

H(Rs||i)[0..4]

where i is the i-th recipient in the transaction. The sender can use the tx_id as a hash map key to identify and differentiate recipients.

Replay Attacks

If any party can be convinced to sign a different message with the same nonce, its private keys will be lost. One way of achieving this would be if a virtual machine could be "snapshotted" or otherwise cloned at any point between sharing the public nonce and signing the message. Both copies of the victim's machine will now continue, unaware that there's a copy participating in a signature round. What then happens is:

$$ \begin{align} &\text{Clone A} & &\text{Clone B} \\ e_1 &= H(r_1 \Vert r_s \Vert \dots) & e_2 &= H(r_1 \Vert r_s^* \Vert \dots) \\ s_1 &= r_1 + e_1 \cdot k_1 & s_2 &= r_1 + e_2 \cdot k_1 \\ \end{align} $$

The attacker receives both signatures and trivially calculates the secret key:

$$ \begin{align} \Delta s &= s_1 - s_2 \\ &= k_1(e_1 - e_2) = k_1\Delta e \\ \Rightarrow k_1 &= \frac{\Delta s}{\Delta e} \end{align} $$

We've demonstrated this with the attacker changing their nonce, but literally any alteration to the challenge will provide a new challenge \(e_2\), enabling the attack.

What can we do about this? In fact, it's not possible to eliminate this attack at all! The reason sits with the proof that the Schnorr scheme works as a zero-knowledge protocol; the demonstration of this proof is precisely the attack we're trying to avoid [GOL19]. If we could eliminate this attack, we'd need to come up with a completely different way of proving the zero-knowledge property.

So we can't stop it, but we can make it as tricky as possible for the attacker to trick the receiver into replaying the signature. MuSig does this by requiring parties to share the hash of their nonces beforehand. At its extreme: in the two-party, single-round scheme, for example, the attacker would need to be able to control the victim's machine code execution (like running a debugger), at which point one might think the attacker could read the private key directly from memory anyway.

Rogue Key Attacks

Rogue Key attacks are another type of attack that can occur in multi-signature schemes.

In this case, the attacker has the freedom to choose a key or nonce after the victim has already disclosed theirs. This may allow the attacker to forge a valid signature on behalf of the victim. A recent paper, [DRI19], suggests that any Schnorr-based, two-round multi-signature scheme is vulnerable to a rogue key attack.

How this might apply in an insecure two-round Tari multi-signature scheme is as follows: A receiver sends their public nonce; output commitment and range proof; and public spending key to the sender, but then decides to cancel the transaction by refusing to provide a signature and sending an "Abort" message to the sender instead. The sender could, if they wanted, forge the 2-of-2 signature using this rogue key attack and broadcast the transaction anyway.

Note: This attack is not applicable in the one-round, two-party scheme, since the receiver returns their information in an all-or-nothing manner. However, the receiver could attempt to forge a signature, since they have the Sender's public nonce, but there's nothing they can really do with this signature; they certainly cannot broadcast a transaction with it because they don't have any of the transaction data at this stage.

We avoid rogue-key attacks in the Tari multi-recipient scheme by employing three rounds. In the first round, parties share a hash of their public nonces, which each party can later use to verify that no nonces were changed after the actual public nonces were shared.

RFC-8003/TariUseCases

Digital Asset Framework

status: draft

Maintainer(s): CjS77

Licence

The 3-Clause BSD Licence.

Copyright 2019 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

There will be many types of digital assets that can be issued on Tari. The aim of this Request for Comment (RFC) is to help potential asset issuers identify use cases for Tari that lead to the design of new types of digital assets that may or may not exist in their ecosystems today.

Description

Tari digital assets may exist on the Tari Digital Assets Network (DAN), are perceived to have value and can be owned, with the associated digital data being classified as intangible, personal property.

Types

Digital assets may have the following high-level classification scheme:

  • Symbolic
    • Insignia
    • Mascots
    • Event-driven or historical, e.g. a rivalry or a highly temporal event
  • Artefacts or objects
    • Legendary
    • Rare
    • High demand
    • Artistic
    • Historical
  • Utility
    • Tickets
    • In-game items
    • Points
    • Currency
    • Full or fractional representation
    • Bearer instruments/access token
    • Chat stickers
  • Persona
    • Personality trait(s)
    • Emotion(s)
    • Statistics
    • Superpower(s)
    • Storyline(s)
    • Relationship(s)
    • Avatar

Behaviours

Digital asset tokens may influence the following behavioural types:

  • Advance purchase
  • Experience enhancement
  • Sharing
  • Loyalty
  • Reward for performance
  • Tipping
  • Donating to a charity
  • Collecting
  • Trading
  • Building/combining

Attributes

Digital assets have many different properties, which may be one or more of the following:

  • Digital assets can be interactive:
    • Easter eggs;
      • media
      • dynamic, e.g. imagine if assets were similar to sounds, visualizations or other kinds of "demos" or "gifs")
    • Game mechanics;
    • Evolutionary.
  • Digital assets can be combined to create super assets.
  • Digital assets can be attribute(s) of another digital asset, e.g. wheels of a vehicle or a VIP ticket has two drink cards.
  • Digital assets can have contingencies, e.g. ownership of a digital asset is contingent on ownership of a different digital asset. Using this digital asset is contingent on holding it for a particular duration, etc.
  • Digital assets can have utility, e.g. be useful.
  • Digital assets can be used across platforms, e.g. a digital asset for a game could be used as avatars in a social network.
  • Digital assets can have history.
  • Digital assets can have user-generated tags and/or metadata.

Interactions

Digital asset owners may have the following interactions with the DAN and/or other people:

  • Digital asset owners can attest that they have ownership over their assets at time t.
  • Digital asset owners may attest ownership to an individual, to a group of friends or to the entire world.

Rules

Rules are the governance of how digital assets may be used or transferred, as defined by the asset issuer:

  • Royalty fees. Digital asset issuers can set a royalty that charges a fee every time the digital asset is transferred between parties. The fee as defined by the issuer can be fixed or dynamic, or follow a complex formula, and value is granted to the issuer(s) and/or other entities.
  • Contingency. Digital asset ownership/interaction may be contingent/dependent on another asset.
  • Timing controls. Digital assets can only be transferred or used at particular times.
  • Sharing. Digital assets can be shared with others or even co-owned.
  • Privacy. Ownership of a digital asset can be changed from private to public.
  • Upgradability/versioning. Digital assets can be upgraded and/or versioned.
  • Redeemability. Digital assets can be used once or multiple times.

Examples

Some examples of how different types of digital assets with different attributes, rules and interactions may be manifested are provided here:

Crystal Skull of Akador

  • Is rare, it is 1 of 5.
  • Is legendary:
    • https://indianajones.fandom.com/wiki/Crystal_Skull_of_Akator;
    • press reports that this artefact could be worth $X.
  • Drives collectability.
  • Drives advance purchase, e.g. if you are one of the first 100,000 people to buy tickets to Indiana Jones World, you have a chance of winning this 1 of 5 artefact.
  • Has superpowers and utility, e.g. if you have this item while visiting Indiana Jones World, you get to skip the line three times.
  • Is a contingency for another asset, e.g. if you collect this item, two Sankara stones and the Cross of Coronado, you can buy the ark of the covenant:
    • Ark of the covenant is rare. It is 1 of 1.
    • Ark of the covenant is legendary.
    • Ark of the covenant gives you lifetime access to Indiana Jones World.
    • Ark of the covenant has rules; 20% of the resale price goes to Indiana Jones World.

AB de Villiers' bat

  • Is not rare, it is 1 of 100,000.
  • Is legendary - https://www.youtube.com/watch?v=HK6B2da3DPA.
  • Drives collectability, it is part of a series of bats from famous batsmen.
  • Can be combined with other assets, e.g. be one of the first 10 people to combine six bats to turn this asset into a One Day International (ODI) century bat:
    • ODI century bats are rare, they are 1 of 10;
    • ODI century bats are legendary.
  • Has no superpowers.
  • Has no utility.
  • Has no rules.

OVO Owl x Supreme

  • Is rare, it is 1 of 200.
  • Is legendary:
    • https://www.supremenewyork.com;
    • https://us.octobersveryown.com.
  • Has a game mechanic:
    • Every time itโ€™s transferred, it may become a golden ticket that grants you access to any Drake show.
    • If it becomes a golden ticket and is transferred, it loses its golden ticket superpower.
  • Has utility:
    • Unlocks exclusive media content feat. Drake hosted by OVO SOUND.
    • May become a golden ticket that grants you access to any Drake show.
  • Has rules - every time its transferred Supreme and OVO SOUND receive 25% of the transaction value.

RFC-0385/StableCoin

Privacy-enabled Stablecoin contract design

status: draft

Maintainer(s): Cayle Sharrock, Aaron Feickert

Licence

The 3-Clause BSD Licence.

Copyright 2022 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:RFC-0303_DanOverview.md

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

This Request for Comment (RFC) describes the a possible manifestation of a privacy-preserving stablecoin on the Tari Digital Assets Network (DAN).

Evaluation of existing stablecoins

The top two stablecoins by issuance, or "total value locked" (TVL) are Tether USD (USDT, under various contracts) and USD Coin (USDC). As of August 2023, these two coins accounted for 87% of the total stablecoin market.

Both coins are fully collateralised and the peg is maintained by the centralised issuer.

Although Tether is under scrutiny by authorities, both stablecoins have been in operation for several years. One might reasonably assume that the intersection of the feature set of the two coins' contracts represent a minimal set of requirements for legal operation.

What follows is a brief summary of the features of the two coins.

Tether USD (USDT)

The Tether USD ERC-20 contract is deployed at address 0xdac17f958d2ee523a2206206994597c13d831ec7. The contract code for this contract is presented in Appendix A. As of August 2023, USDT 39B was help in this contract.

The contract has the following key features:

Administration

The following monetary functions can only be called by the contract owner:

  • issue(amount) - issues new tokens to the contract owner's account.
  • redeem(amount) - redeems tokens from the contract owner's account.
  • setParams(...) - Allows owner to set or change fees for transfers. Currently set to zero.

The owner has access to the following fraud/AML functions:

  • addBlackList(address) - Adds an address to the blacklist. Blacklisted addresses are not allowed to send tokens (but they can receive them).
  • removeBlackList(address) - Removes an address from the blacklist.
  • destroyBlackFunds(address) - Destroys all tokens in the blacklisted address, reducing the total supply.

Finally, the owner has access to the following contract management functions:

  • deprecate(address) - Deprecates the contract and supplies the upgraded contract address.
  • pause - pauses the entire contract, preventing any transfers.
  • unpause - unpauses the contract.
  • transferOwnership(address) - transfers ownership of the contract to a new address.

Account owners

The Tether contract records balances through a simple map of standard wallet addresses to amount. Any ethereum address is eligible to hold a USDT balance by virtue of the ERC-20 contract.

Account owners (ie the address matching the transaction sender) have the following abilities:

  • transfer(to, amount) - transfers tokens to another address. Fees get sent to the owner's account.
  • transferFrom(from, to, value) - allows a 3rd party to transfer tokens from the from account to the to account. The 3rd party must have been authorised by the from account to do so using approve and amount must be less than or equal to allowance(from, sender).
  • approve(spender, amount) - authorises a 3rd party to transfer tokens from the owner's account to another account. The 3rd party must call transferFrom to perform the transfer.

Public read-only functions

The following functions are available to the public:

  • totalSupply - returns the total supply of minted tokens. The unit is in millionths of a USD.
  • balanceOf(address) - returns the balance of the given address.
  • allowance(owner, spender) - returns the amount of tokens that the spender is allowed to spend on behalf of the owner.
  • getBlackListStatus(address) - returns whether the given address is blacklisted.
  • getOwner - returns the owner of the contract.

Circle USD (USDC)

The primary Circle USD contract address is 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48.

This is a proxy contract that relays calls to a secondary contract. This is ostensibly done to allow transparent upgrades to the contract, but it does imply additional risk, since the contract code that actually runs and secures your funds is not actually immutable anymore.

The current proxied contract is presented in Appendix B. As of August 2023,
USDC 24B was held in this contract.

It is an ERC-20 contract like Tether, but adds the ability to carry out signature-based operations. Fees are not supported in this contract.

The contract has the following key features:

Administration

The owner has access to the following contract management functions:

  • updatePauser(address) - gives the Pause role to a new address.
  • updateBlacklister(address) - gives the Blacklist role to a new address.
  • transferOwnership(address) - transfers ownership of the contract to a new address.
  • updateMasterMinter(address) - gives the MasterMinter role to a new address.
  • updateRescuer(address) - gives the Rescuer role to a new address.

The address with the Pauser role has access to the following functions:

  • pause - pauses the entire contract, preventing any transfers. Caller must have the Pauser role.
  • unpause - unpauses the contract. Caller must have the Pauser role.

The address with the Blacklist role has access to the following functions:

  • blacklist(address) - Adds an address to the blacklist. Blacklisted addresses are not allowed to send tokens (but they can receive them).
  • unBlacklist(address) - Removes an address from the blacklist. Caller must have the Blacklist role.

The address with the MasterMinter role has access to the following functions:

  • configureMinter(address, amount) - Allows the MasterMinter to add a new minter. The minter is allowed to mint up to the given amount of tokens. New minters have the Mint role.
  • removeMinter(address) - Removes a minter. The address will no longer be able to mint tokens.

Addresses with the Mint role have access to the following functions:

  • mint(address, amount) - Mints tokens to the given address. The amount must be less than or equal to the amount that the minter is allowed to mint. Unlike in Tether, USDC mints can be injected directly into arbitrary accounts.
  • burn(amount) - Burns tokens from the minter's address. Minter must not be blacklisted.

The address with the Rescuer role has access to the following function:

  • rescueERC20(contract, address, amount) - Unconditionally transfers amount funds from the contract to address. This is ostensibly a backdoor that allows the owner to recover funds in the event of a bug.

Account owners

The Tether contract records balances through a simple map of standard wallet addresses to amount. Any ethereum address is eligible to hold a USDT balance by virtue of the ERC-20 contract.

Account owners (ie the address matching the transaction sender) have the following abilities:

  • transfer(to, amount) - transfers tokens to another address. Fees get sent to the owner's account. Neither party may be blacklisted.
  • transferFrom(from, to, value) - allows a 3rd party to transfer tokens from the from account to the to account. The 3rd party must have been authorised by the from account to do so using approve and amount must be less than or equal to allowance(from, sender). Neither from, to or the sender may be blacklisted.
  • approve(spender, amount) - authorises a 3rd party to transfer tokens from the owner's account to another account. The 3rd party must call transferFrom to perform the transfer. Neither the 3rd party or the authorising account may be blacklisted.
  • in(de)creaseAllowance(spender, amount) - increases (decreases) the amount that the spender is allowed to spend on behalf of the owner. Neither the 3rd party or the authorising account may be blacklisted.
  • transferWithAuthorization(to, value, authParams..) - transfers tokens to another address based on the signature provided. This allows clients to batch transfers and save on gas, or services to pay gas on behalf of clients.
  • cancelAuthorization(authParams..) - cancels a pending transferWithAuthorization.
  • permit(owner, spender, value, ...) - Similar to approve but authorisation is provided by a bearer signature.

Public read-only functions

The following functions are available to the public:

  • totalSupply - returns the total supply of minted tokens. The unit is in millionths of a USD.
  • balanceOf(address) - returns the balance of the given address.
  • allowance(owner, spender) - returns the amount of tokens that the spender is allowed to spend on behalf of the owner.
  • isBlacklisted(address) - returns whether the given address is blacklisted.
  • getOwner - returns the owner of the contract.
  • minterAllowance(address) - returns the amount of tokens that the given address is allowed to mint.
  • isMinter(address) - returns whether the given address is a minter.

Feature Comparison of Tether and Circle USD

FeatureTetherCircle USDMinimal requirements
Contract typeERC-20ERC-20
MintingYes (owner only)Yes (multiple minters)Yes
BurningYes (owner only)Yes (multiple minters)Yes
Minting to arbitrary accountNoYesNo
BlacklistingYesYesYes
Blacklisted account can sendNoNoNo
Blacklisted account can receiveYesNoYes
Blacklisted account can be destroyedYesNoNo
Take funds from arbitrary accountNoNoNo
FeesYes (currently zero)NoNo
Contract upgradeYes (via linked list)Yes (via proxy)N/A
Contract pauseYesYesN/A

Description

Key assumptions and requirements

  1. The stablecoin is account-based.
  2. Issuance and redemptions of the stablecoin tokens are performed by a centralised entity, the issuer. The stability of the token and its peg are completely dependent on the issuer's ability to maintain the peg. The issuer is required to act responsibly and issue and redeem tokens in a timely manner in order to engender confidence in the coin and maintain the peg.
  3. Aside from the administrator privileges conferred on the issuer by the stablecoin contract, the coin is operated in a decentralised manner, and transfers are facilitated by the Tari network, and are not processed by any centralised entity, including the issuer.
  4. The issuer has the following "administrator" powers:
    1. Create and authorise new accounts.
    2. Issue new tokens. The new tokens are credited to the issuer's account. The transactions are in the clear so that anyone can verify the total circulating supply of the stablecoin.
    3. Redeem (burn) existing tokens. The burnt tokens are debited from the issuer's account. These transactions are in the clear.
    4. Have access to the full list of account ids.
    5. Blacklist an account. Blacklisted accounts are not allowed to send, or receive, tokens.
    6. Remove an account from the blacklist.
  5. Account-holders have the following abilities:
    1. View their own balance.
    2. If their account is not blacklisted, transfer funds to any other (non-blacklisted) account.
  6. General users:
    1. Cannot see the balance of any account (other than their own).
    2. Can see the total supply of tokens in circulation.
    3. Can apply for a new account by interacting with the issuer.
  7. The possibility of the issuer charging a fee for transfers is not considered in this design.
  8. The issuer can view the balance of account holders.
  9. The issuer cannot unilaterally spend or seize funds from any account holder.

Validator nodes validate all stablecoin transactions. In particular:

  1. Validator nodes cannot determine the value of any transaction.
  2. However, they are able to, and MUST verify that
    1. no coins are created or destroyed in the transaction, i.e. the sum of the parties' balances before and after the transfer are equal,
    2. the transfer value is positive,
    3. the sender has a positive balance after the transaction,
    4. the sender has authorised the transaction,
    5. the sending party holds a valid account,
    6. the sending party is not on the blacklist,
    7. the receiving party holds a valid account,
    8. the receiving party is not on the blacklist.
  3. If any of the conditions in 2 are not met, the transaction is invalid and the validator node MUST reject the transaction.

Implementation

The broad strategy is as follows:

  • The issuer mints stablecoin tokens in equal quantity to the amount of fiat currency deposited with the issuer. These mint transactions are in the clear and anyone can ascertain the total circulating supply of the stablecoin.
  • Issuers can transfer tokens to any account. These transfers are also in the clear.
  • Transfers between account holders are confidential.
  • The issuer can carry out confidential transfers by first transferring tokens from the cleartext issuer account to another, standard account controlled by the issuer, and then performing a confidential transfer from there to the final destination account.
  • Balances are stored in Pedersen commitments, the masks of which are known only to account holders.
  • Balances are also verifiably encrypted to the issuer.
  • Users create a new account by interacting with the issuer. The issuer provides an account id that can be compared against a blacklist for the purposes of determining whether the account is valid or not. The issuer may choose to conduct a KYC procedure out-of-band as part of the account creation process.
  • The full list of accounts form part of a whitelist. Only the issuer can modify the whitelist.
  • A blacklist, which only the issuer can modify, comprises a list of accounts that may no longer participate in stablecoin transactions.
  • Spending authority rests solely with the account owner and required knowledge of the account private key.
  • Transfers are done in two-steps, via the issuance of an e-cheque by the sender, followed by the claiming of the e-cheque by the recipient.

The remainder of this section describes the implementation in more detail.

This design is experimental and not yet suitable for production.

Mathematical furniture

As a point of notation, we sometimes use superscripts in mathematical notation in this note; unless otherwise indicated, this is symbolic and not to be interpreted as indicating exponentiation.

Hash functions and ciphers

We require the use of multiple cryptographic hash functions, which must be sampled independently. It is possible to do this by carefully applying domain separation to a single cryptographic hash function. We further require that when data is provided as input to such a hash function, it is done safely in a manner that is canonical and cannot induce collisions. We use a comma notation to indicate multiple input values, as in $H(a, b, c, \ldots)$.

We also require the use of a key-committing AEAD (authenticated encryption with additional data) construction. It is possible to extend an arbitrary AEAD design in this manner by including a safe cryptographic hash of a derived key.

Let $H_{\text{AEAD}}$ be a cryptographic hash function whose output is the AEAD key space, suitable for $H_ {\text{AEAD}}$ to operate as a key derivation function for the AEAD.

Proving systems

The design will require several zero-knowledge proving systems that allow validators to assert correctness of operations without revealing protected data.

Verifiable encryption

We require the use of a verifiable ElGamal encryption proving system that asserts the value bound to a Pedersen commitment matches the value encrypted to a given public key. This will be used to assert that the issuer can decrypt account balances without knowing the opening to the account's balance commitment.

The proving relation is

$\{ (C, E, R, P); (v, m, r) | C = vG + mH, E = vG + rP, R = rG \}$.

  • The prover samples $x_v, x_m, x_r$ uniformly at random.
  • It computes $C' = x_v G + x_m H$, $E' = x_v G + x_r P$, and $R' = x_r G$ and sends them to the verifier.
  • The verifier samples nonzero $e$ uniformly at random and sends it to the prover.
  • The prover computes $s_v = ev + x_v$, $s_m = em + x_m$, and $s_r = er + x_r$ and sends them to the verifier.
  • The verifier accepts the proof if and only if $eC + C' = s_v G + s_m H$, $eE + E' = s_v G + s_r P$, and $eR + R' = s_r G$.

The proof can be made non-interactive using the Fiat-Shamir technique. It is a sigma protocol for the relation that is complete, 2-special sound, and special honest-verifier zero knowledge.

Value equality

We require the use of a value equality proving system that asserts two Pedersen commitments bind to the same value. This will be used to assert that commitments used in transfers retain balance.

The proving relation is

$\{ (C_1, C_2); (v, m_1, m_2) | C_1 = vG + m_1 H, C_2 = vG + m_2 H \}$.

  • The prover samples $x_v, x_{m_1}, x_{m_2}$ uniformly at random.
  • It computes $C_1' = x_v G + x_{m_1} H$ and $C_2' = x_v G + x_{m_2} H$ and sends them to the verifier.
  • The verifier samples nonzero $e$ uniformly at random and sends it to the prover.
  • The prover computes $s_v = ev + x_v$, $s_{m_1} = em_1 + x_{m_1}$, and $s_{m_2} = em_2 + x_{m_2}$ and sends them to the verifier.
  • The verifier accepts the proof if and only if $eC_1 + C_1' = s_v G + s_{m_1} H$ and $eC_2 + C_2' = s_v G + s_{m_2} H$.

The proof can be made non-interactive using the Fiat-Shamir technique. It is a sigma protocol for the relation that is complete, 2-special sound, and special honest-verifier zero knowledge.

Schnorr representation

We require the use of a Schnorr representation proving system that asserts knowledge of a discrete logarithm. This will be used to sign messages, as well as to assert that a Pedersen commitment binds to a given value.

The proving relation is

$\{ P; p | P = pG \}$.

  • The prover samples $x_p$ uniformly at random.
  • It computes $P' = x_p G$ and sends it to the verifier.
  • The verifier samples nonzero $e$ uniformly at random and sends it to the prover.
  • The prover computes $s_p = ep + x_p$ and sends it to the verifier.
  • The verifier accepts the proof if and only if $eP + P' = s_p G$.

The proof can be made non-interactive using the Fiat-Shamir technique. It is a sigma protocol for the relation that is complete, 2-special sound, and special honest-verifier zero knowledge.

To use this proving system to assert that a commitment $C$ binds to a given value $v$, we set $P = C - vG$ and use the Schnorr representation proving system on this statement using the generator $H$ instead of $G$, and being careful to bind $v$ into the Fiat-Shamir transcript.

Commitment range

We require the use of a commitment range proving system that asserts that all Pedersen commitments in a set bind to values in a specified range. This will be used to prevent balance underflow and overflow that would inflate supply. We assume the intended range is $[0, 2^n)$ for some globally-fixed $n$; in practice, $n = 64$ is typically used (since it is often the case that $n$ must itself be a power of two).

The proving relation is

$\{ (C_j)_{j=0}^{m-1}; (v_j, m_j)_{j=0}^{m-1} : C_j = v_j G + m_j H, v_j \in [0, 2^n) \forall j \}$.

The popular and efficient Bulletproofs and Bulletproofs+ range proving systems may be used for this purpose.

Design

We now describe the stablecoin design.

Issuer

The stablecoin is instantiated by defining the issuer.

The issuer samples its secret key $p$ uniformly at random, and computes the corresponding public key $P = pG$. It produces a Schnorr representation proof $\Pi_P$ using statement $P$ and witness $p$. It sets up the stablecoin as follows:

  • Public key: $P$
  • Public key proof: $\Pi_P$

Validators check this operation by verifying $\Pi_P$.

The issuer also sets up an account for itself using the structure described below for users.

Users

A user who wishes to open an account interacts with the issuer via a side channel. The user samples its secret key $k$ uniformly at random, and computes the corresponding public key $K = kG$. It produces a Schnorr representation proof $\Pi_K$ using statement $K$ and witness $k$. The issuer checks that $K$ has not been used in any other approved account, and verifies $\Pi_K$.

If the issuer approves the account, it signs $K$ by generating a Schnorr representation proof $\Pi_{P, K}$ using statement $P$ and witness $p$, binding $K$ into the Fiat-Shamir transcript.

The issuer sets up the account structure as follows:

  • Public key: $K$
  • Public key proof: $\Pi_K$
  • Issuer proof: $\Pi_{P, K}$
  • State nonce: 0
  • Balance commitment: 0 (identity group element)
  • Issuer-encrypted balance: None
  • User-encrypted balance: None
  • Pending e-cheques: None
  • The state nonce is a value used to track changes to the account and avoid replaying messages.
  • The issuer-encrypted balance field will be populated with a verifiable encryption of the balance that can be decrypted by the issuer.
  • The user-encrypted balance field will be populated with an authenticated encryption of the value and mask corresponding to the balance commitment that can be decrypted by the user for account recovery purposes.
  • The pending e-cheques field will be populated with e-cheques representing transfers that are destined for the account, but that the user has neither approved nor rejected.

Validators check this operation by verifying $\Pi_K$ and $\Pi_{P, K}$, asserting that $K$ does not appear in any other account, and asserting the other constant values are as expected.

Cheques

When a user wishes to transfer funds to another user, it does so by generating an e-cheque. Once validators check the e-cheque, it is added to the recipient's pending e-cheques list and the sender's account is updated.

The e-cheque remains until the recipient approves or rejects it, or until the e-cheque becomes abandoned, as described later.

Suppose the sender wishes to transfer value $v^\Delta$ to a recipient.

  • Let $K_s = k_s G$ and $K_r$ be the sender and recipient keys, respectively.
  • Let $C = vG + mH$ be the sender balance commitment.
  • Let $i$ be the sender state nonce.

The sender does the following:

  • Samples a scalar $m_s^\Delta$ uniformly at random and uses it to generate a commitment $C_s^\Delta = v^\Delta G + m_s^\Delta H$ to the transfer value.
  • Samples a scalar $m^\Delta$ uniformly at random and uses it to generate a commitment $C^\Delta = v^\Delta G + m^\Delta H$ to the transfer value.
  • Generates a proof of value equality $\Pi_\Delta$ on the statement $(C_s^\Delta, C^\Delta)$ and witness $(v^\Delta, m_s^\Delta, m^\Delta)$.
  • Samples a scalar $r$ uniformly at random, and computes $R = rG$.
  • Generates an AEAD key $H_{\text{AEAD}}(r K_r)$ and uses it to encrypt the tuple $(v^\Delta, m^\Delta)$, producing authenticated ciphertext $c$.
  • Sets $E = (v - v^\Delta) G + rP$ as an ElGamal encryption of its new balance, and generates a verifiable encryption proof $\Pi_{\text{enc}}$ on the statement $(C - C_s^\Delta, E, R, P)$ and witness $(v - v^\Delta, m - m_s^\Delta, r)$.
  • Generates a range proof $\Pi_{\text{range}}$ on the statement $\{ C_s^\Delta, C - C_s^\Delta \}$ and witness $\{ ( v^\Delta, m_s^\Delta), (v - v^\Delta, m - m_s^\Delta) \}$.
  • Generates an AEAD key $H_{\text{AEAD}}(k_s)$ and uses it to encrypt the tuple $(v - v^\Delta, m - m_s^\Delta)$, producing authenticated ciphertext $c_s$.
  • Sets the e-cheque to be the tuple $t = (K_s, K_r, C, i, C_s^\Delta, C^\Delta, E, R, \Pi_\Delta, \Pi_{\text{enc}}, \Pi_ {\text{range}}, c_s, c_{sr})$.
  • Signs the e-cheque by generating a Schnorr representation proof $\Pi_t$ using statement $K_s$ and witness $k_s$, binding $t$ into the Fiat-Shamir transcript.

Prior to accepting the e-cheque as valid, validators perform the following checks:

  • Assert that neither $K_s$ nor $K_r$ appear on the blacklist.
  • Verify the proof $\Pi_t$.
  • Look up the sender's account using $K_s$ and assert that $C$ and $i$ match the corresponding values in the account.
  • Verify the proofs $\Pi_\Delta$, $\Pi_{\text{enc}}$, and $\Pi_{\text{range}}$.

If these checks pass, validators add the e-cheque to the recipient's pending e-cheques list, and update the sender's account as follows:

  • Increment the state nonce.
  • Set the balance commitment to $C - C_s^\Delta$.
  • Set the issuer-encrypted balance to the tuple $(E, R)$.
  • Set the user-encrypted balance to $c_s$.

When the recipient sees the e-cheque, it can either endorse or void it.

Endorsement means the recipient intends to accept the funds and wishes to have its account updated accordingly. Voiding means the recipient does not intend to accept the funds and wishes for the sender to be able to claim them back. Prior to making this determination, the recipient does the following:

  • Generates an AEAD key $H_{\text{AEAD}}(k_r R)$ and uses it to authenticate and decrypt $c$, producing the tuple $( v^\Delta, m^\Delta)$.
  • Checks that $C^\Delta = v^\Delta G + m^\Delta H$.

If these checks pass, it may choose to endorse or void the e-cheque. If they fail, it must void the e-cheque.

Voiding an e-cheque

Suppose the recipient wishes to void an e-cheque $t$.

It does the following:

  • Sets the voiding to be the tuple $t_{\text{void}} = (t)$.
  • Signs the voiding by generating a Schnorr representation proof $\Pi_t$ using statement $K_r$ and witness $k_r$, binding $t_{\text{void}}$ into the Fiat-Shamir transcript.

Prior to accepting the voiding as valid, validators perform the following checks:

  • Assert that neither $K_s$ nor $K_r$ appear on the blacklist.
  • Verify the proof $\Pi_t$.

If these checks pass, validators annotate $t$ in the recipient's pending e-cheques list to indicate the voiding.

Endorsing an e-cheque

Suppose the recipient wishes to endorse an e-cheque $t$ with $C^\Delta = v^\Delta G + m^\Delta H$ from its pending e-cheques list.

Now let $C = vG + mH$ be the recipient balance commitment.

Let $i$ be the recipient state nonce.

The recipient does the following:

  • Samples a scalar $m_r^\Delta$ uniformly at random, and uses it to generate a commitment $C_r^\Delta = v^\Delta G + m_r^\Delta H$ to the transfer value.
  • Generates a proof of value equality $\Pi_\Delta$ on the statement $(C_r^\Delta, C^\Delta)$ and witness $(v^\Delta, m_r^\Delta, m^\Delta)$.
  • Samples a scalar $r$ uniformly at random, and computes $R = rG$.
  • Sets $E = (v + v^\Delta) G + rP$ as an ElGamal encryption of its new balance, and generates a verifiable encryption proof $\Pi_{\text{enc}}$ on the statement $(C + C_r^\Delta, E, R, P)$ and witness $(v + v^\Delta, m + m_r^\Delta, r)$.
  • Generates an AEAD key $H_{\text{AEAD}}(k_r)$ and uses it to encrypt the tuple $(v + v^\Delta, m + m_r^\Delta)$, producing authenticated ciphertext $c_r$.
  • Sets the endorsement to be the tuple $t_{\text{end}} = (t, C, i, C_r^\Delta, E, R, \Pi_\Delta, \Pi_{\text{enc}}, c_r)$.
  • Signs the endorsement by generating a Schnorr representation proof $\Pi_t$ using statement $K_r$ and witness $k_r$, binding $t_{\text{end}}$ into the Fiat-Shamir transcript.

It is also possible for the original sender of an e-cheque to endorse it as well. This can arise in two cases:

  • The recipient of the e-cheque has voided it.
  • The recipient of the e-cheque has neither accepted nor voided it, and a protocol-specified period of time has passed.

The process is the same as above, with the sender now playing the role of the recipient.

Prior to accepting the endorsement as valid, validators perform the following checks:

  • Assert that neither $K_s$ nor $K_r$ appear on the blacklist.
  • Verify the proof $\Pi_t$.
  • Look up the recipient's account using $K_r$ and assert that $C$ and $i$ match the corresponding values in the account, and that $t$ appears in the pending e-cheques list.
  • Verify the proofs $\Pi_\Delta$ and $\Pi_{\text{enc}}$.

If these checks pass, validators remove the e-cheque $t$ from the recipient's pending e-cheques list, and update the recipient's account as follows:

  • Increment the state nonce.
  • Set the balance commitment to $C + C_r^\Delta$.
  • Set the issuer-encrypted balance to the tuple $(E, R)$.
  • Set the user-encrypted balance to $c_r$.

Issuer balance visibility

The issuer can privately view any user's balance at any time using ElGamal decryption. Suppose it wishes to view the balance of a user whose issuer-encrypted balance is $(E, R)$.

It does the following:

  • Sets $V = E - pR$.
  • Finds $v$ such that $V = vG$; this is the user's balance.

Because the search space for balances is limited, the issuer can optimize this process. For example, it could produce a lookup table mapping $vG \mapsto v$ for reasonable values $v$, or simply use brute force on the search space.

User account recovery

If the user loses access to their account balance, they can recover the opening to their balance commitment to regain access, provided they still hold the private key $k$.

Suppose such a user with key $k$ queries validators for its account's user-encrypted balance.

It does the following:

  • Generates an AEAD key $H_{\text{AEAD}}(k)$ and uses it to authenticate and decrypt the user-encrypted balance, producing the tuple $(v, m)$; this is the opening to their balance commitment.

Issuer transfers

When transferring funds from the issuer to a user, or from a user to the issuer, it is required that the value be publicly visible for transparency purposes. However, we wish to reuse as much of the existing design as possible, in order to simplify the design and reduce engineering risk.

If the issuer wishes to transfer funds to a user, it produces an e-cheque with the following modifications:

  • It sets $r = 0$.
  • It uses a zero key to produce $c_s$.

When validating such an e-cheque, validators additionally do the following:

  • Assert that $R = 0$.
  • Assert that $c_s$ decrypts using a zero key, and that the resulting opening is valid.
  • Decrypt $c_s$ and assert the resulting opening is valid.
  • Decrypt $c$ using a zero key and assert the resulting opening is valid.

If a user wishes to transfer funds to the issuer, it produces an e-cheque with the following modifications:

  • It sets $r = 0$.

When validating such an e-cheque, validators additionally do the following:

  • Assert that $R = 0$.
  • Decrypt $c$ using a zero key and asserting the resulting opening is valid.

If the issuer wishes to accept transfer funds from a user, it produces an endorsement with the following modifications:

  • It uses a zero key to produce $c_r$.

When validating such an endorsement, validators additionally do the following:

  • Assert that $c_r$ decrypts using a zero key, and that the resulting opening is valid.

This design allows for transparent analysis of the issuer's balance and e-cheques.

Final notes

There are several variations of this contract that could be implemented.

For example, removing the encrypted balance fields would make user balances opaque to the issuer as well, while also simplifying the design considerably.

The transfer process can be augmented to include dummy inputs and outputs, using a strategy similar to that used in Lelantus Spark. This would obfuscate the parties in a transfer, dramatically improving privacy.

The use of e-cheques as the primary value transfer vehicle opens up many possibilities for adding additional margin of error to on-chain financial transactions:

  • For example, e-cheques could have a holding time associated with them, to allow parties to validate payments out-of-band.
  • They could have additional claim constraints, which would simplify escrow contracts and swap contracts while improving security.

Appendix A - Tether USD contract

/**
 *Submitted for verification at Etherscan.io on 2017-11-28
*/

pragma solidity ^0.4.17;

/**
 * @title SafeMath
 * @dev Math operations with safety checks that throw on error
 */
library SafeMath {
    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        if (a == 0) {
            return 0;
        }
        uint256 c = a * b;
        assert(c / a == b);
        return c;
    }

    function div(uint256 a, uint256 b) internal pure returns (uint256) {
        // assert(b > 0); // Solidity automatically throws when dividing by 0
        uint256 c = a / b;
        // assert(a == b * c + a % b); // There is no case in which this doesn't hold
        return c;
    }

    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        assert(b <= a);
        return a - b;
    }

    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        assert(c >= a);
        return c;
    }
}

/**
 * @title Ownable
 * @dev The Ownable contract has an owner address, and provides basic authorization control
 * functions, this simplifies the implementation of "user permissions".
 */
contract Ownable {
    address public owner;

    /**
      * @dev The Ownable constructor sets the original `owner` of the contract to the sender
      * account.
      */
    function Ownable() public {
        owner = msg.sender;
    }

    /**
      * @dev Throws if called by any account other than the owner.
      */
    modifier onlyOwner() {
        require(msg.sender == owner);
        _;
    }

    /**
    * @dev Allows the current owner to transfer control of the contract to a newOwner.
    * @param newOwner The address to transfer ownership to.
    */
    function transferOwnership(address newOwner) public onlyOwner {
        if (newOwner != address(0)) {
            owner = newOwner;
        }
    }

}

/**
 * @title ERC20Basic
 * @dev Simpler version of ERC20 interface
 * @dev see https://github.com/ethereum/EIPs/issues/20
 */
contract ERC20Basic {
    uint public _totalSupply;
    function totalSupply() public constant returns (uint);
    function balanceOf(address who) public constant returns (uint);
    function transfer(address to, uint value) public;
    event Transfer(address indexed from, address indexed to, uint value);
}

/**
 * @title ERC20 interface
 * @dev see https://github.com/ethereum/EIPs/issues/20
 */
contract ERC20 is ERC20Basic {
    function allowance(address owner, address spender) public constant returns (uint);
    function transferFrom(address from, address to, uint value) public;
    function approve(address spender, uint value) public;
    event Approval(address indexed owner, address indexed spender, uint value);
}

/**
 * @title Basic token
 * @dev Basic version of StandardToken, with no allowances.
 */
contract BasicToken is Ownable, ERC20Basic {
    using SafeMath for uint;

    mapping(address => uint) public balances;

    // additional variables for use if transaction fees ever became necessary
    uint public basisPointsRate = 0;
    uint public maximumFee = 0;

    /**
    * @dev Fix for the ERC20 short address attack.
    */
    modifier onlyPayloadSize(uint size) {
        require(!(msg.data.length < size + 4));
        _;
    }

    /**
    * @dev transfer token for a specified address
    * @param _to The address to transfer to.
    * @param _value The amount to be transferred.
    */
    function transfer(address _to, uint _value) public onlyPayloadSize(2 * 32) {
        uint fee = (_value.mul(basisPointsRate)).div(10000);
        if (fee > maximumFee) {
            fee = maximumFee;
        }
        uint sendAmount = _value.sub(fee);
        balances[msg.sender] = balances[msg.sender].sub(_value);
        balances[_to] = balances[_to].add(sendAmount);
        if (fee > 0) {
            balances[owner] = balances[owner].add(fee);
            Transfer(msg.sender, owner, fee);
        }
        Transfer(msg.sender, _to, sendAmount);
    }

    /**
    * @dev Gets the balance of the specified address.
    * @param _owner The address to query the the balance of.
    * @return An uint representing the amount owned by the passed address.
    */
    function balanceOf(address _owner) public constant returns (uint balance) {
        return balances[_owner];
    }

}

/**
 * @title Standard ERC20 token
 *
 * @dev Implementation of the basic standard token.
 * @dev https://github.com/ethereum/EIPs/issues/20
 * @dev Based oncode by FirstBlood: https://github.com/Firstbloodio/token/blob/master/smart_contract/FirstBloodToken.sol
 */
contract StandardToken is BasicToken, ERC20 {

    mapping (address => mapping (address => uint)) public allowed;

    uint public constant MAX_UINT = 2**256 - 1;

    /**
    * @dev Transfer tokens from one address to another
    * @param _from address The address which you want to send tokens from
    * @param _to address The address which you want to transfer to
    * @param _value uint the amount of tokens to be transferred
    */
    function transferFrom(address _from, address _to, uint _value) public onlyPayloadSize(3 * 32) {
        var _allowance = allowed[_from][msg.sender];

        // Check is not needed because sub(_allowance, _value) will already throw if this condition is not met
        // if (_value > _allowance) throw;

        uint fee = (_value.mul(basisPointsRate)).div(10000);
        if (fee > maximumFee) {
            fee = maximumFee;
        }
        if (_allowance < MAX_UINT) {
            allowed[_from][msg.sender] = _allowance.sub(_value);
        }
        uint sendAmount = _value.sub(fee);
        balances[_from] = balances[_from].sub(_value);
        balances[_to] = balances[_to].add(sendAmount);
        if (fee > 0) {
            balances[owner] = balances[owner].add(fee);
            Transfer(_from, owner, fee);
        }
        Transfer(_from, _to, sendAmount);
    }

    /**
    * @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender.
    * @param _spender The address which will spend the funds.
    * @param _value The amount of tokens to be spent.
    */
    function approve(address _spender, uint _value) public onlyPayloadSize(2 * 32) {

        // To change the approve amount you first have to reduce the addresses`
        //  allowance to zero by calling `approve(_spender, 0)` if it is not
        //  already 0 to mitigate the race condition described here:
        //  https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
        require(!((_value != 0) && (allowed[msg.sender][_spender] != 0)));

        allowed[msg.sender][_spender] = _value;
        Approval(msg.sender, _spender, _value);
    }

    /**
    * @dev Function to check the amount of tokens than an owner allowed to a spender.
    * @param _owner address The address which owns the funds.
    * @param _spender address The address which will spend the funds.
    * @return A uint specifying the amount of tokens still available for the spender.
    */
    function allowance(address _owner, address _spender) public constant returns (uint remaining) {
        return allowed[_owner][_spender];
    }

}


/**
 * @title Pausable
 * @dev Base contract which allows children to implement an emergency stop mechanism.
 */
contract Pausable is Ownable {
  event Pause();
  event Unpause();

  bool public paused = false;


  /**
   * @dev Modifier to make a function callable only when the contract is not paused.
   */
  modifier whenNotPaused() {
    require(!paused);
    _;
  }

  /**
   * @dev Modifier to make a function callable only when the contract is paused.
   */
  modifier whenPaused() {
    require(paused);
    _;
  }

  /**
   * @dev called by the owner to pause, triggers stopped state
   */
  function pause() onlyOwner whenNotPaused public {
    paused = true;
    Pause();
  }

  /**
   * @dev called by the owner to unpause, returns to normal state
   */
  function unpause() onlyOwner whenPaused public {
    paused = false;
    Unpause();
  }
}

contract BlackList is Ownable, BasicToken {

    /////// Getters to allow the same blacklist to be used also by other contracts (including upgraded Tether) ///////
    function getBlackListStatus(address _maker) external constant returns (bool) {
        return isBlackListed[_maker];
    }

    function getOwner() external constant returns (address) {
        return owner;
    }

    mapping (address => bool) public isBlackListed;

    function addBlackList (address _evilUser) public onlyOwner {
        isBlackListed[_evilUser] = true;
        AddedBlackList(_evilUser);
    }

    function removeBlackList (address _clearedUser) public onlyOwner {
        isBlackListed[_clearedUser] = false;
        RemovedBlackList(_clearedUser);
    }

    function destroyBlackFunds (address _blackListedUser) public onlyOwner {
        require(isBlackListed[_blackListedUser]);
        uint dirtyFunds = balanceOf(_blackListedUser);
        balances[_blackListedUser] = 0;
        _totalSupply -= dirtyFunds;
        DestroyedBlackFunds(_blackListedUser, dirtyFunds);
    }

    event DestroyedBlackFunds(address _blackListedUser, uint _balance);

    event AddedBlackList(address _user);

    event RemovedBlackList(address _user);

}

contract UpgradedStandardToken is StandardToken{
    // those methods are called by the legacy contract
    // and they must ensure msg.sender to be the contract address
    function transferByLegacy(address from, address to, uint value) public;
    function transferFromByLegacy(address sender, address from, address spender, uint value) public;
    function approveByLegacy(address from, address spender, uint value) public;
}

contract TetherToken is Pausable, StandardToken, BlackList {

    string public name;
    string public symbol;
    uint public decimals;
    address public upgradedAddress;
    bool public deprecated;

    //  The contract can be initialized with a number of tokens
    //  All the tokens are deposited to the owner address
    //
    // @param _balance Initial supply of the contract
    // @param _name Token Name
    // @param _symbol Token symbol
    // @param _decimals Token decimals
    function TetherToken(uint _initialSupply, string _name, string _symbol, uint _decimals) public {
        _totalSupply = _initialSupply;
        name = _name;
        symbol = _symbol;
        decimals = _decimals;
        balances[owner] = _initialSupply;
        deprecated = false;
    }

    // Forward ERC20 methods to upgraded contract if this one is deprecated
    function transfer(address _to, uint _value) public whenNotPaused {
        require(!isBlackListed[msg.sender]);
        if (deprecated) {
            return UpgradedStandardToken(upgradedAddress).transferByLegacy(msg.sender, _to, _value);
        } else {
            return super.transfer(_to, _value);
        }
    }

    // Forward ERC20 methods to upgraded contract if this one is deprecated
    function transferFrom(address _from, address _to, uint _value) public whenNotPaused {
        require(!isBlackListed[_from]);
        if (deprecated) {
            return UpgradedStandardToken(upgradedAddress).transferFromByLegacy(msg.sender, _from, _to, _value);
        } else {
            return super.transferFrom(_from, _to, _value);
        }
    }

    // Forward ERC20 methods to upgraded contract if this one is deprecated
    function balanceOf(address who) public constant returns (uint) {
        if (deprecated) {
            return UpgradedStandardToken(upgradedAddress).balanceOf(who);
        } else {
            return super.balanceOf(who);
        }
    }

    // Forward ERC20 methods to upgraded contract if this one is deprecated
    function approve(address _spender, uint _value) public onlyPayloadSize(2 * 32) {
        if (deprecated) {
            return UpgradedStandardToken(upgradedAddress).approveByLegacy(msg.sender, _spender, _value);
        } else {
            return super.approve(_spender, _value);
        }
    }

    // Forward ERC20 methods to upgraded contract if this one is deprecated
    function allowance(address _owner, address _spender) public constant returns (uint remaining) {
        if (deprecated) {
            return StandardToken(upgradedAddress).allowance(_owner, _spender);
        } else {
            return super.allowance(_owner, _spender);
        }
    }

    // deprecate current contract in favour of a new one
    function deprecate(address _upgradedAddress) public onlyOwner {
        deprecated = true;
        upgradedAddress = _upgradedAddress;
        Deprecate(_upgradedAddress);
    }

    // deprecate current contract if favour of a new one
    function totalSupply() public constant returns (uint) {
        if (deprecated) {
            return StandardToken(upgradedAddress).totalSupply();
        } else {
            return _totalSupply;
        }
    }

    // Issue a new amount of tokens
    // these tokens are deposited into the owner address
    //
    // @param _amount Number of tokens to be issued
    function issue(uint amount) public onlyOwner {
        require(_totalSupply + amount > _totalSupply);
        require(balances[owner] + amount > balances[owner]);

        balances[owner] += amount;
        _totalSupply += amount;
        Issue(amount);
    }

    // Redeem tokens.
    // These tokens are withdrawn from the owner address
    // if the balance must be enough to cover the redeem
    // or the call will fail.
    // @param _amount Number of tokens to be issued
    function redeem(uint amount) public onlyOwner {
        require(_totalSupply >= amount);
        require(balances[owner] >= amount);

        _totalSupply -= amount;
        balances[owner] -= amount;
        Redeem(amount);
    }

    function setParams(uint newBasisPoints, uint newMaxFee) public onlyOwner {
        // Ensure transparency by hardcoding limit beyond which fees can never be added
        require(newBasisPoints < 20);
        require(newMaxFee < 50);

        basisPointsRate = newBasisPoints;
        maximumFee = newMaxFee.mul(10**decimals);

        Params(basisPointsRate, maximumFee);
    }

    // Called when new token are issued
    event Issue(uint amount);

    // Called when tokens are redeemed
    event Redeem(uint amount);

    // Called when contract is deprecated
    event Deprecate(address newAddress);

    // Called if contract ever adds fees
    event Params(uint feeBasisPoints, uint maxFee);
}

Appendix B - Circle USD contract

The Circle USD contract runs as a proxy contract. Therefore the code that is actually active can be changed at any time. The following is the contract code that was active as of 25 August 2023, deployed to address 0xa2327a938febf5fec13bacfb16ae10ecbc4cbdcf.

/**
 *Submitted for verification at Etherscan.io on 2021-04-17
*/

// File: @openzeppelin/contracts/math/SafeMath.sol

// SPDX-License-Identifier: MIT

pragma solidity ^0.6.0;

/**
 * @dev Wrappers over Solidity's arithmetic operations with added overflow
 * checks.
 *
 * Arithmetic operations in Solidity wrap on overflow. This can easily result
 * in bugs, because programmers usually assume that an overflow raises an
 * error, which is the standard behavior in high level programming languages.
 * `SafeMath` restores this intuition by reverting the transaction when an
 * operation overflows.
 *
 * Using this library instead of the unchecked operations eliminates an entire
 * class of bugs, so it's recommended to use it always.
 */
library SafeMath {
    /**
     * @dev Returns the addition of two unsigned integers, reverting on
     * overflow.
     *
     * Counterpart to Solidity's `+` operator.
     *
     * Requirements:
     *
     * - Addition cannot overflow.
     */
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        require(c >= a, "SafeMath: addition overflow");

        return c;
    }

    /**
     * @dev Returns the subtraction of two unsigned integers, reverting on
     * overflow (when the result is negative).
     *
     * Counterpart to Solidity's `-` operator.
     *
     * Requirements:
     *
     * - Subtraction cannot overflow.
     */
    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        return sub(a, b, "SafeMath: subtraction overflow");
    }

    /**
     * @dev Returns the subtraction of two unsigned integers, reverting with custom message on
     * overflow (when the result is negative).
     *
     * Counterpart to Solidity's `-` operator.
     *
     * Requirements:
     *
     * - Subtraction cannot overflow.
     */
    function sub(
        uint256 a,
        uint256 b,
        string memory errorMessage
    ) internal pure returns (uint256) {
        require(b <= a, errorMessage);
        uint256 c = a - b;

        return c;
    }

    /**
     * @dev Returns the multiplication of two unsigned integers, reverting on
     * overflow.
     *
     * Counterpart to Solidity's `*` operator.
     *
     * Requirements:
     *
     * - Multiplication cannot overflow.
     */
    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        // Gas optimization: this is cheaper than requiring 'a' not being zero, but the
        // benefit is lost if 'b' is also tested.
        // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522
        if (a == 0) {
            return 0;
        }

        uint256 c = a * b;
        require(c / a == b, "SafeMath: multiplication overflow");

        return c;
    }

    /**
     * @dev Returns the integer division of two unsigned integers. Reverts on
     * division by zero. The result is rounded towards zero.
     *
     * Counterpart to Solidity's `/` operator. Note: this function uses a
     * `revert` opcode (which leaves remaining gas untouched) while Solidity
     * uses an invalid opcode to revert (consuming all remaining gas).
     *
     * Requirements:
     *
     * - The divisor cannot be zero.
     */
    function div(uint256 a, uint256 b) internal pure returns (uint256) {
        return div(a, b, "SafeMath: division by zero");
    }

    /**
     * @dev Returns the integer division of two unsigned integers. Reverts with custom message on
     * division by zero. The result is rounded towards zero.
     *
     * Counterpart to Solidity's `/` operator. Note: this function uses a
     * `revert` opcode (which leaves remaining gas untouched) while Solidity
     * uses an invalid opcode to revert (consuming all remaining gas).
     *
     * Requirements:
     *
     * - The divisor cannot be zero.
     */
    function div(
        uint256 a,
        uint256 b,
        string memory errorMessage
    ) internal pure returns (uint256) {
        require(b > 0, errorMessage);
        uint256 c = a / b;
        // assert(a == b * c + a % b); // There is no case in which this doesn't hold

        return c;
    }

    /**
     * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo),
     * Reverts when dividing by zero.
     *
     * Counterpart to Solidity's `%` operator. This function uses a `revert`
     * opcode (which leaves remaining gas untouched) while Solidity uses an
     * invalid opcode to revert (consuming all remaining gas).
     *
     * Requirements:
     *
     * - The divisor cannot be zero.
     */
    function mod(uint256 a, uint256 b) internal pure returns (uint256) {
        return mod(a, b, "SafeMath: modulo by zero");
    }

    /**
     * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo),
     * Reverts with custom message when dividing by zero.
     *
     * Counterpart to Solidity's `%` operator. This function uses a `revert`
     * opcode (which leaves remaining gas untouched) while Solidity uses an
     * invalid opcode to revert (consuming all remaining gas).
     *
     * Requirements:
     *
     * - The divisor cannot be zero.
     */
    function mod(
        uint256 a,
        uint256 b,
        string memory errorMessage
    ) internal pure returns (uint256) {
        require(b != 0, errorMessage);
        return a % b;
    }
}

// File: @openzeppelin/contracts/token/ERC20/IERC20.sol

pragma solidity ^0.6.0;

/**
 * @dev Interface of the ERC20 standard as defined in the EIP.
 */
interface IERC20 {
    /**
     * @dev Returns the amount of tokens in existence.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns the amount of tokens owned by `account`.
     */
    function balanceOf(address account) external view returns (uint256);

    /**
     * @dev Moves `amount` tokens from the caller's account to `recipient`.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transfer(address recipient, uint256 amount)
    external
    returns (bool);

    /**
     * @dev Returns the remaining number of tokens that `spender` will be
     * allowed to spend on behalf of `owner` through {transferFrom}. This is
     * zero by default.
     *
     * This value changes when {approve} or {transferFrom} are called.
     */
    function allowance(address owner, address spender)
    external
    view
    returns (uint256);

    /**
     * @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * IMPORTANT: Beware that changing an allowance with this method brings the risk
     * that someone may use both the old and the new allowance by unfortunate
     * transaction ordering. One possible solution to mitigate this race
     * condition is to first reduce the spender's allowance to 0 and set the
     * desired value afterwards:
     * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
     *
     * Emits an {Approval} event.
     */
    function approve(address spender, uint256 amount) external returns (bool);

    /**
     * @dev Moves `amount` tokens from `sender` to `recipient` using the
     * allowance mechanism. `amount` is then deducted from the caller's
     * allowance.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(
        address sender,
        address recipient,
        uint256 amount
    ) external returns (bool);

    /**
     * @dev Emitted when `value` tokens are moved from one account (`from`) to
     * another (`to`).
     *
     * Note that `value` may be zero.
     */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * @dev Emitted when the allowance of a `spender` for an `owner` is set by
     * a call to {approve}. `value` is the new allowance.
     */
    event Approval(
        address indexed owner,
        address indexed spender,
        uint256 value
    );
}

// File: contracts/v1/AbstractFiatTokenV1.sol

/**
 * Copyright (c) 2018-2020 CENTRE SECZ
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

pragma solidity 0.6.12;

abstract contract AbstractFiatTokenV1 is IERC20 {
    function _approve(
        address owner,
        address spender,
        uint256 value
    ) internal virtual;

    function _transfer(
        address from,
        address to,
        uint256 value
    ) internal virtual;
}

// File: contracts/v1/Ownable.sol

/**
 * Copyright (c) 2018 zOS Global Limited.
 * Copyright (c) 2018-2020 CENTRE SECZ
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
pragma solidity 0.6.12;

/**
 * @notice The Ownable contract has an owner address, and provides basic
 * authorization control functions
 * @dev Forked from https://github.com/OpenZeppelin/openzeppelin-labs/blob/3887ab77b8adafba4a26ace002f3a684c1a3388b/upgradeability_ownership/contracts/ownership/Ownable.sol
 * Modifications:
 * 1. Consolidate OwnableStorage into this contract (7/13/18)
 * 2. Reformat, conform to Solidity 0.6 syntax, and add error messages (5/13/20)
 * 3. Make public functions external (5/27/20)
 */
contract Ownable {
    // Owner of the contract
    address private _owner;

    /**
     * @dev Event to show ownership has been transferred
     * @param previousOwner representing the address of the previous owner
     * @param newOwner representing the address of the new owner
     */
    event OwnershipTransferred(address previousOwner, address newOwner);

    /**
     * @dev The constructor sets the original owner of the contract to the sender account.
     */
    constructor() public {
        setOwner(msg.sender);
    }

    /**
     * @dev Tells the address of the owner
     * @return the address of the owner
     */
    function owner() external view returns (address) {
        return _owner;
    }

    /**
     * @dev Sets a new owner address
     */
    function setOwner(address newOwner) internal {
        _owner = newOwner;
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        require(msg.sender == _owner, "Ownable: caller is not the owner");
        _;
    }

    /**
     * @dev Allows the current owner to transfer control of the contract to a newOwner.
     * @param newOwner The address to transfer ownership to.
     */
    function transferOwnership(address newOwner) external onlyOwner {
        require(
            newOwner != address(0),
            "Ownable: new owner is the zero address"
        );
        emit OwnershipTransferred(_owner, newOwner);
        setOwner(newOwner);
    }
}

// File: contracts/v1/Pausable.sol

/**
 * Copyright (c) 2016 Smart Contract Solutions, Inc.
 * Copyright (c) 2018-2020 CENTRE SECZ0
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

pragma solidity 0.6.12;

/**
 * @notice Base contract which allows children to implement an emergency stop
 * mechanism
 * @dev Forked from https://github.com/OpenZeppelin/openzeppelin-contracts/blob/feb665136c0dae9912e08397c1a21c4af3651ef3/contracts/lifecycle/Pausable.sol
 * Modifications:
 * 1. Added pauser role, switched pause/unpause to be onlyPauser (6/14/2018)
 * 2. Removed whenNotPause/whenPaused from pause/unpause (6/14/2018)
 * 3. Removed whenPaused (6/14/2018)
 * 4. Switches ownable library to use ZeppelinOS (7/12/18)
 * 5. Remove constructor (7/13/18)
 * 6. Reformat, conform to Solidity 0.6 syntax and add error messages (5/13/20)
 * 7. Make public functions external (5/27/20)
 */
contract Pausable is Ownable {
    event Pause();
    event Unpause();
    event PauserChanged(address indexed newAddress);

    address public pauser;
    bool public paused = false;

    /**
     * @dev Modifier to make a function callable only when the contract is not paused.
     */
    modifier whenNotPaused() {
        require(!paused, "Pausable: paused");
        _;
    }

    /**
     * @dev throws if called by any account other than the pauser
     */
    modifier onlyPauser() {
        require(msg.sender == pauser, "Pausable: caller is not the pauser");
        _;
    }

    /**
     * @dev called by the owner to pause, triggers stopped state
     */
    function pause() external onlyPauser {
        paused = true;
        emit Pause();
    }

    /**
     * @dev called by the owner to unpause, returns to normal state
     */
    function unpause() external onlyPauser {
        paused = false;
        emit Unpause();
    }

    /**
     * @dev update the pauser role
     */
    function updatePauser(address _newPauser) external onlyOwner {
        require(
            _newPauser != address(0),
            "Pausable: new pauser is the zero address"
        );
        pauser = _newPauser;
        emit PauserChanged(pauser);
    }
}

// File: contracts/v1/Blacklistable.sol

/**
 * Copyright (c) 2018-2020 CENTRE SECZ
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

pragma solidity 0.6.12;

/**
 * @title Blacklistable Token
 * @dev Allows accounts to be blacklisted by a "blacklister" role
 */
contract Blacklistable is Ownable {
    address public blacklister;
    mapping(address => bool) internal blacklisted;

    event Blacklisted(address indexed _account);
    event UnBlacklisted(address indexed _account);
    event BlacklisterChanged(address indexed newBlacklister);

    /**
     * @dev Throws if called by any account other than the blacklister
     */
    modifier onlyBlacklister() {
        require(
            msg.sender == blacklister,
            "Blacklistable: caller is not the blacklister"
        );
        _;
    }

    /**
     * @dev Throws if argument account is blacklisted
     * @param _account The address to check
     */
    modifier notBlacklisted(address _account) {
        require(
            !blacklisted[_account],
            "Blacklistable: account is blacklisted"
        );
        _;
    }

    /**
     * @dev Checks if account is blacklisted
     * @param _account The address to check
     */
    function isBlacklisted(address _account) external view returns (bool) {
        return blacklisted[_account];
    }

    /**
     * @dev Adds account to blacklist
     * @param _account The address to blacklist
     */
    function blacklist(address _account) external onlyBlacklister {
        blacklisted[_account] = true;
        emit Blacklisted(_account);
    }

    /**
     * @dev Removes account from blacklist
     * @param _account The address to remove from the blacklist
     */
    function unBlacklist(address _account) external onlyBlacklister {
        blacklisted[_account] = false;
        emit UnBlacklisted(_account);
    }

    function updateBlacklister(address _newBlacklister) external onlyOwner {
        require(
            _newBlacklister != address(0),
            "Blacklistable: new blacklister is the zero address"
        );
        blacklister = _newBlacklister;
        emit BlacklisterChanged(blacklister);
    }
}

// File: contracts/v1/FiatTokenV1.sol

/**
 *
 * Copyright (c) 2018-2020 CENTRE SECZ
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

pragma solidity 0.6.12;

/**
 * @title FiatToken
 * @dev ERC20 Token backed by fiat reserves
 */
contract FiatTokenV1 is AbstractFiatTokenV1, Ownable, Pausable, Blacklistable {
    using SafeMath for uint256;

    string public name;
    string public symbol;
    uint8 public decimals;
    string public currency;
    address public masterMinter;
    bool internal initialized;

    mapping(address => uint256) internal balances;
    mapping(address => mapping(address => uint256)) internal allowed;
    uint256 internal totalSupply_ = 0;
    mapping(address => bool) internal minters;
    mapping(address => uint256) internal minterAllowed;

    event Mint(address indexed minter, address indexed to, uint256 amount);
    event Burn(address indexed burner, uint256 amount);
    event MinterConfigured(address indexed minter, uint256 minterAllowedAmount);
    event MinterRemoved(address indexed oldMinter);
    event MasterMinterChanged(address indexed newMasterMinter);

    function initialize(
        string memory tokenName,
        string memory tokenSymbol,
        string memory tokenCurrency,
        uint8 tokenDecimals,
        address newMasterMinter,
        address newPauser,
        address newBlacklister,
        address newOwner
    ) public {
        require(!initialized, "FiatToken: contract is already initialized");
        require(
            newMasterMinter != address(0),
            "FiatToken: new masterMinter is the zero address"
        );
        require(
            newPauser != address(0),
            "FiatToken: new pauser is the zero address"
        );
        require(
            newBlacklister != address(0),
            "FiatToken: new blacklister is the zero address"
        );
        require(
            newOwner != address(0),
            "FiatToken: new owner is the zero address"
        );

        name = tokenName;
        symbol = tokenSymbol;
        currency = tokenCurrency;
        decimals = tokenDecimals;
        masterMinter = newMasterMinter;
        pauser = newPauser;
        blacklister = newBlacklister;
        setOwner(newOwner);
        initialized = true;
    }

    /**
     * @dev Throws if called by any account other than a minter
     */
    modifier onlyMinters() {
        require(minters[msg.sender], "FiatToken: caller is not a minter");
        _;
    }

    /**
     * @dev Function to mint tokens
     * @param _to The address that will receive the minted tokens.
     * @param _amount The amount of tokens to mint. Must be less than or equal
     * to the minterAllowance of the caller.
     * @return A boolean that indicates if the operation was successful.
     */
    function mint(address _to, uint256 _amount)
    external
    whenNotPaused
    onlyMinters
    notBlacklisted(msg.sender)
    notBlacklisted(_to)
    returns (bool)
    {
        require(_to != address(0), "FiatToken: mint to the zero address");
        require(_amount > 0, "FiatToken: mint amount not greater than 0");

        uint256 mintingAllowedAmount = minterAllowed[msg.sender];
        require(
            _amount <= mintingAllowedAmount,
            "FiatToken: mint amount exceeds minterAllowance"
        );

        totalSupply_ = totalSupply_.add(_amount);
        balances[_to] = balances[_to].add(_amount);
        minterAllowed[msg.sender] = mintingAllowedAmount.sub(_amount);
        emit Mint(msg.sender, _to, _amount);
        emit Transfer(address(0), _to, _amount);
        return true;
    }

    /**
     * @dev Throws if called by any account other than the masterMinter
     */
    modifier onlyMasterMinter() {
        require(
            msg.sender == masterMinter,
            "FiatToken: caller is not the masterMinter"
        );
        _;
    }

    /**
     * @dev Get minter allowance for an account
     * @param minter The address of the minter
     */
    function minterAllowance(address minter) external view returns (uint256) {
        return minterAllowed[minter];
    }

    /**
     * @dev Checks if account is a minter
     * @param account The address to check
     */
    function isMinter(address account) external view returns (bool) {
        return minters[account];
    }

    /**
     * @notice Amount of remaining tokens spender is allowed to transfer on
     * behalf of the token owner
     * @param owner     Token owner's address
     * @param spender   Spender's address
     * @return Allowance amount
     */
    function allowance(address owner, address spender)
    external
    override
    view
    returns (uint256)
    {
        return allowed[owner][spender];
    }

    /**
     * @dev Get totalSupply of token
     */
    function totalSupply() external override view returns (uint256) {
        return totalSupply_;
    }

    /**
     * @dev Get token balance of an account
     * @param account address The account
     */
    function balanceOf(address account)
    external
    override
    view
    returns (uint256)
    {
        return balances[account];
    }

    /**
     * @notice Set spender's allowance over the caller's tokens to be a given
     * value.
     * @param spender   Spender's address
     * @param value     Allowance amount
     * @return True if successful
     */
    function approve(address spender, uint256 value)
    external
    override
    whenNotPaused
    notBlacklisted(msg.sender)
    notBlacklisted(spender)
    returns (bool)
    {
        _approve(msg.sender, spender, value);
        return true;
    }

    /**
     * @dev Internal function to set allowance
     * @param owner     Token owner's address
     * @param spender   Spender's address
     * @param value     Allowance amount
     */
    function _approve(
        address owner,
        address spender,
        uint256 value
    ) internal override {
        require(owner != address(0), "ERC20: approve from the zero address");
        require(spender != address(0), "ERC20: approve to the zero address");
        allowed[owner][spender] = value;
        emit Approval(owner, spender, value);
    }

    /**
     * @notice Transfer tokens by spending allowance
     * @param from  Payer's address
     * @param to    Payee's address
     * @param value Transfer amount
     * @return True if successful
     */
    function transferFrom(
        address from,
        address to,
        uint256 value
    )
    external
    override
    whenNotPaused
    notBlacklisted(msg.sender)
    notBlacklisted(from)
    notBlacklisted(to)
    returns (bool)
    {
        require(
            value <= allowed[from][msg.sender],
            "ERC20: transfer amount exceeds allowance"
        );
        _transfer(from, to, value);
        allowed[from][msg.sender] = allowed[from][msg.sender].sub(value);
        return true;
    }

    /**
     * @notice Transfer tokens from the caller
     * @param to    Payee's address
     * @param value Transfer amount
     * @return True if successful
     */
    function transfer(address to, uint256 value)
    external
    override
    whenNotPaused
    notBlacklisted(msg.sender)
    notBlacklisted(to)
    returns (bool)
    {
        _transfer(msg.sender, to, value);
        return true;
    }

    /**
     * @notice Internal function to process transfers
     * @param from  Payer's address
     * @param to    Payee's address
     * @param value Transfer amount
     */
    function _transfer(
        address from,
        address to,
        uint256 value
    ) internal override {
        require(from != address(0), "ERC20: transfer from the zero address");
        require(to != address(0), "ERC20: transfer to the zero address");
        require(
            value <= balances[from],
            "ERC20: transfer amount exceeds balance"
        );

        balances[from] = balances[from].sub(value);
        balances[to] = balances[to].add(value);
        emit Transfer(from, to, value);
    }

    /**
     * @dev Function to add/update a new minter
     * @param minter The address of the minter
     * @param minterAllowedAmount The minting amount allowed for the minter
     * @return True if the operation was successful.
     */
    function configureMinter(address minter, uint256 minterAllowedAmount)
    external
    whenNotPaused
    onlyMasterMinter
    returns (bool)
    {
        minters[minter] = true;
        minterAllowed[minter] = minterAllowedAmount;
        emit MinterConfigured(minter, minterAllowedAmount);
        return true;
    }

    /**
     * @dev Function to remove a minter
     * @param minter The address of the minter to remove
     * @return True if the operation was successful.
     */
    function removeMinter(address minter)
    external
    onlyMasterMinter
    returns (bool)
    {
        minters[minter] = false;
        minterAllowed[minter] = 0;
        emit MinterRemoved(minter);
        return true;
    }

    /**
     * @dev allows a minter to burn some of its own tokens
     * Validates that caller is a minter and that sender is not blacklisted
     * amount is less than or equal to the minter's account balance
     * @param _amount uint256 the amount of tokens to be burned
     */
    function burn(uint256 _amount)
    external
    whenNotPaused
    onlyMinters
    notBlacklisted(msg.sender)
    {
        uint256 balance = balances[msg.sender];
        require(_amount > 0, "FiatToken: burn amount not greater than 0");
        require(balance >= _amount, "FiatToken: burn amount exceeds balance");

        totalSupply_ = totalSupply_.sub(_amount);
        balances[msg.sender] = balance.sub(_amount);
        emit Burn(msg.sender, _amount);
        emit Transfer(msg.sender, address(0), _amount);
    }

    function updateMasterMinter(address _newMasterMinter) external onlyOwner {
        require(
            _newMasterMinter != address(0),
            "FiatToken: new masterMinter is the zero address"
        );
        masterMinter = _newMasterMinter;
        emit MasterMinterChanged(masterMinter);
    }
}

// File: @openzeppelin/contracts/utils/Address.sol

pragma solidity ^0.6.2;

/**
 * @dev Collection of functions related to the address type
 */
library Address {
    /**
     * @dev Returns true if `account` is a contract.
     *
     * [IMPORTANT]
     * ====
     * It is unsafe to assume that an address for which this function returns
     * false is an externally-owned account (EOA) and not a contract.
     *
     * Among others, `isContract` will return false for the following
     * types of addresses:
     *
     *  - an externally-owned account
     *  - a contract in construction
     *  - an address where a contract will be created
     *  - an address where a contract lived, but was destroyed
     * ====
     */
    function isContract(address account) internal view returns (bool) {
        // According to EIP-1052, 0x0 is the value returned for not-yet created accounts
        // and 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 is returned
        // for accounts without code, i.e. `keccak256('')`
        bytes32 codehash;

        bytes32 accountHash
        = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470;
        // solhint-disable-next-line no-inline-assembly
        assembly {
            codehash := extcodehash(account)
        }
        return (codehash != accountHash && codehash != 0x0);
    }

    /**
     * @dev Replacement for Solidity's `transfer`: sends `amount` wei to
     * `recipient`, forwarding all available gas and reverting on errors.
     *
     * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost
     * of certain opcodes, possibly making contracts go over the 2300 gas limit
     * imposed by `transfer`, making them unable to receive funds via
     * `transfer`. {sendValue} removes this limitation.
     *
     * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more].
     *
     * IMPORTANT: because control is transferred to `recipient`, care must be
     * taken to not create reentrancy vulnerabilities. Consider using
     * {ReentrancyGuard} or the
     * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern].
     */
    function sendValue(address payable recipient, uint256 amount) internal {
        require(
            address(this).balance >= amount,
            "Address: insufficient balance"
        );

        // solhint-disable-next-line avoid-low-level-calls, avoid-call-value
        (bool success, ) = recipient.call{ value: amount }("");
        require(
            success,
            "Address: unable to send value, recipient may have reverted"
        );
    }

    /**
     * @dev Performs a Solidity function call using a low level `call`. A
     * plain`call` is an unsafe replacement for a function call: use this
     * function instead.
     *
     * If `target` reverts with a revert reason, it is bubbled up by this
     * function (like regular Solidity function calls).
     *
     * Returns the raw returned data. To convert to the expected return value,
     * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`].
     *
     * Requirements:
     *
     * - `target` must be a contract.
     * - calling `target` with `data` must not revert.
     *
     * _Available since v3.1._
     */
    function functionCall(address target, bytes memory data)
    internal
    returns (bytes memory)
    {
        return functionCall(target, data, "Address: low-level call failed");
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with
     * `errorMessage` as a fallback revert reason when `target` reverts.
     *
     * _Available since v3.1._
     */
    function functionCall(
        address target,
        bytes memory data,
        string memory errorMessage
    ) internal returns (bytes memory) {
        return _functionCallWithValue(target, data, 0, errorMessage);
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
     * but also transferring `value` wei to `target`.
     *
     * Requirements:
     *
     * - the calling contract must have an ETH balance of at least `value`.
     * - the called Solidity function must be `payable`.
     *
     * _Available since v3.1._
     */
    function functionCallWithValue(
        address target,
        bytes memory data,
        uint256 value
    ) internal returns (bytes memory) {
        return
            functionCallWithValue(
            target,
            data,
            value,
            "Address: low-level call with value failed"
        );
    }

    /**
     * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but
     * with `errorMessage` as a fallback revert reason when `target` reverts.
     *
     * _Available since v3.1._
     */
    function functionCallWithValue(
        address target,
        bytes memory data,
        uint256 value,
        string memory errorMessage
    ) internal returns (bytes memory) {
        require(
            address(this).balance >= value,
            "Address: insufficient balance for call"
        );
        return _functionCallWithValue(target, data, value, errorMessage);
    }

    function _functionCallWithValue(
        address target,
        bytes memory data,
        uint256 weiValue,
        string memory errorMessage
    ) private returns (bytes memory) {
        require(isContract(target), "Address: call to non-contract");

        // solhint-disable-next-line avoid-low-level-calls
        (bool success, bytes memory returndata) = target.call{
                value: weiValue
            }(data);
        if (success) {
            return returndata;
        } else {
            // Look for revert reason and bubble it up if present
            if (returndata.length > 0) {
                // The easiest way to bubble the revert reason is using memory via assembly

                // solhint-disable-next-line no-inline-assembly
                assembly {
                    let returndata_size := mload(returndata)
                    revert(add(32, returndata), returndata_size)
                }
            } else {
                revert(errorMessage);
            }
        }
    }
}

// File: @openzeppelin/contracts/token/ERC20/SafeERC20.sol

pragma solidity ^0.6.0;

/**
 * @title SafeERC20
 * @dev Wrappers around ERC20 operations that throw on failure (when the token
 * contract returns false). Tokens that return no value (and instead revert or
 * throw on failure) are also supported, non-reverting calls are assumed to be
 * successful.
 * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
 * which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
 */
library SafeERC20 {
    using SafeMath for uint256;
    using Address for address;

    function safeTransfer(
        IERC20 token,
        address to,
        uint256 value
    ) internal {
        _callOptionalReturn(
            token,
            abi.encodeWithSelector(token.transfer.selector, to, value)
        );
    }

    function safeTransferFrom(
        IERC20 token,
        address from,
        address to,
        uint256 value
    ) internal {
        _callOptionalReturn(
            token,
            abi.encodeWithSelector(token.transferFrom.selector, from, to, value)
        );
    }

    /**
     * @dev Deprecated. This function has issues similar to the ones found in
     * {IERC20-approve}, and its usage is discouraged.
     *
     * Whenever possible, use {safeIncreaseAllowance} and
     * {safeDecreaseAllowance} instead.
     */
    function safeApprove(
        IERC20 token,
        address spender,
        uint256 value
    ) internal {
        // safeApprove should only be called when setting an initial allowance,
        // or when resetting it to zero. To increase and decrease it, use
        // 'safeIncreaseAllowance' and 'safeDecreaseAllowance'
        // solhint-disable-next-line max-line-length
        require(
            (value == 0) || (token.allowance(address(this), spender) == 0),
            "SafeERC20: approve from non-zero to non-zero allowance"
        );
        _callOptionalReturn(
            token,
            abi.encodeWithSelector(token.approve.selector, spender, value)
        );
    }

    function safeIncreaseAllowance(
        IERC20 token,
        address spender,
        uint256 value
    ) internal {
        uint256 newAllowance = token.allowance(address(this), spender).add(
            value
        );
        _callOptionalReturn(
            token,
            abi.encodeWithSelector(
                token.approve.selector,
                spender,
                newAllowance
            )
        );
    }

    function safeDecreaseAllowance(
        IERC20 token,
        address spender,
        uint256 value
    ) internal {
        uint256 newAllowance = token.allowance(address(this), spender).sub(
            value,
            "SafeERC20: decreased allowance below zero"
        );
        _callOptionalReturn(
            token,
            abi.encodeWithSelector(
                token.approve.selector,
                spender,
                newAllowance
            )
        );
    }

    /**
     * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
     * on the return value: the return value is optional (but if data is returned, it must not be false).
     * @param token The token targeted by the call.
     * @param data The call data (encoded using abi.encode or one of its variants).
     */
    function _callOptionalReturn(IERC20 token, bytes memory data) private {
        // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
        // we're implementing it ourselves. We use {Address.functionCall} to perform this call, which verifies that
        // the target address contains contract code and also asserts for success in the low-level call.

        bytes memory returndata = address(token).functionCall(
            data,
            "SafeERC20: low-level call failed"
        );
        if (returndata.length > 0) {
            // Return data is optional
            // solhint-disable-next-line max-line-length
            require(
                abi.decode(returndata, (bool)),
                "SafeERC20: ERC20 operation did not succeed"
            );
        }
    }
}

// File: contracts/v1.1/Rescuable.sol

/**
 * Copyright (c) 2018-2020 CENTRE SECZ
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

pragma solidity 0.6.12;

contract Rescuable is Ownable {
    using SafeERC20 for IERC20;

    address private _rescuer;

    event RescuerChanged(address indexed newRescuer);

    /**
     * @notice Returns current rescuer
     * @return Rescuer's address
     */
    function rescuer() external view returns (address) {
        return _rescuer;
    }

    /**
     * @notice Revert if called by any account other than the rescuer.
     */
    modifier onlyRescuer() {
        require(msg.sender == _rescuer, "Rescuable: caller is not the rescuer");
        _;
    }

    /**
     * @notice Rescue ERC20 tokens locked up in this contract.
     * @param tokenContract ERC20 token contract address
     * @param to        Recipient address
     * @param amount    Amount to withdraw
     */
    function rescueERC20(
        IERC20 tokenContract,
        address to,
        uint256 amount
    ) external onlyRescuer {
        tokenContract.safeTransfer(to, amount);
    }

    /**
     * @notice Assign the rescuer role to a given address.
     * @param newRescuer New rescuer's address
     */
    function updateRescuer(address newRescuer) external onlyOwner {
        require(
            newRescuer != address(0),
            "Rescuable: new rescuer is the zero address"
        );
        _rescuer = newRescuer;
        emit RescuerChanged(newRescuer);
    }
}

// File: contracts/v1.1/FiatTokenV1_1.sol

/**
 * Copyright (c) 2018-2020 CENTRE SECZ
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

pragma solidity 0.6.12;

/**
 * @title FiatTokenV1_1
 * @dev ERC20 Token backed by fiat reserves
 */
contract FiatTokenV1_1 is FiatTokenV1, Rescuable {

}

// File: contracts/v2/AbstractFiatTokenV2.sol

/**
 * Copyright (c) 2018-2020 CENTRE SECZ
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

pragma solidity 0.6.12;

abstract contract AbstractFiatTokenV2 is AbstractFiatTokenV1 {
    function _increaseAllowance(
        address owner,
        address spender,
        uint256 increment
    ) internal virtual;

    function _decreaseAllowance(
        address owner,
        address spender,
        uint256 decrement
    ) internal virtual;
}

// File: contracts/util/ECRecover.sol

/**
 * Copyright (c) 2016-2019 zOS Global Limited
 * Copyright (c) 2018-2020 CENTRE SECZ
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

pragma solidity 0.6.12;

/**
 * @title ECRecover
 * @notice A library that provides a safe ECDSA recovery function
 */
library ECRecover {
    /**
     * @notice Recover signer's address from a signed message
     * @dev Adapted from: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/65e4ffde586ec89af3b7e9140bdc9235d1254853/contracts/cryptography/ECDSA.sol
     * Modifications: Accept v, r, and s as separate arguments
     * @param digest    Keccak-256 hash digest of the signed message
     * @param v         v of the signature
     * @param r         r of the signature
     * @param s         s of the signature
     * @return Signer address
     */
    function recover(
        bytes32 digest,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) internal pure returns (address) {
        // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
        // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
        // the valid range for s in (281): 0 < s < secp256k1n รท 2 + 1, and for v in (282): v โˆˆ {27, 28}. Most
        // signatures from current libraries generate a unique signature with an s-value in the lower half order.
        //
        // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value
        // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or
        // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept
        // these malleable signatures as well.
        if (
            uint256(s) >
            0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0
        ) {
            revert("ECRecover: invalid signature 's' value");
        }

        if (v != 27 && v != 28) {
            revert("ECRecover: invalid signature 'v' value");
        }

        // If the signature is valid (and not malleable), return the signer address
        address signer = ecrecover(digest, v, r, s);
        require(signer != address(0), "ECRecover: invalid signature");

        return signer;
    }
}

// File: contracts/util/EIP712.sol

/**
 * Copyright (c) 2018-2020 CENTRE SECZ
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

pragma solidity 0.6.12;

/**
 * @title EIP712
 * @notice A library that provides EIP712 helper functions
 */
library EIP712 {
    /**
     * @notice Make EIP712 domain separator
     * @param name      Contract name
     * @param version   Contract version
     * @return Domain separator
     */
    function makeDomainSeparator(string memory name, string memory version)
    internal
    view
    returns (bytes32)
    {
        uint256 chainId;
        assembly {
            chainId := chainid()
        }
        return
            keccak256(
            abi.encode(
            // keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
                0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f,
                keccak256(bytes(name)),
                keccak256(bytes(version)),
                chainId,
                address(this)
            )
        );
    }

    /**
     * @notice Recover signer's address from a EIP712 signature
     * @param domainSeparator   Domain separator
     * @param v                 v of the signature
     * @param r                 r of the signature
     * @param s                 s of the signature
     * @param typeHashAndData   Type hash concatenated with data
     * @return Signer's address
     */
    function recover(
        bytes32 domainSeparator,
        uint8 v,
        bytes32 r,
        bytes32 s,
        bytes memory typeHashAndData
    ) internal pure returns (address) {
        bytes32 digest = keccak256(
            abi.encodePacked(
                "\x19\x01",
                domainSeparator,
                keccak256(typeHashAndData)
            )
        );
        return ECRecover.recover(digest, v, r, s);
    }
}

// File: contracts/v2/EIP712Domain.sol

/**
 * Copyright (c) 2018-2020 CENTRE SECZ
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

pragma solidity 0.6.12;

/**
 * @title EIP712 Domain
 */
contract EIP712Domain {
    /**
     * @dev EIP712 Domain Separator
     */
    bytes32 public DOMAIN_SEPARATOR;
}

// File: contracts/v2/EIP3009.sol

/**
 * Copyright (c) 2018-2020 CENTRE SECZ
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

pragma solidity 0.6.12;

/**
 * @title EIP-3009
 * @notice Provide internal implementation for gas-abstracted transfers
 * @dev Contracts that inherit from this must wrap these with publicly
 * accessible functions, optionally adding modifiers where necessary
 */
abstract contract EIP3009 is AbstractFiatTokenV2, EIP712Domain {
    // keccak256("TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)")
    bytes32
    public constant TRANSFER_WITH_AUTHORIZATION_TYPEHASH = 0x7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a2267;

    // keccak256("ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)")
    bytes32
    public constant RECEIVE_WITH_AUTHORIZATION_TYPEHASH = 0xd099cc98ef71107a616c4f0f941f04c322d8e254fe26b3c6668db87aae413de8;

    // keccak256("CancelAuthorization(address authorizer,bytes32 nonce)")
    bytes32
    public constant CANCEL_AUTHORIZATION_TYPEHASH = 0x158b0a9edf7a828aad02f63cd515c68ef2f50ba807396f6d12842833a1597429;

    /**
     * @dev authorizer address => nonce => bool (true if nonce is used)
     */
    mapping(address => mapping(bytes32 => bool)) private _authorizationStates;

    event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce);
    event AuthorizationCanceled(
        address indexed authorizer,
        bytes32 indexed nonce
    );

    /**
     * @notice Returns the state of an authorization
     * @dev Nonces are randomly generated 32-byte data unique to the
     * authorizer's address
     * @param authorizer    Authorizer's address
     * @param nonce         Nonce of the authorization
     * @return True if the nonce is used
     */
    function authorizationState(address authorizer, bytes32 nonce)
    external
    view
    returns (bool)
    {
        return _authorizationStates[authorizer][nonce];
    }

    /**
     * @notice Execute a transfer with a signed authorization
     * @param from          Payer's address (Authorizer)
     * @param to            Payee's address
     * @param value         Amount to be transferred
     * @param validAfter    The time after which this is valid (unix time)
     * @param validBefore   The time before which this is valid (unix time)
     * @param nonce         Unique nonce
     * @param v             v of the signature
     * @param r             r of the signature
     * @param s             s of the signature
     */
    function _transferWithAuthorization(
        address from,
        address to,
        uint256 value,
        uint256 validAfter,
        uint256 validBefore,
        bytes32 nonce,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) internal {
        _requireValidAuthorization(from, nonce, validAfter, validBefore);

        bytes memory data = abi.encode(
            TRANSFER_WITH_AUTHORIZATION_TYPEHASH,
            from,
            to,
            value,
            validAfter,
            validBefore,
            nonce
        );
        require(
            EIP712.recover(DOMAIN_SEPARATOR, v, r, s, data) == from,
            "FiatTokenV2: invalid signature"
        );

        _markAuthorizationAsUsed(from, nonce);
        _transfer(from, to, value);
    }

    /**
     * @notice Receive a transfer with a signed authorization from the payer
     * @dev This has an additional check to ensure that the payee's address
     * matches the caller of this function to prevent front-running attacks.
     * @param from          Payer's address (Authorizer)
     * @param to            Payee's address
     * @param value         Amount to be transferred
     * @param validAfter    The time after which this is valid (unix time)
     * @param validBefore   The time before which this is valid (unix time)
     * @param nonce         Unique nonce
     * @param v             v of the signature
     * @param r             r of the signature
     * @param s             s of the signature
     */
    function _receiveWithAuthorization(
        address from,
        address to,
        uint256 value,
        uint256 validAfter,
        uint256 validBefore,
        bytes32 nonce,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) internal {
        require(to == msg.sender, "FiatTokenV2: caller must be the payee");
        _requireValidAuthorization(from, nonce, validAfter, validBefore);

        bytes memory data = abi.encode(
            RECEIVE_WITH_AUTHORIZATION_TYPEHASH,
            from,
            to,
            value,
            validAfter,
            validBefore,
            nonce
        );
        require(
            EIP712.recover(DOMAIN_SEPARATOR, v, r, s, data) == from,
            "FiatTokenV2: invalid signature"
        );

        _markAuthorizationAsUsed(from, nonce);
        _transfer(from, to, value);
    }

    /**
     * @notice Attempt to cancel an authorization
     * @param authorizer    Authorizer's address
     * @param nonce         Nonce of the authorization
     * @param v             v of the signature
     * @param r             r of the signature
     * @param s             s of the signature
     */
    function _cancelAuthorization(
        address authorizer,
        bytes32 nonce,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) internal {
        _requireUnusedAuthorization(authorizer, nonce);

        bytes memory data = abi.encode(
            CANCEL_AUTHORIZATION_TYPEHASH,
            authorizer,
            nonce
        );
        require(
            EIP712.recover(DOMAIN_SEPARATOR, v, r, s, data) == authorizer,
            "FiatTokenV2: invalid signature"
        );

        _authorizationStates[authorizer][nonce] = true;
        emit AuthorizationCanceled(authorizer, nonce);
    }

    /**
     * @notice Check that an authorization is unused
     * @param authorizer    Authorizer's address
     * @param nonce         Nonce of the authorization
     */
    function _requireUnusedAuthorization(address authorizer, bytes32 nonce)
    private
    view
    {
        require(
            !_authorizationStates[authorizer][nonce],
            "FiatTokenV2: authorization is used or canceled"
        );
    }

    /**
     * @notice Check that authorization is valid
     * @param authorizer    Authorizer's address
     * @param nonce         Nonce of the authorization
     * @param validAfter    The time after which this is valid (unix time)
     * @param validBefore   The time before which this is valid (unix time)
     */
    function _requireValidAuthorization(
        address authorizer,
        bytes32 nonce,
        uint256 validAfter,
        uint256 validBefore
    ) private view {
        require(
            now > validAfter,
            "FiatTokenV2: authorization is not yet valid"
        );
        require(now < validBefore, "FiatTokenV2: authorization is expired");
        _requireUnusedAuthorization(authorizer, nonce);
    }

    /**
     * @notice Mark an authorization as used
     * @param authorizer    Authorizer's address
     * @param nonce         Nonce of the authorization
     */
    function _markAuthorizationAsUsed(address authorizer, bytes32 nonce)
    private
    {
        _authorizationStates[authorizer][nonce] = true;
        emit AuthorizationUsed(authorizer, nonce);
    }
}

// File: contracts/v2/EIP2612.sol

/**
 * Copyright (c) 2018-2020 CENTRE SECZ
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

pragma solidity 0.6.12;

/**
 * @title EIP-2612
 * @notice Provide internal implementation for gas-abstracted approvals
 */
abstract contract EIP2612 is AbstractFiatTokenV2, EIP712Domain {
    // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)")
    bytes32
    public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;

    mapping(address => uint256) private _permitNonces;

    /**
     * @notice Nonces for permit
     * @param owner Token owner's address (Authorizer)
     * @return Next nonce
     */
    function nonces(address owner) external view returns (uint256) {
        return _permitNonces[owner];
    }

    /**
     * @notice Verify a signed approval permit and execute if valid
     * @param owner     Token owner's address (Authorizer)
     * @param spender   Spender's address
     * @param value     Amount of allowance
     * @param deadline  The time at which this expires (unix time)
     * @param v         v of the signature
     * @param r         r of the signature
     * @param s         s of the signature
     */
    function _permit(
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) internal {
        require(deadline >= now, "FiatTokenV2: permit is expired");

        bytes memory data = abi.encode(
            PERMIT_TYPEHASH,
            owner,
            spender,
            value,
            _permitNonces[owner]++,
            deadline
        );
        require(
            EIP712.recover(DOMAIN_SEPARATOR, v, r, s, data) == owner,
            "EIP2612: invalid signature"
        );

        _approve(owner, spender, value);
    }
}

// File: contracts/v2/FiatTokenV2.sol

/**
 * Copyright (c) 2018-2020 CENTRE SECZ
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

pragma solidity 0.6.12;

/**
 * @title FiatToken V2
 * @notice ERC20 Token backed by fiat reserves, version 2
 */
contract FiatTokenV2 is FiatTokenV1_1, EIP3009, EIP2612 {
    uint8 internal _initializedVersion;

    /**
     * @notice Initialize v2
     * @param newName   New token name
     */
    function initializeV2(string calldata newName) external {
        // solhint-disable-next-line reason-string
        require(initialized && _initializedVersion == 0);
        name = newName;
        DOMAIN_SEPARATOR = EIP712.makeDomainSeparator(newName, "2");
        _initializedVersion = 1;
    }

    /**
     * @notice Increase the allowance by a given increment
     * @param spender   Spender's address
     * @param increment Amount of increase in allowance
     * @return True if successful
     */
    function increaseAllowance(address spender, uint256 increment)
    external
    whenNotPaused
    notBlacklisted(msg.sender)
    notBlacklisted(spender)
    returns (bool)
    {
        _increaseAllowance(msg.sender, spender, increment);
        return true;
    }

    /**
     * @notice Decrease the allowance by a given decrement
     * @param spender   Spender's address
     * @param decrement Amount of decrease in allowance
     * @return True if successful
     */
    function decreaseAllowance(address spender, uint256 decrement)
    external
    whenNotPaused
    notBlacklisted(msg.sender)
    notBlacklisted(spender)
    returns (bool)
    {
        _decreaseAllowance(msg.sender, spender, decrement);
        return true;
    }

    /**
     * @notice Execute a transfer with a signed authorization
     * @param from          Payer's address (Authorizer)
     * @param to            Payee's address
     * @param value         Amount to be transferred
     * @param validAfter    The time after which this is valid (unix time)
     * @param validBefore   The time before which this is valid (unix time)
     * @param nonce         Unique nonce
     * @param v             v of the signature
     * @param r             r of the signature
     * @param s             s of the signature
     */
    function transferWithAuthorization(
        address from,
        address to,
        uint256 value,
        uint256 validAfter,
        uint256 validBefore,
        bytes32 nonce,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external whenNotPaused notBlacklisted(from) notBlacklisted(to) {
        _transferWithAuthorization(
            from,
            to,
            value,
            validAfter,
            validBefore,
            nonce,
            v,
            r,
            s
        );
    }

    /**
     * @notice Receive a transfer with a signed authorization from the payer
     * @dev This has an additional check to ensure that the payee's address
     * matches the caller of this function to prevent front-running attacks.
     * @param from          Payer's address (Authorizer)
     * @param to            Payee's address
     * @param value         Amount to be transferred
     * @param validAfter    The time after which this is valid (unix time)
     * @param validBefore   The time before which this is valid (unix time)
     * @param nonce         Unique nonce
     * @param v             v of the signature
     * @param r             r of the signature
     * @param s             s of the signature
     */
    function receiveWithAuthorization(
        address from,
        address to,
        uint256 value,
        uint256 validAfter,
        uint256 validBefore,
        bytes32 nonce,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external whenNotPaused notBlacklisted(from) notBlacklisted(to) {
        _receiveWithAuthorization(
            from,
            to,
            value,
            validAfter,
            validBefore,
            nonce,
            v,
            r,
            s
        );
    }

    /**
     * @notice Attempt to cancel an authorization
     * @dev Works only if the authorization is not yet used.
     * @param authorizer    Authorizer's address
     * @param nonce         Nonce of the authorization
     * @param v             v of the signature
     * @param r             r of the signature
     * @param s             s of the signature
     */
    function cancelAuthorization(
        address authorizer,
        bytes32 nonce,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external whenNotPaused {
        _cancelAuthorization(authorizer, nonce, v, r, s);
    }

    /**
     * @notice Update allowance with a signed permit
     * @param owner       Token owner's address (Authorizer)
     * @param spender     Spender's address
     * @param value       Amount of allowance
     * @param deadline    Expiration time, seconds since the epoch
     * @param v           v of the signature
     * @param r           r of the signature
     * @param s           s of the signature
     */
    function permit(
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external whenNotPaused notBlacklisted(owner) notBlacklisted(spender) {
        _permit(owner, spender, value, deadline, v, r, s);
    }

    /**
     * @notice Internal function to increase the allowance by a given increment
     * @param owner     Token owner's address
     * @param spender   Spender's address
     * @param increment Amount of increase
     */
    function _increaseAllowance(
        address owner,
        address spender,
        uint256 increment
    ) internal override {
        _approve(owner, spender, allowed[owner][spender].add(increment));
    }

    /**
     * @notice Internal function to decrease the allowance by a given decrement
     * @param owner     Token owner's address
     * @param spender   Spender's address
     * @param decrement Amount of decrease
     */
    function _decreaseAllowance(
        address owner,
        address spender,
        uint256 decrement
    ) internal override {
        _approve(
            owner,
            spender,
            allowed[owner][spender].sub(
                decrement,
                "ERC20: decreased allowance below zero"
            )
        );
    }
}

// File: contracts/v2/FiatTokenV2_1.sol

/**
 * Copyright (c) 2018-2020 CENTRE SECZ
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

pragma solidity 0.6.12;

// solhint-disable func-name-mixedcase

/**
 * @title FiatToken V2.1
 * @notice ERC20 Token backed by fiat reserves, version 2.1
 */
contract FiatTokenV2_1 is FiatTokenV2 {
    /**
     * @notice Initialize v2.1
     * @param lostAndFound  The address to which the locked funds are sent
     */
    function initializeV2_1(address lostAndFound) external {
        // solhint-disable-next-line reason-string
        require(_initializedVersion == 1);

        uint256 lockedAmount = balances[address(this)];
        if (lockedAmount > 0) {
            _transfer(address(this), lostAndFound, lockedAmount);
        }
        blacklisted[address(this)] = true;

        _initializedVersion = 2;
    }

    /**
     * @notice Version string for the EIP712 domain separator
     * @return Version string
     */
    function version() external view returns (string memory) {
        return "2";
    }
}

RFC-0323/TariThrottle

The Tari throttle, or Layer 2 burn rate controller

status: draft

Maintainer(s): Cayle Sharrock

Licence

The 3-Clause BSD Licence.

Copyright 2022 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

This RFC provides an introductory exploratory analysis into the mechanisms behind a proposed Tari throttle: a Layer 2 controller for the L2 fee burn rate to control the Tari circulating supply.

Table of Contents

Summary

TariThrottle is a simple process controller designed to modulate the layer two burn rate in order to achieve two goals:

  • Primarily, keep the emission and burn rate roughly balanced (ensuring the long-term sustainability of Tari), and
  • Secondarily, to maintain the total circulating supply at a target value (satisfying an implicit assumption in cryptocurrencies that token supplies are finite).

A proof-of-concept controller has been implemented and tested in a simulation environment (repository). As the results below attest, the controller logic is sufficient to achieve these goals, even under highly volatile layer two fee conditions.

However, the controller achieves the goals at the expense of a rapidly changing layer two burn rate, which may be detrimental to the sustainability of validator nodes.

At the risk of the tail wagging the dog, the primary conclusion of this study is that the TariThrottle controller should likely not aim to maintain a supply target, but instead to ensure a sustainable layer two ecosystem, to whit:

  • maintain a constant demand gradient so that under normal circumstances there is always a demand for new Tari and thus Minotari are constantly being burnt to satisfy this demand,
  • marginal Validator Nodes are able to operate at or near break-even rates, while maintaining a healthy reserve of capacity for surge demand,
  • minimum transaction fees remain below $0.01 in today's money, and
  • the supply of Minotari is sustainable over the long-term.

Therefore, the conclusion of this study is not to abandon the original targets of the TariThrottle completely, but to adjust the priority of the primary goal (a sustainable long-term balance), and make it subservient to the primary goal of ensuring a constant demand gradient.

A modified Tari throttle model that seeks to achieve these aims is outside of the scope of this RFC and is left for a follow-up study.

PID controllers

TariThrottle is based on a simple PID controller design.

PID controllers are a type of control system that uses feedback to maintain a system in a desired state. They are widely used in industrial control systems.

The PID controller has three components:

  • Proportional: This component is proportional to the error between the setpoint and the current value. It is the primary component of the controller and is used to drive the system towards the setpoint.
  • Integral: This component is proportional to the integral of the error over time. It is used to eliminate steady-state error.
  • Derivative: This component is proportional to the rate of change of the error. It is used to reduce overshoot and oscillation.

Tari throttle

The burn-sim repository was used to generate the results in this exploratory study.

The primary module in the repository is the TariThrottle struct.

This struct holds the following data:

  • The controller parameters,
  • The output variable, burn_rate. The burn rate is defined as the fraction of fees collected on the layer 2 that are burnt as exhaust (see the turbine model), and
  • The three functions that describe how the controller responds to the input variables.

Controller parameters

The controller parameters are used to tune the controller behaviour. They give operators the ability to fine-tune the behaviour of the controller without having to change the underlying code.

Specifically, the controller parameters are:

  • kp: determines the weight of the net burn component of the controller. The net burn component is the difference between the emission and the current rate of L2 token burn. When emission equals burn, the total supply of Tari will remain constant.
  • ki: determines the weight of the integral component of the controller. This is calculated as the difference between the current total supply and the target supply. If the total circulating supply is at the target value, then this term will be zero.
  • kd: determines the weight of the derivative component of the controller. This is currently not used, since the controller is able to adequately control supply with just the proportional and integral terms.
  • target_supply: the target circulating supply of Tari.
  • trigger_at: the block height at which the controller logic becomes active.
  • max: the maximum burn rate that the controller will allow.
  • min: the minimum burn rate that the controller will allow. The maximum and minimum are important parameters to prevent extreme burn rates that could be detrimental to the sustainability of the network.

Controller inputs

The controller operates over periods. A period is the number of blocks over which the controller will operate without being able to change the burn rate. In practice, this is determined by the epoch length of the Layer two.

The total quantity of fees collected across the entire network is only known at the end of every epoch. This is a key input into the controller, and therefore we are limited to updating the burn rate at the end of each epoch as well.

For this study, the period length is set to 720 blocks, or roughly one day.

Fee models

Since we don't know what the Tari fees will be in the future, we can only carry out simulations based on various scenarios of fee growth. For example, we can model a low fee environment, an exponential fee growth environment, a highly volatile fee environment, and so on.

In this study, we looked at the following fee models:

  • Sigmoidal: a sigmoidal growth model, which looks like an exponential growth model initially, but then levels off as the fees approach a maximum value. This is the most likely pattern to appear in practice, since it incorporates the idea that as fees become very high, the minimum transaction fee can be reduced so that total fee revenue grows sustainably, even as the network usage (in terms of transactions per second) continues to grow.
  • Exponential: a simple exponential growth model. This model assumes a constant annual growth rate in network fees.
  • Sinusoidal: a highly volatile fee environment, where the fees oscillate between a minimum and maximum value. This is the not likely scenario to appear in practice, but it is useful to test the robustness of the controller logic.

Emission model

The second input to the controller is the number of tokens emitted over the preceding period. This is straightforward to model, since the emission curve is known a priori. The currently proposed Minotari mainnet emission curve parameters were used to generate the emission inputs for this study, including a 30% premine and a 1% tail emission inflation rate.

Miscellaneous parameters

The trigger_at parameter was generally set to start the controller after the first year of operation, when we expect the layer two to go live on mainnet.

The target_supply parameter was set to 15, 18 and 21 billion Tari to determine the effect of different supply targets.

The initital_valuewas set to min to allow the circulating supply to approach the target supply as quickly as possible.

The min and max parameters were set to 5% and 50% fee burn rates respectively.

THe period was set to 720 blocks, or roughly one day. The epoch length for Tari has not been finalised yet, but it should not be wildly different from this value.

Integer-based division

Typical PID controllers use floating-point math in their control algorithms. However, the TariThrottle controller will be run on a distributed set of machines that may be operating under different models for floating-point operations, since the IEEE leaves some aspects of floating-point math unspecified.

This is not permissible in Tari, and therefore an integer-based approach to control is implemented in the tari-sim repository.

Simply put, this involves scaling the error values by a constant to convert them to integers of roughly the same order of magnitude, (via error_i_scale). The control parameters are also integers, expressed as "parts per million" so that a value of 500,000 corresponds to 0.5 for example, while 10 corresponds to 0.00001. This give sufficient granularity to the control parameters to allow for effective tuning of the controller.

The simulations

Controller parameters

A preliminary batch of simulations was run to set the controller parameters to values that roughly achieve the stated goal above. These simulations are omitted for brevity, but the result indicate that the following parameter range provide a good balance between robustness and responsiveness of the Tari throttle:

  • kp = 0.0001 - 0.0003. A value closer to 100ppm gives more weight to achieving the target supply, while a value closer to 300ppm gives more weight to the net burn rate. The simulations run scenarios for a ki values of 100, 200 and 300 ppm.
  • ki = -0.00035. This value is negative, since if we're above the target supply, we must increase the burn rate, and vice versa.
  • kd = 0. This value is not used in the current implementation, since the controller is able to adequately control supply with just the proportional and integral terms.

Target supply

The simulations were run for target circulating supplies of 15, 18 and 21 billion Tari.

Target values other than the quoted "initial supply" of 21 billion Tari were used, because it is actually quite difficult to achieve the 21 billion target in practice. This is because the earliest it is possible to even reach this value (at ZERO burn rate) is after about 15 years.

This offers very little flexibility to the control logic, and could easily introduce instability into the layer two ecosystem.

For this reason, simulations were run with alternative target supplies of 15 and 18 billion Tari to compare how the controller reacts with more scope to adjust the burn rate.

Fee models

Five different fee models scenarios were employed:

  1. "Strong growth, tapering fees". This scenario describes strong fee growth in the network, reaching 5,000,000 Tari per day around 6 years after the launch of the layer 2. At a minimum fee of 0.01 Tari per transaction, this corresponds to a network activity of around 5,800 tx/s, or roughly double the average activity of the Visa network. The 'tapering fees' part of the scenario refers to the fact that this scenario does allow the network to continue to grow, but that the total fee revenue grows at a modest 100,000 XTR/d per year. This would be achieved by reducing the minimum transaction fee. This also matches the expectation that the price of XMR increases with network activity, and so reducing the transaction fee maintains sub-penny transaction fees in nominal USD terms.
  2. "Slower growth, tapering fees". This scenario is identical to "Strong growth, tapering fees", but with a lower maximum fee rate of 1,000,000 Tari per day.
  3. "25% annual fee growth". This scenario describes a network that grows at 25% per year, without compensating by decreasing the minimum transaction fee.
  4. "10% annual fee growth". This scenario describes a network that grows at 10% per year, without compensating by decreasing the minimum transaction fee.
  5. "Unstable fees, low frequency". This scenario describes a network that has volatile fees, oscillating between 0 and 500,000 XTR per day over a 90-day period. This is not a realistic scenario, but it is useful to test the robustness of the controller logic.
  6. "Unstable fees, high frequency". This scenario describes a network that has extremely volatile fees, oscillating between 0 and 1,000,000 XTR per day over a 14-day period. This is not a very realistic scenario, but it is useful to test the robustness of the controller logic under extremely volatile conditions.

Results

A simulation was run for every combination of the variations in kp, target_supply, and fee_model. The results of all 50+ simulation runs are given below.

21 billion Tari target supply

All of the following simulations were run with a target supply of 21 billion Tari.

10% annual fee growth (low fee scenario)

10% annual fee growth

Figure 1. Supply target: 21 billion XTR. 10% annual fee growth fee model. kp = 0.000100.

10% annual fee growth

Figure 2. Supply target: 21 billion XTR. 10% annual fee growth fee model. kp = 0.000200.

10% annual fee growth

Figure 3. Supply target: 21 billion XTR. 10% annual fee growth fee model. kp = 0.000300.

10% annual fee growth (high fee scenario)

25% annual fee growth

Figure 4. Supply target: 21 billion XTR. 25% annual fee growth fee model. kp = 0.000100.

25% annual fee growth

Figure 5. Supply target: 21 billion XTR. 25% annual fee growth fee model. kp = 0.000300.

Slower growth, tapering fees

Slower growth, tapering fees

Figure 6. Supply target: 21 billion XTR. Slower growth, tapering fees fee model. kp = 0.000100.

Slower growth, tapering fees

Figure 7. Supply target: 21 billion XTR. Slower growth, tapering fees fee model. kp = 0.000200.

Slower growth, tapering fees

Figure 8. Supply target: 21 billion XTR. Slower growth, tapering fees fee model. kp = 0.000300.

Strong growth, tapering fees

Strong growth, tapering fees

Figure 9. Supply target: 21 billion XTR. Strong growth, tapering fees fee model. kp = 0.000100.

Strong growth, tapering fees

Figure 10. Supply target: 21 billion XTR. Strong growth, tapering fees fee model. kp = 0.000200.

Strong growth, tapering fees

Figure 11. Supply target: 21 billion XTR. Strong growth, tapering fees fee model. kp = 0.000300.

Unstable fees, high frequency

Unstable fees, high frequency

Figure 12. Supply target: 21 billion XTR. Unstable fees, high frequency fee model. kp = 0.000100.

Unstable fees, high frequency

Figure 13. Supply target: 21 billion XTR. Unstable fees, high frequency fee model. kp = 0.000300.

Unstable fees, low frequency

Unstable fees, low frequency

Figure 14. Supply target: 21 billion XTR. Unstable fees, low frequency fee model. kp = 0.000100.

Unstable fees, low frequency

Figure 15. Supply target: 21 billion XTR. Unstable fees, low frequency fee model. kp = 0.000200.

Unstable fees, low frequency

Figure 16. Supply target: 21 billion XTR. Unstable fees, low frequency fee model. kp = 0.000300.

18 billion Tari target supply

All of the following simulations were run with a target supply of 18 billion Tari.

10% annual fee growth (low fee scenario)

10% annual fee growth

Figure 17. Supply target: 18 billion XTR. 10% annual fee growth fee model. kp = 0.000100.

10% annual fee growth

Figure 18. Supply target: 18 billion XTR. 10% annual fee growth fee model. kp = 0.000200.

10% annual fee growth

Figure 19. Supply target: 18 billion XTR. 10% annual fee growth fee model. kp = 0.000300.

10% annual fee growth (high fee scenario)

25% annual fee growth

Figure 20. Supply target: 18 billion XTR. 25% annual fee growth fee model. kp = 0.000100.

25% annual fee growth

Figure 21. Supply target: 18 billion XTR. 25% annual fee growth fee model. kp = 0.000200.

25% annual fee growth

Figure 22. Supply target: 18 billion XTR. 25% annual fee growth fee model. kp = 0.000300.

Slower growth, tapering fees

Slower growth, tapering fees

Figure 23. Supply target: 18 billion XTR. Slower growth, tapering fees fee model. kp = 0.000100.

Slower growth, tapering fees

Figure 24. Supply target: 18 billion XTR. Slower growth, tapering fees fee model. kp = 0.000200.

Slower growth, tapering fees

Figure 25. Supply target: 18 billion XTR. Slower growth, tapering fees fee model. kp = 0.000300.

Strong growth, tapering fees

Strong growth, tapering fees

Figure 26. Supply target: 18 billion XTR. Strong growth, tapering fees fee model. kp = 0.000100.

Strong growth, tapering fees

Figure 27. Supply target: 18 billion XTR. Strong growth, tapering fees fee model. kp = 0.000200.

Strong growth, tapering fees

Figure 28. Supply target: 18 billion XTR. Strong growth, tapering fees fee model. kp = 0.000300.

Unstable fees, high frequency

Unstable fees, high frequency

Figure 29. Supply target: 18 billion XTR. Unstable fees, high frequency fee model. kp = 0.000100.

Unstable fees, high frequency

Figure 30. Supply target: 18 billion XTR. Unstable fees, high frequency fee model. kp = 0.000200.

Unstable fees, high frequency

Figure 31. Supply target: 18 billion XTR. Unstable fees, high frequency fee model. kp = 0.000300.

Unstable fees, low frequency

Unstable fees, low frequency

Figure 32. Supply target: 18 billion XTR. Unstable fees, low frequency fee model. kp = 0.000100.

Unstable fees, low frequency

Figure 33. Supply target: 18 billion XTR. Unstable fees, low frequency fee model. kp = 0.000200.

Unstable fees, low frequency

Figure 34. Supply target: 18 billion XTR. Unstable fees, low frequency fee model. kp = 0.000300.

15 billion Tari target supply

All of the following simulations were run with a target supply of 15 billion Tari.

10% annual fee growth (low fee scenario)

10% annual fee growth

Figure 35. Supply target: 15 billion XTR. 10% annual fee growth fee model. kp = 0.000100.

10% annual fee growth

Figure 36. Supply target: 15 billion XTR. 10% annual fee growth fee model. kp = 0.000200.

10% annual fee growth

Figure 37. Supply target: 15 billion XTR. 10% annual fee growth fee model. kp = 0.000300.

10% annual fee growth (high fee scenario)

25% annual fee growth

Figure 38. Supply target: 15 billion XTR. 25% annual fee growth fee model. kp = 0.000100.

25% annual fee growth

Figure 39. Supply target: 15 billion XTR. 25% annual fee growth fee model. kp = 0.000300.

Slower growth, tapering fees

Slower growth, tapering fees

Figure 40. Supply target: 15 billion XTR. Slower growth, tapering fees fee model. kp = 0.000100.

Slower growth, tapering fees

Figure 41. Supply target: 15 billion XTR. Slower growth, tapering fees fee model. kp = 0.000200.

Slower growth, tapering fees

Figure 42. Supply target: 15 billion XTR. Slower growth, tapering fees fee model. kp = 0.000300.

Strong growth, tapering fees

Strong growth, tapering fees

Figure 43. Supply target: 15 billion XTR. Strong growth, tapering fees fee model. kp = 0.000100.

Strong growth, tapering fees

Figure 44. Supply target: 15 billion XTR. Strong growth, tapering fees fee model. kp = 0.000200.

Strong growth, tapering fees

Figure 45. Supply target: 15 billion XTR. Strong growth, tapering fees fee model. kp = 0.000300.

Unstable fees, high frequency

Unstable fees, high frequency

Figure 46. Supply target: 15 billion XTR. Unstable fees, high frequency fee model. kp = 0.000100.

Unstable fees, high frequency

Figure 47. Supply target: 15 billion XTR. Unstable fees, high frequency fee model. kp = 0.000200.

Unstable fees, high frequency

Figure 48. Supply target: 15 billion XTR. Unstable fees, high frequency fee model. kp = 0.000300.

Unstable fees, low frequency

Unstable fees, low frequency

Figure 49. Supply target: 15 billion XTR. Unstable fees, low frequency fee model. kp = 0.000100.

Unstable fees, low frequency

Figure 50. Supply target: 15 billion XTR. Unstable fees, low frequency fee model. kp = 0.000200.

Unstable fees, low frequency

Figure 51. Supply target: 15 billion XTR. Unstable fees, low frequency fee model. kp = 0.000300.

Discussion

Low fees growth

Figures 1-3, 17-19, and 35-37 show the results of the simulations for the 10% annual fee growth scenario. None of these scenarios produce sufficient fees to reduce the circulating supply to the target within the 33-year simulation timeframe. The common feature of these simulations is that the controller hits the maximum burn rate of 50% and stays there for the remainder of the simulation. Increasing the max burn rate to a higher value might succeed in achieving the target supply, but this might mean that validator nodes cannot operate at break-even rates.

Ultimately a poor growth in fee revenue -- in this model, never exceeding 30 tx/s over 30 years of operation, represents a failure mode, not just of the controller, but of the network as a whole, since Tari adoption has never taken off, and there's nothing a controller can do to fix that.

High fees growth

At the other end of the spectrum, if Tari achieves runaway success, and the minimum transaction fee is never reduced to compensate, then the controller will be able to maintain the target circulating supply of Tari during the 33-year simulation timeframe.

This is shown in Figures 4 and 5. In Figures 20-21, and 38-39, the tapering of supply only begins towards the end of the simulation period.

However, it should be noted that indefinite fee growth is not sustainable from a token supply point of view, unless the inflation rate of the emission curve is increased to compensate. This is not evident in these charts, but if the simulation were to continue for several more years, the fee growth would eventually set the burn rate to its minimum of 5% and the total circulating supply would eventually fall to zero.

So, a few things to note about this scenario:

  • It's very unlikely to happen in practice that 25% network growth is achieved consistently for 30-40 years. Therefore, this scenario is largely illustrative of the controller's ability to handle extreme fee growth. And within fairly wide limits, we demonstrate that the throttle manages this very well.
  • The minimum burn rate of 5% could be reduced further without negatively impacting validator node revenues (in fact reducing the burn rate is beneficial for validator nodes).
  • In practice, it is not unreasonable to expect that the price of Tari in nominal USD terms would increase as the network usage increases, and so the minimum transaction fee would have to be reduced to maintain sub-penny transaction fees -- keeping the Tari network cheap to use for the average user. This entails that the total fee revenue would taper off, even as the network continues to grow. The Sigmoidal fee model is a better representation of this eventuality.

Unstable fee revenue

Figures 12-16, 29-34, and 46-51 show the controller's response to various scenarios under which the fee revenue is oscillating wildly. Under some scenarios, notably Figures 12 and 13, the target supply cannot be achieved even with the controller at minimum burn rates. This is symptomatic of the point made previously that a target supply of 21 billion offers very little room for the controller to maneuver.

In Figures 14-16, 32-34 and 46-51, the controller is able to maintain the supply target with very small amounts of oscillation, by synchronising the fee oscillation with an oscillating burn rate.

While this demonstrates that the throttle is able to achieve its design goals, one must question whether the wildly oscillating burn rates needed to achieve these goals are healthy for the network.

To maintain a constant supply, as the fees increase, the burn rate decreases to try and match the emission (which is roughly constant over the oscillation period). From a validator node perspective, the portion of fees paid to them increases as fees increase. That sounds great, but when fees decrease, they receive a smaller percentage of the shrinking pool of fees, and so they are doubly disadvantaged. Marginal validator nodes will be hit twice as hard, and be forced off the network due to unfavourable economics, reducing the overall network capacity. When fees pick up again, additional capacity will come online, but the lag between the increased usage and capacity could well lead to a degradation in the user experience.

Slower growth with tapering fees

The previous scenarios provide valuable insights into the controller's ability to handle extremes in network demand in terms of fee growth patterns. However, these scenarios are not likely to play out in reality, at least not for extended multi-decade periods.

The scenarios represented in the "tapering fees" series are more indicative of the long-term macro behaviour of the Tari fee growth. The two specific scenarios in this category reflect an initial exponential growth period followed by a more sustainable linear fee growth rate, corresponding to reducing the minimum transaction fee over time while the network continues to grow.

In the "Slower growth" variant, the fee revenue grows from zero to 1,000,000 XTR per day over 5 years, and then increases by 100,000 XTR/d per year after that.

Figures 6-8 are all quite similar. These all use 21 billion XTR as a target and differ only in the weighting applied to trying to achieve the target supply, vs. balancing burn rate and emission.

The slow growth rate allows the supply to reach the target value of 21 billion in about 20 years, at which point the burn rate adjusts slightly to between 15% and 20% to maintain the target supply.

Figures 23-25 shows what happens with a target supply of 18 billion XTR. Here the target is reached much sooner, after around 9 years of mainnet operation. In these simulations, the effect of the controller is more pronounced, since an additional 3 billion XTR must be burned to maintain the target supply compared to the scenario with a 21 billion XTR target.

Figures 40-42 show the scenario with a target supply of 15 billion XTR. The lower target requires several years of max burn with the supply overshooting the target before being pulled back onto the target.

Strong growth with tapering fees

The "Strong growth" variant of the tapering fees scenario assumes a much higher adoption rate, with fees growing to 5,000,000 XTR/d over 3 years, and then growing by 100,000 XTR/d per year after that.

Figures 9-11 show the results of the simulations with a target supply of 21 billion XTR. Here the controller cannot meet the target supply since it would require a burn rate of under 5%. This is illustrated by the purple lines not moving off the min burn rate level for the entirety of the simulation period.

Figures 26-28 show the results of the simulations with a target supply of 18 billion XTR. Here the controller is able to meet the target supply, but only just. The burn rate rises only slightly above the min burn rate level and is essentially "just holding on".

Finally, Figures 43-45 tell a similar but slightly more exaggerated version of the story.

In practice, if the strong growth scenario were to play out, several interventions would be necessary to give the controller room to maneuver. These would include reducing the min burn rate, and reucing the minimum transaction fee to maintain sub-penny transaction fees.

Conclusions

This study shows that the TariThrottle controller is able to maintain a dual mandate of maintaining a target circulating supply of Tari as well as a steady net burn rate over a wide range of fee growth scenarios.

Some scenarios are unlikely to play out as described in practice. But they give valuable insights into how the controller would respond in stressful situations, short-term shocks and deviations, and offer an indication of the robustness of the controller algorithm.

The most common instances of the controller failing to achieve its target was when the fee growth was so low that the maximum burn rate was less than the tail emission.

Under these conditions, the tokens that are emitted exceed the maximum that can be burnt. In practice, this mode represents a failure of the Tari ecosystem as a whole and a limiting controller would not be the greatest concern at this point.

However, the biggest conclusion to draw from this study is that the stated dual mandate is ill-advised.

There are several scenarios where maintaining a target supply is detrimental to the sustainability of validator nodes and to the ability of the network to provide surge capacity during sudden high periods of network demand.

The oscillating fee revenue scenarios are particularly illuminating, since they demonstrate that as fees are falling, the controller increases the burn rate to try and match the emission. This results in a double loss of revenue for validator nodes, since they're receiving a smaller slice of a shrinking pie. This will force them offline sooner than they otherwise would have. When fees recover, it may take some time for those nodes to come back online, and as a result, users experience lags in the network.

Many scenarios illustrate that the controller can effectively do nothing for between 5 and 15 years while it waits for the circulating supply to reach its target. This effectively means that the controller is powerless to do anything to help with validator node profitability or excessive network fees during this period, because its control mandate is misaligned with the health of the network.

It's clear that some additional studies are needed. In particular, we need to identify and maximise for the health of the network, rather than a fixed supply target. Sometimes these two goals will be aligned, but it behooves us to remember which variables represent the tail, and which represent the dog.

As a minimum, the following factors should be considered, since they directly relate to the health of the ecosystem, in that they incentivise validation nodes to remain online and keep the Tari network affordable:

  • There must be a near-constant driving force to burn Minotari and redeem them as Tari, similar to how a kite must always have tension in the string for the operator to be able to fly the kite. Brief periods of slack, corresponding to the price of Tari dropping below 1 XTR, are acceptable, since the operator can draw the slack in to regain tension (by continuing to burn Tari with no new redemptions), but if the situation persists, the kite will crash.
  • Marginal validator nodes must be able to operate at break-even rates at approximately 50% utilisation. Marginal VNs are those that can spin up very quickly to meet surge demand, but are more expensive to run because of this. Typically, VNs running on spot EC2 instances fall into this category.
  • Long-term validator nodes must be able to operate at break-even rates at approximately 20% utilisation. These nodes are those that are always online, and are typically running on reserved EC2 instances or even cheaper server infrastructure. The 20% value means that they will stay online all the time, whilst keeping 5x surge capacity in reserve.
  • The supply of Minotari never goes to zero.
  • The minimum transaction fee should be under 1c in USD terms.

A controller model that incorporates these target conditions will be the subject of future work.

Deprecated RFC

The following documents are either obsolete, or have been superseded by newer RFC documents.

RFC-0010/CodeStructure

Tari Code Structure and Organization

status: deprecated

Maintainer(s): Cayle Sharrock

Licence

The 3-Clause BSD Licence.

Copyright 2018 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

The aim of this Request for Comment (RFC) is to describe and explain the Tari codebase layout.

None.

Description

The code follows a Domain-driven Design (DDD) layout, with top-level directories falling into infrastructure, domain and application layers.

Infrastructure Layer

The infrastructure layer provides a set of crates that have general infrastructural utility. The rest of the Tari codebase can make use of these crates to obtain persistence, communication and cryptographic services. The infrastructure layer doesn't know anything about blockchains, transactions or digital assets.

We recommend that code in this layer generalizes infrastructure services behind abstraction layers as much as is reasonable, so that specific implementations can be swapped out with relative ease.

Domain Layer

The domain layer houses the Tari "business logic". All protocol-related concepts and procedures are defined and implemented here.

This means that any and all terms defined in the Glossary will have a software implementation here, and only here. They can be used in the application layer, but must be implemented in the domain layer.

The domain layer can make use of crates in the infrastructure layer to achieve its goals.

Application Layer

In the application layer, applications build on top of the domain layer to produce the executable software that is deployed as part of the Tari network.

As an example, the following base layer applications may be developed as part of the Tari protocol release:

  • A base node executable (tari_base_node)
  • A Command Line Interface (CLI) wallet for the Tari cryptocurrency (tari_console_wallet)
  • A standalone miner (tari_miner)
  • A mining proxy to enable merge mining Monero (tari_merge_mining_proxy)
  • An Application Programming Interface (API) server for the base node (REST, gRPC, etc.)

Code Layout

  1. Tari Protocol

Github: tari-project/tari

The Tari Protocol code repository is a Rust workspace consisting of multiple packages. A package is a set of crates, which is the source code of a binary or library.

The source code is organized into the following directories.

  • applications contains crates for all the application-layer executables that form part of the Tari codebase.

    • tari_base_node - the Base Node application
    • tari_console_wallet - the CLI Wallet application
    • tari_miner - the SHA3 Miner (CPU)
    • tari_merge_mining_proxy - the Merge Mining Proxy
    • tari_explorer - a local web based block explorer
  • base_layer is the fundamental domain-layer directory and contains multiple packages and crates.

    • core - core classes and traits, such as Transactions, Blocks, and consensus, mempool, and blockchain database code;
    • key_manager - construction and storage of key derivations and mnemonic seed phrases;
    • mmr - an independent implementation of a Merkle Mountain Range;
    • p2p - the block and transaction propagation module;
    • service_framework - asynchronous service stack builder;
    • wallet - a wallet library including services and storage classes to create Tari wallets;
    • wallet_ffi - a Foreign Function Interface (FFI) library to create Tari wallets in other programming languages;
  • comms is the networking and messaging subsystem, used across the base layer and applications;

  • infrastructure contains application-layer code and is not Tari-specific. It holds the following crates:

    • derive - a crate to contain derive(...) macros;
    • shutdown - a convenient way for threads to let each other know to stop working;
    • storage - data persistence services, including a Lightning Memory-mapped Database (LMDB) persistence implementation;
  • other utility and test libraries.

  1. Tari Cryptography

Github: tari-project/tari-crypto

Tari Crypto was refactored into its own crate, for ease of use and integration across different projects. It includes all cryptographic services, including a Curve25519 implementation.

Change Log

DateDescriptionAuthor
21 Dec 2018First draftCjS77
14 Jan 2022Deprecated in favour of READMECjS77

RFC-0121/Consensus Encoding

Consensus Encoding

status: deprecated

Maintainer(s): Stanley Bondi

Licence

The 3-Clause BSD Licence.

Copyright 2022 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and discussion by the community of the technological merits of the potential system outlined herein.

Goals

The aim of this Request for Comment (RFC) is to describe the encoding used for various consensus-critical data types, as well as the construction of hash pre-images and signature challenges used in base-layer consensus.

Description

A Tari base node must validate each block containing a block header as well as set of transaction inputs, transaction outputs and transaction kernels, each containing a number of fields pertinent to their function within the [base layer]. The data contained within these structures needs to be consistently encoded (represented as bytes) across platforms and implementations so that the network can agree on a single correct state.

This RFC defines the low-level specification for how these data types MUST be encoded to construct a valid hash and signature on the Tari network.

Consensus Encoding

The primary goal of consensus encoding is to provide a consistent data format that is committed to in hashes and signatures.

Consensus encoding defines what "raw" data is included in the encoding, the order in which it should appear and the length for variable length elements. To keep encoding as simple as possible, no type information, field names etc. are catered for in the format as this is always statically known. This is particularly appropriate for hashes and signatures where many fields must be consistently represented and concatenated together.

The rest of this section defines some encodings for common primitives used in the Tari codebase.

Unsigned integer encoding

Varint encoding is used for integer fields greater than 1 byte. Describing varint is out of scope for this RFC but there are many resources online to understand this fairly basic encoding. The only rule we apply is that the encoding has a limit of 10 bytes, a little more than what is required to store a 64-bit integer.

Dynamically-sized vec encoding

This type refers to a contiguous block of data of any length. Because the size is dynamic, the size is included in the encoding.

|len(data)| data for type | data for type | ...

Fixed size arrays

If the size of the array is constant (static). The length is omitted and the data is encoded.

| data for type | ...

Optional or nullable encoding

An optional field starts with a 0x00 byte to indicate the value is not provided (None, null, nil etc) or a 0x01 byte to indicate that the value is provided followed by the encoding of the value.

| 0 or 1 | encoding for type |

Ristretto Keys

RistrettoPublicKey and RistrettoPrivateKey types defined in the tari_crypto crate both have 32-byte canonical formats and are encoded as a 32-byte fixed array.

The tari_crypto Rust crate provides an FFI interface that allows generating of the canonical byte formats in any language that supports FFI.

Commitment

A commitment is a RistrettoPublicKey and so has identical encoding.

Schnorr Signature

See the TLU on Schnorr Signatures

A Schnorr signature tuple is <R, s> where R is a RistrettoPublicKey and s is a the signature scalar wrapped in RistrettoPrivateKey.

The encoding is fixed at 64-bytes:

| 32-byte public key | 32-byte scalar |

Signature

A signature tuple consists of a <R, s> where R is the public nonce and s is the signature scalar.

The encoding is fixed at 64-bytes:

| 32-byte commitment (R) | 32-byte scalar (s) |

Commitment Signature

A commitment signature tuple consists of a <R, u, v> where R is the Pederson commitment \(r_u.G + r_v.H\) for the signature scalars u and v.

The encoding is fixed at 96-bytes:

| 32-byte commitment (R) | 32-byte scalar (u) | 32-byte scalar (v) |

Example

Given the following data and types:

{
  // Type: Fixed array of 5 bytes
  short_id: [1,2,3,4,5],
  // Type: variable length bytes
  name: Buffer.from("Case"),
  // Type: unsigned integer
  age: 40,
  // Type: struct
  details: {
      // Type: variable length bytes
      kind: Buffer.from("Hacker"),
  },
  // Type: nullable varint
  dob: null
}

Encoded (hex) as follows:

short idlennameagelenkindnull?dob
0102030405044361736528054861636b657200

Note that nested structs are flattened and the order must be preserved to allow decoding. The 00 null byte is important so that for e.g. the kind bytes cannot be manipulated to produce the same encoding as non-null dob.

Block Header

The block hash pre-image is constructed by first constructing the merge mining hash. Each encoding is concatenated in order as follows:

  1. version - 1 byte
  2. height - varint
  3. prev_hash - fixed 32-bytes
  4. timestamp - varint
  5. input_mr - fixed 32-bytes
  6. output_mr - fixed 32-bytes
  7. output_mmr_size - varint
  8. witness_mr - fixed 32-bytes
  9. kernel_mr - fixed 32-bytes
  10. kernel_mmr_size - `varint
  11. total_kernel_offset - 32-byte Scalar, see RistrettoPrivateKey
  12. total_script_offset - 32-byte Scalar, see RistrettoPrivateKey

This pre-image is hashed and block hash is constructed, in order, as follows:

  1. merge_mining_hash - As above
  2. pow_algo - enumeration of types of PoW as a single unsigned byte, where Monero = 0x00 and Sha3 = 0x01
  3. pow_data - raw variable bytes (no length varint)
  4. nonce - the PoW nonce, u64 converted to a fixed 8-byte array (little endian)

Output Features

pub struct OutputFeatures {
  pub version: OutputFeaturesVersion,
  pub maturity: u64,
  pub flags: OutputFlags,
  pub metadata: Vec<u8>,
  pub unique_id: Option<Vec<u8>>,
  pub parent_public_key: Option<PublicKey>,
  pub asset: Option<AssetOutputFeatures>,
  pub mint_non_fungible: Option<MintNonFungibleFeatures>,
  pub sidechain_checkpoint: Option<SideChainCheckpointFeatures>,
}

Output features consensus encoding is defined as follows (in order):

  1. version - 1 unsigned byte. This should always be 0x00 but is reserved for future proofing.
  2. maturity - varint
  3. flags - 1 unsigned byte
  4. metadata - dynamic vector
  5. unique_id - nullable + dynamic vector
  6. parent_public_key - nullable + 32-byte compressed public key
  7. asset - nullable + AssetOutputFeatures
  8. mint_non_fungible - nullable + MintNonFungibleFeatures
  9. sidechain_checkpoint - nullable + SideChainCheckpointFeatures
AssetOutputFeatures
MintNonFungibleFeatures
SideChainCheckpointFeatures

Transaction Output

pub struct TransactionOutput {
    pub version: TransactionInputVersion,
    pub features: OutputFeatures,
    pub commitment: Commitment,
    pub proof: RangeProof,
    pub script: TariScript,
    pub sender_offset_public_key: PublicKey,
    pub metadata_signature: ComSignature,
    pub covenant: Covenant,
}

The canonical output hash is appended to the output Merkle tree and commits to the common data between an output and the input spending that output i.e. output_hash = Hash(version | features | commitment | script | covenant).

The encoding is defined as follows:

Witness hash

The witness hash is appended to the witness Merkle tree.

  • proof - Raw proof bytes encoded using dynamic vector encoding
  • metadata_signature - [CommitmentSignature]
Metadata signature challenge

See Metadata Signature for details.

Transaction Input

The following struct represents the full transaction input data for reference. The actual input struct does not duplicate the output data to optimise storage and transmission of the input.

pub struct TransactionInput {
  pub version: u8,
  pub input_data: ExecutionStack,
  pub script_signature: ComSignature,
  pub output_version: TransactionOutputVersion,
  pub features: OutputFeatures,
  pub commitment: Commitment,
  pub script: TariScript,
  pub sender_offset_public_key: PublicKey,
  pub covenant: Covenant, 
}

The transaction input canonical hash pre-image is constructed as follows:

  • input_version - 1 byte
  • output_hash - See [TransactionOutput]
  • sender_offset_public_key - RistrettoPublicKey
  • input_data - TariScript Stack
  • script_signature - [CommitmentSignature]

Transaction Kernel

The following struct represents the full transaction input data for reference. The actual input struct does not duplicate the output data to optimise storage and transmission of the input.

pub struct TransactionKernel {
    pub version: TransactionKernelVersion,
    pub features: KernelFeatures,
    pub fee: MicroTari,
    pub lock_height: u64,
    pub excess: Commitment,
    pub excess_sig: Signature,
}

The transaction kernel is encoded as follows:

The canonical hash pre-image is constructed from this encoding.

Script Challenge

For details see RFC-0201_TariScript.md.

The script challenge is constructed as follows:

RFC-0130/Mining

Full-node Mining on Tari Base Layer

status: deprecated

Maintainer(s): Stringhandler

Licence

The 3-Clause BSD Licence.

Copyright 2018 The Tari Development Community

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of this document must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS DOCUMENT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Language

The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY" and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (covering RFC2119 and RFC8174) when, and only when, they appear in all capitals, as shown here.

Disclaimer

This document and its content are intended for information purposes only and may be subject to change or update without notice.

This document may include preliminary concepts that may or may not be in the process of being developed by the Tari community. The release of this document is intended solely for review and d