Filecoin Specification
Filecoin Specification
protocol version: v0.1.0
spec doc version: v1.0-69d3ee9
2019-10-22_03:00:40Z

Introduction

Warning: This draft of the Filecoin protocol specification is a work in progress. It is intended to establish the rough overall structure of the document, enabling experts to fill in different sections in parallel. However, within each section, content may be out-of-order, incorrect, and/or incomplete. The reader is advised to refer to the official Filecoin spec document for specification and implementation questions.

Filecoin is a distributed storage network based on a blockchain mechanism. Filecoin miners can elect to provide storage capacity for the network, and thereby earn units of the Filecoin cryptocurrency (FIL) by periodically producing cryptographic proofs that certify that they are providing the capacity specified. In addition, Filecoin enables parties to exchange FIL currency through transactions recorded in a shared ledger on the Filecoin blockchain. Rather than using Nakamoto-style proof of work to maintain consensus on the chain, however, Filecoin uses proof of storage itself: a miner’s power in the consensus protocol is proportional to the amount of storage it provides.

The Filecoin blockchain not only maintains the ledger for FIL transactions and accounts, but also implements the Filecoin VM, a replicated state machine which executes a variety of cryptographic contracts and market mechanisms among participants on the network. These contracts include storage deals, in which clients pay FIL currency to miners in exchange for storing the specific file data that the clients request. Via the distributed implementation of the Filecoin VM, storage deals and other contract mechanisms recorded on the chain continue to be processed over time, without requiring further interaction from the original parties (such as the clients who requested the data storage).

Architecture Diagrams

Filecoin Systems

Overview Diagram

TODO:

  • cleanup / reorganize
    • this diagram is accurate, and helps lots to navigate, but it’s still a bit confusing
    • the arrows and lines make it a bit hard to follow. We should have a much cleaner version (maybe based on C4)
  • reflect addition of Token system
    • move data_transfers into Token
Protocol Overview Diagram (open in new tab)

Protocol Flow Diagram – deals off chain

Protocol Sequence Diagram - Deals off Chain (open in new tab)

Protocol Flow Diagram – deals on chain

Protocol Sequence Diagram - Deals on Chain (open in new tab)

Parameter Calculation Dependency Graph

This is a diagram of the model for parameter calculation. This is made with orient, our tool for modeling and solving for constraints.

Parameter Calculation Dependency Graph (open in new tab)

Key Concepts

For clarity, we refer the following types of entities to describe implementations of the Filecoin protocol:

  • Data structures are collections of semantically-tagged data members (e.g., structs, interfaces, or enums).

  • Functions are computational procedures that do not depend on external state (i.e., mathematical functions, or programming language functions that do not refer to global variables).

  • Components are sets of functionality that are intended to be represented as single software units in the implementation structure. Depending on the choice of language and the particular component, this might correspond to a single software module, a thread or process running some main loop, a disk-backed database, or a variety of other design choices. For example, the is a component: it could be implemented as a process or thread running a single specified main loop, which waits for network messages and responds accordingly by recording and/or forwarding block data.

  • APIs are messages that can be sent to components. A client’s view of a given sub-protocol, such as a request to a miner node’s {% {} %} component to store files in the storage market, may require the execution of a series of APIs.

  • Nodes are complete software and hardware systems that interact with the protocol. A node might be constantly running several of the above components, participating in several subsystems, and exposing APIs locally and/or over the network, depending on the node configuration. The term full node refers to a system that runs all of the above components, and supports all of the APIs detailed in the spec.

  • Subsystems are conceptual divisions of the entire Filecoin protocol, either in terms of complete protocols (such as the Storage Market or Retrieval Market), or in terms of functionality (such as the VM - Virtual Machine). They do not necessarily correspond to any particular node or software component.

  • Actors are virtual entities embodied in the state of the Filecoin VM. Protocol actors are analogous to participants in smart contracts; an actor carries a FIL currency balance and can interact with other actors via the operations of the VM, but does not necessarily correspond to any particular node or software component.

Filecoin VM

The majority of Filecoin’s user facing functionality (payments, storage market, power table, etc) is managed through the Filecoin Virtual Machine (Filecoin VM). The network generates a series of blocks, and agrees which ‘chain’ of blocks is the correct one. Each block contains a series of state transitions called messages, and a checkpoint of the current global state after the application of those messages.

The global state here consists of a set of actors, each with their own private state.

An actor is the Filecoin equivalent of Ethereum’s smart contracts, it is essentially an ‘object’ in the filecoin network with state and a set of methods that can be used to interact with it. Every actor has a Filecoin balance attributed to it, a state pointer, a code CID which tells the system what type of actor it is, and a nonce which tracks the number of messages sent by this actor. (TODO: the nonce is really only needed for external user interface actors, AKA account actors. Maybe we should find a way to clean that up?)

There are two routes to calling a method on an actor. First, to call a method as an external participant of the system (aka, a normal user with Filecoin) you must send a signed message to the network, and pay a fee to the miner that includes your message. The signature on the message must match the key associated with an account with sufficient Filecoin to pay for the messages execution. The fee here is equivalent to transaction fees in Bitcoin and Ethereum, where it is proportional to the work that is done to process the message (Bitcoin prices messages per byte, Ethereum uses the concept of ‘gas’. We also use ‘gas’).

Second, an actor may call a method on another actor during the invocation of one of its methods. However, the only time this may happen is as a result of some actor being invoked by an external users message (note: an actor called by a user may call another actor that then calls another actor, as many layers deep as the execution can afford to run for).

For full implementation details, see the VM subsystem.

Filecoin Spec Process (v1)

πŸš€ Pre-launch mode

Until we launch, we are making lots of changes to the spec to finish documenting the current version of the protocol. Changes will be made to the spec by a simple PR process, with approvals by key stakeholders. Some refinements are still to happen and testnet is expected to bring a few significant fixes/improvements. Most changes now are changing the document, NOT changing the protocol, at least not in a major way.

Until we launch, if something is missing, PR it in. If something is wrong, PR a fix. If something needs to be elaborated, PR in updates. What is in the top level of this repo, in master, is the spec, is the Filecoin Protocol. Nothing else matters (ie. no other documents, issues contain “the protocol”).

New Proposals -> Drafts -> Spec

⚠️ WARNING: Filecoin is in pre-launch mode, and we are finishing protocol spec and implementations of the current construction/version of the protocol only. We are highly unlikely to merge anything new into the Filecoin Protocol until after mainnet. Feel free to explore ideas anyway and propeare improvements for the future.

For anything that is not part of the currently speced systems (like ‘repair’, for example) the process we will use is:

  • (1) First, discuss the problem(s) and solution(s) in an issue
    • Or several issues, if the space is large and multithreaded enough.
    • Work out all the details required to make this proposal work.
  • (2) Write a draft with all the details.
    • When you feel like a solution is near, write up a draft document that contains all the details, and includes what changes would need to happen to the spec
    • E.g. “Add a System called X with …”, or “Add a library called Y, …”, or “Modify vm/state_tree to include …”
    • Place this document inside the src/drafts/ directory.
    • Anybody is welcome to contribute well-reasoned and detailed drafts.
    • (Note: these drafts will give way to FIPs in the future)
  • (3) Seek approval to merge this into the specification.
    • To seek approval, open an issue and discuss it.
    • If the draft approved by the owners of the filecoin-spec, then the changes to the spec will need to be made in a PR.
    • Once changes make it into the spec, remove the draft.

It is acceptable for a PR for a draft to stay open for quite a while, as thought and discussion on the topic happens. At some point, if the reviewers and the author feel that the current state of the draft is stable enough (though not ‘done’) then it should be merged into the repo. Further changes to the draft are additional PRs, which may generate more discussion. Comments on these drafts are welcome from anyone, but if you wish to be involved in the actual research process, you will need to devote very considerable time and energy to the process.

On merging

For anything in the drafts or notes folder, merge yourself after a review from a relevant person. For anything in the top level (canonical spec), @whyrusleeping or @jbenet will merge after proper review.

Issues

Issues in the specs repo will be high signal, they will either be proposals, or issues directly relating to problems in the spec. More speculative research questions and discussion will happen in the research repo.

About this specification

TODO

FIPs - Filecoin Improvement Proposals

TODO

Contributing to the Filecoin spec

TODO

System Decomposition

What are Systems? How do they work?

Filecoin decouples and modularizes functionality into loosely-joined systems. Each system adds significant functionality, usually to achieve a set of important and tightly related goals.

For example, the Blockchain System provides structures like Block, Tipset, and Chain, and provides functionality like Block Sync, Block Propagation, Block Validation, Chain Selection, and Chain Access. This is separated from the Files, Pieces, Piece Preparation, and Data Transfer. Both of these systems are separated from the Markets, which provide Orders, Deals, Market Visibility, and Deal Settlement.

Why is System decoupling useful?

This decoupling is useful for:

  • Implementation Boundaries: it is possible to build implementations of Filecoin that only implement a subset of systems. This is especially useful for Implementation Diversity: we want many implementations of security critical systems (eg Blockchain), but do not need many implementations of Systems that can be decoupled.
  • Runtime Decoupling: system decoupling makes it easier to build and run Filecoin Nodes that isolate Systems into separate programs, and even separate physical computers.
  • Security Isolation: some systems require higher operational security than others. System decoupling allows implementations to meet their security and functionality needs. A good example of this is separating Blockchain processing from Data Transfer.
  • Scalability: systems and various use cases may drive different performance requirements for different opertators. System decoupling makes it easier for operators to scale their deployments along system boundaries.

Filecoin Nodes don’t need all the systems

Filecoin Nodes vary significantly, and do not need all the systems. Most systems are only needed for a subset of use cases.

For example, the Blockchain System is required for synchronizing the chain, participating in secure consensus, storage mining, and chain validation. Many Filecoin Nodes do not need the chain and can perform their work by just fetching content from the latest StateTree, from a node they trust. Of course, such nodes

Note: Filecoin does not use the “full node” or “light client” terminology, in wide use in Bitcoin and other blockchain networks. In filecoin, these terms are not well defined. It is best to define nodes in terms of their capabilities, and therefore, in terms of the Systems they run. For example:

  • Chain Verifier Node: Runs the Blockchain system. Can sync and validate the chain. Cannot mine or produce blocks.
  • Client Node: Runs the Blockchain, Market, and Data Transfer systems. Can sync and validate the chain. Cannot mine or produce blocks.
  • Retrieval Miner Node: Runs the Market and Data Transfer systems. Does not need the chain. Can make Retrieval Deals (Retrieval Provider side). Can send Clients data, and get paid for it.
  • Storage Miner Node: Runs the Blockchain, Storage Market, Storage Mining systems. Can sync and validate the chain. Can make Storage Deals (Storage Provider side). Can seal stored data into sectors. Can acquire storage consensus power. Can mine and produce blocks.

Separating Systems

How do we determine what functionality belongs in one system vs another?

Drawing boundaries between systems is the art of separating tightly related functionality from unrelated parts. In a sense, we seek to keep tightly integrated components in the same system, and away from other unrelated components. This is sometimes straightforward, the boundaries naturally spring from the data structures or functionality. For example, it is straightforward to observe that Clients and Miners negotiating a deal with each other is very unrelated to VM Execution.

Sometimes this is harder, and it requires detangling, adding, or removing abstractions. For example, the StoragePowerActor and the StorageMarketActor were a single Actor previously. This caused a large coupling of functionality across StorageDeal making, the StorageMarket, markets in general, with Storage Mining, Sector Sealing, PoSt Generation, and more. Detangling these two sets of related functionality requried breaking apart the one actor into two.

Decomposing within a System

Systems themselves decompose into smaller subunits. These are sometimes called “subsystems” to avoid confusion with the much larger, first-class Systems. Subsystems themselves may break down further. The naming here is not strictly enforced, as these subdivisions are more related to protocol and implementation engineering concerns than to user capabilities.

Implementing Systems

System Requirements

In order to make it easier to decouple functionality into systems, the Filecoin Protocol assumes a set of functionality available to all systems. This functionality can be achieved by implementations in a variety of ways, and should take the guidance here as a recommendation (SHOULD).

All Systems, as defined in this document, require the following:

  • Repository:
    • Local IpldStore. Some amount of persistent local storage for data structures (small structured objects). Systems expect to be initialized with an IpldStore in which to store data structures they expect to persist across crashes.
    • User Configuration Values. A small amount of user-editable configuration values. These should be easy for end-users to access, view, and edit.
    • Local, Secure KeyStore. A facility to use to generate and use cryptographic keys, which MUST remain secret to the Filecoin Node. Systems SHOULD NOT access the keys directly, and should do so over an abstraction (ie the KeyStore) which provides the ability to Encrypt, Decrypt, Sign, SigVerify, and more.
  • Local FileStore. Some amount of persistent local storage for files (large byte arrays). Systems expect to be initialized with a FileStore in which to store large files. Some systems (like Markets) may need to store and delete large volumes of smaller files (1MB - 10GB). Other systems (like Storage Mining) may need to store and delete large volumes of large files (1GB - 1TB).
  • Network. Most systems need access to the network, to be able to connect to their counterparts in other Filecoin Nodes. Systems expect to be initialized with a libp2p.Node on which they can mount their own protocols.
  • Clock. Some systems need access to current network time, some with low tolerance for drift. Systems expect to be initialized with a Clock from which to tell network time. Some systems (like Blockchain) require very little clock drift, and require secure time.

For this purpose, we use the FilecoinNode data structure, which is passed into all systems at initialization:

import repo "github.com/filecoin-project/specs/systems/filecoin_nodes/repository"
import filestore "github.com/filecoin-project/specs/systems/filecoin_files/file"
import clock "github.com/filecoin-project/specs/systems/filecoin_nodes/clock"
import libp2p "github.com/filecoin-project/specs/libraries/libp2p"

type FilecoinNode struct {
    Node        libp2p.Node

    Repository  repo.Repository
    FileStore   filestore.FileStore
    Clock       clock.WallClock
}
import ipld "github.com/filecoin-project/specs/libraries/ipld"
import key "github.com/filecoin-project/specs/systems/filecoin_nodes/repository/key"
import config "github.com/filecoin-project/specs/systems/filecoin_nodes/repository/config"

type Repository struct {
    config          config.Config
    ipldStore       ipld.Store
    keyStore        key.Store

    // CreateRepository(config Config, ipldStore IPLDDagStore, keyStore KeyStore) &Repository
    GetIPLDStore()  ipld.Store
    GetKeyStore()   key.Store
    GetConfig()     config.Config
}

System Limitations

Further, Systems MUST abide by the following limitations:

  • Random crashes. A Filecoin Node may crash at any moment. Systems must be secure and consistent through crashes. This is primarily achived by limiting the use of persistent state, persisting such state through Ipld data structures, and through the use of initialization routines that check state, and perhaps correct errors.
  • Isolation. Systems must communicate over well-defined, isolated interfaces. They must not build their critical functionality over a shared memory space. (Note: for performance, shared memory abstractions can be used to power IpldStore, FileStore, and libp2p, but the systems themselves should not require it). This is not just an operational concern; it also significantly simplifies the protocol and makes it easier to understand, analyze, debug, and change.
  • No direct access to host OS Filesystem or Disk. Systems cannot access disks directly – they do so over the FileStore and IpldStore abstractions. This is to provide a high degree of portability and flexibility for end-users, especially storage miners and clients of large amounts of data, which need to be able to easily replace how their Filecoin Nodes access local storage.
  • No direct access to host OS Network stack or TCP/IP. Systems cannot access the network directly – they do so over the libp2p library. There must not be any other kind of network access. This provides a high degree of portability across platforms and network protocols, enabling Filecoin Nodes (and all their critical systems) to run in a wide variety of settings, using all kinds of protocols (eg Bluetooth, LANs, etc).

Systems

Filecoin Nodes

Node Types

Node Interface

import repo "github.com/filecoin-project/specs/systems/filecoin_nodes/repository"
import filestore "github.com/filecoin-project/specs/systems/filecoin_files/file"
import clock "github.com/filecoin-project/specs/systems/filecoin_nodes/clock"
import libp2p "github.com/filecoin-project/specs/libraries/libp2p"

type FilecoinNode struct {
    Node        libp2p.Node

    Repository  repo.Repository
    FileStore   filestore.FileStore
    Clock       clock.WallClock
}

Examples

There are many kinds of Filecoin Nodes …

This section should contain:

  • what all nodes must have, and why
  • examples of using different systems

Chain Verifier Node

type ChainVerifierNode interface {
  FilecoinNode

  systems.Blockchain
}

Client Node

type ClientNode struct {
  FilecoinNode

  systems.Blockchain
  markets.StorageMarketClient
  markets.RetrievalMarketClient
  markets.MarketOrderBook
  markets.DataTransfers
}

Storage Miner Node

type StorageMinerNode interface {
  FilecoinNode

  systems.Blockchain
  systems.Mining
  markets.StorageMarketProvider
  markets.MarketOrderBook
  markets.DataTransfers
}

Retrieval Miner Node

type RetrievalMinerNode interface {
  FilecoinNode

  blockchain.Blockchain
  markets.RetrievalMarketProvider
  markets.MarketOrderBook
  markets.DataTransfers
}

Relayer Node

type RelayerNode interface {
  FilecoinNode

  blockchain.MessagePool
  markets.MarketOrderBook
}

Repository - Local Storage for Chain Data and Systems

import ipld "github.com/filecoin-project/specs/libraries/ipld"
import key "github.com/filecoin-project/specs/systems/filecoin_nodes/repository/key"
import config "github.com/filecoin-project/specs/systems/filecoin_nodes/repository/config"

type Repository struct {
    config          config.Config
    ipldStore       ipld.Store
    keyStore        key.Store

    // CreateRepository(config Config, ipldStore IPLDDagStore, keyStore KeyStore) &Repository
    GetIPLDStore()  ipld.Store
    GetKeyStore()   key.Store
    GetConfig()     config.Config
}

Config - Local Storage for ConfigurationValues

Filecoin Node configuration

type ConfigKey string
type ConfigVal Bytes

type Config struct {
    Get(k ConfigKey) union {c ConfigVal, e error}
    Put(k ConfigKey, v ConfigVal) error

    Subconfig(k ConfigKey) Config
}

KeyStore & user keys

type Key struct {
    //  Algo Algorithm
    Data Bytes
}

// key.Name
type Name string

// key.Store
// TODO: redo this providing access to enc, dec, sign, sigverify operations, and not the keys.
type Store struct {
    Put(n Name, key Key) error
    Get(n Name) union {k Key, e error}
    //  Sign(n Name, data Bytes) Signature
}

type Algorithm union {
    Sig SignatureAlgorithm
}

type SignatureAlgoC struct {
    Sign(b Bytes) union {s Signature, e error}
    Verify(b Bytes, s Signature) union {b bool, e error}
}

type EdDSASignatureAlgorithm SignatureAlgoC
type Secp256k1SignatureAlgorithm SignatureAlgoC
type BLSAggregateSignatureAlgorithm SignatureAlgoC

type SignatureAlgorithm union {
    EdDSASigAlgo      EdDSASignatureAlgorithm
    Secp256k1SigAlgo  Secp256k1SignatureAlgorithm
    BLSSigAlgo        BLSAggregateSignatureAlgorithm
}

type Signature struct {
    Algo           SignatureAlgorithm
    Data           Bytes

    Verify(k Key)  union {b bool, e error}
}

IpldStore - Local Storage for hash-linked data

type Store GraphStore

// imported as ipld.Object
type Object struct {
    CID() CID

    // Populate(v interface{}) error
}

TODO:

  • What is IPLD
    • hash linked data
    • from IPFS
  • Why is it relevant to filecoin
    • all network datastructures are definitively IPLD
    • all local datastructures can be IPLD
  • What is an IpldStore
    • local storage of dags
  • How to use IpldStores in filecoin
    • pass it around
  • One ipldstore or many
    • temporary caches
    • intermediately computed state
  • Garbage Collection

Usage in Systems

TODO: - Explain how repo is used with systems and subsystems - compartmentalized local storage - store ipld datastructures of stateful objects

Network Interface

import libp2p "github.com/filecoin-project/specs/libraries/libp2p"

type Node libp2p.Node

Filecoin nodes use the libp2p protocol for peer discovery, peer routing, and message multicast, and so on. Libp2p is a set of modular protocols common to the peer-to-peer networking stack. Nodes open connections with one another and mount different protocols or streams over the same connection. In the initial handshake, nodes exchange the protocols that each of them supports and all Filecoin related protcols will be mounted under /filecoin/... protocol identifiers.

Here is the list of libp2p protocols used by Filecoin.

  • Graphsync: TODO
  • Bitswap: TODO
  • Gossipsub: block headers and messages are broadcasted through a Gossip PubSub protocol where nodes can subscribe to topics such as NewBlock, BlockHeader, BlockMessage, etc and receive messages in those topics. When receiving messages related to a topic, nodes processes the message and forwards it to its peers who also subscribed to the same topic.
  • Kad-DHT: Kademlia DHT is a distributed hash table with a logarithmic bound on the maximum number of lookups for a particular node. Kad DHT is used primarily for peer routing as well as peer discovery in the Filecoin protocol.
  • Bootstrap: Bootstrap is a list of nodes that a new node attempts to connect upon joining the network. The list of bootstrap nodes and their addresses are defined by the users.

Clock

type Time string // ISO nano timestamp
type UnixTime int // unix timestamp
type Round int // Blockchain round

type Clock interface {
  UTCTime() Time
  UnixNano() UnixTime

  CurrentRound() Round
  LastRoundObserved() Round
}

TODO:

  • explain why we need a system clock
  • explain where it is used
    • for rejecting/accepting blocks
  • explain synchrony requirements
    • small clock drift – <2s
    • very important to have accurate time
  • explain how we can resync
    • Network
    • recommend various NTP servers
    • Cesium clocks
  • Future work:
    • VDF Clocks

Files & Data

Filecoin’s primary aim is to store client’s Files and Data. This section details data structures and tooling related to working with files, chunking, encoding, graph representations, Pieces, storage abstractions, and more.

File

// Path is an opaque locator for a file (e.g. in a unix-style filesystem).
type Path string

// File is a variable length data container.
// The File interface is modeled after a unix-style file, but abstracts the 
// underlying storage system.
type File struct {
    Path()            Path
    Size()            int
    Seek(offset int)  error
    Read(buf Bytes)   struct {size int, e error}
    Write(buf Bytes)  struct {size int, e error}
    Close()           error
}

FileStore - Local Storage for Files

// FileStore is an object that can store and retrieve files by path.
type FileStore struct {
    Open(p Path)           union {f File, e error}
    Create(p Path)         union {f File, e error}
    Store(p Path, f File)  error
    Delete(p Path)         error

    // maybe add:
    // Copy(SrcPath, DstPath)
}

TODO:

  • explain why this abstraction is needed
  • explain OS filesystem as basic impl
  • explain that users can replace w/ other systems
  • give examples:
    • networked filesystems
    • raw disk sectors - like haystack
    • databases

Piece - a part of a file

A Piece is an object that represents a whole or part of a File, and is used by Clients and Miners in Deals. Clients hire Miners to store Pieces.

import ipld "github.com/filecoin-project/specs/libraries/ipld"

// PieceID is the main reference to pieces in Filecoin. It is the CID
// of the Piece.
type PieceID ipld.CID

type NumBytes UVarint  // TODO: move into util

// PieceSize is the size of a piece, in bytes
type PieceSize struct {
    PayloadSize   NumBytes
    OverheadSize  NumBytes

    Total()       NumBytes
}

// PieceInfo is an object that describes details about a piece, and allows
// decoupling storage of this information from the piece itself.
type PieceInfo struct {
    ID    PieceID
    Size  UVarint
    // TODO: store which algorithms were used to construct this piece.
}

// Piece represents the basic unit of tradeable data in Filecoin. Clients
// break files and data up into Pieces, maybe apply some transformations,
// and then hire Miners to store the Pieces.
//
// The kinds of transformations that may ocurr include erasure coding,
// encryption, and more.
//
// Note: pieces are well formed.
type Piece struct {
    Info       PieceInfo

    // tree is the internal representation of Piece. It is a tree
    // formed according to a sequence of algorithms, which make the
    // piece able to be verified.
    tree       PieceTree

    // Payload is the user's data.
    Payload()  Bytes

    // Data returns the serialized representation of the Piece.
    // It includes the payload data, and intermediate tree objects,
    // formed according to relevant storage algorithms.
    Data()     Bytes
}

// // LocalPieceRef is an object used to refer to pieces in local storage.
// // This is used by subsystems to store and locate pieces.
// type LocalPieceRef struct {
//   ID   PieceID
//   Path file.Path
// }

// PieceTree is a data structure used to form pieces. The algorithms involved
// in the storage proofs determine the shape of PieceTree and how it must be
// constructed.
//
// Usually, a node in PieceTree will include either Children or Data, but not
// both.
//
// TODO: move this into filproofs -- use a tree from there, as that's where
// the algorightms are defined. Or keep this as an interface, met by others.
type PieceTree struct {
    Children  [PieceTree]
    Data      Bytes
}

PieceStore - storing and indexing pieces

A PieceStore is an object that can store and retrieve pieces from some local storage. The PieceStore additionally keeps an index of pieces.

import ipld "github.com/filecoin-project/specs/libraries/ipld"

// PieceStore is an object that stores pieces into some local storage.
// it is internally backed by an IpldStore.
type PieceStore struct {
    Store              ipld.Store
    Index              {PieceID: Piece}

    Get(i PieceID)     struct {p Piece, e error}
    Put(p Piece)       error
    Delete(i PieceID)  error
}

Data Transfer in Filecoin

type Address struct {}
type Storage struct {}
type StorageDealID struct {}
type Function struct {}
type Piece struct {}

type DataTransferSubsystem struct @(mutable) {
    dataTransfers  {UInt: DataTransferChannel}
    volumes        [Storage]

    OpenDataTransferChannel(x StorageDealID) UInt
    CloseDataTransferChannel(x UInt)
    TransferChannelStatus(x UInt) DataTransferStatus
    ReportDataTransferEvent(x Function) union {Ret DataTransferStatus, Err error}

    findData(x CID) Piece
}

// Assumes access to error-checked, ordered, reliable transmission protocol
type Scheduler struct {
    ScheduleTransfer(x UInt, y Piece)
    requestData(x DataTransferChannel) union {Ret Bytes, Err error}
    sendData(x DataTransferChannel, y Bytes) error?
}

type DataTransferChannel struct {
    channelID      UInt
    contentID      CID
    dataIncrement  UVarint
    sender         Address
    recipient      Address
    totalSize      UVarint
    sent           UVarint
    received       UVarint

    channelType()  DataTransferType  @(cached)
    transferNum()  Float             @(cached)
}

type DataTransferParams struct {
    channelType    DataTransferType
    contentID      CID
    dataIncrement  UVarint
    sender         Address
    recipient      Address
    totalSize      Address
}

type DataTransferType union {
    Sender
    Recipient
}

type Sender struct {}  // TODO: remove this
type Recipient struct {}  // TODO: remove this

type Ongoing struct {}
type Completed struct {}
type Failed struct {}
type ChannelNotFoundError struct {}
type DataTransferStatus union {
    Ongoing
    Completed
    Failed
    ChannelNotFoundError
}

VM - Virtual Machine

import msg "github.com/filecoin-project/specs/systems/filecoin_vm/message"
import st "github.com/filecoin-project/specs/systems/filecoin_vm/state_tree"

// VM is the object that controls execution.
// It is a stateless, pure function. It uses no local storage.
//
// TODO: make it just a function: VMExec(...) ?
type VM struct {
    // Execute computes and returns outTree, a new StateTree which is the
    // application of msgs to inTree.
    //
    // *Important:* Execute is intended to be a pure function, with no side-effects.
    // however, storage of the new parts of the computed outTree may exist in
    // local storage.
    //
    // *TODO:* define whether this should take 0, 1, or 2 IpldStores:
    // - (): storage of IPLD datastructures is assumed implicit
    // - (store): get and put to same IpldStore
    // - (inStore, outStore): get from inStore, put new structures into outStore
    //
    // This decision impacts callers, and potentially impacts how we reason about
    // local storage, and intermediate storage. It is definitely the case that
    // implementations may want to operate on this differently, depending on
    // how their IpldStores work.
    Execute(inTree st.StateTree, msgs [msg.Message]) union {outTree st.StateTree, err error}
}

VM Actor Interface

// This contains actor things that are _outside_ of VM exection.
// The VM uses this to execute actors.

import ipld "github.com/filecoin-project/specs/libraries/ipld"

// TokenAmount is an amount of Filecoin tokens. This type is used within
// the VM in message execution, to account movement of tokens, payment
// of VM gas, and more.
type TokenAmount UVarint  // TODO: bigint or varint?

// MethodNum is an integer that represents a particular method
// in an actor's function table. These numbers are used to compress
// invocation of actor code, and to decouple human language concerns
// about method names from the ability to uniquely refer to a particular
// method.
//
// Consider MethodNum numbers to be similar in concerns as for
// offsets in function tables (in programming languages), and for
// tags in ProtocolBuffer fields. Tags in ProtocolBuffers recommend
// assigning a unique tag to a field and never reusing that tag.
// If a field is no longer used, the field name may change but should
// still remain defined in the code to ensure the tag number is not
// reused accidentally. The same should apply to the MethodNum
// associated with methods in Filecoin VM Actors.
type MethodNum UVarint

// MethodParams is an array of objects to pass into a method. This
// is the list of arguments/parameters.
//
// TODO: serialized or deserialized? (serialized for now)
// TODO: force CIDs or by value is fine?
type MethodParams [Bytes]

// CallSeqNum is an invocation (Call) sequence (Seq) number (Num).
// This is a value used for securing against replay attacks:
// each AccountActor (user) invocation must have a unique CallSeqNum
// value. The sequenctiality of the numbers is used to make it
// easy to verify, and to order messages.
//
// Q&A
// - > Does it have to be sequential?
//   No, a random nonce could work against replay attacks, but
//   making it sequential makes it much easier to verify.
// - > Can it be used to order events?
//   Yes, a user may submit N separate messages with increasing
//   sequence number, causing them to execute in order.
//
type CallSeqNum UVarint

// Code is a serialized object that contains the code for an Actor.
// Until we accept external user-provided contracts, this is the
// serialized code for the actor in the Filecoin Specification.
type Code Bytes

// CodeCID represents a CID for a Code object.
type CodeCID ipld.CID

// Actor is a base computation object in the Filecoin VM. Similar
// to Actors in the Actor Model (programming), or Objects in Object-
// Oriented Programming, or Ethereum Contracts in the EVM.
//
// Questions for id language:
// - we should not do inheritance, we should do composition.
//   but we should make including actor state nicer.
//
// TODO: do we need this type? what is the difference between this and
// ActorState
type Actor struct {
    State ActorState
}

// ActorState represents the on-chain storage actors keep.
type ActorState struct {
    // common fields for all actors

    CodeCID
    // use a CID here, load it in interpreter.
    // Alternative is to use ActorState here but tricky w/ the type system
    State       ActorSubstateCID

    Balance     TokenAmount
    CallSeqNum  // FKA Nonce
}

// ActorID is a sequential number assigned to actors in a Filecoin Chain.
// ActorIDs are assigned by the InitActor, when an Actor is introduced into
// the Runtime.
type ActorID UVarint

type ActorSubstateCID ipld.CID

// ActorState represents the on-chain storage actors keep. This type is a
// union of concrete types, for each of the Actors:
// - InitActor
// - CronActor
// - AccountActor
// - PaymentChannelActor
// - StoragePowerActor
// - StorageMinerActor
// - StroageMarketActor
//
// TODO: move this into a directory inside the VM that patches in all
// the actors from across the system. this will be where we declare/mount
// all actors in the VM.
// type ActorState union {
//     Init struct {
//         AddressMap  {addr.Address: ActorID}
//         NextID      ActorID
//     }
// }

Address

// Address is defined here because this is where addresses start to make sense.
// Addresses refer to actors defined in the StateTree, so Addresses are defined
// on top of the StateTree.
//
// TODO: potentially move into a library, or its own directory.
type Address struct {
    NetworkID enum {
        Testnet
        Mainnet
    }

    Type enum {
        ID
        Secp256k1
        Actor
        BLS
    }

    VerifySyntax(addrType Address_Type) bool
    String() AddressString
}

type AddressString string

State Tree

The State Tree is the output of applying operations on the Filecoin Blockchain.

import addr "github.com/filecoin-project/specs/systems/filecoin_vm/actor/address"
import actor "github.com/filecoin-project/specs/systems/filecoin_vm/actor"

// Epoch is an epoch in time in the StateTree.
// It corresponds to rounds in the blockchain, but it
// is defined here as actors need a notion of time.
type Epoch UVarint

// TODO: move this into a directory w/ all the actors + states
type ActorName enum {
    StoragePowerActor
    StorageMarketActor
    StorageMinerActor
    PaymentChannelActor
    InitActor
    AccountActor
    CronActor
}

type StateTree struct {
    SystemActors              {ActorName: addr.Address}
    ActorStates               {addr.Address: actor.ActorState}

    // TODO: API ConvenienceAPI

    GetActor(a addr.Address)  actor.Actor
}

TODO

  • Add ConvenienceAPI state to provide more user-friendly views.

VM Message - Actor Method Invocation

import filcrypto "github.com/filecoin-project/specs/libraries/filcrypto"
import addr "github.com/filecoin-project/specs/systems/filecoin_vm/actor/address"
import actor "github.com/filecoin-project/specs/systems/filecoin_vm/actor"

// GasAmount is a quantity of gas.
type GasAmount UVarint

// GasPrice is a Gas-to-FIL cost
type GasPrice actor.TokenAmount

type Message union {
    UnsignedMessage
    SignedMessage
}  // representation keyed

type UnsignedMessage struct {
    From        addr.Address
    To          addr.Address

    Method      actor.MethodNum
    Params      actor.MethodParams  // Serialized parameters to the method.

    // When receiving a message from a user account the nonce in the message must match
    // the expected nonce in the "from" actor. This prevents replay attacks.
    CallSeqNum  actor.CallSeqNum
    Value       actor.TokenAmount

    GasPrice
    GasLimit    GasAmount
}  // representation tuple

type SignedMessage struct {
    Message    UnsignedMessage
    Signature  filcrypto.Signature
}  // representation tuple

type ExitCode UVarint

type MessageReceipt struct {
    ExitCode
    ReturnValue  Bytes
    GasUsed      GasAmount
}  // representation tuple

VM Runtime Environment (Inside the VM)

import ipld "github.com/filecoin-project/specs/libraries/ipld"
import st "github.com/filecoin-project/specs/systems/filecoin_vm/state_tree"
import msg "github.com/filecoin-project/specs/systems/filecoin_vm/message"
import addr "github.com/filecoin-project/specs/systems/filecoin_vm/actor/address"
import actor "github.com/filecoin-project/specs/systems/filecoin_vm/actor"

// Randomness is a string of random bytes
type Randomness Bytes

// Runtime is the VM's internal runtime object.
// this is everything that is accessible to actors, beyond parameters.
// FKA as vm.Context
type Runtime struct {
    // Invocation is the inputs for the current invocation
    Invocation     InvocInput
    State          VMState

    // Fatal is a fatal error, and halts VM execution.
    // This is the normal error condition of actor execution.
    // On Fatal errors, the VM simply does not apply the state transition.
    // This is atomic across the entire contract execution.
    //
    // TODO: @why wants to get rid of this.
    Fatal(string)

    // Send allows the current execution context to invoke methods on other actors in the system
    //
    // TODO: this should change to async -- put the message on the queue.
    //       do definied callback methods, with maybe a glue closure to align params or carry intermediate state.
    //
    // TODO: what are the return values here?
    Send(
        to      addr.Address
        method  actor.MethodNum
        params  actor.MethodParams
        value   actor.TokenAmount
    ) msg.MessageReceipt
}

// VMState is Chain state accessible to all contracts via the VM interface
type VMState struct {
    // StateTree returns the current state tree
    StateTree  st.StateTree

    // Storage provides access to the VM storage layer
    Storage    VMStorage

    // BlockHeight returns the height of the block this message was added to the chain in
    Epoch()    st.Epoch

    // Balance returns the balance of a given actor
    Balance(id actor.ActorID) actor.TokenAmount

    // Randomness returns the randomness (ticket) for a particular epoch, and offset.
    Randomness(e st.Epoch, offset UInt) Randomness

    // ComputeActorAddress computes the address of the contract,
    // based on the creator (invoking address) and nonce given.
    // TODO: why is this needed? -- InitActor
    // TODO: this has to be the origin call. and it's broken: could yield the same address
    //       need a truly unique way to assign an address.
    ComputeActorAddress(creator addr.Address, nonce actor.CallSeqNum) addr.Address
}

type VMStorage struct {
    // Put writes the given object to the storage staging area and returns its CID
    Put(o IpldObject) struct {c ipld.CID, err error}

    // Get fetches the given object from storage (either staging, or local) and returns
    // the serialized data.
    Get(c ipld.CID) struct {o IpldObject, err error}

    // Commit updates the actual stored state for the actor. This is a compare and swap
    // operation, and will fail if 'old' is not equal to the current return value of `Head`.
    // This functionality is used to prevent issues with re-entrancy
    //
    // TODO: YIKES i dont think we need commit to prevent re-entrancy. if we do, the model
    // is wrong.
    Commit(old ipld.CID, new ipld.CID) error

    // Head returns the CID of the current actor state
    Head() ipld.CID
}

type IpldObject struct {}
package runtime

import ipld "github.com/filecoin-project/specs/libraries/ipld"
import st "github.com/filecoin-project/specs/systems/filecoin_vm/state_tree"
import msg "github.com/filecoin-project/specs/systems/filecoin_vm/message"
import addr "github.com/filecoin-project/specs/systems/filecoin_vm/actor/address"
import actor "github.com/filecoin-project/specs/systems/filecoin_vm/actor"
import gascost "github.com/filecoin-project/specs/systems/filecoin_vm/runtime/gascost"
import exitcode "github.com/filecoin-project/specs/systems/filecoin_vm/runtime/exitcode"

// InvocInput represents inputs to a particular actor invocation.
type InvocInput struct {
	Runtime     Runtime
	InTree      st.StateTree
	OriginActor actor.Actor
	CallSeqNum  actor.CallSeqNum
	FromActor   actor.Actor
	ToActor     actor.Actor
	Method      actor.MethodNum
	Params      actor.MethodParams
	Value       actor.TokenAmount
	GasLimit    msg.GasAmount
	GasUsed     msg.GasAmount
	// GasPrice    GasPrice
}

type InvocOutput struct {
	OutTree     st.StateTree
	ExitCode    msg.ExitCode
	ReturnValue []byte
	GasUsed     msg.GasAmount
}

func ErrorInvocOutput(tree st.StateTree, ec msg.ExitCode) InvocOutput {
	return InvocOutput{
		OutTree:     tree,
		GasUsed:     gascost.CodeLookupFail,
		ExitCode:    exitcode.InvalidMethod,
		ReturnValue: nil,
	}
}

func (r *Runtime_I) Fatal(string) Runtime_Fatal_FunRet {
	panic("TODO")
}

func (r *Runtime_I) Send(to addr.Address, method actor.MethodNum, params actor.MethodParams, value actor.TokenAmount) msg.MessageReceipt {
	panic("TODO")
}

func (s *VMState_I) Epoch() st.Epoch {
	panic("TODO")
}

func (s *VMState_I) Balance(id actor.ActorID) actor.TokenAmount {
	panic("TODO")
}

func (s *VMState_I) Randomness(e st.Epoch, offset uint64) Randomness {
	panic("TODO")
}

func (s *VMState_I) ComputeActorAddress(creator addr.Address, nonce actor.CallSeqNum) addr.Address {
	panic("TODO")
}

func (s *VMStorage_I) Put(o IpldObject) VMStorage_Put_FunRet {
	panic("TODO")
}

func (s *VMStorage_I) Get(c ipld.CID) VMStorage_Get_FunRet {
	panic("TODO")
}

func (s *VMStorage_I) Commit(old ipld.CID, new ipld.CID) error {
	panic("TODO")
}

func (s *VMStorage_I) Head() ipld.CID {
	panic("TODO")
}

VM Exit Code Constants

package runtime

import msg "github.com/filecoin-project/specs/systems/filecoin_vm/message"

// TODO: assign all of these.
var (
	// OK is the success return value, similar to unix exit code 0.
	OK = msg.ExitCode(0)

	// ActorNotFound represents a failure to find an actor.
	ActorNotFound = msg.ExitCode(1)

	// ActorCodeNotFound represents a failure to find the code for a
	// particular actor in the VM registry.
	ActorCodeNotFound = msg.ExitCode(2)

	// InvalidMethod represents a failure to find a method in
	// an actor
	InvalidMethod = msg.ExitCode(3)

	// InsufficientFunds represents a failure to apply a message, as
	// it did not carry sufficient funds for its application.
	InsufficientFunds = msg.ExitCode(4)

	// InvalidCallSeqNum represents a message invocation out of sequence.
	// This happens when message.CallSeqNum is not exactly actor.CallSeqNum + 1
	InvalidCallSeqNum = msg.ExitCode(5)
)

VM Gas Cost Constants

package runtime

import msg "github.com/filecoin-project/specs/systems/filecoin_vm/message"

// TODO: assign all of these.
var (
	// SimpleValueSend is the amount of gas charged for sending value from one
	// contract to another, without executing any other code.
	SimpleValueSend = msg.GasAmount(1)

	// // ActorLookupFail is the amount of gas charged for a failure to lookup
	// // an actor
	// ActorLookupFail = msg.GasAmount(1)

	// CodeLookupFail is the amount of gas charged for a failure to lookup
	// code in the VM's code registry.
	CodeLookupFail = msg.GasAmount(1)

	// ApplyMessageFail represents the gas cost for failures to apply message.
	// These failures are basic failures encountered at first application.
	ApplyMessageFail = msg.GasAmount(1)
)

System Actors

  • There are two system actors required for VM processing:
  • There is one more VM level actor:

InitActor

(You can see the old InitActor here )

import addr "github.com/filecoin-project/specs/systems/filecoin_vm/actor/address"
import actor "github.com/filecoin-project/specs/systems/filecoin_vm/actor"
import vmr "github.com/filecoin-project/specs/systems/filecoin_vm/runtime"

type InitActorState struct {
    // responsible for create new actors
    AddressMap  {addr.Address: actor.ActorID}
    IDMap       {actor.ActorID: addr.Address}
    NextID      actor.ActorID
}

type InitActorCode struct {
    // 0
    Constructor(r vmr.Runtime)

    // 1
    Exec(r vmr.Runtime, code actor.CodeCID, params actor.MethodParams) addr.Address

    // 2
    GetActorIDForAddress(r vmr.Runtime, address addr.Address) actor.ActorID
}
package sysactors

import addr "github.com/filecoin-project/specs/systems/filecoin_vm/actor/address"
import actor "github.com/filecoin-project/specs/systems/filecoin_vm/actor"
import vmr "github.com/filecoin-project/specs/systems/filecoin_vm/runtime"
import exitcode "github.com/filecoin-project/specs/systems/filecoin_vm/runtime/exitcode"

// import st "github.com/filecoin-project/specs/systems/filecoin_vm/state_tree"
// import util "github.com/filecoin-project/specs/util"

func (a *InitActorCode_I) Constructor(rt vmr.Runtime) {
	panic("TODO")
}

func (a *InitActorCode_I) Exec(rt vmr.Runtime, state InitActorState, codeCID actor.CodeCID, method actor.MethodNum, params actor.MethodParams) addr.Address {
	// Make sure that only the actors defined in the spec can be launched.
	if !a._isBuiltinActor(codeCID) {
		rt.Fatal("cannot launch actor instance that is not a builtin actor")
	}

	// Get the actor ID for this actor.
	actorID := a._assignNextID(state)

	// Ensure that singeltons can only be launched once.
	// TODO: do we want to enforce this? If so how should actors be marked as such?
	if a._isSingletonActor(codeCID) {
		rt.Fatal("cannot launch another actor of this type")
	}

	// This generates a unique address for this actor that is stable across message
	// reordering
	// TODO: where do `creator` and `nonce` come from?
	// TODO: CallSeqNum is not related to From -- it's related to Origin
	// addr := rt.ComputeActorAddress(rt.Invocation().FromActor(), rt.Invocation().CallSeqNum())
	var addr addr.Address // TODO

	// Set up the actor itself
	actorState := &actor.ActorState_I{
		CodeCID_: codeCID,
		// State_:   nil, // TODO: do we need to init the state? probably not
		Balance_:    rt.Invocation().Value,
		CallSeqNum_: 0,
	}

	// The call to the actors constructor will set up the initial state
	// from the given parameters, setting `actor.Head` to a new value when successfull.
	// TODO: can constructors fail?
	// TODO: this needs to be written such that the specific type Constructor is called.
	//       right now actor.Constructor(p) calls the Actor type, not the concrete type.
	// a.Constructor(params) // TODO: uncomment this.

	// TODO: where is this VM.GlobalState?
	// TODO: do we need this?
	// runtime.State().Storage().Set(actorID, actor)

	// Store the mappings of address to actor ID.
	state.AddressMap()[addr] = actorID
	state.IDMap()[actorID] = addr

	// TODO: adjust this to be proper state setting.
	rt.State().StateTree().ActorStates()[addr] = actorState // atm it's nil

	return addr
}

func (_ *InitActorCode_I) _assignNextID(state InitActorState) actor.ActorID {
	stateI := state.Impl() // TODO: unwrapping like this is ugly.
	actorID := stateI.NextID_
	stateI.NextID_++
	return actorID
}

func (_ *InitActorCode_I) GetActorIDForAddress(state InitActorState, address addr.Address) actor.ActorID {
	return state.AddressMap()[address]
}

// TODO: derive this OR from a union type
func (_ *InitActorCode_I) _isSingletonActor(codeCID actor.CodeCID) bool {
	return true
	// TODO: uncomment this
	// return codeCID == StorageMarketActor ||
	// 	codeCID == StoragePowerActor ||
	// 	codeCID == CronActor ||
	// 	codeCID == InitActor
}

// TODO: derive this OR from a union type
func (_ *InitActorCode_I) _isBuiltinActor(codeCID actor.CodeCID) bool {
	return true
	// TODO: uncomment this
	// return codeCID == StorageMarketActor ||
	// 	codeCID == StoragePowerActor ||
	// 	codeCID == CronActor ||
	// 	codeCID == InitActor ||
	// 	codeCID == StorageMinerActor ||
	// 	codeCID == PaymentChannelActor
}

// TODO
func (a *InitActorCode_I) InvokeMethod(input vmr.InvocInput, method actor.MethodNum, params actor.MethodParams) vmr.InvocOutput {
	// TODO: load state
	// var state InitActorState
	// storage := input.Runtime().State().Storage()
	// err := loadActorState(storage, input.ToActor().State(), &state)

	switch method {
	// case 0: -- disable: value send
	// case 1: -- disable: cron. init has no cron action
	// case 2:
	// 	return a.InitConstructor(input, state)
	// case 3:
	// 	return a.Exec(input, state, params[0], params[1])
	// case 4:
	// 	return a.GetActorIDForAddress(input, state, params[0])
	default:
		return vmr.ErrorInvocOutput(input.InTree, exitcode.InvalidMethod)
	}
}

CronActor

import addr "github.com/filecoin-project/specs/systems/filecoin_vm/actor/address"
import vmr "github.com/filecoin-project/specs/systems/filecoin_vm/runtime"

type CronActorState struct {
    // Cron has no internal state
}

type CronActorCode struct {
    // actors is a set of actors to call during EpochTick.
    // This can be done a bunch of ways. We do it this way here to make it easy to add
    // a handler to Cron elsewhere in the spec code. How to do this is implementation
    // specific.
    Actors [addr.Address]

    // EpochTick executes built-in periodic actions, run at every Epoch.
    // EpochTick(r) is called after all other messages in the epoch have been applied.
    // This can be seen as an implicit last message.
    EpochTick(r vmr.Runtime)
}

AccountActor

(You can see the old AccountActor here )

import addr "github.com/filecoin-project/specs/systems/filecoin_vm/actor/address"
import actor "github.com/filecoin-project/specs/systems/filecoin_vm/actor"

type AccountActorState struct {
    // normal keypair backed accounts
    Address addr.Address
}

type AccountActor struct {
    actorState    actor.ActorState
    accountState  AccountActorState
}

VM Interpreter - Message Invocation (Outside VM)

(You can see the old VM interpreter here )

import msg "github.com/filecoin-project/specs/systems/filecoin_vm/message"
import st "github.com/filecoin-project/specs/systems/filecoin_vm/state_tree"
import addr "github.com/filecoin-project/specs/systems/filecoin_vm/actor/address"

type MessageRef struct {
    Message  msg.UnsignedMessage
    Miner    addr.Address
}

type VMInterpreter struct {
    ApplyMessageBatch(inTree st.StateTree, msgs [MessageRef]) struct {outTree st.StateTree, ret [msg.MessageReceipt]}
    ApplyMessage(inTree st.StateTree, msg msg.Message, minerAddr addr.Address) struct {outTree st.StateTree, ret msg.MessageReceipt}
}
package interpreter

import "errors"
import msg "github.com/filecoin-project/specs/systems/filecoin_vm/message"
import addr "github.com/filecoin-project/specs/systems/filecoin_vm/actor/address"
import actor "github.com/filecoin-project/specs/systems/filecoin_vm/actor"
import st "github.com/filecoin-project/specs/systems/filecoin_vm/state_tree"
import vmr "github.com/filecoin-project/specs/systems/filecoin_vm/runtime"
import exitcode "github.com/filecoin-project/specs/systems/filecoin_vm/runtime/exitcode"
import gascost "github.com/filecoin-project/specs/systems/filecoin_vm/runtime/gascost"
import util "github.com/filecoin-project/specs/util"

func (vmi *VMInterpreter_I) ApplyMessageBatch(inTree st.StateTree, msgs []MessageRef) (outTree st.StateTree, ret []msg.MessageReceipt) {
	compTree := inTree
	for _, m := range msgs {
		oT, r := vmi.ApplyMessage(compTree, m.Message(), m.Miner())
		compTree = oT        // assign the current tree. (this call always succeeds)
		ret = append(ret, r) // add message receipt
	}
	return compTree, ret
}

func (vmi *VMInterpreter_I) ApplyMessage(inTree st.StateTree, message msg.UnsignedMessage, minerAddr addr.Address) (outTree st.StateTree, ret msg.MessageReceipt) {

	compTree := inTree
	fromActor := compTree.GetActor(message.From())
	if fromActor == nil {
		return applyError(inTree, exitcode.InvalidMethod, gascost.ApplyMessageFail)
	}

	// make sure fromActor has enough money to run the max invocation
	maxGasCost := gasToFIL(message.GasLimit(), message.GasPrice())
	totalCost := message.Value() + actor.TokenAmount(maxGasCost)
	if fromActor.State().Balance() < totalCost {
		return applyError(inTree, exitcode.InsufficientFunds, gascost.ApplyMessageFail)
	}

	// make sure this is the right message order for fromActor
	// (this is protection against replay attacks, and useful sequencing)
	if message.CallSeqNum() != fromActor.State().CallSeqNum()+1 {
		return applyError(inTree, exitcode.InvalidCallSeqNum, gascost.ApplyMessageFail)
	}

	// may return a different tree on succeess.
	// this MUST get rolled back if the invocation fails.
	var toActor actor.Actor
	var err error
	compTree, toActor, err = treeGetOrCreateAccountActor(compTree, message.To())
	if err != nil {
		return applyError(inTree, exitcode.ActorNotFound, gascost.ApplyMessageFail)
	}

	// deduct maximum expenditure gas funds first
	// TODO: use a single "transfer"
	compTree = treeDeductFunds(compTree, fromActor, maxGasCost)

	// transfer funds fromActor -> toActor
	// (yes deductions can be combined, spelled out here for clarity)
	// TODO: use a single "transfer"
	compTree = treeDeductFunds(compTree, fromActor, message.Value())
	compTree = treeDepositFunds(compTree, toActor, message.Value())

	// Prepare invocInput.
	invocInput := vmr.InvocInput{
		InTree:    compTree,
		FromActor: fromActor,
		ToActor:   toActor,
		Method:    message.Method(),
		Params:    message.Params(),
		Value:     message.Value(),
		GasLimit:  message.GasLimit(),
	}
	// TODO: this is mega jank. need to rework invocationInput + runtime boundaries.
	invocInput.Runtime = makeRuntime(compTree, invocInput)

	// perform the method call to the actor
	// TODO: eval if we should lift gas tracking and calc to the beginning of invocation
	// (ie, include account creation, gas accounting itself)
	out := invocationMethodDispatch(invocInput)

	// var outTree StateTree
	if out.ExitCode != 0 {
		// error -- revert all state changes -- ie drop updates. burn used gas.
		outTree = inTree // wipe!
		outTree = treeDeductFunds(outTree, fromActor, gasToFIL(out.GasUsed, message.GasPrice()))

	} else {
		// success -- refund unused gas
		outTree = out.OutTree // take the state from the invocation output
		refundGas := message.GasLimit() - out.GasUsed
		outTree = treeDepositFunds(outTree, fromActor, gasToFIL(refundGas, message.GasPrice()))
		outTree = treeIncrementActorSeqNo(outTree, fromActor)
	}

	// reward miner gas fees
	minerActor := compTree.GetActor(minerAddr) // TODO: may be nil.
	outTree = treeDepositFunds(outTree, minerActor, gasToFIL(out.GasUsed, message.GasPrice()))

	return outTree, &msg.MessageReceipt_I{
		ExitCode_:    out.ExitCode,
		ReturnValue_: out.ReturnValue,
		GasUsed_:     out.GasUsed,
	}
}

func invocationMethodDispatch(input vmr.InvocInput) vmr.InvocOutput {
	if input.Method == 0 {
		// just sending money. move along.
		return vmr.InvocOutput{
			OutTree:     input.InTree,
			GasUsed:     gascost.SimpleValueSend,
			ExitCode:    exitcode.OK,
			ReturnValue: nil,
		}
	}

	//TODO: actually invoke the funtion here.
	// put any vtable lookups in this function.

	actorCode, err := loadActorCode(input, input.ToActor.State().CodeCID())
	if err != nil {
		return vmr.InvocOutput{
			OutTree:     input.InTree,
			GasUsed:     gascost.ApplyMessageFail,
			ExitCode:    exitcode.ActorCodeNotFound,
			ReturnValue: nil, // TODO: maybe: err
		}
	}

	return actorCode.InvokeMethod(input, input.Method, input.Params)
}

func treeIncrementActorSeqNo(inTree st.StateTree, a actor.Actor) (outTree st.StateTree) {
	panic("todo")
}

func treeDeductFunds(inTree st.StateTree, a actor.Actor, amt actor.TokenAmount) (outTree st.StateTree) {
	// TODO: turn this into a single transfer call.
	panic("todo")
}

func treeDepositFunds(inTree st.StateTree, a actor.Actor, amt actor.TokenAmount) (outTree st.StateTree) {
	// TODO: turn this into a single transfer call.
	panic("todo")
}

func treeGetOrCreateAccountActor(inTree st.StateTree, a addr.Address) (outTree st.StateTree, _ actor.Actor, err error) {

	toActor := inTree.GetActor(a)
	if toActor != nil { // found
		return inTree, toActor, nil
	}

	switch a.Type().Which() {
	case addr.Address_Type_Case_BLS:
		return treeNewBLSAccountActor(inTree, a)
	case addr.Address_Type_Case_Secp256k1:
		return treeNewSecp256k1AccountActor(inTree, a)
	case addr.Address_Type_Case_ID:
		return inTree, nil, errors.New("no actor with given ID")
	case addr.Address_Type_Case_Actor:
		return inTree, nil, errors.New("no such actor")
	default:
		return inTree, nil, errors.New("unknown address type")
	}
}

func treeNewBLSAccountActor(inTree st.StateTree, addr addr.Address) (outTree st.StateTree, _ actor.Actor, err error) {
	panic("todo")
}

func treeNewSecp256k1AccountActor(inTree st.StateTree, addr addr.Address) (outTree st.StateTree, _ actor.Actor, err error) {
	panic("todo")
}

func applyError(tree st.StateTree, exitCode msg.ExitCode, gasUsed msg.GasAmount) (outTree st.StateTree, ret msg.MessageReceipt) {
	return outTree, &msg.MessageReceipt_I{
		ExitCode_:    exitCode,
		ReturnValue_: nil,
		GasUsed_:     gasUsed,
	}
}

func gasToFIL(gas msg.GasAmount, price msg.GasPrice) actor.TokenAmount {
	return actor.TokenAmount(util.UVarint(gas) * util.UVarint(price))
}

func makeRuntime(tree st.StateTree, input vmr.InvocInput) vmr.Runtime {
	return &vmr.Runtime_I{
		Invocation_: input,
		State_: &vmr.VMState_I{
			StateTree_: tree, // TODO: also in input.InTree.
			Storage_:   &vmr.VMStorage_I{},
		},
	}
}
package interpreter

import actor "github.com/filecoin-project/specs/systems/filecoin_vm/actor"
import runtime "github.com/filecoin-project/specs/systems/filecoin_vm/runtime"

func loadActorCode(input runtime.InvocInput, codeCID actor.CodeCID) (ActorCode, error) {

	// load the code from StateTree.
	// TODO: this is going to be enabled in the future.
	// code, err := loadCodeFromStateTree(input.InTree, codeCID)
	return staticActorCodeRegistry.LoadActor(codeCID)
}
package interpreter

import "errors"
import actor "github.com/filecoin-project/specs/systems/filecoin_vm/actor"
import addr "github.com/filecoin-project/specs/systems/filecoin_vm/actor/address"
import vmr "github.com/filecoin-project/specs/systems/filecoin_vm/runtime"
import sysactors "github.com/filecoin-project/specs/systems/filecoin_vm/sysactors"

var (
	ErrActorNotFound = errors.New("Actor Not Found")
)

var staticActorCodeRegistry = &actorCodeRegistry{}

// CodeCIDs for system actors
var (
	InitActorCodeCID           = actor.CodeCID("filecoin/1.0/InitActor")
	CronActorCodeCID           = actor.CodeCID("filecoin/1.0/CronActor")
	AccountActorCodeCID        = actor.CodeCID("filecoin/1.0/AccountActor")
	StoragePowerActorCodeCID   = actor.CodeCID("filecoin/1.0/StoragePowerActor")
	StorageMinerActorCodeCID   = actor.CodeCID("filecoin/1.0/StorageMinerActor")
	StorageMarketActorCodeCID  = actor.CodeCID("filecoin/1.0/StorageMarketActor")
	PaymentChannelActorCodeCID = actor.CodeCID("filecoin/1.0/PaymentChannelActor")
)

// Addresses for singleton system actors
var (
	InitActorAddr           = &addr.Address_I{} // TODO
	CronActorAddr           = &addr.Address_I{} // TODO
	StoragePowerActorAddr   = &addr.Address_I{} // TODO
	StorageMarketActorAddr  = &addr.Address_I{} // TODO
	PaymentChannelActorAddr = &addr.Address_I{} // TODO
)

// init is called in Go during initialization of a program.
// this is an idiomatic way to do this. Implementations should approach this
// howevery they wish. The point is to initialize a static registry with
// built in pure types that have the code for each actor. Once we have
// a way to load code from the StateTree, use that instead.
func init() {
	registerBuiltinActors(staticActorCodeRegistry)
}

func registerBuiltinActors(r *actorCodeRegistry) {
	// TODO

	cron := &sysactors.CronActorCode_I{}

	r.RegisterActor(InitActorCodeCID, &sysactors.InitActorCode_I{})
	r.RegisterActor(CronActorCodeCID, cron)

	// wire in CRON actions.
	// TODO: there's probably a better place to put this, but for now, do it here.
	cron.Actors_ = append(cron.Actors_, StoragePowerActorAddr)
	cron.Actors_ = append(cron.Actors_, StorageMarketActorAddr)
}

// ActorCode is the interface that all actor code types should satisfy.
// It is merely a method dispatch interface.
type ActorCode interface {
	InvokeMethod(input vmr.InvocInput, method actor.MethodNum, params actor.MethodParams) vmr.InvocOutput
}

type actorCodeRegistry struct {
	code map[actor.CodeCID]ActorCode
}

func (r *actorCodeRegistry) RegisterActor(cid actor.CodeCID, actor ActorCode) {
	r.code[cid] = actor
}

func (r *actorCodeRegistry) LoadActor(cid actor.CodeCID) (ActorCode, error) {
	a, ok := r.code[cid]
	if !ok {
		return nil, ErrActorNotFound
	}
	return a, nil
}

Blockchain

The Filecoin Blockchain is a distributed virtual machine that achieves consensus, processes messages, accounts for storage, and maintains security in the Filecoin Protocol.

It includes:

  • A Message Pool subsystem that nodes use to track and propagate messages related to the storage market throughout a gossip network.
  • A susbystem that tracks and propagates validated message blocks, assembling them into subchains corresponding to versions of the system state.
  • A VM - Virtual Machine subsystem used to interpret and execute messages in order to update system state.
  • A subsystem which manages the creation and maintenance of state trees (the system state) deterministically generated by the vm from a given subchain.
  • A Storage Power Consensus subsystem which tracks storage state for a given chain and helps the blockchain system choose subchains to extend and blocks to include in them.

At a high-level, the Filecoin blockchain grows through successive rounds of leader election in which a number of miners are elected to generate a block, whose inclusion in the chain will earn them block rewards.

Most of the functions of the Filecoin blockchain system are detailed in the code below. We focus here on particular points of interest.

Storage Power

Filecoin’s blockchain runs on storage power. That is, its consensus algorithm by which miners agree on which subchain to mine is predicated on the amount of storage backing that subchain. At a high-level, the Storage Power Consensus subsystem maintains a Power Table that tracks the amount of storage storage miner actors have contributed to the network through Sector commitments and Proofs of Spacetime.

Leader Election and Expected Consensus

The leader election process is likewise handled by the Storage Power Consensus subsystem (SPC). Using the Power Table it maintains, this subsystem runs a Nakamoto-style leader election process called Expected Consensus at every round to elect miners who can extend the block chain by generating new blocks.

Beyond participating in the Storage Market (see the sref storage_market_subsystem), participation in Filecoin’s consensus is the other way storage miners can earn Filecoin tokens.

Expected Consensus has two main components: a leader election process and a chain selection algorithm dependent on a weight function.

Tipsets

EC can elect multiple leaders in a given round meaning Filecoin chains can contain multiple blocks at each height (one per winning miner). This greatly increases chain throughput by allowing blocks to propagate through the network of nodes more efficiently but also means miners should coordinate how they select messages for inclusion in their blocks in order to avoid duplicates and maximize their earnings from transaction fees (see Message Pool).

Accordingly, blocks from a given round are assembled into Tipsets according to certain rules (they must share the same parents and have been mined at the same height). The Filecoin state tree is modified by the execution of all messages in a given Tipset. Different miners may mine on different Tipsets because of network propagation delay.

Due to this fact, adding new blocks to the chain actually validate those blocks’ parent Tipsets, that is: executing the messages of a new block, a miner cannot know exactly what state tree this will yield. That state tree is only known once all messages in that block’s Tipset have been executed. Accordingly, it is in the next round (and based on the number of blocks mined on a given Tipset) that a miner will be able to choose which state tree to extend.

Tipsets

All valid blocks generated in a round form a Tipset that participants will attempt to mine off of in the subsequent round (see above). Tipsets are valid so long as:

  • All blocks in a Tipset have the same parent Tipset
  • All blocks in a Tipset have the same number of tickets in their Tickets array

These conditions imply that all blocks in a Tipset were mined at the same height. This rule is key to helping ensure that EC converges over time. While multiple new blocks can be mined in a round, subsequent blocks all mine off of a Tipset bringing these blocks together. The second rule means blocks in a Tipset are mined in a same round.

The blocks in a tipset have no defined order in representation. During state computation, blocks in a tipset are processed in order of block ticket, breaking ties with the block CID bytes.

Due to network propagation delay, it is possible for a miner in round N+1 to omit valid blocks mined at round N from their Tipset. This does not make the newly generated block invalid, it does however reduce its weight and chances of being part of the canonical chain in the protocol as defined by EC’s Chain Selection function.

TODO – reorder this

The Filecoin blockchain is the main interface linking various actors in the Filecoin system. It ensures that the system’s state is verifiably updated over time and dictates how nodes are meant to extend the network through block reception and validation and extend it through block propagation.

Its components include the:

  • {{ }} – which receives and propagates blocks, maintaining sets of candidate chains on which the miner may mine and running syntactic validation on incoming blocks.
  • {{ }} – which maintains a given chain’s state, providing facilities to other blockchain subsystems which will query state about the latest chain in order to run, and ensuring incoming blocks are semantically validated before inclusion into the chain.
  • {{ }} – which is called in the event of a successful leader election in order to produce a new block that will extend the current heaviest chain before forwarding it to the syncer for propagation.

Blocks

Block

The Block is a unit of the Filecoin blockchain.

import ipld "github.com/filecoin-project/specs/libraries/ipld"
import filcrypto "github.com/filecoin-project/specs/libraries/filcrypto"

import st "github.com/filecoin-project/specs/systems/filecoin_vm/state_tree"
import clock "github.com/filecoin-project/specs/systems/filecoin_nodes/clock"
import addr "github.com/filecoin-project/specs/systems/filecoin_vm/actor/address"
import msg "github.com/filecoin-project/specs/systems/filecoin_vm/message"

type BlockCID ipld.CID
type MessageRoot ipld.CID
type ReceiptRoot ipld.CID
type ChainWeight UVarint
type ChainEpoch UVarint

type BlockHeader struct {
    // Chain linking
    Parents          Tipset
    Weight           ChainWeight
    Epoch            ChainEpoch

    // Miner Info
    MinerAddress     addr.Address

    // State
    StateTree        st.StateTree
    Messages         MessageRoot
    MessageReceipts  ReceiptRoot

    // Consensus things
    Timestamp        clock.Time
    Ticket
    ElectionProof

    // Signatures
    BlockSig         filcrypto.Signature
    BLSAggregateSig  filcrypto.Signature

    //	SerializeSigned()            []byte
    //	ComputeUnsignedFingerprint() []
}

// TODO: remove this. header is signed
type SignedBlock struct {
    block  &Block
    sig    filcrypto.Signature
}

type Block struct {
    Header    BlockHeader
    Messages  [msg.Message]
    Receipts  [msg.MessageReceipt]
}

type Chain struct {
    HeadEpoch()         ChainEpoch
    HeadTipset()        Tipset
    FinalizedEpoch()    ChainEpoch
    LatestCheckpoint()  ChainEpoch

    TipsetAtEpoch(epoch ChainEpoch) Tipset
    TicketAtEpoch(epoch ChainEpoch) Ticket
}
package block

import (
	util "github.com/filecoin-project/specs/util"
)

func SmallerBytes(a, b util.Bytes) util.Bytes {
	if util.CompareBytesStrict(a, b) > 0 {
		return b
	}
	return a
}

func (chain *Chain_I) TipsetAtEpoch(epoch ChainEpoch) Tipset {
	panic("")

	// dist := chain.HeadEpoch() - epoch
	// current := chain.HeadTipset()
	// parents := current.Parents()
	// for i := 0; i < dist; i++ {
	// 	current = parents
	// 	parents = current.Parents
	// }

	// return current
}

func (chain *Chain_I) TicketAtEpoch(epoch ChainEpoch) Ticket {
	ts := chain.TipsetAtEpoch(epoch)
	return ts.MinTicket()
}

func (chain *Chain_I) FinalizedEpoch() ChainEpoch {
	panic("")
	// ep := chain.HeadEpoch()
	// return ep - GetFinality()
}

func (chain *Chain_I) HeadEpoch() ChainEpoch {
	panic("")
}

func (chain *Chain_I) HeadTipset() Tipset {
	panic("")
}

func (bl *Block_I) TipsetAtEpoch(epoch ChainEpoch) Tipset_I {
	panic("")
	// dist := bl.Epoch - epoch - 1
	// current := bl.ParentTipset
	// parents := current.Parents
	// for i := range dist {
	// 	current = parents
	// 	parent = current.Parents
	// }
	// return current
}

func (bl *Block_I) TicketAtEpoch(epoch ChainEpoch) Ticket {
	ts := bl.TipsetAtEpoch(epoch)
	return ts.MinTicket()
}
Tipset

The Tipset is a group of blocks in the same exact round, that all share the exact same parents.

import st "github.com/filecoin-project/specs/systems/filecoin_vm/state_tree"
import clock "github.com/filecoin-project/specs/systems/filecoin_nodes/clock"

type Tipset struct {
    BlockCIDs          [BlockCID]
    Blocks             [BlockHeader]

    Has(block Block)   bool           @(cached)
    Parents            Tipset         @(cached)
    StateTree          st.StateTree   @(cached)
    Weight             ChainWeight    @(cached)
    Epoch              ChainEpoch     @(cached)

    ValidateSyntax()   bool           @(cached)
    LatestTimestamp()  clock.Time     @(cached)
    MinTicket()        Ticket         @(cached)
}
package block

import (
	clock "github.com/filecoin-project/specs/systems/filecoin_nodes/clock"
)

func (ts *Tipset_I) MinTicket() Ticket {
	var ret Ticket

	// for _, currBlock := range ts.Blocks() {
	// 	tix := currBlock.Ticket()
	// 	if ret == nil {
	// 		ret = tix
	// 	} else {
	// 		ret = SmallerBytes(tix, ret)
	// 	}
	// }

	return ret
}

func (ts *Tipset_I) ValidateSyntax() bool {

	if len(ts.Blocks_) <= 0 {
		return false
	}

	panic("TODO")

	// parents := ts.Parents_
	// grandparent := parents[0].Parents_
	// for i := 1; i < len(parents); i++ {
	// 	if grandparent != parents[i].Parents_ {
	// 		return false
	// 	}
	// }

	// numTickets := len(ts.Blocks_[0].Tickets_)
	// for i := 1; i < len(ts.Blocks_); i++ {
	// 	if numTickets != len(ts.Blocks_[i].Tickets_) {
	// 		return false
	// 	}
	// }

	return true
}

func (ts *Tipset_I) LatestTimestamp() clock.Time {
	var latest clock.Time
	panic("TODO")
	// for _, blk := range ts.Blocks_ {
	// 	if blk.Timestamp().After(latest) || latest.IsZero() {
	// 		latest = blk.Timestamp()
	// 	}
	// }
	return latest
}

// func (tipset *Tipset_I) StateTree() stateTree.StateTree {
// 	var currTree stateTree.StateTree = nil
// 	for _, block := range tipset.Blocks() {
// 		if currTree == nil {
// 			currTree = block.StateTree()
// 		} else {
// 			Assert(block.StateTree().CID().Equals(currTree.CID()))
// 		}
// 	}
// 	Assert(currTree != nil)
// 	for _, block := range tipset.Blocks() {
// 		currTree = UpdateStateTree(currTree, block)
// 	}
// 	return currTree
// }

Chain

A Chain is a sequence of tipsets, linked together. It is a single history of execution in the Filecoin blockchain.

Something's not right. The chain.id file was not found.

Something's not right. The chain.go file was not found.

Chain Manager

The Chain Manager is a central component in the blockchain system. It tracks and updates competing subchains received by a given node in order to select the appropriate blockchain head: the latest block of the heaviest subchain it is aware of in the system.

In so doing, the chain manager is the central subsystem that handles bookkeeping for numerous other systems in a Filecoin node and exposes convenience methods for use by those systems, enabling systems to sample randomness from the chain for instance, or to see which block has been finalized most recently.

The chain manager interfaces and functions are included here, but we expand on important details below for clarity.

Chain Expansion
Incoming blocks and semantic validation

Once a block has been received and syntactically validated by the , it must be semantically validated by the chain manager for inclusion on a given chain.

A semantically valid block:

  • must be from a valid miner with power on the chain
  • must only have valid parents in the tipset, meaning
    • that each parent itself must be a valid block
    • that they all have the same parents themselves
    • that they are all at the same height (i.e. include the same number of tickets)
  • must have valid tickets:
    • the first ticket in its tickets array was generated from the smallest ticket in the parent tipset
    • any following ticket in the array was generated from the prior one (meaning all tickets in the ticket array have been generated by the same miner)
  • must have a valid timestamp, meaning
    • must be later than the latest parent block timestamp, and must have been mined within the appropriate mining period

must be later than the earliest parent block time plus appreopriate delay, which is BLOCK_DELAY (30s by default) * len(blk.Tickets). see BLock Validation - must only have valid state transitions: - all messages in the block must be valid - the execution of each message, in the order they are in the block, must produce a receipt matching the corresponding one in the receipt set of the block, see the state machine spec. - the resulting state root after all messages are applied, must match the one in the block

Once the block passes validation, it must be added to the local datastore, regardless whether it is understood as the best tip at this point. Future blocks from other miners may be mined on top of it and in that case we will want to have it around to avoid refetching.
To make certain validation checks simpler, blocks should be indexed by height and by parent set. That way sets of blocks with a given height and common parents may be quickly queried. It may also be useful to compute and cache the resultant aggregate state of blocks in these sets, this saves extra state computation when checking which state root to start a block at when it has multiple parents.

The following requires having and processing (executing) the messages

  • Messages can be checked by verifying the messages hash correctly to the value.
  • MessageAggregateSig can be checked by verifying the messages sign correctly
  • MessageReceipts can only be checked by executing the messages
  • StateRoot is the result of the execution of the messages, and can only be verified by executing them
Block reception algorithm

Chain selection is a crucial component of how the Filecoin blockchain works. Every chain has an associated weight accounting for the number of blocks mined on it and so the power (storage) they track. It is always preferable to mine atop a heavier Tipset rather than a lighter one. While a miner may be foregoing block rewards earned in the past, this lighter chain is likely to be abandoned by other miners forfeiting any block reward earned as miners converge on a final chain. For more on this, see chain selection in the Expected Consensus spec.

However, ahead of finality, a given subchain may be abandoned in order of another, heavier one mined in a given round. In order to rapidly adapt to this, the chain manager must maintain and update all subchains being considered up to finality.

That is, for every incoming block, even if the incoming block is not added to the current heaviest tipset, the chain manager should add it to the appropriate subchain it is tracking, or keep track of it independently until either: - it is able to do so, through the reception of another block in that subchain - it is able to discard it, as that block was mined before finality

We give an example of how this could work in the block reception algorithm.

ChainTipsManager

The Chain Tips Manager is a subcomponent of Filecoin consensus that is technically up to the implementer, but since the pseudocode in previous sections reference it, it is documented here for clarity.

The Chain Tips Manager is responsible for tracking all live tips of the Filecoin blockchain, and tracking what the current ‘best’ tipset is.

// Returns the ticket that is at round 'r' in the chain behind 'head'
func TicketFromRound(head Tipset, r Round) {}

// Returns the tipset that contains round r (Note: multiple rounds' worth of tickets may exist within a single block due to losing tickets being added to the eventually successfully generated block)
func TipsetFromRound(head Tipset, r Round) {}

// GetBestTipset returns the best known tipset. If the 'best' tipset hasn't changed, then this
// will return the previous best tipset.
func GetBestTipset()

// Adds the losing ticket to the chaintips manager so that blocks can be mined on top of it
func AddLosingTicket(parent Tipset, t Ticket)

Block Producer

Mining Blocks

Having registered as a miner, it’s time to start making and checking tickets. At this point, the miner should already be running chain validation, which includes keeping track of the latest Tipsets seen on the network.

For additional details around how consensus works in Filecoin, see the expected consensus spec. For the purposes of this section, there is a consensus protocol (Expected Consensus) that guarantees a fair process for determining what blocks have been generated in a round, whether a miner should mine a block themselves, and some rules pertaining to how “Tickets” should be validated during block validation.

Ticket Generation

For details of ticket generation, see the expected consensus spec.

New tickets are generated using the last ticket in the ticket-chain. Generating a new ticket will take some amount of time (as imposed by the VDF in Expected Consensus).

Because of this, on expectation, as it is produced, the miner will hear about other blocks being mined on the network. By the time they have generated their new ticket, they can check whether they themselves are eligible to mine a new block (see block creation).

At any height H, there are three possible situations:

  • The miner is eligible to mine a block: they produce their block and form a Tipset with it and other blocks received in this round (if there are any), and resume mining at the next height H+1.
  • The miner is not eligible to mine a block but has received blocks: they form a Tipset with them and resume mining at the next height H+1.
  • The miner is not eligible to mine a block and has received no blocks: they run leader election again, using:
    • their losing ticket from the last leader election to produce a new ticket (the Tickets array in the block to be published grows with each new ticket generated).
    • the ticket H + 1 - K blocks back to attempt to generate an ElectionProof.

This process is repeated until either a winning ticket is found (and block published) or a new valid Tipset comes in from the network.

Let’s illustrate this with an example.

Miner M is mining at Height H. Heaviest tipset at H-1 is {B0}

  • New Round:
    • M produces a ticket at H, from B0’s ticket (the min ticket at H-1)
    • M draws the ticket from height H-K to generate an ElectionProof
    • That ElectionProof is invalid
    • M has not heard about other blocks on the network.
  • New Round:
    • M produces a ticket at H + 1 using the ticket produced at H last round.
    • M draws a ticket from height H+1-K to generate an ElectionProof
    • That ElectionProof is valid
    • M generates a block B1
    • M has received blocks B2, B3 from the network with the same parents and same height.
    • M forms a tipset {B1, B2, B3}
  • Finding the new min ticket/extending the ticket chain:
    • M compares the final tickets in {B1,B2,B3} (each has two tickets in their Tickets array). B2 has the smallest final ticket. B2 should be used to extend the ticket chain, conceptually.
  • New Round:
    • M produces a new ticket at H + 2 using B2’s final ticket (the min final ticket in {B1, B2, B3})
    • M draws a ticket from H+2-K to generate an ElectionProof
    • That ElectionProof is invalid
    • M has received B4 from the network, mined atop {B1,B2,B3}
  • New Round with M mining atop B4

Anytime a miner receives new blocks, it should evaluate which is the heaviest Tipset it knows about and mine atop it.

Block Creation

Scratching a winning ticket, and armed with a valid ElectionProof, a miner can now publish a new block!

To create a block, the eligible miner must compute a few fields:

  • Tickets - An array containing a new ticket, and, if applicable, any intermediary tickets generated to prove appropriate delay for any failed election attempts. See ticket generation.
  • ElectionProof - A signature over the final ticket from the Tickets array proving. See checking election results.
  • ParentWeight - As described in Chain Weighting.
  • Parents - the CIDs of the parent blocks.
  • ParentState - Note that it will not end up in the newly generated block, but is necessary to compute to generate other fields. To compute this:
    • Take the ParentState of one of the blocks in the chosen parent set (invariant: this is the same value for all blocks in a given parent set).
    • For each block in the parent set, ordered by their tickets:
    • Apply each message in the block to the parent state, in order. If a message was already applied in a previous block, skip it.
    • Transaction fees are given to the miner of the block that the first occurance of the message is included in. If there are two blocks in the parent set, and they both contain the exact same set of messages, the second one will receive no fees.
    • It is valid for messages in two different blocks of the parent set to conflict, that is, A conflicting message from the combined set of messages will always error. Regardless of conflicts all messages are applied to the state.
    • TODO: define message conflicts in the state-machine doc, and link to it from here
  • MsgRoot - To compute this:
    • Select a set of messages from the mempool to include in the block.
    • Separate the messages into BLS signed messages and secpk signed messages
    • For the BLS messages:
    • Strip the signatures off of the messages, and insert all the bare Messages for them into a sharray.
    • Aggregate all of the bls signatures into a single signature and use this to fill out the BLSAggregate field
    • For the secpk messages:
    • Insert each of the secpk SignedMessages into a sharray
    • Create a TxMeta object and fill each of its fields as follows:
    • blsMessages: the root cid of the bls messages sharray
    • secpkMessages: the root cid of the secp messages sharray
    • The cid of this TxMeta object should be used to fill the MsgRoot field of the block header.
  • BLSAggregate - The aggregated signatures of all messages in the block that used BLS signing.
  • StateRoot - Apply each chosen message to the ParentState to get this.
    • Note: first apply bls messages in the order that they appear in the blsMsgs sharray, then apply secpk messages in the order that they appear in the secpkMessages sharray.
  • ReceiptsRoot - To compute this:
    • Apply the set of messages to the parent state as described above, collecting invocation receipts as this happens.
    • Insert them into a sharray and take its root.
  • Timestamp - A Unix Timestamp generated at block creation. We use an unsigned integer to represent a UTC timestamp (in seconds). The Timestamp in the newly created block must satisfy the following conditions:
    • the timestamp on the block is not in the future (with ALLOWABLE_CLOCK_DRIFT grace to account for relative asynchrony)
    • the timestamp on the block is at least BLOCK_DELAY * len(block.Tickets) higher than the latest of its parents, with BLOCK_DELAY taking on the same value as that needed to generate a valid VDF proof for a new Ticket (currently set to 30 seconds).
    • We also recommend the use of a networkTime() function to be booted on node launch and run every so frequently to call on a networked time service (e.g. ntp) and ensure relative synchrony with the rest of the network.
  • BlockSig - A signature with the miner’s private key (must also match the ticket signature) over the entire block. This is to ensure that nobody tampers with the block after it propagates to the network, since unlike normal PoW blockchains, a winning ticket is found independently of block generation.

An eligible miner can start by filling out Parents, Tickets and ElectionProof with values from the ticket checking process.

Next, they compute the aggregate state of their selected parent blocks, the ParentState. This is done by taking the aggregate parent state of the blocks’ parent Tipset, sorting the parent blocks by their tickets, and applying each message in each block to that state. Any message whose nonce is already used (duplicate message) in an earlier block should be skipped (application of this message should fail anyway). Note that re-applied messages may result in different receipts than they produced in their original blocks, an open question is how to represent the receipt trie of this tipsets ‘virtual block’. For more details on message execution and state transitions, see the Filecoin state machine document.

Once the miner has the aggregate ParentState, they must apply the block reward. This is done by adding the correct block reward amount to the miner owner’s account balance in the state tree. The reward will be spendable immediately in this block. See block reward for details on how the block reward is structured. See Notes on Block Reward Application for some of the nuances in applying block rewards.

Now, a set of messages is selected to put into the block. For each message, the miner subtracts msg.GasPrice * msg.GasLimit from the sender’s account balance, returning a fatal processing error if the sender does not have enough funds (this message should not be included in the chain).

They then apply the messages state transition, and generate a receipt for it containing the total gas actually used by the execution, the executions exit code, and the return value (see receipt for more details). Then, they refund the sender in the amount of (msg.GasLimit - GasUsed) * msg.GasPrice. In the event of a message processing error, the remaining gas is refunded to the user, and all other state changes are reverted. (Note: this is a divergence from the way things are done in Ethereum)

Each message should be applied on the resultant state of the previous message execution, unless that message execution failed, in which case all state changes caused by that message are thrown out. The final state tree after this process will be the block’s StateRoot.

The miner merklizes the set of messages selected, and put the root in MsgRoot. They gather the receipts from each execution into a set, merklize them, and put that root in ReceiptsRoot. Finally, they set the StateRoot field with the resultant state.

Note that the ParentState field from the expected consensus document is left out, this is to help minimize the size of the block header. The parent state for any given parent set should be computed by the client and cached locally.

Finally, the miner can generate a Unix Timestamp to add to their block, to show that the block generation was appropriately delayed.

The miner will wait until BLOCK_DELAY has passed since the latest block in the parent set was generated to timestamp and send out their block. We recommend using NTP or another clock synchronization protocol to ensure that the timestamp is correctly generated (lest the block be rejected). While this timestamp does not provide a hard proof that the block was delayed (we rely on the VDF in the ticket-chain to do so), it provides some softer form of block delay by ensuring that honest miners will reject undelayed blocks.

Now the block is complete, all that’s left is to sign it. The miner serializes the block now (without the signature field), takes the sha256 hash of it, and signs that hash. They place the resultant signature in the BlockSig field.

Block Broadcast

An eligible miner broadcasts the completed block to the network (via block propagation), and assuming everything was done correctly, the network will accept it and other miners will mine on top of it, earning the miner a block reward!

Block Rewards

Over the entire lifetime of the protocol, 1,400,000,000 FIL (TotalIssuance) will be given out to miners. The rate at which the funds are given out is set to halve every six years, smoothly (not a fixed jump like in Bitcoin). These funds are initially held by the network account actor, and are transferred to miners in blocks that they mine. Over time, the reward will eventually become close zero as the fractional amount given out at each step shrinks the network account’s balance to 0.

The equation for the current block reward is of the form:

Reward = (IV * RemainingInNetworkActor) / TotalIssuance

IV is the initial value, and is set to:

IV = 153856861913558700202 attoFIL // 153.85 FIL

IV was derived from:

// Given one block every 30 seconds, this is how many blocks are in six years
HalvingPeriodBlocks = 6 * 365 * 24 * 60 * 2 = 6,307,200 blocks
Ξ» = ln(2) / HalvingPeriodBlocks
IV = TotalIssuance * (1-e^(-Ξ»)) // Converted to attoFIL (10e18)

Note: Due to jitter in EC, and the gregorian calendar, there may be some error in the issuance schedule over time. This is expected to be small enough that it’s not worth correcting for. Additionally, since the payout mechanism is transferring from the network account to the miner, there is no risk of minting too much FIL.

TODO: Ensure that if a miner earns a block reward while undercollateralized, then min(blockReward, requiredCollateral-availableBalance) is garnished (transfered to the miner actor instead of the owner).

Notes on Block Reward Application

As mentioned above, every round, a miner checks to see if they have been selected as the leader for that particular round (see Secret Leader Election in the Expected Consensus spec for more detail). Thus, it is possible that multiple miners may be selected as winners in a given round, and thus, that there will be multiple blocks with the same parents that are produced at the same block height (forming a Tipset). Each of the winning miners will apply the block reward directly to their actor’s state in their state tree.

Other nodes will receive these blocks and form a Tipset out of the eligible blocks (those that have the same parents and are at the same block height). These nodes will then validate the Tipset. The full procedure for how to verify a Tipset can be found above in Block Validation. To validate Tipset state, the validating node will, for each block in the Tipset, first apply the block reward value directly to the mining node’s account and then apply the messages contained in the block.

Thus, each of the miners who produced a block in the Tipset will receive a block reward. There will be no lockup. These rewards can be spent immediately.

Messages in Filecoin also have an associated transaction fee (based on the gas costs of executing the message). In the case where multiple winning miners included the same message in their blocks, only the first miner will be paid this transaction fee. The first miner is the miner with the lowest ticket value (sorted lexicographically). More details on message execution can be found in the State Machine spec.

Open Questions
  • How should receipts for tipsets ‘virtual blocks’ be referenced? It is common for applications to provide the merkleproof of a receipt to prove that a transaction was successfully executed.

Message Pool

The Message Pool is a subsystem in the Filecoin blockchain system. The message pool is acts as the interface between Filecoin nodes and a peer-to-peer network used for off-chain message transmission. It is used by nodes to maintain a set of messages to transmit to the Filecoin VM (for “on-chain” execution).

import addr "github.com/filecoin-project/specs/systems/filecoin_vm/actor/address"
import msg "github.com/filecoin-project/specs/systems/filecoin_vm/message"

type MessagePoolSubsystem struct {
    // needs access to:
    // - BlockchainSubsystem
    //   - needs access to StateTree
    //   - needs access to Messages mined into blocks (probably past finality)
    //     to remove from the MessagePool
    // - NetworkSubsystem
    //   - needs access to MessagePubsub
    //
    // Important remaining questions:
    // - how does BlockchainSubsystem.BlockReceiver handle asking for messages?
    // - how do we note messages are now part of the blockchain
    //   - how are they cleared from the mempool
    // - do we need to have some sort of purge?

    // AddNewMessage is called to add messages created at this node,
    // or to be propagated by this node. All messages enter the network
    // through one of these calls, in at least one filecoin node. They
    // are then propagated to other filecoin nodes via the MessagePool
    // subsystem. Other nodes receive and propagate Messages via their
    // own MessagePools.
    AddNewMessage(m msg.Message)

    // Stats returns information about the MessagePool contents.
    Stats() MessagePoolStats

    // FindMessage receives a descriptor query q, and returns a set of
    // messages currently in the mempool that match the Query constraints.
    // q may have all, any, or no constraints specified.
    // FindMessage(q MessageQuery) union {
    //  [base.Message],
    //  Error
    // }

    // MostProfitableMessages returns messages that are most profitable
    // to mine for this miner.
    //
    // Note: This is where algorithms about chosing best messages given
    //       many leaders should go.
    GetMostProfitableMessages(miner addr.Address) [msg.Message]
}

type MessagePoolStats struct {
    // Size is the amount of messages in the MessagePool
    Size UInt
}

// MessageQuery is a descriptor used to find messages matching one or more
// of the constraints specified.
type MessageQuery struct {
    /*
  From   base.Address
  To     base.Address
  Method ActorMethodId
  Params ActorMethodParams

  ValueMin    TokenAmount
  ValueMax    TokenAmount
  GasPriceMin TokenAmount
  GasPriceMax TokenAmount
  GasLimitMin TokenAmount
  GasLimitMax TokenAmount
  */
}

Clients that use a message pool include:

  • storage market provider and client nodes - for transmission of deals on chain
  • storage miner nodes - for transmission of PoSts, sector commitments, deals, and other operations tracked on chain
  • verifier nodes - for transmission of potential faults on chain
  • relayer nodes - for forwarding and discarding messages appropriately.

The message pool subsystem is made of two components:

  • The message syncer Message Syncer – which receives and propagates messages.
  • Message storage Message Storage – which caches messages according to a given policy.

TODOs:

  • discuss how messages are meant to propagate slowly/async
  • explain algorithms for choosing profitable txns

Message Syncer

TODO:

  • explain message syncer works
  • include the message syncer code
Message Propagation

Messages are propagated over the libp2p pubsub channel /fil/messages. On this channel, every serialised SignedMessage is announced.

Upon receiving the message, its validity must be checked: the signature must be valid, and the account in question must have enough funds to cover the actions specified. If the message is not valid it should be dropped and must not be forwarded.

discuss checking signatures and account balances, some tricky bits that need consideration. Does the fund check cause improper dropping? E.g. I have a message sending funds then use the newly constructed account to send funds, as long as the previous wasn’t executed the second will be considered “invalid” … though it won’t be at the time of execution.

Message Storage

TODO:

  • give sample algorithm for miner message selection in block production (to avoid dups)
  • give sample algorithm for message storage caching/purging policies.

Block Syncer

In order to ensure they are always on the correct latest state of the blockchain, a Filecoin node must continuously monitor and propagate blocks on the network.

When a node receives blocks, it must also validate them. Validation is split into two stages, syntactic and semantic. The syntactic stage may be validated without reference to additional data, the semantic stage requires access to the chain which the block extends.

For clarity, we separate these stages into separate components: syntactic validation is performed by the , and once collected and validated, the blocks are forwarded to the which performs semantic validation and adds the blocks to the node’s current view of the blockchain state.

The block syncer interfaces and functions are included here, but we expand on important details below for clarity.

goFile block_syncer

Block reception and Syntactic Validation

  • Called by: libp2p
  • Calls on:

On reception of a new block over the appropriate libp2p channel (see gossib_sub), the Block Syncer’s OnNewBlock method is invoked. Thereafter, the syncer must perform syntactic validation on the block to discard invalid blocks and forward the others for further validation by the . At a high level, a syntactically valid block:

  • must include a well-formed miner address
  • must include at least one well-formed ticket
  • must include an election proof which is a valid signature by the miner address of the final ticket
  • must include at least one parent CID
  • must include a positive parent weight
  • must include a positive height
  • must include a well-formed state root
  • must include well-formed messages, and corresponding receipts CIDs
  • must include a valid timestamp
Timestamp Syntactic validation

In order to ensure that block producers release blocks as soon as possible, filecoin nodes have a cutoff time within each leader election round after which they cease to accept new blocks for this round.

Specifically, the block syncer validation process will use the subsystem to associate a wall clock time to the given round number. Any block coming in after the cutoff time is discarded.

In practice, appropriate parameters will not impact nodes regardless of their network connectivity, but helps clearly demarkate rounds by creating a buffer between a round’s end and another’s beginning.

Block Propagation

Blocks are propagated over the libp2p pubsub channel /fil/blocks. The following structure is filled out with the appropriate information, serialized (with IPLD), and sent over the wire:

type BlockMessage struct {
  header BlockHeader
  secpkMessages []&SignedMessage
  blsMessages []&Message
}

Storage Power Consensus

The Storage Power Consensus subsystem is the main interface which enables Filecoin nodes to agree on the state of the system. SPC accounts for individual storage miners’ effective power over consensus in given chains in its Power Table. It also runs Expected Consensus (the underlying consensus algorithm in use by Filecoin), enabling storage miners to run leader election and generate new blocks updating the state of the Filecoin system.

Succinctly, the SPC subsystem offers the following services: - Access to the Power Table for every subchain, accounting for individual storage miner power and total power on-chain. - Access to Expected Consensus for individual storage miners, enabling: - Access to verifiable randomness Tickets as needed in the rest of the protocol. - Running Secret Leader Election to produce new blocks. - Running Chain Selection across subchains using EC’s weighting function. - Identification of the most recently finalized tipset, for use by all protocol participants.

Much of the Storage Power Consensus’ subsystem functionality is detailed in the code below but we touch upon some of its behaviors in more detail.

import addr "github.com/filecoin-project/specs/systems/filecoin_vm/actor/address"
import block "github.com/filecoin-project/specs/systems/filecoin_blockchain/struct/block"
import st "github.com/filecoin-project/specs/systems/filecoin_vm/state_tree"
import base_mining "github.com/filecoin-project/specs/systems/filecoin_mining"
import filcrypto "github.com/filecoin-project/specs/libraries/filcrypto"

type StoragePowerConsensusSubsystem struct {//(@mutable)
    actor                StoragePowerActor
    associatedStateTree  &st.StateTree  // TODO: remove this. should not store this here.

    GenerateElectionProof(tipset block.Tipset) block.ElectionProof
    ChooseTipsetToMine(tipsets [block.Tipset]) [block.Tipset]

    ec ExpectedConsensus

    // call by BlockchainSubsystem during block reception
    ValidateBlock(block block.Block) error

    validateElectionProof(
        height         block.ChainEpoch
        electionProof  block.ElectionProof
        workerAddr     addr.Address
    ) bool

    validateTicket(tix block.Ticket, pk filcrypto.VRFPublicKey) bool

    computeTipsetWeight(tipset block.Tipset) block.ChainWeight

    IsWinningElectionProof(electionProof block.ElectionProof, workerAddr addr.Address) bool

    StoragePowerConsensusError() StoragePowerConsensusError

    // Randomness methods

    // call by StorageMiningSubsystem during block production
    GetTicketProductionSeed(chain block.Chain, epoch block.ChainEpoch) block.Ticket

    // call by StorageMiningSubsystem during block production
    GetElectionProofSeed(chain block.Chain, epoch block.ChainEpoch) block.Ticket

    // call by StorageMiningSubsystem in sealing sector
    GetSealSeed(chain block.Chain, epoch block.ChainEpoch) base_mining.SealSeed

    // call by StorageMiningSubsystem after sealing
    GetPoStChallenge(chain block.Chain, epoch block.ChainEpoch) base_mining.PoStChallenge

    GetFinality() block.ChainEpoch
}

type ExpectedConsensus struct {
    ComputeWeight(chain block.Chain) block.ChainWeight
    IsValidConsensusFault(faults ConsensusFaultType, blocks [block.Block]) bool
}

type ConsensusFaultType union {
    DoubleForkMiningFault  ConsensusFault
    ParentGrindingFault    ConsensusFault
}

type ConsensusFault struct {}
type StoragePowerConsensusError struct {}
Distinguishing between storage miners and block miners

There are two ways to earn Filecoin tokens in the Filecoin network: - By participating in the Storage Market as a storage provider and being paid by clients for file storage deals. - By mining new blocks on the network, helping modify system state and secure the Filecoin consensus mechanism.

We must distinguish between both types of “miners” (storage and block miners). Secret Leader Election in Filecoin is predicated on a miner’s storage power. Thus, while all block miners will be storage miners, the reverse is not necessarily true.

However, given Filecoin’s “useful Proof-of-Work” is achieved through file storage (PoRep and PoSt), there is little overhead cost for storage miners to participate in leader election. Such a Storage Miner Actor need only register with the Storage Power Actor in order to participate in Expected Consensus and mine blocks.

Repeated leader election attempts

In the case that no miner is eligible to produce a block in a given round of EC, the storage power consensus subsystem will be called by the block producer to attempt another leader election by incrementing the nonce appended to the ticket drawn from the past in order to attempt to craft a new valid ElectionProof and trying again.

The Ticket chain and randomness on-chain

While each Filecoin block header contains a ticket field (see Tickets), it is useful to provide nodes with a ticket chain abstraction.

Namely, tickets are used throughout the Filecoin system as sources of on-chain randomness. For instance, - The Sector Sealer uses tickets as SealSeeds to bind sector commitments to a given subchain. - The PoSt Generator likewise uses tickets as PoStChallenges to prove sectors remain committed as of a given block. - They are drawn by the Storage Power subsystem as randomness in Secret Leader Election to determine their eligibility to mine a block - They are drawn by the Storage Power subsystem in order to generate new tickets for future use.

Each of these ticket uses may require drawing tickets at different chain heights, according to the security requirements of the particular protocol making use of tickets. Due to the nature of Filecoin’s Tipsets and the possibility of using losing tickets (that did not yield leaders in leader election) for randomness at a given height, tracking the canonical ticket of a subchain at a given height can be arduous to reason about in terms of blocks. To that end, it is helpful to create a ticket chain abstraction made up of only those tickets to be used for randomness at a given height.

This ticket chain will track one-to-one with a block at each height in a given subchain, but omits certain details including other blocks mined at that height.

It is composed inductively as follows. For a given chain:

  • At height 0, take the genesis block, return its ticket
  • At height n+1, take the heaviest tipset in our chain at height n.
    • select the block in that tipset with the smallest final ticket, return its ticket

Because a Tipset can contain multiple blocks, the smallest ticket in the Tipset must be drawn otherwise the block will be invalid.

   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚                      β”‚
   β”‚                      β”‚
   β”‚β”Œβ”€β”€β”€β”€β”                β”‚
   β”‚β”‚ TA β”‚              A β”‚
   β””β”΄β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

   β”Œ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
                          β”‚
   β”‚
    β”Œβ”€β”€β”€β”€β”                β”‚       TA < TB < TC
   β”‚β”‚ TB β”‚              B
    β”΄β”€β”€β”€β”€β”˜β”€ ─ ─ ─ ─ ─ ─ ─ β”˜

   β”Œ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
                          β”‚
   β”‚
    β”Œβ”€β”€β”€β”€β”                β”‚
   β”‚β”‚ TC β”‚              C
    β”΄β”€β”€β”€β”€β”˜β”€ ─ ─ ─ ─ ─ ─ ─ β”˜

In the above diagram, a miner will use block A’s Ticket to generate a new ticket (or an election proof farther in the future) since it is the smallest in the Tipset.

Drawing randomness for sector commitments

Tickets are used as input to the SEAL above in order to tie Proofs-of-Replication to a given chain, thereby preventing long-range attacks (from another miner in the future trying to reuse SEALs).

The ticket has to be drawn from a finalized block in order to prevent the miner from potential losing storage (in case of a chain reorg) even though their storage is intact.

Verification should ensure that the ticket was drawn no farther back than necessary by the miner. We note that tickets can uniquely be associated to a given round in the protocol (lest a hash collision be found), but that the round number is explicited by the miner in commitSector.

We present precisely how ticket selection and verification should work. In the below, we use the following notation:

  • F– Finality (number of rounds)
  • X– round in which SEALing starts
  • Z– round in which the SEAL appears (in a block)
  • Y– round announced in the SEAL commitSector (should be X, but a miner could use any Y <= X), denoted by the ticket selection
    • T– estimated time for SEAL, dependent on sector size
    • G = T + variance– necessary flexibility to account for network delay and SEAL-time variance.

We expect Filecoin will be able to produce estimates for sector commitment time based on sector sizes, e.g.: (estimate, variance) <--- SEALTime(sectors) G and T will be selected using these.

Picking a Ticket to Seal

When starting to prepare a SEAL in round X, the miner should draw a ticket from X-F with which to compute the SEAL.

Verifying a Seal’s ticket

When verifying a SEAL in round Z, a verifier should ensure that the ticket used to generate the SEAL is found in the range of rounds [Z-T-F-G, Z-T-F+G].

In Detail
                               Prover
           ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
          β”‚

          β–Ό
         X-F ◀───────F────────▢ X ◀──────────T─────────▢ Z
     -G   .  +G                 .                        .
  ───(β”Œβ”€β”€β”€β”€β”€β”€β”€β”)───────────────( )──────────────────────( )────────▢
      β””β”€β”€β”€β”€β”€β”€β”€β”˜                 '                        '        time
 [Z-T-F-G, Z-T-F+G]
          β–²

          β”” ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
                              Verifier

Note that the prover here is submitting a message on chain (i.e. the SEAL). Using an older ticket than necessary to generate the SEAL is something the miner may do to gain more confidence about finality (since we are in a probabilistically final system). However it has a cost in terms of securing the chain in the face of long-range attacks (specifically, by mixing in chain randomness here, we ensure that an attacker going back a month in time to try and create their own chain would have to completely regenerate any and all sectors drawing randomness since to use for their fork’s power).

We break this down as follows:

  • The miner should draw from X-F.
  • The verifier wants to find what X-F should have been (to ensure the miner is not drawing from farther back) even though Y (i.e. the round of the ticket actually used) is an unverifiable value.
  • Thus, the verifier will need to make an inference about what X-F is likely to have been based on:
    • (known) round in which the message is received (Z)
    • (known) finality value (F)
    • (approximate) SEAL time (T)
  • Because T is an approximate value, and to account for network delay and variance in SEAL time across miners, the verifier allows for G offset from the assumed value of X-F: Z-T-F, hence verifying that the ticket is drawn from the range [Z-T-F-G, Z-T-F+G].

Expected Consensus

Algorithm

Expected Consensus (EC) is a probabilistic Byzantine fault-tolerant consensus protocol. At a high level, it operates by running a leader election every round in which, on expectation, one participant may be eligible to submit a block. EC guarantees that this winner will be anonymous until they reveal themselves by submitting a proof of their election (we call this proof an Election Proof). All valid blocks submitted in a given round form a Tipset. Every block in a Tipset adds weight to its chain. The ‘best’ chain is the one with the highest weight, which is to say that the fork choice rule is to choose the heaviest known chain. For more details on how to select the heaviest chain, see Chain Selection.

At a very high level, with every new block generated, a miner will craft a new ticket from the prior one in the chain. While on expectation at least one block will be generated at every round, in cases where no one finds a block in a given round, a miner may increment a given nonce as part of the input with which they attempt to run leader election in order to ensure liveness in the protocol. These nonces help mark block height. Every block in a given Tipset will contain election proofs with the same nonce (i.e. they are mined at the same height).

The Storage Power Consensus subsystem uses access to EC to use the following facilities: - Access to verifiable randomness for the protocol, derived from Tickets. - Running and verifying leader election for block generation. - Access to a weighting function enabling Chain Selection by the chain manager. - Access to the most recently finalized tipset available to all protocol participants.

Tickets

For leader election in EC, participants win in proportion to the power they have within the network.

A ticket is drawn from the past at the beginning of each new round to perform leader election. EC also generates a new ticket in every round for future use. Tickets are chained independently of the main blockchain. A ticket only depends on the ticket before it, and not any other data in the block. On expectation, in Filecoin, every block header contains one ticket, though it could contain more if that block was generated over multiple rounds.

Tickets are used across the protocol as sources of randomness: - The Sector Sealer uses tickets to bind sector commitments to a given subchain. - The PoSt Generator likewise uses tickets to prove sectors remain committed as of a given block. - EC uses them to run leader election and generates new ones for use by the protocol, as detailed below.

You can find the Ticket data structure here.

Comparing Tickets in a Tipset

Whenever comparing tickets is evoked in Filecoin, for instance when discussing selecting the “min ticket” in a Tipset, the comparison is that of the little endian representation of the ticket’s VFOutput bytes.

Tickets in EC

Within EC, a miner generates a new ticket in their block for every ticket they use running leader election, thereby ensuring the ticket chain is always as long as the block chain.

Tickets are used to achieve the following: - Ensure leader secrecy – meaning a block producer will not be known until they release their block to the network. - Prove leader election – meaning a block producer can be verified by any participant in the network.

In practice, EC defines two different fields within a block:

  • A Ticket field β€” this stores the new ticket generated during this block generation attempt. It is from this ticket that miners will sample randomness to run leader election in K rounds.
  • An ElectionProof β€” this stores a proof that a given miner has won a leader election using the appropriate ticket K rounds back along with a nonce showing how many rounds generating the EP took. It proves that the leader was elected in this round.

    But why the randomness lookback?
    
    The randomness lookback helps turn independent ticket generation from a block one round back
    into a global ticket generation game instead. Rather than having a distinct chance of winning or losing
    for each potential fork in a given round, a miner will either win on all or lose on all
    forks descended from the block in which the ticket is sampled.
    
    This is useful as it reduces opportunities for grinding, across forks or sybil identities.
    
    However this introduces a tradeoff:
    - The randomness lookback means that a miner can know K rounds in advance that they will win,
    decreasing the cost of running a targeted attack (given they have local predictability).
    - It means electionProofs are stored separately from new tickets on a block, taking up
    more space on-chain.
    
    How is K selected?
    - On the one end, there is no advantage to picking K larger than finality.
    - On the other, making K smaller reduces adversarial power to grind.
    
Ticket generation

This section discusses how tickets are generated by EC for the Ticket field.

At round N, a new ticket is generated using tickets drawn from the Tipset at round N-1 (for more on how tickets are drawn see Ticket Chain).

The miner runs the prior ticket through a Verifiable Random Function (VRF) to get a new unique output.

The VRF’s deterministic output adds entropy to the ticket chain, limiting a miner’s ability to alter one block to influence a future ticket (given a miner does not know who will win a given round in advance).

We use the ECVRF algorithm from Goldberg et al. Section 5, with: - Sha256 for our hashing function - Secp256k1 for our curve - Note that the operation type in step 2.1 is necessary to prevent an adversary from guessing an election proof for a miner ahead of time.

Ticket Validation

Each Ticket should be generated from the prior one in the ticket-chain.

Secret Leader Election

Expected Consensus is a consensus protocol that works by electing a miner from a weighted set in proportion to their power. In the case of Filecoin, participants and powers are drawn from the storage power table, where power is equivalent to storage provided through time.

Leader Election in Expected Consensus must be Secret, Fair and Verifiable. This is achieved through the use of randomness used to run the election. In the case of Filecoin’s EC, the blockchain tracks an independent ticket chain. These tickets are used as randomness inputs for Leader Election. Every block generated references an ElectionProof derived from a past ticket. The ticket chain is extended by the miner who generates a new block for each successful leader election.

Running a leader election

Now, a miner must also check whether they are eligible to mine a block in this round.

To do so, the miner will use tickets from K rounds back as randomness to uniformly draw a value from 0 to 1. Comparing this value to their power, they determine whether they are eligible to mine. A user’s power is defined as the ratio of the amount of storage they proved as of their last PoSt submission to the total storage in the network as of the current block.

We use the ECVRF algorithm (must yield a pseudorandom, deterministic output) from Goldberg et al. Section 5, with: - Sha256 for our hashing function - Secp256k1 for our curve

If the miner wins the election in this round, it can use newEP, along with a newTicket to generate and publish a new block. Otherwise, it waits to hear of another block generated in this round.

It is important to note that every block contains two artifacts: one, a ticket derived from last block’s ticket to extend the ticket-chain, and two, an election proof derived from the ticket K rounds back used to run leader election.

Succinctly, the process of crafting a new ElectionProof in round N is as follows. We use:

The ECVRF algorithm (must yield a pseudorandom, deterministic output) from Goldberg et al. Section 5, with:
    Sha256 for our hashing function
    Secp256k1 for our curve

Note: We draw the miner power from the prior round. This means that if a miner wins a block on their ProvingPeriodEnd even if they have not yet resubmitted a PoSt, they retain their power (until the next round).

If successful, the miner can craft a block, passing it to the block producer. If unsuccessful, it will wait to hear of another block mined this round to try again. In the case no other block was found in this round the miner can increment its nonce and try leader election again using the same past ticket and new nonce. While a miner could try to run through multiple nonces in parallel in order to quickly generate a block, this effort will be futile as the rational majority of miners will reject blocks crafted with ElectionProofs whose nonces prove too high (see below).

Election Validation

In order to determine that the mined block was generated by an eligible miner, one must check its ElectionProof.

Chain Selection

Just as there can be 0 miners win in a round, multiple miners can be elected in a given round. This in turn means multiple blocks can be created in a round. In order to avoid wasting valid work done by miners, EC makes use of all valid blocks generated in a round.

Chain Weighting

It is possible for forks to emerge naturally in Expected Consensus. EC relies on weighted chains in order to quickly converge on ‘one true chain’, with every block adding to the chain’s weight. This means the heaviest chain should reflect the most amount of work performed, or in Filecoin’s case, the most storage provided.

In short, the weight at each block is equal to its ParentWeight plus that block’s delta weight. Details of Filecoin’s chain weighting function are included here.

Delta weight is a term composed of a few elements: - wForkFactor: which seeks to cut the weight derived from rounds in which produced Tipsets do not correspond to what an honest chain is likely to have yielded (pointing to selfish mining or other non-collaborative miner behavior). - wPowerFactor: which adds weight to the chain proportional to the total power backing the chain, i.e. accounted for in the chain’s power table. - wBlocksFactor: which adds weight to the chain proportional to the number of blocks mined in a given round. Like wForkFactor, it rewards miner cooperation (which will yield more blocks per round on expectation).

The weight should be calculated using big integer arithmetic with order of operations defined above. We use brackets instead of parentheses below for legibility. We have:

w[r+1] = w[r] + (wPowerFactor[r+1] + wBlocksFactor[r+1]) * 2^8

For a given tipset ts in round r+1, we define:

  • wPowerFactor[r+1] = wFunction(totalPowerAtTipset(ts))
  • wBlocksFactor[r+1] = wPowerFactor[r+1] * wRatio * b / e
    • with b = |blocksInTipset(ts)|
    • e = expected number of blocks per round in the protocol
    • and wRatio in ]0, 1[ Thus, for stability of weight across implementations, we take:
  • wBlocksFactor[r+1] = (wPowerFactor[r+1] * b * wRatio_num) / (e * wRatio_den)

We get: - w[r+1] = w[r] + wFunction(totalPowerAtTipset(ts)) * 2^8 + (wFunction(totalPowerAtTipset(ts)) * len(ts.blocks) * wRatio_num * 2^8) / (e * wRatio_den) Using the 2^8 here to prevent precision loss ahead of the division in the wBlocksFactor.

The exact value for these parameters remain to be determined, but for testing purposes, you may use: - e = 5 - wRatio = .5, or wRatio_num = 1, wRatio_den = 2 - wFunction = log2b with - log2b(X) = floor(log2(x)) = (binary length of X) - 1 and log2b(0) = 0. Note that that special case should never be used (given it would mean an empty power table.

Note that if your implementation does not allow for rounding to the fourth decimal, miners should apply the [tie-breaker below](#selecting-between-tipsets-with-equal-weight). Weight changes will be on the order of single digit numbers on expectation, so this should not have an outsized impact on chain consensus across implementations.

ParentWeight is the aggregate chain weight of a given block’s parent set. It is calculated as the ParentWeight of any of its parent blocks (all blocks in a given Tipset should have the same ParentWeight value) plus the delta weight of each parent. To make the computation a bit easier, a block’s ParentWeight is stored in the block itself (otherwise potentially long chain scans would be required to compute a given block’s weight).

Selecting between Tipsets with equal weight

When selecting between Tipsets of equal weight, a miner chooses the one with the smallest final ticket.

In the case where two Tipsets of equal weight have the same min ticket, the miner will compare the next smallest ticket (and select the Tipset with the next smaller ticket). This continues until one Tipset is selected.

The above case may happen in situations under certain block propagation conditions. Assume three blocks B, C, and D have been mined (by miners 1, 2, and 3 respectively) off of block A, with minTicket(B) < minTicket© < minTicket (D).

Miner 1 outputs their block B and shuts down. Miners 2 and 3 both receive B but not each others’ blocks. We have miner 2 mining a Tipset made of B and C and miner 3 mining a Tipset made of B and D. If both succesfully mine blocks now, other miners in the network will receive new blocks built off of Tipsets with equal weight and the same smallest ticket (that of block B). They should select the block mined atop [B, C] since minTicket© < minTicket(D).

The probability that two Tipsets with different blocks would have all the same tickets can be considered negligible: this would amount to finding a collision between two 256-bit (or more) collision-resistant hashes.

Finality in EC

EC enforces a version of soft finality whereby all miners at round N will reject all blocks that fork off prior to round N-F. For illustrative purposes, we can take F to be 500. While strictly speaking EC is a probabilistically final protocol, choosing such an F simplifies miner implementations and enforces a macroeconomically-enforced finality at no cost to liveness in the chain.

Consensus Faults

Due to the existence of potential forks in EC, a miner can try to unduly influence protocol fairness. This means they may choose to disregard the protocol in order to gain an advantage over the power they should normally get from their storage on the network. A miner should be slashed if they are provably deviating from the honest protocol.

This is detectable when a given miner submits two blocks that satisfy any of the following “consensus faults”:

  • (1) double-fork mining fault: two blocks contains the same electionProof nonce, mined at the same height.
  • (2) parent grinding fault: one block’s parent is a Tipset that could have validly included the other block according to Tipset validity rules, however the parent of the first block does not include the other block.

    • While it cannot be proven that a miner omits known blocks from a Tipset in general (i.e. network latency could simply mean the miner did not receive a particular block) in this case it can be proven because a miner must be aware of a block they mined in a previous round.

Any node that detects either of the above events should submit both block headers to the StoragePowerActor’s ReportConsensusFault method. The “slasher” will receive a portion (TODO: define how much) of the offending miner’s as a reward for notifying the network of the fault. (TODO: FIP of submitting commitments to block headers to prevent miners censoring slashers in order to gain rewards).

It is important to note that there exists a third type of consensus fault directly reported by the CronActor on StorageDeal failures via the ReportUncommittedPowerFault method: - (3) uncommitted power fault which occurs when a miner fails to submit their PostProof and is thus participating in leader election with undue power (see Faults).

Storage Power Actor

import filcrypto "github.com/filecoin-project/specs/libraries/filcrypto"
import addr "github.com/filecoin-project/specs/systems/filecoin_vm/actor/address"
import libp2p "github.com/filecoin-project/specs/libraries/libp2p"
import deal "github.com/filecoin-project/specs/systems/filecoin_markets/deal"
import sector "github.com/filecoin-project/specs/systems/filecoin_mining/sector"
import block "github.com/filecoin-project/specs/systems/filecoin_blockchain/struct/block"

type StoragePowerActor struct {
    // TODO: bytesamount might be inefficient
    // Miners [addr.Address]
    Miners {addr.Address: block.StoragePower}

    // call by StorageMiningSubsytem on miner creation
    CreateStorageMiner(
        ownerAddr     addr.Address
        workerPubKey  filcrypto.PubKey
        sectorSize    util.UInt
        peerId        libp2p.PeerID  // TODO: will be removed likely (see: https://github.com/filecoin-project/specs/pull/555#pullrequestreview-300991681)
    ) addr.Address

    // TODO add NotifyConsensusFault(addr.Address, [block.Block]) TODO maybe rename to Arbitrate
    UpdatePower(address addr.Address, newPower block.StoragePower)

    TotalStorage() block.StoragePower

    PowerLookup(minerAddr addr.Address) block.StoragePower

    // call by StorageMinerActor on successful seal
    // TODO: workerKey of type StorageMiner.WorkerPubKey
    IncrementPower(minerAddress addr.Address)

    SuspendMiner(address addr.Address)

    expectedConsensus ExpectedConsensus
    ReportConsensusFault(
        slasherAddr  addr.Address
        faultType    ConsensusFaultType
        proof        [block.Block]
    )
    ReportUncommittedPowerFault(cheaterAddr addr.Address, numSectors UVarint)
    PowerTable

    // VerifyElectionProof(addr.Address, ElectionProof)	bool

    Surprise(ticket block.Ticket) [addr.Address]

    CheckFaults()

    // // What does graceful removal look like?

    // TODO: old methods, should be removed
    // UpdateStoragePowerSubsystem(Tipset, StateTree)
    // VerifyElectionProof(addr.Address, ElectionProof)	bool
    // NotifyStorageFault(addr.Address)
    // TryPublishBlock(StateTree)					block.Block
    CommitPledgeCollateral(deals [deal.StorageDeal])
    DecommitPledgeCollateral(deals [deal.StorageDeal])

    pledgeCollateralBalances {addr.Address: UVarint}
}

type PowerTable struct {
    // all power here is always verified
    RegisterMiner(
        addr        addr.Address
        pk          filcrypto.PubKey
        sectorSize  sector.SectorSize
    )
    miners {addr.Address: StorageMiner}
    GetMinerPower(addr addr.Address) block.StoragePower
    GetTotalPower() block.StoragePower
    GetMinerPublicKey(addr addr.Address) filcrypto.PubKey
    IncrementPower(addr addr.Address, numSectors UVarint)
    SuspendPower(addr addr.Address, numSectors UVarint)
    UnsuspendPower(addr addr.Address, numSectors UVarint)
    RemovePower(addr addr.Address, numSectors UVarint)
    RemoveAllPower(addr addr.Address, numSectors UVarint)
}

type StorageMiner struct {
    MinerAddress         addr.Address
    MinerStoragePower    block.StoragePower
    MinerSuspendedPower  block.StoragePower
    MinerPK              filcrypto.PubKey
    MinerSectorSize      sector.SectorSize
}
The Power Table

The portion of blocks a given miner generates through leader election in EC (and so the block rewards they earn) is proportional to their Power Fraction over time. That is, a miner whose storage represents 1% of total storage on the network should mine 1% of blocks on expectation.

SPC provides a power table abstraction which tracks miner power (i.e. miner storage in relation to network storage) over time. The power table is updated for new sector commitments (incrementing miner power), when PoSts fail to be put on-chain (decrementing miner power) or for other storage and consensus faults.

An invariant of the storage power consensus subsystem is that all storage in the power table must be verified. That is, miners can only derive power from storage they have already proven to the network.

In order to achieve this, Filecoin delays updating power for new sector commitments until the first valid PoSt in the next proving period corresponding to that sector. Conversely, storage faults only lead to power loss once they are detected (up to one proving period after the fault) so miners will mine with no more power than they have used to store data over time.

Put another way, power accounting in the SPC is delayed between storage being proven or faulted, and power being updated in the power table (and so for leader election). This ensures fairness over time. Finally, we also account for “suspended” power in order to make accounting simple in the presence of faults (see Consensus Faults) and subsequent recovery.

The Miner lifecycle in the power table should be roughly as follows: - MinerRegistration: A new miner with an associated worker public key and address is registered on the power table by the storage mining subsystem, along with their associated sector size (there is only one per worker). - UpdatePower: These power increments and decrements (in multiples of the associated sector size) are called by various storage actor (and must thus be verified by every full node on the network). Specifically: - Power is incremented to account for a new SectorCommitment at the first PoSt past the first ProvingPeriod. - All Power is decremented immediately after a missed PoSt. - Power is decremented immediately after faults are declared, proportional to the faulty sector size. - Power is incremented after a PoSt recovering from a fault. - Power is definitively removed from the Power Table past the sector failure timeout (see Faults)

Pledge Collateral

Consensus in Filecoin is secured in part by economic incentives enforced by Pledge Collateral.

Pledge collateral amount is committed based on power pledged to the system (i.e. proportional to number of sectors committed and sector size for a miner). It is a system-wide parameter and is committed to the StoragePowerActor. TODO: define parameter value. Pledge Collateral submission methods take on storage deals to determine the appropriate amount of collateral to be pledged. Pledge collateral can be posted by the StorageMinerActor at any time by a miner up to sector commitments. A sector commitment without the requisite posted pledge collateral will be deemed invalid.

Pledge Collateral will be slashed when Consensus Faults are reported to the StoragePowerActor’s ReportConsensusFault method or when the CronActor calls the StoragePowerActor’s ReportUncommittedPowerFault method.

Pledge Collateral is slashed for any fault affecting storage-power consensus, these include: - faults to expected consensus in particular (see Consensus Faults) which will be reported by a slasher to the StoragePowerActor in exchange for a reward. - faults affecting consensus power more generally, specifically uncommitted power faults (i.e. Faults) which will be reported by the CronActor automatically.

Token

FIL Wallet

Payments

Payment Channels

Payment Channel Actor

(You can see the old Payment Channel Actor here )

type Voucher struct {}
type VouchersApprovalResponse struct {}
type PieceInclusionProof struct {}

type PaymentChannelActor struct {
    RedeemVoucherWithApproval(voucher Voucher)
    RedeemVoucherWithPIP(voucher Voucher, pip PieceInclusionProof)
}

Multisig - Wallet requiring multiple signatures

Multisig Actor

(You can see the old Multisig Actor here )

import address "github.com/filecoin-project/specs/systems/filecoin_vm/actor/address"
import actor "github.com/filecoin-project/specs/systems/filecoin_vm/actor"

type TxSeqNo UVarint
type NumRequired UVarint
type EpochDuration UVarint
type Epoch UVarint

type MultisigActor struct {
    signers         [address.Address]
    required        NumRequired
    nextTxId        TxSeqNo
    initialBalance  actor.TokenAmount
    startingBlock   Epoch
    unlockDuration  EpochDuration
    // transactions    {TxSeqNo: Transaction} // TODO Transaction type does not exist

    Construct(
        signers         [address.Address]
        required        NumRequired
        unlockDuration  EpochDuration
    )
    Propose(
        to      address.Address
        value   actor.TokenAmount
        method  string
        params  Bytes
    ) TxSeqNo
    Approve(txid TxSeqNo)
    Cancel(txid TxSeqNo)
    ClearCompleted()
    AddSigner(signer address.Address, increaseReq bool)
    RemoveSigner(signer address.Address, decreaseReq bool)
    SwapSigner(old address.Address, new address.Address)
    ChangeRequirement(req NumRequired)
}

Storage Mining System - proving storage for producing blocks

The Storage Mining System is the part of the Filecoin Protocol that deals with storing Client’s data, producing proof artifacts that demonstrate correct storage behavior, and managing the work involved.

Storing data and producing proofs is a complex, highly optimizable process, with lots of tunable choices. Miners should explore the design space to arrive at something that (a) satisfies protocol and network-wide constraints, (b) satisfies clients’ requests and expectations (as expressed in Deals), and (c) gives them the most cost-effective operation. This part of the Filecoin Spec primarily describes in detail what MUST and SHOULD happen here, and leaves ample room for various optimizations for implementers, miners, and users to make. In some parts, we describe algorithms that could be replaced by other, more optimized versions, but in those cases it is important that the protocol constraints are satisfied. The protocol constraints are spelled out in clear detail (an unclear, unmentioned constraint is a “spec error”). It is up to implementers who deviate from the algorithms presented here to ensure their modifications satisfy those constraints, especially those relating to protocol security.

Storage Miner

TODO:

  • rename “Storage Mining Worker” ?

Filecoin Storage Mining Subsystem

import sectoridx "github.com/filecoin-project/specs/systems/filecoin_mining/sector_index"
import spc "github.com/filecoin-project/specs/systems/filecoin_blockchain/storage_power_consensus"
import filcrypto "github.com/filecoin-project/specs/libraries/filcrypto"
import actor "github.com/filecoin-project/specs/systems/filecoin_vm/actor"
import address "github.com/filecoin-project/specs/systems/filecoin_vm/actor/address"
import block "github.com/filecoin-project/specs/systems/filecoin_blockchain/struct/block"
import blockchain "github.com/filecoin-project/specs/systems/filecoin_blockchain"
import blockproducer "github.com/filecoin-project/specs/systems/filecoin_blockchain/struct/block_producer"
import deal "github.com/filecoin-project/specs/systems/filecoin_markets/deal"
import storage_proving "github.com/filecoin-project/specs/systems/filecoin_mining/storage_proving"

type StorageMiningSubsystem struct {
    // TODO: constructor
    // InitStorageMiningSubsystem() struct{}

    // Component subsystems
    // StorageProvider    storage_provider.StorageProvider
    StoragePowerActor  spc.StoragePowerActor
    MinerActor         StorageMinerActor
    SectorIndex        sectoridx.SectorIndexerSubsystem
    StorageProving     storage_proving.StorageProvingSubsystem
    BlockProducer      blockproducer.BlockProducer
    electionNonce      block.ElectionNonce

    // Need access to SPC in order to mine a block
    Consensus          spc.StoragePowerConsensusSubsystem

    // Need access to the blockchain system in order to query for things in the chain
    Blockchain         blockchain.BlockchainSubsystem

    // TODO: why are these here? remove?
    StartMining()
    StopMining()

    // call by StorageMiningSubsystem itself to create miner
    createMiner(
        ownerPubKey   filcrypto.PubKey
        workerPubKey  filcrypto.PubKey
        pledgeAmt     actor.TokenAmount
    )

    // get miner key by address
    GetMinerKeyByAddress(addr address.Address) filcrypto.PubKey

    // call by StorageMarket.StorageProvider at the start of a deal.
    // Triggers AddNewDeal on SectorIndexer
    // StorageDeal contains DealCID
    HandleStorageDeal(deal deal.StorageDeal)

    // call by StorageMinerActor when error in sealing
    CommitSectorError()

    // call by StorageMiningSubsystem itself in BlockProduction
    DrawElectionProof(
        lookbackTicket  block.Ticket
        nonce           block.ElectionNonce
        vrfKP           filcrypto.VRFKeyPair
    ) block.ElectionProof

    // call by StorageMiningSubsystem itself in BlockProduction
    PrepareNewTicket(priorTicket block.Ticket, workerKeyPair filcrypto.VRFKeyPair) block.Ticket

    // call by BlockChain when a new block is produced
    OnNewBestChain()

    // call by clock during BlockProduction
    // TODO: define clock better
    OnNewRound()

    tryLeaderElection()
}

Sector in StorageMiner State Machine (new one)

Sector State (new one) (open in new tab)
Sector State Legend (new one) (open in new tab)

Sector in StorageMiner State Machine (both)

Sector State Machine (both) (open in new tab)

Storage Mining Cycle

Block miners should constantly be performing Proofs of SpaceTime, and also checking if they have a winning ticket to propose a block at each height/in each round. Rounds are currently set to take around 30 seconds, in order to account for network propagation around the world. The details of both processes are defined here.

The Miner Actor

After successfully calling CreateStorageMiner, a miner actor will be created on-chain, and registered in the storage market. This miner, like all other Filecoin State Machine actors, has a fixed set of methods that can be used to interact with or control it.

For details on the methods on the miner actor, see its entry in the actors spec.

Owner Worker distinction

The miner actor has two distinct ‘controller’ addresses. One is the worker, which is the address which will be responsible for doing all of the work, submitting proofs, committing new sectors, and all other day to day activities. The owner address is the address that created the miner, paid the collateral, and has block rewards paid out to it. The reason for the distinction is to allow different parties to fulfil the different roles. One example would be for the owner to be a multisig wallet, or a cold storage key, and the worker key to be a ‘hot wallet’ key.

Storage Mining Cycle

Storage miners must continually produce Proofs of SpaceTime over their storage to convince the network that they are actually storing the sectors that they have committed to. Each PoSt covers a miner’s entire storage.

Step 0: Registration

To initially become a miner, a miner first register a new miner actor on-chain. This is done through the storage market actor’s CreateStorageMiner method. The call will then create a new miner actor instance and return its address.

The next step is to place one or more storage market asks on the market. This is done through the storage markets AddAsk method. A miner may create a single ask for their entire storage, or partition their storage up in some way with multiple asks (at potentially different prices).

After that, they need to make deals with clients and begin filling up sectors with data. For more information on making deals, see the section on deal.

When they have a full sector, they should seal it. This is done by invoking PoRep.Seal on the sector.

Step 1: Commit

When the miner has completed their first seal, they should post it on-chain using CommitSector. If the miner had zero committed sectors prior to this call, this begins their proving period.

The proving period is a fixed amount of time in which the miner must submit a Proof of Space Time to the network.

During this period, the miner may also commit to new sectors, but they will not be included in proofs of space time until the next proving period starts. For example, if a miner currently PoSts for 10 sectors, and commits to 20 more sectors. The next PoSt they submit (i.e. the one they’re currently proving) will be for 10 sectors again, the subsequent one will be for 30.

TODO: sectors need to be globally unique. This can be done either by having the seal proof prove the sector is unique to this miner in some way, or by having a giant global map on-chain is checked against on each submission. As the system moves towards sector aggregation, the latter option will become unworkable, so more thought needs to go into how that proof statement could work.

Step 2: Proving Storage (PoSt creation)
func ProveStorage(sectorSize BytesAmount, sectors []commR) PoStProof {
    challengeBlockHeight := miner.ProvingPeriodEnd - POST_CHALLENGE_TIME

    // Faults to be used are the currentFaultSet for the miner.
    faults := miner.currentFaultSet
    seed := GetRandFromBlock(challengeBlockHeight)
    return GeneratePoSt(sectorSize, sectors, seed, faults)
}

Note: See ‘Proof of Space Time’ for more details.

The proving set remains consistent during the proving period. Any sectors added in the meantime will be included in the next proving set, at the beginning of the next proving period.

Step 3: PoSt Submission

When the miner has completed their PoSt, they must submit it to the network by calling SubmitPoSt. There are two different times that this could be done.

  1. Standard Submission: A standard submission is one that makes it on-chain before the end of the proving period. The length of time it takes to compute the PoSts is set such that there is a grace period between then and the actual end of the proving period, so that the effects of network congestion on typical miner actions is minimized.
  2. Penalized Submission: A penalized submission is one that makes it on-chain after the end of the proving period, but before the generation attack threshold. These submissions count as valid PoSt submissions, but the miner must pay a penalty for their late submission. (See ‘Faults’ for more information)
    • Note: In this case, the next PoSt should still be started at the beginning of the proving period, even if the current one is not yet complete. Miners must submit one PoSt per proving period.

Along with the PoSt submission, miners may also submit a set of sectors that they wish to remove from their proving set. This is done by selecting the sectors in the ‘done’ bitfield passed to SubmitPoSt.

Stop Mining

In order to stop mining, a miner must complete all of its storage contracts, and remove them from their proving set during a PoSt submission. A miner may then call DePledge() to retrieve their collateral. DePledge must be called twice, once to start the cooldown, and once again after the cooldown to reclaim the funds. The cooldown period is to allow clients whose files have been dropped by a miner to slash them before they get their money back and get away with it.

Faults

Faults are described in the faults document.

On Being Slashed (WIP, needs discussion)

If a miner is slashed for failing to submit their PoSt on time, they currently lose all their pledge collateral. They do not necessarily lose their storage collateral. Storage collateral is lost when a miner’s clients slash them for no longer having the data. Missing a PoSt does not necessarily imply that a miner no longer has the data. There should be an additional timeout here where the miner can submit a PoSt, along with ‘refilling’ their pledge collateral. If a miner does this, they can continue mining, their mining power will be reinstated, and clients can be assured that their data is still there.

TODO: disambiguate the two collaterals across the entire spec

Review Discussion Note: Taking all of a miners collateral for going over the deadline for PoSt submission is really really painful, and is likely to dissuade people from even mining filecoin in the first place (If my internet going out could cause me to lose a very large amount of money, that leads to some pretty hard decisions around profitability). One potential strategy could be to only penalize miners for the amount of sectors they could have generated in that timeframe.

Future Work

There are many ideas for improving upon the storage miner, here are ideas that may be potentially implemented in the future.

  • Sector Resealing: Miners should be able to ’re-seal’ sectors, to allow them to take a set of sectors with mostly expired pieces, and combine the not-yet-expired pieces into a single (or multiple) sectors.
  • Sector Transfer: Miners should be able to re-delegate the responsibility of storing data to another miner. This is tricky for many reasons, and will not be implemented in the initial release of Filecoin, but could provide interesting capabilities down the road.

Storage Miner Actor

(You can see the old Storage Miner Actor here )

import libp2p "github.com/filecoin-project/specs/libraries/libp2p"
import util "github.com/filecoin-project/specs/util"
import sealing "github.com/filecoin-project/specs/systems/filecoin_mining/sector"
import sector "github.com/filecoin-project/specs/systems/filecoin_mining/sector"
import address "github.com/filecoin-project/specs/systems/filecoin_vm/actor/address"
import block "github.com/filecoin-project/specs/systems/filecoin_blockchain/struct/block"
import poster "github.com/filecoin-project/specs/systems/filecoin_mining/storage_proving/poster"

type Seed struct {}
type SectorCommitment struct {}

type SectorExpirationQueueItem struct {
    SectorNumber  sector.SectorNumber
    Expiration    block.ChainEpoch
}

type SectorExpirationQueue struct {
    Add(i SectorExpirationQueueItem)
    Pop() SectorExpirationQueueItem
    Peek() SectorExpirationQueueItem
    Remove(n sector.SectorNumber)
}

type StorageMinerActor struct {
    // CollateralVault CollateralVault

    // TODO: consider merging SectorState into sector.SealCommitment to keep
    // only one map, instead of two.
    Sectors                {sector.SectorNumber: sector.SealCommitment}  // this should be an AMT
    SectorStates           {sector.SectorNumber: SectorState}
    ProvingSet             sector.CompactSectorSet
    LastChallengePoSt      block.ChainEpoch

    SectorExpirationQueue

    // contains mostly static info about this miner
    Info                   &MinerInfo

    // TODO: add the DePledgeCollateral
    // TODO: add the Sectors
    // TODO sectors []SectorInfo CID to AMT
    // TODO provingSet []SectorInfo CID to AMT

    // TODO: should we keep the same structure as before or putting all of that into a struct?
    // TODO do we need a `slashedAt`?
    // TODO owedStorageCollateral
    // TODO ProvingPeriodEnd   Epoch

    CommitSector(onChainInfo sealing.OnChainSealVerifyInfo)  // TODO: check with Magik on sizes

    SubmitPoSt(postSubmission poster.PoStSubmission)

    // TODO: should depledge be in here or in storage market actor?

    // TODO: add GetOwner()
    // TODO: add GetPeerID()
    // TODO: add GetSectorSize()
    // TODO: add UpdatePeerID()
    // TODO: add UpdatePeerID()
    // TODO: add ChangeWorker()

    // TODO: do IsSlashed, IsLate belong here?

    DeclareFault(faultSet sector.FaultSet)

    computeProvingPeriodEndSectorState()  // TODO

    verifyPoStSubmission(postSubmission poster.PoStSubmission) bool
}

type MinerInfo struct {
    // Account that owns this miner.
    // - Income and returned collateral are paid to this address.
    // - This address is also allowed to change the worker address for the miner.
    Owner       address.Address

    // Worker account for this miner.
    // This will be the key that is used to sign blocks created by this miner, and
    // sign messages sent on behalf of this miner to commit sectors, submit PoSts, and
    // other day to day miner activities.
    Worker      address.Address

    // Libp2p identity that should be used when connecting to this miner.
    PeerId      libp2p.PeerID

    // Amount of space in each sector committed to the network by this miner.
    SectorSize  util.BytesAmount
}

/*
type CollateralVault struct {
    Pledged(Collateral) TokenAmount
    Pledge(Collateral, TokenAmount)
    DePledge(Collateral, TokenAmount)

    pledgedStorageCollateral UVarint
    pledgedConsensusCollateral UVarint
}

type Collateral union {
    | StorageDealCollateral
    | ConsensusCollateral
}

type TokenAmount UVarint # What are the units? Attofil?

type MinedSector {
    SectorID           UInt
    CommR              sector.Commitment
    FaultStatus(Epoch) Fault
}

type Fault union {
    | None
    | GracePeriod
    | Fault
}
*/

/*
ype StorageMinerActor struct {
  // Amount of power this miner has.
  power UInt

  provingPeriodEnd Epoch


  // Collateral that is waiting to be withdrawn.
  dePledgeCollateral TokenAmount

  // Time at which the depledged collateral may be withdrawn.
  dePledgeTime Epoch

  // All sectors this miner has committed.
  sectors &SectorSet

  // Sectors this miner is currently mining. It is only updated
  // when a PoSt is submitted (not as each new sector commitment is added).
  provingSet &SectorSet

  // Faulty sectors reported since last SubmitPost, up to the current proving period's challenge time.
  currentFaultSet FaultSet

  // Faults submitted after the current proving period's challenge time, but before the PoSt for that period
  // is submitted. These become the currentFaultSet when a PoSt is submitted.
  nextFaultSet FaultSet

  // Sectors reported during the last PoSt submission as being 'done'. The collateral
  // for them is still being held until the next PoSt submission in case early sector
  // removal penalization is needed.
  nextDoneSet DoneSet

  // List of sectors that this miner was slashed for.
  slashedSet optional &SectorSet

  // Deals this miner has been slashed for since the last post submission.
  arbitratedDeals {Cid:Null} // TODO

  // The height at which this miner was slashed at.
  slashedAt optional Epoch

  // The amount of storage collateral that is owed to clients, and cannot be used for collateral anymore.
  owedStorageCollateral TokenAmount

  // Internal methods
  verifySeal(sectorID SectorID, comm SealCommitment, proof SealProof)
  verifyPoSt(proofs base.PoStProof, doneSet Bitfield)

  // Getters
  GetOwner() address.Address
  GetWorkerAddr() address.Address
  GetPower() BytesAmount
  GetPeerID() PeerID
  GetSectorSize() BytesAmount
  GetCurrentProvingSet() BitField

  // SubmitPost verifies the PoSt
  SubmitPost(proofs base.PoStProof, doneSet DoneSet) bool // TODO: rename to ProvePower?
  DePledge(amt TokenAmount)

  AddCollateral()

  AbitrateDeal (deal Deal)
  SlashStorageFault() // TODO maybe add CheckStorageFault?
  UpdateFaults(faults FaultSet) // TODO rename into ReportFaults
  IsLate() (bool)
  IsSlashed() (bool)

  UpdatePeerID(pid PeerID)
  ChangeWorker(addr address.Address)

  PaymentVerifyInclusion(extra PieceInclusionVoucherData, proof InclusionProof) (bool)
  PaymentVerifyInclusion(extra BigInt, proof Bytes) (bool)
}

*/

Mining Scheduler

import poster "github.com/filecoin-project/specs/systems/filecoin_mining/storage_proving/poster"
import sector "github.com/filecoin-project/specs/systems/filecoin_mining/sector"
import actor "github.com/filecoin-project/specs/systems/filecoin_vm/actor"
import mining "github.com/filecoin-project/specs/systems/filecoin_mining"
// import storage_indexer "github.com/filecoin-project/specs/systems/filecoin_mining/storage_indexer"

type ReplicaID CID

type MiningScheduler struct {
    getStagedSectors()    sector.SectorSet
    getSealedSectors()    sector.SealedSectorSet
    getFaultySectors()    sector.SectorSet
    getRepairedSectors()  sector.SectorSet
    // same as completedSectors/doneSectors
    getExpiredSectors()   sector.SectorSet

    ProducePost(sectors sector.SectorSet) poster.PoStSubmission
    VerifyPost(sectors sector.SectorSet) poster.PoStSubmission

    ReportFaults(
        actor &StorageMinerActor
    ) bool

    RemoveSectors(remove sector.SectorSet) bool

    DePledge(
        amount actor.TokenAmount
    ) bool

    // receives from sector storage subsystem
    SealedSector(
        sealedSector mining.SealedSector
    ) bool

    AddSector(
        pledge    actor.TokenAmount
        sectorID  &sector.SectorID
        comm      &sector.OnChainSealVerifyInfo
    ) sector.SectorID

    // generateReplicaID(CommD Commitment, block Block)
}

Sector

The Sector is a fundamental “storage container” abstraction used in Filecoin Storage Mining. It is the basic unit of storage, and serves to make storage conform to a set of expectations.

import piece "github.com/filecoin-project/specs/systems/filecoin_files/piece"
import ipld "github.com/filecoin-project/specs/libraries/ipld"
import deal "github.com/filecoin-project/specs/systems/filecoin_markets/deal"
import addr "github.com/filecoin-project/specs/systems/filecoin_vm/actor/address"

type Bytes32 Bytes
type MinerID addr.Address
type Commitment Bytes32  // TODO
type UnsealedSectorCID ipld.CID
type SealedSectorCID ipld.CID

// SectorNumber is a numeric identifier for a sector. It is usually
// relative to a Miner.
type SectorNumber UInt

type FaultSet CompactSectorSet

// SectorSize indicates one of a set of possible sizes in the network.
type SectorSize UInt

// Ideally, SectorSize would be an enum
// type SectorSize enum {
//   1KiB = UInt 1024
//   1MiB = Uint 1048576
//   1GiB = Uint 1073741824
//   1TiB = Uint 1099511627776
//   1PiB = Uint 1125899906842624
// }

// TODO make sure this is globally unique
type SectorID struct {
    MinerID
    Number SectorNumber
}

// SectorInDetail describes all the bits of information associated
// with each sector.
// - ID   - a unique identifier assigned once the Sector is registered on chain
// - Size - the size of the sector. there are a set of allowable sizes
//
// NOTE: do not use this struct. It is for illustrative purposes only.
type SectorInDetail struct {
    ID    SectorID
    Size  SectorSize

    Unsealed struct {
        CID     UnsealedSectorCID
        Deals   [deal.StorageDeal]
        Pieces  [piece.Piece]
        // Pieces Tree<Piece> // some tree for proofs
        Bytes
    }

    Sealed struct {
        CID SealedSectorCID
        Bytes
        SealCfg
    }
}

// SectorInfo is an object that gathers all the information miners know about their
// sectors. This is meant to be used for a local index.
type SectorInfo struct {
    ID              SectorID
    UnsealedInfo    UnsealedSectorInfo
    SealedInfo      SealedSectorInfo
    SealVerifyInfo
    ProofAux
}

// UnsealedSectorInfo is an object that tracks the relevant data to keep in a sector
type UnsealedSectorInfo struct {
    UnsealedCID  UnsealedSectorCID  // CommD
    Size         SectorSize
    PieceCount   UVarint  // number of pieces in this sector (can get it from len(Pieces) too)
    Pieces       [piece.PieceInfo]  // wont get externalized easy, -- it's big
    SealCfg  // this will be here as well. it's determined.
    // Deals       [deal.StorageDeal]
}

// SealedSectorInfo keeps around information about a sector that has been sealed.
type SealedSectorInfo struct {
    SealedCID  SealedSectorCID
    Size       SectorSize
    SealCfg
    SealArgs   SealArguments
}

TODO:

  • sector illustration
  • describe how Sectors are used in practice
  • describe sizing ranges of sectors
  • describe “storage/shipping container” analogy

Sector Set

// sector sets
type SectorSet [SectorID]
type UnsealedSectorSet SectorSet
type SealedSectorSet SectorSet

// compact sector sets
type Bitfield Bytes  // TODO: move to the right place -- a lib?
type RLEpBitfield Bitfield  // TODO: move to the right place -- a lib?
type CompactSectorSet RLEpBitfield

Sector Sealing

import file "github.com/filecoin-project/specs/systems/filecoin_files/file"
import deal "github.com/filecoin-project/specs/systems/filecoin_markets/deal"
import block "github.com/filecoin-project/specs/systems/filecoin_blockchain/struct/block"

type Path struct {}  // TODO

type SealRandomness Bytes
type PoStRandomness Bytes

// SealSeed is unique to each Sector
// SealSeed is:
//    SealSeedHash(MinerID, SectorNumber, SealRandomness, UnsealedSectorCID)
type SealSeed Bytes

type SealCfg struct {
    SectorSize
    SubsectorCount  UInt
    Partitions      UInt
}

// SealVerifyInfo is the structure of all thte information a verifier
// needs to verify a Seal.
type SealVerifyInfo struct {
    SectorID
    OnChain OnChainSealVerifyInfo
    SealCfg
}

// OnChainSealVerifyInfo is the structure of information that must be sent with
// a message to commit a sector. Most of this information is not needed in the
// state tree but will be verified in sm.CommitSector. See SealCommitment for
// data stored on the state tree for each sector.
type OnChainSealVerifyInfo struct {
    SealedCID     SealedSectorCID  // CommR
    Epoch         block.ChainEpoch
    Proof         SealProof
    DealIDs       [deal.DealID]
    SectorNumber
}

// SealCommitment is the information kept in the state tree about a sector.
// SealCommitment is a subset of OnChainSealVerifyInfo.
type SealCommitment struct {
    UnsealedCID  UnsealedSectorCID  // CommD
    SealedCID    SealedSectorCID  // CommR
    DealIDs      [deal.DealID]
    Expiration   block.ChainEpoch
}

// ProofAux is meta data required to generate certain proofs
// for a sector, for example PoSt.
// These should be stored and indexed somewhere by CommR.
type ProofAux struct {
    CommRLast          Commitment
    CommC              Commitment

    // TODO: This may be a partially-cached tree.
    // this may be empty
    CommRLastTreePath  file.Path
}

type ProofAuxTmp struct {
    PersistentAux  ProofAux

    SectorID
    CommD          Commitment
    CommR          SealedSectorCID
    CommDTreePath  file.Path

    Seed           SealRandomness
    Data           Bytes
    Replica        Bytes
    KeyLayers      [Bytes]
}

type SealArguments struct {
    Algorithm        SealAlgorithm
    OutputArtifacts  SealOutputArtifacts
}

type SealProof struct {//<curve, system> {
    Config      SealProofConfig
    ProofBytes  Bytes
}

type SealProofConfig struct {// TODO
}

// TODO: move into proofs lib
type FilecoinSNARKProof struct {}  //<bls12-381, Groth16>

type SealAlgorithm enum {
    // ZigZagPoRep
    StackedDRG
}

// TODO
type SealOutputArtifacts struct {}

Sector Index

import block "github.com/filecoin-project/specs/systems/filecoin_blockchain/struct/block"
import sector "github.com/filecoin-project/specs/systems/filecoin_mining/sector"
import piece "github.com/filecoin-project/specs/systems/filecoin_files/piece"
import deal "github.com/filecoin-project/specs/systems/filecoin_markets/deal"

// TODO import this from StorageMarket
type SectorIndex struct {
    BySectorID     {sector.SectorID: sector.SectorInfo}
    ByUnsealedCID  {sector.UnsealedSectorCID: sector.SectorInfo}
    BySealedCID    {sector.SealedSectorCID: sector.SectorInfo}
    ByPieceID      {piece.PieceID: sector.SectorInfo}
    ByDealID       {deal.DealID: sector.SectorInfo}
}

type SectorIndexerSubsystem struct {
    Index    SectorIndex
    Store    SectorStore
    Builder  SectorBuilder

    // AddNewDeal is called by StorageMiningSubsystem after the StorageMarket
    // has made a deal. AddNewDeal returns an error when:
    // - there is no capacity to store more deals and their pieces
    AddNewDeal(deal deal.StorageDeal) StageDealResponse

    // bring back if needed.
    // OnNewTipset(chain Chain, epoch blockchain.Epoch) struct {}

    // SectorsExpiredAtEpoch returns the set of sectors that expire
    // at a particular epoch.
    SectorsExpiredAtEpoch(epoch block.ChainEpoch) [sector.SectorID]

    // removeSectors removes the given sectorIDs from storage.
    removeSectors(sectorIDs [sector.SectorID])
}

Sector Builder

import sector "github.com/filecoin-project/specs/systems/filecoin_mining/sector"
// import smkt "github.com/filecoin-project/specs/systems/filecoin_markets/storage_market"
import deal "github.com/filecoin-project/specs/systems/filecoin_markets/deal"

// SectorBuilder accumulates deals, keeping track of their
// sector configuration requirements and the piece sizes.
// Once there is a sector ready to be sealed, NextSector
// will return a sector.

type StageDealResponse struct {
    SectorID sector.SectorID
}

type SectorBuilder struct {
    // DealsToSeal keeps a set of StorageDeal objects.
    // These include the info for the relevant pieces.
    // This builder just accumulates deals, keeping track of their
    // sector configuration requirements, and the piece sizes.
    DealsToSeal [deal.StorageDeal]

    // StageDeal adds a deal to be packed into a sector.
    StageDeal(d deal.StorageDeal) StageDealResponse

    // NextSector returns an UnsealedSectorInfo, which includes the (ordered) set of
    // pieces, and the SealCfg. An error may be returned if SectorBuilder is not
    // ready to produce a Sector.
    //
    // TODO: use go channels? or notifications?
    NextSector() union {i sector.UnsealedSectorInfo, err error}
}

SectorStore

import sector "github.com/filecoin-project/specs/systems/filecoin_mining/sector"
import piece "github.com/filecoin-project/specs/systems/filecoin_files/piece"
import file "github.com/filecoin-project/specs/systems/filecoin_files/file"

type SectorStore struct {
    // FileStore stores all the unsealed and sealed sectors.
    FileStore   file.FileStore

    // PieceStore is shared with DataTransfer, and is a way to store or read
    // pieces temporarily. This may or may not be backed by the FileStore above.
    PieceStore  piece.PieceStore

    // GetSectorFile returns the file for a given sector id.
    // If the SectorID does not have any sector files associated yet, GetSectorFiles
    // returns an error.
    GetSectorFiles(id sector.SectorID) union {f SectorFiles, err error}

    // CreateSectorFiles allocates two sector files, one for unsealed and one for
    // sealed sector.
    CreateSectorFiles(id sector.SectorID) union {f SectorFiles, err error}
}

// SectorFiles is a datastructure that groups two file objects and a sectorID.
// These files are where unsealed and sealed sectors should go.
type SectorFiles struct {
    SectorID  sector.SectorID
    Unsealed  file.File
    Sealed    file.File
}

TODO

  • talk about how sectors are stored

Storage Proving

Filecoin Poving Subsystem

import sector "github.com/filecoin-project/specs/systems/filecoin_mining/sector"
import poster "github.com/filecoin-project/specs/systems/filecoin_mining/storage_proving/poster"
import sealer "github.com/filecoin-project/specs/systems/filecoin_mining/storage_proving/sealer"

// type seal.SealOutput struct {}
// type seal.SectorSealer struct {}
// type seal.Seed struct {}
// type sector.SectorID struct {}
// type sector.SealCfg struct {}
type Commitment struct {}
type Ticket struct {}
type Block struct {}
type SectorSealer struct {}

type StorageProvingSubsystem struct {
    SectorSealer   sealer.SectorSealer
    PostGenerator  poster.PostGenerator

    SealSector(si sealer.SealInputs) union {so sealer.SealOutputs, err error}
    CreateSealProof(si sealer.CreateSealProofInputs) union {so sealer.CreateSealProofOutputs, err error}
    VerifySeal(sv sector.SealVerifyInfo) union {ok bool, err error}

    ValidateBlock(block Block)

    // TODO: remove this?
    // GetPieceInclusionProof(pieceRef CID) union { PieceInclusionProofs, error }
}

Sector Sealer

Sector Sealer

import sector "github.com/filecoin-project/specs/systems/filecoin_mining/sector"
import file "github.com/filecoin-project/specs/systems/filecoin_files/file"
import addr "github.com/filecoin-project/specs/systems/filecoin_vm/actor/address"
import deal "github.com/filecoin-project/specs/systems/filecoin_markets/deal"

type SealInputs struct {
    SectorID      sector.SectorID
    SealCfg       sector.SealCfg
    MinerID       addr.Address
    RandomSeed    sector.SealRandomness
    UnsealedPath  file.Path
    SealedPath    file.Path
    DealIDs       [deal.DealID]
}

type CreateSealProofInputs struct {
    SectorID     sector.SectorID
    SealCfg      sector.SealCfg
    RandomSeed   sector.SealRandomness
    SealedPath   file.Path
    SealOutputs
}

type SealOutputs struct {
    ProofAuxTmp sector.ProofAuxTmp
}

type CreateSealProofOutputs struct {
    SealInfo  sector.SealVerifyInfo
    ProofAux  sector.ProofAux
}

type SectorSealer struct {
    SealSector() union {so SealOutputs, err error}
    CreateSealProof(si CreateSealProofInputs) union {so CreateSealProofOutputs, err error}

    MaxUnsealedBytesPerSector(SectorSize UInt) UInt
}

Sector Poster

import sector "github.com/filecoin-project/specs/systems/filecoin_mining/sector"
import block "github.com/filecoin-project/specs/systems/filecoin_blockchain/struct/block"

type UInt64 UInt
type Challenge UInt64
type PoStProof struct {}  // TODO

// TODO: move this to somewhere the blockchain can import
// candidates:
// - filproofs - may have to learn about Sectors (and if we move Seal stuff, Deals)
// - "blockchain/builtins" or something like that - a component in the blockchain that handles storage verification
type PoStSubmission struct {
    PostProof   PoStProof
    ChainEpoch  block.ChainEpoch
}

type PostGenerator struct {
    GeneratePoSt(challenge Challenge, IDs [sector.SectorID]) PoStProof
}
package poster

// See "Proof-of-Spacetime Parameters" Section
const POST_PROVING_PERIOD = uint(5760)
const POST_CHALLENGE_DEADLINE = uint(480)

// // this must  GetRandFromBlock(self.ProvingPeriodEnd - POST_CHALLENGE_TIME)
//   GetChallenge(minerActor &StorageMinerActor, currBlock) Challenge

//   GeneratePoSt(challenge Challenge, sectors [Sector]) PoStProof {
//       sectorsMetadata := sectors.map(func(sector) { SectorStorage.GetMetadata(sector.CommR) });

//       // Question: Should we pass metadata into FilProofs so it can interact with SectorStore directly?
//       // Like this:
//       PoStReponse := SectorStorageSubsystem.GeneratePoSt(sectorSize, challenge, faults, sectorsMetatada);

//       // Question: Or should we resolve + manifest trees here and pass them in?
//       // Like this:
//       trees := sectorsMetadata.map(func(md) { SectorStorage.GetMerkleTree(md.MerkleTreePath) });
//       // Done this way, we redundantly pass the tree paths in the metadata. At first thought, the other way
//       // seems cleaner.
//       PoStReponse := SectorStorageSubsystem.GeneratePoSt(sectorSize, challenge, faults, sectorsMetadata, trees);
//   }
PoSt Generator object

Markets in Filecoin

The Filecoin project is a protocol, a platform, and a marketplace. There are two major components to Filecoin markets, storage market and retrieval market. While both markets are expected to happen primarily off the blockchain, storage deals made in storage market will be published on chain and enforced by the protocol. Storage deal negotiation and order matching are expected to happen off chain in the first version of Filecoin. Retrieval deals are also negotiated off chain and executed with micropayments between transacting parties in payment channels.

Even though most of the market actions happen off the blockchain, there are on-chain invariant and structure that create economic structure for network success and allow for positive emergent behavior. Storage Mining in Filecoin can be compared to maintaining a storage cargo container (a Sector reference to Sector here) with storage deals sitting in them. There must be at least one active deal in a Sector for the Sector to count towards a miner’s power. Miners should be able to update storage deals in a Sector without changing their power. Once a Sector is filled with active StorageDeals, retrieval miners can then serve data stored in the Sector to users.

Market Orders - Asks and Bids

TODO:

  • Write asks
  • Write bids
  • Write how market orders propagate (gossipsub)
type Ask struct {

}

type Bid struct {

}

Verifiability

TODO:

  • write what parts of market orders are verifiable, and how
    • eg: miner storage ask could carry the amount of storage available (which should be at mot (pledge - sectors sealed))
    • eg: client storage bid price could be checked against available money in the StorageMarket

Market Deals

There are two types of deals in Filecoin markets, storage deals and retrieval deals. Storage deals are recorded on the blockchain and enforced by the protocol. Retrieval deals are off chain and enabled by micropayment channel by transacting parties. All deal negotiation happen off chain and a request-response style storage deal protocol is in place to submit agreed-upon storage deals onto the network with CommitSector to gain storage power on chain. Hence, there is a StorageDealProposal and a RetrievalDealProposal that are half-signed contracts submitted by clients to be counter-signed and posted on-chain by the miners.

Filecoin Storage Market Deal Flow

Add Storage Deal and Power

  • 1. StorageClient and StorageProvider call StorageMarketActor.AddBalance to deposit funds into Storage Market. There are two fund states in the Storage Market, Locked and Available.
    • StorageClient and StorageProvider can call WithdrawBalance before any deal is made. (move to state X)
  • 2. StorageClient and StorageProvider negotiate a deal off chain. StorageClient sends a StorageDealProposal to a StorageProvider.
    • StorageProvider verifies the StorageDeal by checking address and signature of StorageClient, checking the proposal has not expired, checking StorageClient did not call withdraw in the last X Epoch, checking both StorageProvider and StorageClient have sufficient available balances in StorageMarketActor.
  • 3. StorageProvider signs the StorageDealProposal gets a StorageDeal.
    • a. StorageProvider calls PublishStorageDeals in StorageMarketActor which will generate a DealID for each StorageDeal and store a mapping from DealID to StorageDeal. However, the deals are not active at this point.
      • As a backup, StorageClient MAY call PublishStorageDeals with the StorageDeal, to activate the deal.
      • It is possible for either StorageProvider or StorageClient to try to enter into two deals simultaneously with funds available only for one. Only the first deal to commit in the chain would clear, the second would fail with error errorcode.InsufficientFunds.
    • b. StorageProvider calls HandleStorageDeal in StorageMiningSubsystem which will then add the StorageDeal into a Sector.
  • 4. Once the miner finishes packing a Sector, it generates a Sealed Sector and calls StorageMinerActor.CommitSector to verify the seal, store sector expiration, and record the mapping from SectorNumber to SealCommitment. It will also place this newly added Sector in the list of CommittedSectors in StorageMinerActor. StorageMiner does not earn any power for this newly added sector until its first PoSt has been submitted.

Declare and Recover Faults

  • 5. Declared faults are penalized to a smaller degree than spotted faults by CronActor. Miners declare faulty sectors by invoking StorageMinerActor.DeclareFaults and X of the StorageDealCollateral will be slashed and power corresponding to these sectors will be tempororily lost.
  • 6. Miners can then recover faults by invoking StorageMinerActor.RecoverFaults and have sufficient StorageDealCollateral in their available balances. FaultySectors are recommitted and power is only restored at the next PoSt submission.
  • 7. Sectors that are declared faulty for storagemining.MaxFaults consecutive ChainEpochs will result in StoragePowerActor.SlashPledgeCollateral.
    • TODO: set X parameter

Submit PoSt

(TODO: move into Storage Mining)

On every PoSt Submission, the following steps happen.

  • 8. StorageMinerActor first verifies the PoSt Submission. All Sectors will be considered in SpottedFaults if PoSt submission has failed (move to State 14).
  • 9. If CommittedSectors are proven in PoStSubmission.SectorSet, Storage Miner gains power for these newly committed sectors.
  • 10. If there are DeclaredFaultySectors , Sector in that set will not be challenged.
  • 11. For all other sectors, payment will be processed by invoking StorageMarketActor.ProcessStorageDealsPayment and miner available balances will be updated.
  • 12. Decide which Sectors have expired by looking at the SectorExpirationQueue. Sectors expire when all deals in that Sector have expired. StorageDealCollateral for both miners and users will only be returned when all deals in the Sector have expired. This is done by calling StorageMarketActor.SettleExpiredDeals and the Sector will be deleted from StorageMinerActor.Sectors.

Spot Faults

(TODO: move into Storage Mining)

  • 13. CronActor calls StoragePowerActor.EpochTick at every block. This calls StorageMinerActor.CheckPoSt on all the miners whose ProvingPeriod is up.
    • If no PoSt is submitted by the end of the ProvingPeriod, StorageMinerActor spots the missing PoSt, and sets all sectors to Failing.
    • TODO: reword in terms of a conditional in the mining cycle
    • When there are sector faults are spotted, both StorageDealCollateral and PledgeCollateral are slashed, and power is lost.
    • If the faults persist for storagemining.MaxFaults then sectors are removed/cleared from StorageMinerActor.

Deal Code

import ipld "github.com/filecoin-project/specs/libraries/ipld"
import actor "github.com/filecoin-project/specs/systems/filecoin_vm/actor"
import addr "github.com/filecoin-project/specs/systems/filecoin_vm/actor/address"
import block "github.com/filecoin-project/specs/systems/filecoin_blockchain/struct/block"

type DealID UVarint
type DealCID ipld.CID
type Signature struct {}  // TODO

type StorageDealProposal struct {
    PieceRef            DealCID
    PieceSize           UInt
    Client              addr.Address
    Provider            addr.Address
    ProposalExpiration  block.ChainEpoch
    DealExpiration      block.ChainEpoch
    StoragePrice        actor.TokenAmount
    StorageCollateral   actor.TokenAmount
    ProposerSignature   Signature
}

type StorageDeal struct {
    Proposal          StorageDealProposal
    CounterSignature  Signature
}

type RetrievalDealProposal struct {}  // TODO
type RetrievalDeal struct {
    Proposal          RetrievalDealProposal
    CounterSignature  Signature
}

Storage Market in Filecoin

Storage Market subsystem is the data entry point into the network. Storage miners only earn power from data stored in a storage deal and all deals live on the Filecoin network. Specific deal negotiation process happens off chain, clients and miners enter a storage deal after an agreement has been reached and post storage deals on the Filecoin network to earn block rewards and get paid for storing the data in the storage deal. A deal is only valid when it is posted on chain with signatures from both parties and at the time of posting, there are sufficient balances for both parties in StorageMarketActor to honor the deal in terms of deal price and deal collateral.

Both StorageClient and StorageProvider need to first deposit Filecoin token into StorageMarketActor before participating in the storage market. StorageClient can then send a StorageDealProposal to the StorageProvider along with the data. A partially signed StorageDeal is called a StorageDealProposal. StorageProvider can then put this storage deal in their Sector, countersign the StorageDealProposal and result in a StorageDeal. A StorageDeal is only in effect when it is submitted to and accepted by the StorageMarketActor on chain before the ProposalExpiryEpoch. StorageDeal does not include a StartEpoch as it will come into effect at the block when the deal gets accepted into the network. Hence, StorageProvider should publish the deal as soon as possible.

StorageDeal payments are processed at every successful PoSt submission and StorageMarketActor will move locked funds from StorageClient to StorageProvider. SlashStorageDealCollateral is also triggered on PoSt submission when a Sector containing a particular StorageDeal is faulty or miners fail to submit PoSt related to a StorageDeal. Note that StorageProvider does not need to be the same entity as the StorageMinerActor as long as the deal is stored in at least one Sector throughout the life time of the storage deal.

TODO: process StorageDeal payments are larger interval beyond every PoSt submission

Storage Market Actor

StorageMarketActor is responsible for processing and managing on-chain deals. This is also the entry point of all storage deals and data into the system. It maintains a mapping of StorageDealID to StorageDeal and keeps track of locked balances of StorageClient and StorageProvider. When a deal is posted on chain through the StorageMarketActor, it will first check if both transacting parties have sufficient balances locked up and include the deal on chain. On every successful submission of PoStProof, StorageMarketActor will credit the StorageProvider a fraction of the storage fee based on how many blocks have passed since the last PoStProof. In the event that there are sectors included in the FaultSet, StorageMarketActor will fetch deal information from the chain and SlashStorageFault for faulting on those deals. Similarly, when a PoStProof is missed by the end of a ProvingPeriod, SlashStorageFault will also be called by the CronActor to penalize StorageProvider for dropping a StorageDeal.

(You can see the old Storage Market Actor here )

import actor "github.com/filecoin-project/specs/systems/filecoin_vm/actor"
import block "github.com/filecoin-project/specs/systems/filecoin_blockchain/struct/block"
import addr "github.com/filecoin-project/specs/systems/filecoin_vm/actor/address"
import deal "github.com/filecoin-project/specs/systems/filecoin_markets/deal"

type StorageParticipantBalance struct {
    Locked     actor.TokenAmount
    Available  actor.TokenAmount
}

type StorageMarketActor struct {
    // need access to:
    // - DataTransferSubsystem
    //   - transfer data
    // - NetworkSubsystem
    //   - needs access to MessagePubsub

    Balances  {addr.Address: StorageParticipantBalance}
    Deals     {deal.DealID: deal.StorageDeal}

    WithdrawBalance(balance actor.TokenAmount)
    AddBalance(balance actor.TokenAmount)
    CheckLockedBalance(participantAddr addr.Address) actor.TokenAmount

    // call by CommitSector in StorageMiningSubsystem
    // a StorageDeal is only published on chain when it passes verifyStorageDeal
    // a DealID will be assigned and stored in the mapping of DealID to StorageDeal
    // PublishStorageDeal should be called before SecotrCommits
    // an unregistered StorageDeal will not be processed
    PublishStorageDeals(deals [deal.StorageDeal]) [PublishStorageDealResponse]

    // check if StorageDeal is signed before expiry
    // check if StorageDeal has the right signatures
    // check if minimum StoragePrice and StorageCollateral are met
    // check if provider and client have sufficient balances
    verifyStorageDeal(deal deal.StorageDeal) bool

    // generate storage deal id
    generateStorageDealID(deal deal.StorageDeal) deal.DealID

    // TODO: StorageDeals should be renewable
    // UpdateStorageDeal(newStorageDeals [deal.StorageDeal])

    // call by CronActor when no PoSt is submitted within a ProvingPeriod
    // trigger subsequent calls on different SectorSet
    // pull SectorSet from the run time
    HandleCronAction()

    // call by CronActor / onPoStSubmission on ExpiredSet
    // remove StorageDeal from StorageMarketActor
    // if no more active deals contain in the sector
    // return StorageCollateral to miners
    SettleExpiredDeals(dealIDs [deal.DealID])

    // call by CronActor / onPoStSubmission on ActiveSet to process deal payment
    // go through StorageDealIDs, if IDs are active in MarketActor
    // payment will be processed
    ProcessStorageDealsPayment(dealIDs [deal.DealID])

    // call by CronActor / on DeclareFault on FaultSet to slash deal collateral
    // Deals should be slashed for a single proving period
    SlashStorageDealCollateral(dealIDs [deal.DealID])

    GetLastExpirationFromDealIDs(dealIDs [deal.DealID]) block.ChainEpoch
}
Storage Deal Collateral

Storage Deals have an associated collateral amount. This StorageDealCollateral is held in the StorageMarketActor. Its value is agreed upon by the storage provider and client off-chain, but must be greater than a protocol-defined minimum in any deal. Storage providers will choose to offer greater collateral to signal high-quality storage to clients.

On SectorFailureTimeout (see Faults), the StorageDealCollateral will be burned. In the future, the Filecoin protocol may be amended to send up to half of the collateral to storage clients as damages in such cases.

Upon graceful deal expiration, storage providers must wait for finality number of epochs (as defined in EC Finality) before being able to withdraw their StorageDealCollateral from the StorageMarketActor.

Storage Provider

Both StorageProvider and StorageClient are StorageMarketParticipant. Any party can be a storage provider or client or both at the same time. Storage deal negotiation is expected to happen completely off chain and the request-response style storage deal protocol is to submit agreed-upon storage deal onto the network and gain storage power on chain. StorageClient will initiate the storage deal protocol by submitting a StorageDealProposal to the StorageProvider who will then add the deal data to a Sector and commit the sector onto the blockchain.

import addr "github.com/filecoin-project/specs/systems/filecoin_vm/actor/address"
import deal "github.com/filecoin-project/specs/systems/filecoin_markets/deal"
import sector "github.com/filecoin-project/specs/systems/filecoin_mining/sector"

type StorageDealStagedNotification struct {
    Deal      deal.StorageDeal
    SectorID  sector.SectorID
}

type StorageProvider struct {
    DealStatus {deal.DealCID: StorageDealStatus}

    // calls between StorageClient and StorageProvider happen over libp2p
    // Client calls this to submit new StorageDealProposal
    HandleNewStorageDealProposal(proposal deal.StorageDealProposal)

    // Call by StorageProvider to sign a StorageDealProposal
    signStorageDealProposal(proposal deal.StorageDealProposal) deal.StorageDeal

    // Call by StorageProvider to reject a StorageDealProposal
    rejectStorageDealProposal(proposal deal.StorageDealProposal)

    // Check client balance and signature
    verifyStorageClient(address addr.Address, signature deal.Signature) bool

    // TODO: Call by StorageMiningSubsystem
    NotifyStorageDealStaged(storageDealNotification StorageDealStagedNotification)

    // Call by StorageClient
    HandleStorageDealQuery(dealCID deal.DealCID) StorageDealStatus
}
package storage_market

import (
	actor "github.com/filecoin-project/specs/systems/filecoin_vm/actor"
	addr "github.com/filecoin-project/specs/systems/filecoin_vm/actor/address"

	deal "github.com/filecoin-project/specs/systems/filecoin_markets/deal"
)

// import deal_status "github.com/filecoin-project/specs/systems/filecoin_markets/storage_market"

func (provider *StorageProvider_I) HandleNewStorageDealProposal(proposal deal.StorageDealProposal) {
	if provider.verifyStorageClient(proposal.Client(), proposal.ProposerSignature(), proposal.StoragePrice()) {
		// status := &deal.StorageDealStatus_StorageDealProposed_I{}
		// s := deal.StorageDealStatus_Make_StorageDealProposed(status)
		provider.DealStatus()[proposal.PieceRef()] = StorageDealProposed
		// TODO notify StorageClient that a deal has been received
		// TODO notify StorageMiningSubsystem to add deals to sector
		provider.signStorageDealProposal(proposal)
		// DO THIS TODAY Call StorageMarketActor.publishStorageDeal()
	}
}

func (provider *StorageProvider_I) signStorageDealProposal(proposal deal.StorageDealProposal) deal.StorageDeal {
	// TODO add signature to the proposal
	// TODO notify StorageClient that a deal has been signed
	panic("TODO")
}

func (provider *StorageProvider_I) rejectStorageDealProposal(proposal deal.StorageDealProposal) {
	provider.DealStatus()[proposal.PieceRef()] = StorageDealRejected
	// TODO send notification to client
}

func (provider *StorageProvider_I) verifyStorageClient(address addr.Address, signature deal.Signature, price actor.TokenAmount) bool {
	// TODO make call to StorageMarketActor
	// balance, found := StorageMarketActor.Balances()[address]

	// if !found {
	// 	return false
	// }

	// if balance < price {
	// 	return false
	// }

	// TODO Check on Signature
	// return true
	panic("TODO")
}

// TODO: func (provider *StorageProvider_I) NotifyStorageDealStaged(storageDealNotification StorageDealStagedNotification) {
// 	panic("TODO")
// }

func (provider *StorageProvider_I) HandleStorageDealQuery(dealCID deal.DealCID) StorageDealStatus {
	dealStatus, found := provider.DealStatus()[dealCID]

	if found {
		return dealStatus
	}

	return StorageDealNotFound
}

// TODO this should be moved into storage market
func (sp *StorageProvider_I) NotifyStorageDealStaged(storageDealNotification StorageDealStagedNotification) {
	panic("TODO")
}

Storage Client

Both StorageProvider and StorageClient are StorageMarketParticipant. Any party can be a storage provider or client or both at the same time. Storage deal negotiation is expected to happen completely off chain and the request-response style storage deal protocol is to submit agreed-upon storage deal onto the network and gain storage power on chain. StorageClient will initiate the storage deal protocol by submitting a StorageDealProposal to the StorageProvider who will then add the deal data to a Sector and commit the sector onto the blockchain.

import deal "github.com/filecoin-project/specs/systems/filecoin_markets/deal"
import addr "github.com/filecoin-project/specs/systems/filecoin_vm/actor/address"

type StorageClient struct {
    // calls between StorageClient and StorageProvider happen over libp2p
    // make call to StorageProvider in StorageDealProposal
    ProposeStorageDeal(proposal deal.StorageDealProposal)

    // make call to StorageProvider
    QueryStorageDeal(dealCID deal.DealCID, provider addr.Address)
}

Something's not right. The storage_client.go file was not found.

Faults

There are two main categories of faults in the Filecoin network.

  • ConsensusFaults
  • StorageDealFaults

ConsensusFaults are faults that impact network consensus and StorageDealFaults are faults where data in a StorageDeal is not maintained by the providers pursuant to deal terms.

is slashed for ConsensusFaults and Storage Deal Collateral for StorageDealFaults.

Any misbehavior may result in more than one fault thus lead to slashing on both collaterals. For example, missing a PoStProof will incur a penalty on both PledgeCollateral and StorageDealCollateral given it impacts both a given StorageDeal and power derived from the sector commitments in Storage Power Consensus.

Storage Faults

TODO: complete this.

Retrieval Market in Filecoin

Components

Version 0 of the retrieval market protocol is what we (tentatively) will launch the filecoin network with. It is version zero because it will only be good enough to fit the bill as a way to pay another node for a file.

The main components are as follows:

  • A payment channel actor (See payment channels for details)
  • ‘retrieval-v0’ libp2p services
  • A chain-based content routing interface
  • A set of commands to interact with the above

Retrieval V0 libp2p Services

The v0 retrieval market will initially be implemented as two libp2p services. It will be request response based, where the client who is requesting a file sends a retrieval deal proposal to the miner. The miner chooses whether or not to accept it, sends their response which (if they accept the proposal) includes a signed retrieval deal, followed by the actual requested content, streamed as a series of bitswap block messages, using a pre-order traversal of the dag. Each block should use the bitswap block message format. This way, the client should be able to verify the data incrementally as it receives it. Once the client has received all the data, it should then send a payment channel SpendVoucher of the proposed amount to the miner. This protocol may be easily extended to include payments from the client to the miner every N blocks, but for now we omit that feature.

Retrieval Client

import actor "github.com/filecoin-project/specs/systems/filecoin_vm/actor"
import addr "github.com/filecoin-project/specs/systems/filecoin_vm/actor/address"

type RetrievalClient struct {
    CreatePaymentChannel(provider addr.Address, payment actor.TokenAmount) PaymentChannel
}

Retrieval Provider (Miner)

import actor "github.com/filecoin-project/specs/systems/filecoin_vm/actor"
import addr "github.com/filecoin-project/specs/systems/filecoin_vm/actor/address"

type PaymentChannel struct {}
type CID struct {}

// File Retrieval Query
type FileRetrievalAvailable struct {
    MinPrice  actor.TokenAmount
    Miner     addr.Address
}
type FileRetrievalUnavailable struct {}
type RetrievalQueryResponse union {
    FileRetrievalAvailable
    FileRetrievalUnavailable
}

type RetrievalQuery struct {
    File CID
}

// File Retrieval Deal Proposal and Deal
type RetrievalDealProposalError struct {}
type RetrievalDealProposalRejected struct {}
type RetrievalDealProposalAccepted struct {
    CounterParty  addr.Address
    Payment       PaymentChannel
}
type RetrievalDealProposalResponse union {
    RetrievalDealProposalAccepted
    RetrievalDealProposalRejected
    RetrievalDealProposalError
}
type RetrievalDealProposal struct {
    File      CID
    Payment   PaymentChannel
    MinPrice  actor.TokenAmount
}

type RetrievalProvider struct {
    NewRetrievalQuery(query RetrievalQuery) RetrievalQueryResponse

    // NewRetrievalDealProposal is called to propose a retrieval
    NewRetrievalDealProposal(proposal RetrievalDealProposal) RetrievalDealProposalResponse

    // AcceptRetrievalDeal is called to accept a retrieval deal
    AcceptRetrievalDealProposal(deal RetrievalDealProposal) RetrievalDealProposalResponse
}

Libraries used in Filecoin

filcrypto - Filecoin Cryptographic Primitives

type VRFPublicKey Bytes
type VRFSecretKey Bytes
type SigPublicKey Bytes
type SigSecretKey Bytes
type PubKey Bytes
type PrivKey Bytes  // TODO merge it into SigSecretKey

type VRFKeyPair struct {
    PublicKey              VRFPublicKey
    SecretKey              VRFSecretKey

    Generate(input Bytes)  VRFResult     @(static)
}

type VRFResult struct {
    Output  Bytes  @(internal)
    Proof   Bytes  @(internal)

    Verify(input Bytes, pk VRFPublicKey) bool
    ValidateSyntax() bool
}

type SigKeyPair struct {
    PublicKey  SigPublicKey
    SecretKey  SigSecretKey

    Generate(input Bytes, _type SigType) Signature
}

type Signature struct {
    Signature  Bytes    @(internal)
    sigType    SigType  @(internal)

    Verify(pk SigPublicKey, input Bytes) bool
}

type SigType enum {
    RSASigType
    BLSSigType
    EdDSASigType
}

type BLS struct {}
type Secp256k1 struct {}

TODO: describe BLS VRF in implementable detail

filproofs - Filecoin Storage Proofs

import sector "github.com/filecoin-project/specs/systems/filecoin_mining/sector"

type SectorInfo struct {}
type Block struct {}
type SectorID struct {}

type FilecoinProofsSubsystem struct {
    VerifySeal(sealVerifyInfo sector.SealVerifyInfo) bool
    ValidateBlock(block Block) bool
}
import file "github.com/filecoin-project/specs/systems/filecoin_files/file"
import sector "github.com/filecoin-project/specs/systems/filecoin_mining/sector"

type StackedDRGLayers UInt
type StackedDRGNodeSize UInt
type StackedDRGChallenges UInt
type DRGDepth struct {}
type DRGFraction struct {}
type DRGDegree UInt
type DRGSeed struct {}
type DRGNodeCount UInt
type ExpanderGraphNodeCount UInt
type ChungExpanderPermutationFeistelKeys [UInt]
type ChungExpanderPermutationFeistelRounds UInt
type ChungExpanderPermutationFeistelHashFunction enum {
    Blake2S
}
type ChungExpanderAlpha struct {}
type ChungExpanderBeta struct {}
type ExpanderGraphDegree UInt
type ExpanderGraphSeed struct {}
type DRGNodeSize UInt

type SealAlgorithmArtifacts struct {
    AlgorithmWideSetupArtifacts struct {
        // trusted setup output parameters go here
        // updates to public parameters go here
    }

    SealSetupArtifacts

    // ProveArtifacts or
    ChallengeArtifacts struct {
        // inputs into prove() go here
    }

    VerifyArtifacts struct {
        // inputs into verify() go here
    }
}

// per-sector setup artifacts go here   
type SealSetupArtifacts struct {
    CommD              sector.Commitment
    CommR              sector.SealedSectorCID
    CommC              sector.Commitment
    CommRLast          sector.Commitment
    CommDTreePath      file.Path
    CommCTreePath      file.Path
    CommRLastTreePath  file.Path
    KeyLayers          [Bytes]
    Replica            Bytes
}

type PoRep union {
    ZigZagPoRep
}

type PoSpace union {
    PoRep
}

type ZigZagPoRep StackedDRG

type SDRPoSpace StackedDRG

type EllipticCurve struct {
    FieldModulus &BigInt
}

//type StackedDRG_Algorithm struct {}

type StackedDRG struct {
    Layers            StackedDRGLayers
    NodeSize          StackedDRGNodeSize
    Challenges        StackedDRGChallenges
    Algorithm         struct {}
    DRGCfg
    ExpanderGraphCfg
    // invariant: DRGCfg.Nodes == ExpanderGraphCfg.Nodes
    Curve             EllipticCurve
}

type DRGCfg struct {
    Algorithm struct {
        Depth     DRGDepth  // D
        Fraction  DRGFraction  // E

        ParentsAlgorithm enum {
            DRSample
        }

        RNGAlgorithm enum {
            ChaCha20
        }
    }
    Degree DRGDegree
    Seed DRGSeed
    Nodes DRGNodeCount
}

type DRG struct {
    Config         DRGCfg
    Parents(UInt)  [UInt]
}

type ExpanderGraphCfg struct {
    Algorithm union {
        ChungExpanderAlgorithm
    }

    Degree  ExpanderGraphDegree
    Seed    ExpanderGraphSeed
    Nodes   ExpanderGraphNodeCount
}

type ExpanderGraph struct {
    Config         ExpanderGraphCfg
    Parents(UInt)  [UInt]
}

type ChungExpanderAlgorithm struct {
    Alpha  ChungExpanderAlpha
    Beta   ChungExpanderBeta
    PermutationAlgorithm union {
        Feistel
    }
    Parents(node UInt, ExpanderGraphDegree, nodes ExpanderGraphNodeCount) [UInt]
}

type Feistel struct {
    Keys          ChungExpanderPermutationFeistelKeys
    Rounds        ChungExpanderPermutationFeistelRounds
    HashFunction  ChungExpanderPermutationFeistelHashFunction
    Permute(size UInt, n UInt) UInt
}
package filproofs

import "math"
import "math/rand"
import big "math/big"
import "encoding/binary"

import util "github.com/filecoin-project/specs/util"
import file "github.com/filecoin-project/specs/systems/filecoin_files/file"
import sector "github.com/filecoin-project/specs/systems/filecoin_mining/sector"

type Blake2sHash Bytes32
type PedersenHash Bytes32
type Bytes32 []byte
type UInt = util.UInt

func SDRParams(sealCfg sector.SealCfg) *StackedDRG_I {
	// TODO: Bridge constants with orient model.
	const LAYERS = 10
	const NODE_SIZE = 32
	const OFFLINE_CHALLENGES = 6666
	const FEISTEL_ROUNDS = 3
	var FEISTEL_KEYS = [FEISTEL_ROUNDS]UInt{1, 2, 3}
	var FIELD_MODULUS = new(big.Int)
	// https://github.com/zkcrypto/pairing/blob/master/src/bls12_381/fr.rs#L4
	FIELD_MODULUS.SetString("52435875175126190479447740508185965837690552500527637822603658699938581184513", 10)

	nodes := UInt(sealCfg.SectorSize() / NODE_SIZE)

	return &StackedDRG_I{
		Layers_:     StackedDRGLayers(LAYERS),
		Challenges_: StackedDRGChallenges(OFFLINE_CHALLENGES),
		NodeSize_:   StackedDRGNodeSize(NODE_SIZE),
		Algorithm_:  &StackedDRG_Algorithm_I{},
		DRGCfg_: &DRGCfg_I{
			Algorithm_: &DRGCfg_Algorithm_I{
				ParentsAlgorithm_: DRGCfg_Algorithm_ParentsAlgorithm_Make_DRSample(&DRGCfg_Algorithm_ParentsAlgorithm_DRSample_I{}),
				RNGAlgorithm_:     DRGCfg_Algorithm_RNGAlgorithm_Make_ChaCha20(&DRGCfg_Algorithm_RNGAlgorithm_ChaCha20_I{}),
			},
			Degree_: 6,
			Nodes_:  DRGNodeCount(nodes),
		},
		ExpanderGraphCfg_: &ExpanderGraphCfg_I{
			Algorithm_: ExpanderGraphCfg_Algorithm_Make_ChungExpanderAlgorithm(
				&ChungExpanderAlgorithm_I{
					PermutationAlgorithm_: ChungExpanderAlgorithm_PermutationAlgorithm_Make_Feistel(&Feistel_I{
						Keys_:   FEISTEL_KEYS[:],
						Rounds_: FEISTEL_ROUNDS,
						HashFunction_: ChungExpanderPermutationFeistelHashFunction_Make_Blake2S(
							&ChungExpanderPermutationFeistelHashFunction_Blake2S_I{}),
					}),
				}),
			Degree_: 8,
			Nodes_:  ExpanderGraphNodeCount(nodes),
		},
		Curve_: &EllipticCurve_I{
			FieldModulus_: *FIELD_MODULUS,
		},
	}
}

func (drg *DRG_I) Parents(node UInt) []UInt {
	config := drg.Config()
	degree := UInt(config.Degree())
	return config.Algorithm().ParentsAlgorithm().As_DRSample().Impl().Parents(degree, node)
}

// TODO: Verify this. Both the port from impl and the algorithm.
func (drs *DRGCfg_Algorithm_ParentsAlgorithm_DRSample_I) Parents(degree, node UInt) (parents []UInt) {
	util.Assert(node > 0)
	parents = append(parents, node-1)

	m := degree - 1

	var k UInt
	for k = 0; k < m; k++ {
		logi := int(math.Floor(math.Log2(float64(node * m))))
		// FIXME: Make RNG parameterizable and specify it.
		j := rand.Intn(logi)
		jj := math.Min(float64(node*m+k), float64(UInt(1)<<uint(j+1)))
		backDist := randInRange(int(math.Max(float64(UInt(jj)>>1), 2)), int(jj+1))
		out := (node*m + k - backDist) / m

		parents = append(parents, out)
	}

	return parents
}

func randInRange(lowInclusive int, highExclusive int) UInt {
	return UInt(rand.Intn(highExclusive-lowInclusive) + lowInclusive)
}

func (exp *ExpanderGraph_I) Parents(node UInt) []UInt {
	d := exp.Config().Degree()

	// TODO: How do we handle choice of algorithm generically?
	return exp.Config().Algorithm().As_ChungExpanderAlgorithm().Parents(node, d, exp.Config().Nodes())
}

func (chung *ChungExpanderAlgorithm_I) Parents(node UInt, d ExpanderGraphDegree, nodes ExpanderGraphNodeCount) []UInt {
	var parents []UInt
	var i UInt
	for i = 0; i < UInt(d); i++ {
		parent := chung.ithParent(node, i, d, nodes)
		parents = append(parents, parent)
	}
	return parents
}

func (chung *ChungExpanderAlgorithm_I) ithParent(node UInt, i UInt, degree ExpanderGraphDegree, nodes ExpanderGraphNodeCount) UInt {
	// ithParent generates one of d parents of node.
	d := UInt(degree)

	// This is done by operating on permutations of a set with d elements per node.
	setSize := UInt(nodes) * d

	// There are d ways of mapping each node into the set, and we choose the ith.
	// Note that we can project the element back to the original node: element / d == node.
	element := node*d + i

	// Permutations of the d elements corresponding to each node yield d new elements,
	permuted := chung.PermutationAlgorithm().As_Feistel().Permute(setSize, element)

	// each of which can be projected back to a node.
	projected := permuted / d

	// We have selected the ith such parent of node.
	return projected
}

func (f *Feistel_I) Permute(size UInt, i UInt) UInt {
	panic("TODO")
}

func (sdr *StackedDRG_I) Seal(sid sector.SectorID, data []byte, seed sector.SealRandomness) SealSetupArtifacts {
	commD, commDTreePath := ComputeDataCommitment(data)
	replicaID := ComputeReplicaID(sid, commD, seed)

	drg := DRG_I{
		Config_: sdr.DRGCfg(),
	}

	expander := ExpanderGraph_I{
		Config_: sdr.ExpanderGraphCfg(),
	}

	nodeSize := int(sdr.NodeSize())
	nodes := len(data) / nodeSize
	curveModulus := sdr.Curve().FieldModulus()
	layers := int(sdr.Layers())

	keyLayers := generateSDRKeyLayers(&drg, &expander, replicaID, nodes, layers, nodeSize, curveModulus)
	key := keyLayers[len(keyLayers)-1]

	replica := encodeData(data, key, nodeSize, &curveModulus)

	commRLast, commRLastTreePath := RepHash_PedersenHash(replica)
	commC, commCTreePath := computeCommC(keyLayers, nodeSize)
	commR := RepCompress_PedersenHash(commC, commRLast)

	result := SealSetupArtifacts_I{
		CommD_:             sector.Commitment(commC),
		CommR_:             SealedSectorCID(commR),
		CommC_:             sector.Commitment(commC),
		CommRLast_:         sector.Commitment(commRLast),
		CommDTreePath_:     commDTreePath,
		CommCTreePath_:     commCTreePath,
		CommRLastTreePath_: commRLastTreePath,
		KeyLayers_:         keyLayers,
		Replica_:           replica,
	}
	return &result
}

func ComputeReplicaID(sid sector.SectorID, commD sector.Commitment, seed sector.SealRandomness) Bytes32 {
	_, _ = sid.MinerID(), (sid.Number())

	// FIXME: Implement
	return Bytes32{}
}

func generateSDRKeyLayers(drg *DRG_I, expander *ExpanderGraph_I, replicaID []byte, nodes int, layers int, nodeSize int, modulus big.Int) [][]byte {
	var keyLayers [][]byte
	var prevLayer []byte

	for i := 0; i <= layers; i++ {
		currentLayer := labelLayer(drg, expander, replicaID, nodes, nodeSize, prevLayer)
		keyLayers = append(keyLayers, currentLayer)
		prevLayer = currentLayer
	}

	return keyLayers
}

func labelLayer(drg *DRG_I, expander *ExpanderGraph_I, replicaID []byte, nodeSize int, nodes int, prevLayer []byte) []byte {
	size := nodes * nodeSize
	labels := make([]byte, size)

	for i := 0; i < nodes; i++ {
		var parents []byte

		// The first node of every layer has no DRG Parents.
		if i > 0 {
			for parent := range drg.Parents(UInt(i)) {
				start := parent * nodeSize
				parents = append(parents, labels[start:start+nodeSize]...)
			}
		}

		// The first layer has no expander parents.
		if prevLayer != nil {
			for parent := range expander.Parents(UInt(i)) {
				start := parent * nodeSize
				parents = append(parents, labels[start:start+nodeSize]...)
			}
		}

		label := generateLabel(replicaID, i, parents)
		labels = append(labels, label...)
	}

	return labels
}

func encodeData(data []byte, key []byte, nodeSize int, modulus *big.Int) []byte {
	if len(data) != len(key) {
		panic("Key and data must be same length.")
	}

	encoded := make([]byte, len(data))
	for i := 0; i < len(data); i += nodeSize {
		copy(encoded[i:i+nodeSize], encodeNode(data[i:i+nodeSize], key[i:i+nodeSize], modulus, nodeSize))
	}

	return encoded
}

func generateLabel(replicaID []byte, node int, dependencies []byte) []byte {
	nodeBytes := make([]byte, 8)
	binary.LittleEndian.PutUint64(nodeBytes, uint64(node))

	preimage := append(replicaID, nodeBytes...)
	preimage = append(preimage, dependencies...)

	return deriveLabel(preimage)
}

func deriveLabel(elements []byte) []byte {
	return WideRepCompress_Blake2sHash(elements)
}

func computeCommC(keyLayers [][]byte, nodeSize int) (PedersenHash, file.Path) {
	leaves := make([]byte, len(keyLayers[0]))

	// For each node in the graph,
	for start := 0; start < len(leaves); start += nodeSize {
		end := start + nodeSize

		var column []byte
		// Concatenate that node's label at each layer, in order, into a column.
		for i := 0; i < len(keyLayers); i++ {
			label := keyLayers[i][start:end]
			column = append(column, label...)
		}

		// And hash that column to create the leaf of a new tree.
		hashed := hashColumn(column)
		copy(leaves[start:end], hashed[:])
	}

	// Return the root of and path to the column tree.
	return RepHash_PedersenHash(leaves)
}

func hashColumn(column []byte) PedersenHash {
	return WideRepCompress_PedersenHash(column)
}

func (sdr *StackedDRG_I) CreateSealProof(randomSeed sector.SealRandomness, aux sector.ProofAuxTmp) sector.SealProof {
	replicaID := ComputeReplicaID(aux.SectorID(), aux.CommD(), aux.Seed())

	drg := DRG_I{
		Config_: sdr.DRGCfg(),
	}

	expander := ExpanderGraph_I{
		Config_: sdr.ExpanderGraphCfg(),
	}

	nodeSize := UInt(sdr.NodeSize())
	challenges := sdr.GenerateOfflineChallenges(randomSeed, int(sdr.Challenges()))

	var challengeProofs []OfflineSDRChallengeProof

	for c := range challenges {
		challengeProofs = append(challengeProofs, CreateChallengeProof(&drg, &expander, replicaID, UInt(c), nodeSize, aux))
	}

	return sdr.CreateOfflineCircuitProof(challengeProofs, aux)
}

func CreateChallengeProof(drg *DRG_I, expander *ExpanderGraph_I, replicaID []byte, challenge UInt, nodeSize UInt, aux sector.ProofAuxTmp) (proof OfflineSDRChallengeProof) {
	var columnElements []UInt
	columnElements = append(columnElements, challenge)
	columnElements = append(columnElements, drg.Parents(challenge)...)
	columnElements = append(columnElements, expander.Parents(challenge)...)

	var columnProofs []SDRColumnProof
	for c := range columnElements {
		columnProof := CreateColumnProof(UInt(c), nodeSize, aux)
		columnProofs = append(columnProofs, columnProof)
	}

	dataProof := createInclusionProof(aux.Data()[challenge*nodeSize:(challenge+1)*nodeSize], aux.Data())
	replicaProof := createInclusionProof(aux.Replica()[challenge*nodeSize:(challenge+1)*nodeSize], aux.Data())

	proof = OfflineSDRChallengeProof{
		DataProof:    dataProof,
		ColumnProofs: columnProofs,
		ReplicaProof: replicaProof,
	}

	return proof
}

func CreateColumnProof(c UInt, nodeSize UInt, aux sector.ProofAuxTmp) (columnProof SDRColumnProof) {
	commC := aux.PersistentAux().CommC()
	layers := aux.KeyLayers()
	var column []byte

	for i := 0; i < len(layers); i++ {
		column = append(column, layers[i][c*nodeSize:(c+1)*nodeSize]...)
	}

	leaf := hashColumn(column)
	columnProof = SDRColumnProof{
		ColumnElements: column,
		InclusionProof: createInclusionProof(leaf, commC),
	}

	return columnProof
}

func createInclusionProof(leaf []byte, root []byte) InclusionProof {
	panic("TODO")
}

type OfflineSDRChallengeProof struct {
	CommRLast sector.Commitment
	CommC     sector.Commitment

	// TODO: these proofs need to depend on hash function.
	DataProof    InclusionProof // Blake2s
	ColumnProofs []SDRColumnProof
	ReplicaProof InclusionProof // Pedersen

}

type InclusionProof struct{}

type SDRColumnProof struct {
	ColumnElements []byte
	InclusionProof InclusionProof
}

func (sdr *StackedDRG_I) CreateOfflineCircuitProof(challengeProofs []OfflineSDRChallengeProof, aux sector.ProofAuxTmp) sector.SealProof {
	panic("TODO")
}

func (sdr *StackedDRG_I) GenerateOfflineChallenges(randomSeed sector.SealRandomness, challenges int) []UInt {
	panic("TODO")
}

func encodeNode(data []byte, key []byte, modulus *big.Int, nodeSize int) []byte {
	// TODO: Make this a method of StackedDRG.
	return addEncode(data, key, modulus, nodeSize)
}

func addEncode(data []byte, key []byte, modulus *big.Int, nodeSize int) []byte {

	d := bigIntFromLittleEndianBytes(data)
	k := bigIntFromLittleEndianBytes(key)

	sum := new(big.Int).Add(d, k)
	result := new(big.Int).Mod(sum, modulus)

	return littleEndianBytesFromBigInt(result, nodeSize)
}

////////////////////////////////////////////////////////////////////////////////
// Verification

func (sdr *StackedDRG_I) VerifySeal(sv sector.SealVerifyInfo) bool {
	onChain := sv.OnChain()

	sealProof := onChain.Proof()

	var commD sector.Commitment
	commR := Commitment_SealedSectorCID(sector.SealedSectorCID(onChain.SealedCID()))

	sdr.VerifyOfflineCircuitProof(commD, commR, sealProof)

	panic("TODO")
}

func (sdr *StackedDRG_I) VerifyOfflineCircuitProof(commD sector.Commitment, commR sector.Commitment, sv sector.SealProof) bool {
	panic("TODO")
}

////////////////////////////////////////////////////////////////////////////////
/// Generic Hashing and Merkle Tree generation

/// Binary hash compression.
// RepCompress<T>
func RepCompress_T(left []byte, right []byte) util.T {
	return util.T{}
}

// RepCompress<PedersenHash>
func RepCompress_PedersenHash(left []byte, right []byte) PedersenHash {
	return PedersenHash{}
}

// RepCompress<Blake2sHash>
func RepCompress_Blake2sHash(left []byte, right []byte) Blake2sHash {
	return Blake2sHash{}
}

////////////////////////////////////////////////////////////////////////////////

/// Digest
// WideRepCompress<T>
func WideRepCompress_T(data []byte) util.T {
	return util.T{}
}

// RepCompress<PedersenHash>
func WideRepCompress_PedersenHash(data []byte) PedersenHash {
	return PedersenHash{}
}

// RepCompress<Blake2sHash>
func WideRepCompress_Blake2sHash(data []byte) Blake2sHash {
	return Blake2sHash{}
}

////////////////////////////////////////////////////////////////////////////////

func DigestSize_T() int {
	panic("Unspecialized")
}

func DigestSize_PedersenHash() int {
	return 32
}

func DigestSize_Blake2sHash() int {
	return 32
}

////////////////////////////////////////////////////////////////////////////////
/// Binary Merkle-tree generation

// RepHash<T>
func RepHash_T(data []byte) (util.T, file.Path) {
	// Plan: define this in terms of RepCompress_T, then copy-paste changes into T-specific specializations, for now.

	// Nodes are always the digest size so data cannot be compressed to digest for storage.
	nodeSize := DigestSize_T()

	// TODO: Fail if len(dat) is not a power of 2 and a multiple of the node size.

	rows := [][]byte{data}

	for row := []byte{}; len(row) > nodeSize; {
		for i := 0; i < len(data); i += 2 * nodeSize {
			left := data[i : i+nodeSize]
			right := data[i+nodeSize : i+2*nodeSize]
			hashed := RepCompress_T(left, right)

			row = append(row, asBytes(hashed)...)
		}
		rows = append(rows, row)
	}

	// Last row is the root
	root := rows[len(rows)-1]

	if len(root) != nodeSize {
		panic("math failed us")
	}

	var filePath file.Path // TODO: dump tree to file.
	// NOTE: merkle tree file layout is illustrative, not prescriptive.

	// TODO: Check above more carefully. It's just an untested sketch for the moment.
	return fromBytes_T(root), filePath
}

// RepHash<PedersenHash>
func RepHash_PedersenHash(data []byte) (PedersenHash, file.Path) {
	return PedersenHash{}, file.Path("") // FIXME
}

//  RepHash<Blake2sHash>
func RepHash_Blake2sHash(data []byte) (Blake2sHash, file.Path) {
	return []byte{}, file.Path("") // FIXME
}

////////////////////////////////////////////////////////////////////////////////

func UnsealedSectorCID(h Blake2sHash) sector.UnsealedSectorCID {
	panic("not implemented -- re-arrange bits")
}

func SealedSectorCID(h PedersenHash) sector.SealedSectorCID {
	panic("not implemented -- re-arrange bits")
}

func Commitment_UnsealedSectorCID(cid sector.UnsealedSectorCID) sector.Commitment {
	panic("not implemented -- re-arrange bits")
}

func Commitment_SealedSectorCID(cid sector.SealedSectorCID) sector.Commitment {
	panic("not implemented -- re-arrange bits")
}

func ComputeDataCommitment(data []byte) ([]byte, file.Path) {
	// TODO: make hash parameterizable
	return RepHash_Blake2sHash(data)
}

// Compute CommP or CommD.
func ComputeUnsealedSectorCID(data []byte) (sector.UnsealedSectorCID, file.Path) {
	// TODO: check that len(data) > minimum piece size and is a power of 2.
	hash, treePath := RepHash_Blake2sHash(data)
	return UnsealedSectorCID(hash), treePath
}

// Utilities

func reverse(bytes []byte) {
	for i, j := 0, len(bytes)-1; i < j; i, j = i+1, j-1 {
		bytes[i], bytes[j] = bytes[j], bytes[i]
	}
}

func bigIntFromLittleEndianBytes(bytes []byte) *big.Int {
	reverse(bytes)
	return new(big.Int).SetBytes(bytes)
}

// size is number of bytes to return
func littleEndianBytesFromBigInt(z *big.Int, size int) []byte {
	bytes := z.Bytes()[0:size]
	reverse(bytes)

	return bytes
}

func asBytes(t util.T) []byte {
	panic("Unimplemented for T")

	return []byte{}
}

func fromBytes_T(_ interface{}) util.T {
	panic("Unimplemented for T")
	return util.T{}
}

FCS

Something's not right. The fcs.id file was not found.

IPLD - InterPlanetary Linked Data

type Store GraphStore

// imported as ipld.Object
type Object struct {
    CID() CID

    // Populate(v interface{}) error
}

CIDs - Content IDentifiers

type BytesKey string  // so that we can use it in go maps

type CID BytesKey  // TODO: remove util.

Data Model

Selectors

GraphStore - IPLD Data Storage

// imported as ipld.Store
type GraphStore struct {
    Get(cid CID)   union {o Object, err error}
    Put(o Object)  union {cid CID, err error}
}

libp2p

import ipld "github.com/filecoin-project/specs/libraries/ipld"
import mf "github.com/filecoin-project/specs/libraries/multiformats"

// PeerID is the CID of the public key of this peer
type PeerID ipld.CID

// PeerInfo is a simple datastructure that relates PeerIDs to corresponding partial Multiaddrs.
// This is a convenience struct used in interfaces where we must specify both, or may specify
// either.
type PeerInfo struct {
    PeerID
    Addrs [mf.Multiaddr]
}

type Node struct {
    // PeerID returns the PeerID associated with this libp2p Node
    PeerID() PeerID

    // MountProtocol adds given Protocol under specified protocol id.
    MountProtocol(path ProtocolPath, protocol Protocol)

    // ConnectPeerID establishes a connection to peer matching given PeerInfo.
    //
    // PeerInfo.Addrs may be empty. If so:
    // - Libp2pNode will try to use any Multiaddrs it knows (internal PeerStore)
    // - Libp2pNode may use any `PeerRouting` protocol mounted onto the libp2p node.
    //     TODO: how to define this.
    //     NOTE: probably implies using kad-dht or gossipsub for this.
    //
    // Idempotent. If a connection already exists, this method returns silently.
    Connect(peerInfo PeerInfo)
}

type ProtocolPath string

type Protocol union {
    StreamProtocol
    DatagramProtocol
}

// Stream is an interface to deal with networked processes, which communicate
// via streams of bytes.
//
// See golang.org/pkg/io -- as this is modelled after io.Reader and io.Writer
type Stream struct {
    // Read reads bytes from the underlying stream and copies them to buf.
    // Read returns the number of bytes read (n), and potentially an error
    // encountered while reading. Read reads at most len(buf) byte.
    // Read may read 0 bytes.
    Read(buf Bytes) union {n int, err error}

    // Write writes bytes to the underlying stream, copying them from buf.
    // Write returns the number of bytes written (n), and potentially an error
    // encountered while writing. Write writes at most len(buf) byte.
    // Write may read 0 bytes.
    Write(buf Bytes) union {n int, err error}

    // Close terminates client's use of the stream.
    // Calling Read or Write after Close is an error.
    Close() error
}

type StreamProtocol struct {
    // AcceptStream accepts an incoming stream connection.
    AcceptStream() struct {
        stream    Stream
        peerInfo  PeerInfo
        err       error
    }

    // OpenStream opens a stream to a particular PeerID.
    OpenStream(peerInfo PeerInfo) struct {
        stream  Stream
        err     error
    }
}

// Datagram
type Datagram Bytes

// Datagrams are "messages" in the network packet sense of the word.
//
// "message-oriented network protocols" should use this interface,
// not the StreamProtocol interface.
//
// We call it "Datagram" here because unfortunately the word "Message"
// is very overloaded in Filecoin.
// Suggestion for libp2p: use datagram too.
type DatagramProtocol struct {
    // AcceptDatagram accepts an incoming message.
    AcceptDatagram() struct {
        datagram  Datagram
        peerInfo  PeerInfo
        err       error
    }

    // OpenStream opens a stream to a particular PeerID
    SendDatagram(datagram Datagram, peerInfo PeerInfo) struct {err error}
}

// type StorageDealLibp2pProtocol struct {
//   StreamProtocol StreamProtocol
//   // ---
//   AcceptStream() struct {}
//   OpenStream() struct {}
// }

Gossipsub for broadcasts

Kademlia DHT for Peer Routing

Filecoin libp2p Nodes

IPFS - InterPlanetary File System

BitSwap

GraphSync

UnixFS

Multiformats - self describing protocol values

Multihash - self describing hash values

type Multihash Bytes

Multiaddr - self describing network addresses

type Multiaddr Bytes

Algorithms

Expected Consensus

Algorithm

Expected Consensus (EC) is a probabilistic Byzantine fault-tolerant consensus protocol. At a high level, it operates by running a leader election every round in which, on expectation, one participant may be eligible to submit a block. EC guarantees that this winner will be anonymous until they reveal themselves by submitting a proof of their election (we call this proof an Election Proof). All valid blocks submitted in a given round form a Tipset. Every block in a Tipset adds weight to its chain. The ‘best’ chain is the one with the highest weight, which is to say that the fork choice rule is to choose the heaviest known chain. For more details on how to select the heaviest chain, see Chain Selection.

At a very high level, with every new block generated, a miner will craft a new ticket from the prior one in the chain. While on expectation at least one block will be generated at every round, in cases where no one finds a block in a given round, a miner may increment a given nonce as part of the input with which they attempt to run leader election in order to ensure liveness in the protocol. These nonces help mark block height. Every block in a given Tipset will contain election proofs with the same nonce (i.e. they are mined at the same height).

The Storage Power Consensus subsystem uses access to EC to use the following facilities: - Access to verifiable randomness for the protocol, derived from Tickets. - Running and verifying leader election for block generation. - Access to a weighting function enabling Chain Selection by the chain manager. - Access to the most recently finalized tipset available to all protocol participants.

Tickets

For leader election in EC, participants win in proportion to the power they have within the network.

A ticket is drawn from the past at the beginning of each new round to perform leader election. EC also generates a new ticket in every round for future use. Tickets are chained independently of the main blockchain. A ticket only depends on the ticket before it, and not any other data in the block. On expectation, in Filecoin, every block header contains one ticket, though it could contain more if that block was generated over multiple rounds.

Tickets are used across the protocol as sources of randomness: - The Sector Sealer uses tickets to bind sector commitments to a given subchain. - The PoSt Generator likewise uses tickets to prove sectors remain committed as of a given block. - EC uses them to run leader election and generates new ones for use by the protocol, as detailed below.

You can find the Ticket data structure here.

Comparing Tickets in a Tipset

Whenever comparing tickets is evoked in Filecoin, for instance when discussing selecting the “min ticket” in a Tipset, the comparison is that of the little endian representation of the ticket’s VFOutput bytes.

Tickets in EC

Within EC, a miner generates a new ticket in their block for every ticket they use running leader election, thereby ensuring the ticket chain is always as long as the block chain.

Tickets are used to achieve the following: - Ensure leader secrecy – meaning a block producer will not be known until they release their block to the network. - Prove leader election – meaning a block producer can be verified by any participant in the network.

In practice, EC defines two different fields within a block:

  • A Ticket field β€” this stores the new ticket generated during this block generation attempt. It is from this ticket that miners will sample randomness to run leader election in K rounds.
  • An ElectionProof β€” this stores a proof that a given miner has won a leader election using the appropriate ticket K rounds back along with a nonce showing how many rounds generating the EP took. It proves that the leader was elected in this round.

    But why the randomness lookback?
    
    The randomness lookback helps turn independent ticket generation from a block one round back
    into a global ticket generation game instead. Rather than having a distinct chance of winning or losing
    for each potential fork in a given round, a miner will either win on all or lose on all
    forks descended from the block in which the ticket is sampled.
    
    This is useful as it reduces opportunities for grinding, across forks or sybil identities.
    
    However this introduces a tradeoff:
    - The randomness lookback means that a miner can know K rounds in advance that they will win,
    decreasing the cost of running a targeted attack (given they have local predictability).
    - It means electionProofs are stored separately from new tickets on a block, taking up
    more space on-chain.
    
    How is K selected?
    - On the one end, there is no advantage to picking K larger than finality.
    - On the other, making K smaller reduces adversarial power to grind.
    
Ticket generation

This section discusses how tickets are generated by EC for the Ticket field.

At round N, a new ticket is generated using tickets drawn from the Tipset at round N-1 (for more on how tickets are drawn see Ticket Chain).

The miner runs the prior ticket through a Verifiable Random Function (VRF) to get a new unique output.

The VRF’s deterministic output adds entropy to the ticket chain, limiting a miner’s ability to alter one block to influence a future ticket (given a miner does not know who will win a given round in advance).

We use the ECVRF algorithm from Goldberg et al. Section 5, with: - Sha256 for our hashing function - Secp256k1 for our curve - Note that the operation type in step 2.1 is necessary to prevent an adversary from guessing an election proof for a miner ahead of time.

Ticket Validation

Each Ticket should be generated from the prior one in the ticket-chain.

Secret Leader Election

Expected Consensus is a consensus protocol that works by electing a miner from a weighted set in proportion to their power. In the case of Filecoin, participants and powers are drawn from the storage power table, where power is equivalent to storage provided through time.

Leader Election in Expected Consensus must be Secret, Fair and Verifiable. This is achieved through the use of randomness used to run the election. In the case of Filecoin’s EC, the blockchain tracks an independent ticket chain. These tickets are used as randomness inputs for Leader Election. Every block generated references an ElectionProof derived from a past ticket. The ticket chain is extended by the miner who generates a new block for each successful leader election.

Running a leader election

Now, a miner must also check whether they are eligible to mine a block in this round.

To do so, the miner will use tickets from K rounds back as randomness to uniformly draw a value from 0 to 1. Comparing this value to their power, they determine whether they are eligible to mine. A user’s power is defined as the ratio of the amount of storage they proved as of their last PoSt submission to the total storage in the network as of the current block.

We use the ECVRF algorithm (must yield a pseudorandom, deterministic output) from Goldberg et al. Section 5, with: - Sha256 for our hashing function - Secp256k1 for our curve

If the miner wins the election in this round, it can use newEP, along with a newTicket to generate and publish a new block. Otherwise, it waits to hear of another block generated in this round.

It is important to note that every block contains two artifacts: one, a ticket derived from last block’s ticket to extend the ticket-chain, and two, an election proof derived from the ticket K rounds back used to run leader election.

Succinctly, the process of crafting a new ElectionProof in round N is as follows. We use:

The ECVRF algorithm (must yield a pseudorandom, deterministic output) from Goldberg et al. Section 5, with:
    Sha256 for our hashing function
    Secp256k1 for our curve

Note: We draw the miner power from the prior round. This means that if a miner wins a block on their ProvingPeriodEnd even if they have not yet resubmitted a PoSt, they retain their power (until the next round).

If successful, the miner can craft a block, passing it to the block producer. If unsuccessful, it will wait to hear of another block mined this round to try again. In the case no other block was found in this round the miner can increment its nonce and try leader election again using the same past ticket and new nonce. While a miner could try to run through multiple nonces in parallel in order to quickly generate a block, this effort will be futile as the rational majority of miners will reject blocks crafted with ElectionProofs whose nonces prove too high (see below).

Election Validation

In order to determine that the mined block was generated by an eligible miner, one must check its ElectionProof.

Chain Selection

Just as there can be 0 miners win in a round, multiple miners can be elected in a given round. This in turn means multiple blocks can be created in a round. In order to avoid wasting valid work done by miners, EC makes use of all valid blocks generated in a round.

Chain Weighting

It is possible for forks to emerge naturally in Expected Consensus. EC relies on weighted chains in order to quickly converge on ‘one true chain’, with every block adding to the chain’s weight. This means the heaviest chain should reflect the most amount of work performed, or in Filecoin’s case, the most storage provided.

In short, the weight at each block is equal to its ParentWeight plus that block’s delta weight. Details of Filecoin’s chain weighting function are included here.

Delta weight is a term composed of a few elements: - wForkFactor: which seeks to cut the weight derived from rounds in which produced Tipsets do not correspond to what an honest chain is likely to have yielded (pointing to selfish mining or other non-collaborative miner behavior). - wPowerFactor: which adds weight to the chain proportional to the total power backing the chain, i.e. accounted for in the chain’s power table. - wBlocksFactor: which adds weight to the chain proportional to the number of blocks mined in a given round. Like wForkFactor, it rewards miner cooperation (which will yield more blocks per round on expectation).

The weight should be calculated using big integer arithmetic with order of operations defined above. We use brackets instead of parentheses below for legibility. We have:

w[r+1] = w[r] + (wPowerFactor[r+1] + wBlocksFactor[r+1]) * 2^8

For a given tipset ts in round r+1, we define:

  • wPowerFactor[r+1] = wFunction(totalPowerAtTipset(ts))
  • wBlocksFactor[r+1] = wPowerFactor[r+1] * wRatio * b / e
    • with b = |blocksInTipset(ts)|
    • e = expected number of blocks per round in the protocol
    • and wRatio in ]0, 1[ Thus, for stability of weight across implementations, we take:
  • wBlocksFactor[r+1] = (wPowerFactor[r+1] * b * wRatio_num) / (e * wRatio_den)

We get: - w[r+1] = w[r] + wFunction(totalPowerAtTipset(ts)) * 2^8 + (wFunction(totalPowerAtTipset(ts)) * len(ts.blocks) * wRatio_num * 2^8) / (e * wRatio_den) Using the 2^8 here to prevent precision loss ahead of the division in the wBlocksFactor.

The exact value for these parameters remain to be determined, but for testing purposes, you may use: - e = 5 - wRatio = .5, or wRatio_num = 1, wRatio_den = 2 - wFunction = log2b with - log2b(X) = floor(log2(x)) = (binary length of X) - 1 and log2b(0) = 0. Note that that special case should never be used (given it would mean an empty power table.

Note that if your implementation does not allow for rounding to the fourth decimal, miners should apply the [tie-breaker below](#selecting-between-tipsets-with-equal-weight). Weight changes will be on the order of single digit numbers on expectation, so this should not have an outsized impact on chain consensus across implementations.

ParentWeight is the aggregate chain weight of a given block’s parent set. It is calculated as the ParentWeight of any of its parent blocks (all blocks in a given Tipset should have the same ParentWeight value) plus the delta weight of each parent. To make the computation a bit easier, a block’s ParentWeight is stored in the block itself (otherwise potentially long chain scans would be required to compute a given block’s weight).

Selecting between Tipsets with equal weight

When selecting between Tipsets of equal weight, a miner chooses the one with the smallest final ticket.

In the case where two Tipsets of equal weight have the same min ticket, the miner will compare the next smallest ticket (and select the Tipset with the next smaller ticket). This continues until one Tipset is selected.

The above case may happen in situations under certain block propagation conditions. Assume three blocks B, C, and D have been mined (by miners 1, 2, and 3 respectively) off of block A, with minTicket(B) < minTicket© < minTicket (D).

Miner 1 outputs their block B and shuts down. Miners 2 and 3 both receive B but not each others’ blocks. We have miner 2 mining a Tipset made of B and C and miner 3 mining a Tipset made of B and D. If both succesfully mine blocks now, other miners in the network will receive new blocks built off of Tipsets with equal weight and the same smallest ticket (that of block B). They should select the block mined atop [B, C] since minTicket© < minTicket(D).

The probability that two Tipsets with different blocks would have all the same tickets can be considered negligible: this would amount to finding a collision between two 256-bit (or more) collision-resistant hashes.

Finality in EC

EC enforces a version of soft finality whereby all miners at round N will reject all blocks that fork off prior to round N-F. For illustrative purposes, we can take F to be 500. While strictly speaking EC is a probabilistically final protocol, choosing such an F simplifies miner implementations and enforces a macroeconomically-enforced finality at no cost to liveness in the chain.

Consensus Faults

Due to the existence of potential forks in EC, a miner can try to unduly influence protocol fairness. This means they may choose to disregard the protocol in order to gain an advantage over the power they should normally get from their storage on the network. A miner should be slashed if they are provably deviating from the honest protocol.

This is detectable when a given miner submits two blocks that satisfy any of the following “consensus faults”:

  • (1) double-fork mining fault: two blocks contains the same electionProof nonce, mined at the same height.
  • (2) parent grinding fault: one block’s parent is a Tipset that could have validly included the other block according to Tipset validity rules, however the parent of the first block does not include the other block.

    • While it cannot be proven that a miner omits known blocks from a Tipset in general (i.e. network latency could simply mean the miner did not receive a particular block) in this case it can be proven because a miner must be aware of a block they mined in a previous round.

Any node that detects either of the above events should submit both block headers to the StoragePowerActor’s ReportConsensusFault method. The “slasher” will receive a portion (TODO: define how much) of the offending miner’s as a reward for notifying the network of the fault. (TODO: FIP of submitting commitments to block headers to prevent miners censoring slashers in order to gain rewards).

It is important to note that there exists a third type of consensus fault directly reported by the CronActor on StorageDeal failures via the ReportUncommittedPowerFault method: - (3) uncommitted power fault which occurs when a miner fails to submit their PostProof and is thus participating in leader election with undue power (see Faults).

Proof-of-Replication

Stacked DRG PoRep

This section describes Stacked DRG PoRep (SDR), the specific Proof-of-Replication (PoRep) used in Filecoin. In this construction, the prover encodes the original data into a replica and commits to it. An offline PoRep proves that the commitment to the replica is a valid commitment of the encoded original data.

SDR has been presented by Ben Fisch at EUROCRYPT19.

Introduction

Background on Proof-of-Replication

Proof-of-Replication enables a prover P to convince a verifier V that P is storing a replica R, a physically independent copy of some data D, unique to P. The scheme is defined by a tuple of polynomial time algorithms (Setup, Replication, Prove, Verify). The assumption is that generation of a replica after Replicate must be difficult (if not impossible) to generate.

  • Setup: On setup, the public parameters of the proving systems are set.
  • Replicate: On replication, either a party or both (depending on the scheme, in our case the prover only!) generate a unique permutation of the original data D, which we call replica R.
  • Prove: On receiving a challenge, the prover must generate a proof that it is in possession of the replica and that it was derived from data D. The prover must only be able to respond to the challenge successfully if it is in possession of the replica, since would be difficult (if not impossible) to generate a replica that can be used to generate the proof at this stage
  • Verify: On receiving the proof, the verifier checks the validity of the proof and accepts or rejects the proof.
sequenceDiagram Note right of Prover: CommD Prover-->>Prover: R, CommR ← Replicate(D) Prover->>Verifier: CommR Verifier-->>Verifier: Generate random challenge Verifier->>Prover: challenge Prover-->>Prover: proof ← Prove(D, R, challenge) Prover->>Verifier: proof
Time-bounded Proof-of-Replication

Timing assumption. Time-bounded Proof-of-Replication are constructions of PoRep with timing assumptions. The assumption is that generation of the replica (hence the Replication) takes some time t that is substantially larger than the time it takes to produce a proof (hence time(Prove)) and the round-trip time (RTT) for sending a challenge and receiving a proof.

Distinguishing Malicious provers. A malicious prover that does not have R, must obtain it (or generate it), before the Prove step. A verifier can distinguish an honest prover from a malicious prover, since the malicious one will take too long to answer the challenge. A verifier will reject if receiving the proof from the prover takes longer than a timeout (bounded between proving time and replication time).

Background on Stacked DRG PoRep

Stacked DRG PoRep (SDR) is a specific Proof-of-Replication construction that we use in Filecoin. SDR has been designed by Ben Fisch at EUROCRYPT19. At a high level, SDR ensures that the Replicate step is a slow non-parallelizable sequential process by using a special type of graph called Depth Robust Graphs (DRG).

Encoding using DRGs. A key is generated by sequentially labeling nodes in the graph such that each label depends on the labels of its parents. The depth robustness property of these graphs ensure that the sequential labeling steps are not parallelizable. The final labels are used as a key to encode the original data.

TODO: This probably needs a more thorough rewrite.

** Stacked DRGs**. SDR builds on the above by stacking DRG graphs into LAYERS layers. Each layer is connected to the previous by a Bipartite Expander Graph. The combination of DRGs and expander graphs guarantee the security property of PoRep. As before, the key produced by the final layer is used to encode the original data, yielding the replica.

Generating SDR proofs. Given the following public parameters:

  • ReplicaId is a unique replica identifier (see the Filecoin Proofs spec for details).
  • CommD is the Merkle tree root hash of the input data to the first layer.
  • CommC is the Merkle tree root hash of the SDR column commitments.
  • CommRLast is the Merkle tree root hash of the replica.
  • CommR is the on-chain commitment to the replica, dervied as the hash of the concatenation of CommC and CommRLast.

An SDR proof proves that some data whose committment is CommD has been used to run a Replicate algorithm and generated some data. CommR is the on-chain commitment to both the replicated data and to intermediate stages required to prove Replicate was performed correctly.

An SDR proof consists of a set of challenged DRG nodes for each layer, a set of parent nodes for each challenged node and a Merkle tree inclusion proof for each node provided. The verifier can then verify the correct labeling of each node and that the nodes given were consistent with the prover’s commitments.

Making proofs succinct with SNARKs: The proof size in SDR is too large for blockchain usage (~100MB TODO: check this), mostly due to the large amount of Merkle tree inclusion proofs required to achieve security. We use SNARKs to generate a proof of knowledge of a correct SDR proof. In other words, we implement the SDR proof verification algorithm in an arithmetic circuit and use SNARKs to prove that it was evaluated correctly.

The SNARK circuit proves that given Merkle roots CommD, and CommR, the prover correctly derived the labels at each layer and correctly performed the final encoding.

PoRep in Filecoin

Proof-of-Replication proves that a Storage Miner is dedicating unique storage for each sector. Filecoin Storage Miners collect new clients’ data in a sector, run a slow encoding process (called Seal) and generate a proof (SealProof) that the encoding was generated correctly.

In Filecoin, PoRep provides two guarantees: (1) space-hardness: Storage Miners cannot lie about the amount of space they are dedicating to Filecoin in order to gain more power in the consensus; (2) replication: Storage Miners are dedicating unique storage for each copy of their clients data.

Glossary:

  • sector: a fixed-size block of data of SECTOR_SIZE bytes which generally contains clients’ data.
  • unsealed sector: a concrete representation (on disk or in memory) of a sector’s that follows the “Storage Format” described in Client Data Processing (currently paddedfr32v1 is the required default).
  • sealed sector: a concrete representation (on disk or in memory) of the unique replica generated by Seal from an unsealed sector. A sector contains one or more pieces.
  • piece: a block of data of at most SECTOR_SIZE bytes which is generally a client’s file or part of.

Stacked DRG Construction

Public Parameters

The following public parameters are used in the Stacked DRG Replication and Proof Generation algorithms:

TODO: the Appendix should explain why we picked those values TODO: Just interpolate a table of the Orient parameters and reconcile naming.

name type description value
SECTOR_SIZE uint Number of nodes in the DRG in bytes 68,719,476,736
LAYERS uint Number of Depth Robust Graph stacked layers. 10
BASE_DEGREE uint In-Degree of each Depth Robust Graph. 6
EXPANSION_DEGREE uint Degree of each Bipartite Expander Graph to extend dependencies between layers. 8
GRAPH_SEED uint Seed used for random number generation in baseParents. TODO
NODE_SIZE uint Size of each node in bytes. 32B

The following constants are computed from the public parameters:

name type description computation value
PARENTS_COUNT uint Total number of parent nodes EXPANSION_DEGREE + BASE_DEGREE 13
GRAPH_SIZE uint Number of nodes in the graph SECTOR_SIZE / NODE_SIZE 2,147,483,648
TREE_DEPTH uint Height of the Merkle Tree of a sector LOG_2(GRAPH_SIZE) 31

The following additional public parameters are required:

  • TAPER : Float: Fraction of each layer’s challenges by which to reduce next-lowest layer’s challenge count.
  • TAPER_LAYERS: uint: Number of layers Data is a byte array initialized to the content of unsealed sector and will be mutated in-place by the replication process.
Hash Functions

We have describe three hash functions:

name description size of input size of output construction
KDFHash Hash function used as a KDF to derive the key used to label a single node. TODO 32B Blake2s-256
ColumnHash Hash function used to hash the labeled leaves of each layer (see SDR Column Commitments). TODO 32B JubjubPedersen
RepCompress Collision Resistant Hash function used for the Merkle tree. 2 x 32B + integer height 32B JubjubPedersen
RepHash Balanced binary Merkle tree based used to generate commitments to sealed sectors, unsealed sectors, piece commitments, and intermediate parts of the Proof-of-Replication. TODO 32B Uses RepCompress
RepHash

RepHash is a vector commitment used to generate commitments to sealed sectors, unsealed sectors, piece commitments and intermediate stepds of the Proof-of-Replication. Filecoin uses a balanced binary Merkle tree for RepHash. The leaves of the Merkle tree are pairs of adjacent nodes.

RepHash inputs MUST respect a valid Storage Format. [TODO: What does this mean?]

type node [32]uint8

// Create and return a balanced binary Merkle tree and its root commitment.
// len(leaves) must be a power of 2.
func RepHash(leaves []node) ([][]node, node) {
	rows = [][]node

	currentRow := leaves
	for height := 0; len(currentRow) > 1; height += 1 {
		rows.push(currentRow)
		var nextRow []node

		for i := 0; i < len(row)/2; i += 2 {
			left := row[i]
			right := row[i+1]

			// NOTE: Depending on choice of RepCompress, heightPart may be trimmed to fewer than 8 bits.
			heightPart := []uint8{height}

			input1 := append(heightPart, left...)
			input := append(input1, right...)
			hashed = RepCompress(input, height)
			nextRow = append(nextRow, hashed)
		}

		currentRow = nextRow
	}
    // The tree returned here is just a vector of rows for later use. Its representation is not part of the spec.
	return rows, currentRow[0]
}
Stacked DRG Graph

The slow sequential encoding required is enforced by the depth robusness property of the SDR graph.

Encoding with SDR: The data from a sector (of size SECTOR_SIZE) is divided in NODE_SIZE nodes (for a total of GRAPH_SIZE nodes) and arranged in a directed acyclic graph. The structure of the graph is used to label the nodes sequentially to generate a key with which to encode the original data: in order to label a node, its parents must be labeled (see the “Layer Labeling” section below). We repeat this process for LAYERS layers, where the input to a next layer is the output of the previous one.

Generating the SDR graph: The SDR graph is divided in LAYERS layers. Each layer is a directed acyclic graph and it combines a Depth Robust Graph (DRG) and a Bipartite Expander graph. [TODO: this isn’t quite right.]

We provide an algorithm (SDR) which computes the parents of a node. In high level, the parents of a node are computed by combining two algorithms: some parents (BASE_DEGREE of them) are computed via the BucketSample algorithm extended with a direct ordering of nodes, others (EXPANSION_DEGREE of them) are computed via the Chung algorithm.

SDRGraph: SDR Graph algorithm

Overview: Compute the DRG and Bipartite Expander parents using respectively BucketSample and ChungExpander.

Inputs
name description Type
node The node for which the parents are being computed uint
layer The layer of the SDR graph uint
Outputs
name description Type
parents The ordered parents of node node on layer layer [PARENTS_COUNT]uint
Algorithm
  • If layer = 1:

    • Compute drgParents = BucketSample(node)
    • Set parents to be drgParents.
  • If layer > 1:

    • Compute drgParents = BucketSample(node)
    • Compute expanderParents = ChungExpander(node)
    • Set parents to be the concatenation of drgParents and expanderParents
Pseudocode

We provide below a more succinct representation of the algorithm:

func SDRParents(node uint, layer uint) {

  if layer == 1 {
  	// On first layer
  	let drgParents = BucketSample(node)
  	return drgParents
  } else {
    // On subsequent layers
    let drgParents = BucketSample(node)
  	let expanderParents = ChungExpander(node)
  	return concat(drgParents, expanderParents)
  }
}
Tests

FIXME: Change this.

  • Each parent in parents MUST not be greater than GRAPH_SIZE-1 and lower than 0
  • If layer is even:
    • Each parent in parents MUST be greater than node
    • EXCEPT: if node is 0, then all parents MUST be 0
  • if layer is odd:
    • Each parent in parents MUST be less than node
    • EXCEPT: if node is GRAPH_SIZE-1, then all parents MUST be GRAPH_SIZE-1
Time-space tradeoff

Computing the parents using both BucketSample and ChungExpander for every layer can be an expensive operation, however, this can be avoided by caching the parents.

BucketSample: Depth Robust Graphs algorithm

This section describes how to compute the “base parents” of the SDR graph, which is the equivalent of computing the parents of a Depth Robust Graph.

The properties of DRG graphs guarantee that a sector has been encoded with a slow, non-parallelizable process. We use the BucketSample algorithm that is based on DRSample (ABH17) and described in FBGB18 and generates a directed acyclic graph of in-degree BASE_DEGREE.

BucketSample DRG graphs are random graphs that can be deterministically generated from a seed; different seed lead with high probability to different graphs. In SDR, we use the same seed GRAPH_SEED for each layer of the SDR graph such that they are all based on the same underlying DRG graph.

The parents of any node can be locally computed without computing the entire graph. We call the parents of a node calculated in this way base parents.

func BucketSampleInner(node uint) (parents [BASE_DEGREE]uint) {
    switch node {
        // Special case for the first node, it self references.
        // Special case for the second node, it references only the first one.
        case 0:
        case 1:
            for i := 0; i < BASE_DEGREE; i++ {
                parents[i] = 0
            }
        default:
            rng := ChaChaRng.from_seed(GRAPH_SEED)

            for k := 0; k < BASE_DEGREE; k++ {
                // iterate over m meta nodes of the ith real node
                // simulate the edges that we would add from previous graph nodes
                // if any edge is added from a meta node of jth real node then add edge (j,i)
                logi := floor(log2(node * BASE_DEGREE))
                j := rng.gen() % logi
                jj := min(node * BASE_DEGREE + k, 1 << (j + 1))
                backDist := rng.gen_range(max(jj >> 1, 2), jj + 1)
                out := (node * BASE_DEGREE + k - backDist) / BASE_DEGREE

                // remove self references and replace with reference to previous node
                if out == node {
                    parents[i] = node - 1
                } else {
                    parents[i] = out;
                }
            }

            sort(parents)
    }
}

BucketSample extends BucketSampleInner to include the node’s ‘immediate predecessor’. Each node except the first in a DRG generated by BucketSample has the node whose index is one less than its own as a parent. This ensures that visiting nodes whose indexes are sequential will result in a graph traversal in topological order.

ChungExpander: Bipartite Expander Graphs

TODO: explain why we link nodes in the current layer

Each node in layers other than the first has EXPANSION_DEGREE parents generated via the ChungExpander algorithm. Note that the indexes returned refer to labels from the previous layer. TODO: Make this all clearer with explicit notation.

func ChungExpander(node uint) (parents []uint) {
	parents := make([]uint, EXPANSION_DEGREE)

	feistelKeys := []uint{1, 2, 3, 4} // TODO

	for i := 0, p := 0; i < EXPANSION_DEGREE; i++ {
		a := node * EXPANSION_DEGREE + i
    transformed := feistelPermute(GRAPH_SIZE * EXPANSION_DEGREE, a, feistelKeys)
    other := transformed / EXPANSION_DEGREE
    if other < node {
      parents[p] = other
      p += 1
    }
  }
}
Time-Space tradeoff

Computing these parents can be expensive (especially due to the hashing required by the Feistel algorithm). A miner can trade this computation by storing the expansion parents.

Feistel construction

We use three rounds of Feistel to generate a permutation to compute the parents of the Bipartite Expander graph.

TODO: Add FEISTEL_ROUNDS and FEISTEL_BYTES (or find its definitions)

func permute(numElements uint, index uint, keys [FEISTEL_ROUNDS]uint) uint {
    u := feistelEncode(index, keys)

    while u >= numElements {
        u = feistelEncode(u, keys)
    }
    // Since we are representing `numElements` using an even number of bits,
    // that can encode many values above it, so keep repeating the operation
    // until we land in the permitted range.

    return u
}

func feistelEncode(index uint, keys [FEISTEL_ROUNDS]uint) uint {
    left, right, rightMask, halfBits := commonSetup(index)

    for _, key := range keys {
        left, right = right, left ^ feistel(right, key, rightMask)
    }

    return  (left << halfBits) | right
}

func commonSetup(index uint) (uint, uint, uint, uint) {
    numElements := GRAPH_SIZE * EXPANSION_DEGREE
    nextPow4 := 4;
    halfBits := 1
    while nextPow4 < numElements {
        nextPow4 *= 4
        halfBits += 1
    }

    rightMask = (1 << halfBits) - 1
    leftMask = rightMask << halfBits

    right := index & rightMask
    left := (index & leftMask) >> halfBits

    return  (left, right, rightMask, halfBits)
}

// Round function of the Feistel network: `F(Ri, Ki)`. Joins the `right`
// piece and the `key`, hashes it and returns the lower `uint32` part of
// the hash filtered trough the `rightMask`.
func feistel(right uint, key uint, rightMask uint) uint {
    var data [FEISTEL_BYTES]uint

    var r uint
    if FEISTEL_BYTES <= 8 {
        data[0] = uint8(right >> 24)
        data[1] = uint8(right >> 16)
        data[2] = uint8(right >> 8)
        data[3] = uint8(right)

        data[4] = uint8(key >> 24)
        data[5] = uint8(key >> 16)
        data[6] = uint8(key >> 8)
        data[7] = uint8(key)

        hash := blake2b(data)

        r =   hash[0]) << 24
            | hash[1]) << 16
            | hash[2]) << 8
            | hash[3])
    } else {
        data[0]  = uint8(right >> 56)
        data[1]  = uint8(right >> 48)
        data[2]  = uint8(right >> 40)
        data[3]  = uint8(right >> 32)
        data[4]  = uint8(right >> 24)
        data[5]  = uint8(right >> 16)
        data[6]  = uint8(right >> 8)
        data[7]  = uint8(right)

        data[8]  = uint8(key >> 56)
        data[9]  = uint8(key >> 48)
        data[10] = uint8(key >> 40)
        data[11] = uint8(key >> 32)
        data[12] = uint8(key >> 24)
        data[13] = uint8(key >> 16)
        data[14] = uint8(key >> 8)
        data[15] = uint8(key)

        hash := blake2b(data)

        r =   hash[0] << 56
            | hash[1] << 48
            | hash[2] << 40
            | hash[3] << 32
            | hash[4] << 24
            | hash[5] << 16
            | hash[6] << 8
            | hash[7]
    }

    return r & rightMask
}

Replication

The Replication phase turns an unsealed sector into a sealed sector by first generating a key, then using the key to encode the orignal data.

Before running the Replicate algorithm, the prover must ensure that the sector is correctly formatted with a valid “Storage Format” described in Filecoin Client Data Processing (currently paddedfr32v1 is the required default).

TODO: inputs are missing

The Replication Algorithm proceeds as follows:

  • Calculate ReplicaID using Hash (Blake2s):

ReplicaID is a 32-byte array constructed by hashing the concatenation of the following values - ProverId is a 32-byte array uniquely identifying a prover. - SectorNumber is an unsigned 64-bit integer in little-endian encoding represented as an 8-byte array. - RandomSeed is a 32-byte array of randomness extracted from the chain. - CommD is the Merkle root obtained by performing RepHash on the original data represented in paddedfr32v1.

ReplicaID := Hash(ProverID || SectorNumber || RandomSeed || CommD)
  • Perform RepHash on Data to yield CommD and TreeD:

    CommD, TreeD = RepHash(data)
    

For each of LAYERS layers, l, perform one Layer Replication, yielding a replica, tree, and commitment (CommR_<l>) per layer:

let layer_replicas = [LAYERS][nodes]uint8
let layer_trees = [LAYERS]MerkleTree
let CommRs = []commitment

let layer = data
for l in 0..layers {
	let layer_replica = ReplicateLayer(layer)
	layer_replicas[l] = layer_replica
	CommRs[l], layers_trees[l] = RepTree(layer_replica)
	layer = layer_replica
}

The replicated data is the output of the final Layer Replication,layer_replicas[layers-1]. Set CommRLast to be CommR_<Layers>. Set CommRStar to be CommRHash(ReplicaID || CommR_0 || CommR_<i> || ... || CommRLast).

Replica := layer_replicas[layers - 1]
CommRLast :- CommRs[layers-1]
CommRStar := CommRHash(replicaID, ...CommRs)
Layer Labeling

TODO: Define Graph. We need to decide if this is an object we’ll explicitly define or if its properties (e.g., GRAPH_SIZE) are just part of the replication parameters and all the functions just refer to the same graphs being manipulated across the entire replication process. (At the moment I’ve avoided defining a Graph structure as in other specs I didn’t see any object methods, just standalone functions.)

FIXME, move these to .go files.

func generateKey (replicaId Domain, data []Domain) [LAYERS][NODES]Domain
func encodeData (data []Domain, key []Domain) []Domain
func replicate (replicaId Domain, data []Domain) ([]Domain, []Tree)

fun seal (replicaId Domain, data []Domain) ([]Domain,  ????)
func label(replicaId uint, data []byte) {
    var parents [PARENT_COUNT]uint
    for n := 0; n < GRAPH_SIZE; n++ {
        var node uint := n

        parents = parents(node)

        key := KDFHash(replicaId, node, parents, data)

        start := node * NODE_SIZE
        end := start + NODE_SIZE;

        data[start:end] = key
    }
}

Proof Generation

Overview:

  • Challenge Derivation
  • Proof Generation
  • Circuit Proof Generation

TODO: write a single algorithm which includes the spec below

Challenge Derivation

TODO: define Domain (for practical purposes a uint) and LayerChallenges (or find existing definition).

// TODO: we should replace the word commitment with the word seed, this will be more interactive porep friendly
func DeriveChallenges(challenges LayerChallenges, layer uint, leaves uint, randomness RandomSeed, k uint) []uint {

    n := challenges.ChallengesForLayer(layer)
    var derivedChallenges [n]uint
    for i := 0; i < n; i++ {
        bytes := []byte(ranodomness)
        bytes.append(layer);
        bytes.append(toLittleEndian(n * k + i))

        // For now, we cannot try to prove the first or last node, so make
        // sure the challenge can never be 0 or leaves - 1.
        big_mod_challenge := blake2s(bytes) % (leaves - 2);
        derivedChallenges[i] = big_mod_challenge + 1
    }
}
Challenge Generation

TODO: we may need to remove this section.

Calculate LAYER_CHALLENGES : [LAYERS]uint: Number of challenges per layer. (This will be passed to the SDR circuit proof.)

Derive challenges for each layer (call DeriveChallenges()).

Witness Generation
let layer_proofs = []

for l in 0..LAYERS {
  let replica = layer_replicas[l]
  let replica_tree = layer_trees[l]

  for c in derive_challenges(LAYER_CHALLENGES[l])
    data_inclusion_proof = inclusion_proof(data[c], DataTree, CommR_<l>)
    replica_inclusion_proof = inclusion_proof(replica[c], replica_tree, CommR_<l+1>) || FAIL// Prove the replica. TODO explain replica[].

    // *** let kdf_preimage = [replica_id] ***
    let parent_replica_inclusion_proofs = []
    for p in parents(c) {
      // *** kdf_preimage.push(p)***
      parent_replica_inclusion_proofs.push(inclusion_proof(p, CommR_<l+1>))
    }
    // *** let key = kdf(kdf_preimage); ***

    // *** encode(key, data[c]) == replica[c]
    // *** We don't actually need to encode in the proof. ***
    // TODO: move this ***stuff*** to verification.

    layer_proof.push((data_inclusion_proof, replication_inclusion_proof, parent_replica_inclusion_proofs))
  }
}

return layer_proofs, CommRstar, CommRLast

TODO: reconcile outputs of non-circuit proof with inputs to circuit proof.

Layer Challenge Counts

TODO: define Challenge (or find existing definition)

TODO: we should just list current parameters and show this as a calculation for correctness, this should not mandatory to implement.

func ChallengesForLayer(challenge Challenge, layer uint) uint {

    switch challenge.Type {
      // TODO: remove ambiguity, there should not be a "fixed" case
        case Fixed:
            return challenge.Count
        case Tapered:
      // FIXME
            assert(layer < LAYERS)
            l := (LAYERS - 1) - layer
            r := 1.0 - TAPER;
            t := min(l, TAPER_LAYERS)

            totalTaper := pow(r, t)

            calculated := ceil(totalTaper * challenge.count)

            // Although implied by the call to `ceil()` above, be explicit
            // that a layer cannot contain 0 challenges.
            max(1, calculated)
    }
}

PoRep Commitments

Stacked DRG Commitments

Stacked DRG Commitments

This section summarizes the Stacked DRG (SDR) Column Commitments algorithm described in Tight PoS - ZigZag.

Graph

In the following graphs, DRG and expander parents are both generated by a pseudorandom permutation and are provided only to illustrate the nature of the SDR commitment scheme. They accurately represent how parent-child relationships function between layers, and are accurate for expander parents. However, this is not representative of the DRG parent selection algorithm.

The following graphs illustrate the positions of challenges, DRG parents, and expander parents between layers. Only a single DRG parent and a single expander parent are shown. The immediate predecessor parent is shown for graph topology, but it is not tracked in the tables below.

In order to have a compact and concrete example, we use a graph containing only 8 nodes replicated in 4 layers.

Legend
Data Layer: \(Comm_D\) Tree
Replica Column Layers: \(Comm_C\) Tree
Final Layer: \(Comm_{R_{LAST}}\) Tree
Commitment Algorithm
Goal

We will generate two commitments \(Comm_R, Comm_D\) to be placed on chain.

\(Comm_D\) is the merkle root of the original data.

\(Comm_R = H(Comm_C || Comm_{R_{LAST}})\).

Their construction is described below.

Definitions and Notation

We will perform \(L\) layers of SDR key generation over \(N\) labeled nodes.

In the running example, \(L\) is 4 and \(N\) is 8.

Merkle roots (commitments) are generated with the vector-commitment function \(VC(…)\).

Hashes are produced with a hash function \(H(…)\), which is not necessarily that used by \(VC(…)\).

\(Comm = VC(l_1||…||l_N)\), where the \(l_i\) are the data (labels or hashes) to be committed.

Generated trees are retained until the proving phase, when merkle proofs of a given label’s inclusion in \(Comm\) will be created. We will designate such proofs \(l_i \rightarrow Comm\).

We use the notation \(e{_i}^{(l)}\), correlated in the table below with the \((l, i)\) notation used in the graphs above, where \(l\) indexes layers, and \(i\) indexes labels or columns.

Graph \((1, 1)\) \((1, 2)\) \((1, 3)\) \((1, 4)\) \((1, 5)\) \((1, 6)\) \((1, 7)\) \((1, 8)\)
Notation \(e_1^{(1)}\) \(e_2^{(1)}\) \(e_3^{(1)}\) \(e_4^{(1)}\) \(e_5^{(1)}\) \(e_6^{(1)}\) \(e_7^{(1)}\) \(e_8^{(1)}\)
Graph \((2, 1)\) \((2, 2)\) \((2, 3)\) \((2, 4)\) \((2, 5)\) \((2, 6)\) \((2, 7)\) \((2, 8)\)
Notation \(e_1^{(2)}\) \(e_2^{(2)}\) \(e_3^{(2)}\) \(e_4^{(2)}\) \(e_5^{(2)}\) \(e_6^{(2)}\) \(e_7^{(2)}\) \(e_8^{(2)}\)

…

Graph \((4, 1)\) \((4, 2)\) \((4, 3)\) \((4, 4)\) \((4, 5)\) \((4, 6)\) \((4, 7)\) \((4, 8)\)
Notation \(e_1^{(4)}\) \(e_2^{(4)}\) \(e_3^{(4)}\) \(e_4^{(4)}\) \(e_5^{(4)}\) \(e_6^{(4)}\) \(e_7^{(4)}\) \(e_8^{(4)}\)
Initial Data Layer
~~~~ ~~~~ ~~~~ ~~~~ ~~~~ ~~~~ Challenge ~~~~
\((0, 1)\) \((0, 2)\) \((0, 3)\) \((0, 4)\) \((0, 5)\) \((0, 6)\) \((0, 7)\) \((0, 8)\)
  • Vector Commitment

    Generate Merkle root for data leaves.

    \(Comm_D = VC(D_1 || D_2 || … || D_N)\), where \(D_i = e_i^{(0)}\).

    This example: \(Comm_D = VC(e_1^{(0)}, e_2^{(0)}, e_3^{(0)}, e_4^{(0)}, e_5^{(0)}, e_6^{(0)}, e_7^{(0)}, e_8^{(0)})\).

  • Opening

    To open \(D_i\), provide a merkle proof \(D_i \rightarrow Comm_D\).

SDR Replica Columns
  • Columns

    ~~~~ DRG Parents ~~~~ Expander Parents ~~~~ ~~~~ Challenges ~~~~
    \((1, 1)\) \((1, 2)\) \((1, 3)\) \((1, 4)^{*}\) \((1, 5)\) \((1, 6)\) \((1, 7)\) \((1, 8)\)
    \((2, 1)\) \((2, 2)\) \((2, 3)\) \((2, 4)\) \((2, 5)\) \((2, 6)\) \((2, 7)\) \((2, 8)\)
    \((3, 1)\) \((3, 2)\) \((3, 3)\) \((3, 4)\) \((3, 5)\) \((3, 6)\) \((3, 7)\) \((3, 8)\)
    \((4, 1)\) \((4, 2)\) \((4, 3)\) \((4, 4)\) \((4, 5)\) \((4, 6)\) \((4, 7)\) \((4, 8)\)

    \(^{*}\) Indicates labels which must be hashed for column commitments but need not be opened for label checks.

    Concatenate and hash rows of column \(i\) to construct \(O_i\).

    Column hash \(C_i = H(e_i^{(1)} || e_i^{(2)} || … || e_i^{(L)})\).

  • Vector Commitment

    Generate Merkle tree for column leaves, \(C_i\):

    \(Comm_C = VC(C_1 || C_2 || … || C_N)\).

  • Opening

    • To open labels for column \(i\):

      • Reveal all labels and prove they hash to \(C_i\) as above. (\(L\) hash proofs).
      • Provide a merkle proof \(C_i \rightarrow Comm_C\).
    • Then once, reusable for all columns,

      • Reveal \(Comm_{R_{LAST}}\) and prove that \(H(Comm_C || Comm_{R_{LAST}}) = Comm_R\).
Final Replica Layer
~~~~ ~~~~ ~~~~ ~~~~ ~~~~ ~~~~ Challenge ~~~~
\((5, 1)\) \((5, 2)\) \((5, 3)\) \((5, 4)\) \((5, 5)\) \((5, 6)\) \((5, 7)\) \((5, 8)\)
  • Vector Commitment

    Generate Merkle tree for replica leaves.

    \(R_{LAST_i} = e_i^{(L+1)}\).

    \(Comm_{R_{LAST}} = VC(R_{LAST_1} || R_{LAST_2} || … || R_{LAST_N})\).

  • Opening

    • To open \(R_{LAST_i}\),

      • Provide a merkle proof \(R_{LAST_i} \rightarrow Comm_{R_{LAST}}\).
    • Then once (shared with Replica Columns β€” see above):

      • Reveal \(Comm_C\) and prove that \(H(Comm_C || Comm_{R_{LAST}}) = Comm_pR\).
Replica Commitment
  • Commitment

    • Produce \(Comm_R\) from its constituents.

    • \(Comm_R = H(Comm_C || Comm_{R_{LAST}})\).

  • Opening (performed once per PoRep)

    • Reveal \(Comm_C\) and \(Comm_{R_{LAST}}\) and prove that \(H(Comm_C || Comm_{R_{LAST}}) = Comm_R\).
Challenge Selection

For each challenge \(\chi\), we challenge each node \(e_{\chi}^{(l)}\) for \(l = 1, 2, .. L\).

Opening Commitments for Offline Proof

For use in all challenge proofs, reveal \(Comm_C\) and \(Comm_{R_{LAST}}\) and prove that \(H(Comm_C || Comm_{R_{LAST}}) = Comm_R\).

To prove encoding for a challenged label \(\chi\):

  • Initial data layer openings
    • Open label for challenged data node \(e_\chi^{(0)} β€” using Comm_D\).
  • SDR replica column openings
    • Open all labels in \(C_\chi\) containing challenged label’s ‘replica node’, (\(C_\chi\)) β€” using \(Comm_C\).
    • Open all labels in the columns containing challenged label’s DRG parents β€” using \(Comm_C\).
    • Open all labels in the columns containing challenged label’s expander parents β€” using \(Comm_C\).
  • Final replica layer openings

    • Open all challenged labels (\(e_{\chi}^{(L+1)}\)) using \(Comm_{R_{LAST}}\).
  • Prove labeling for all challenged labels $e{_χ}(l)) for \(l = 1, 2, .. L\).

  • Prove encoding for all challenged nodes \(e{_\chi}^{(L+1))}\).

Opening Commitments for Online Proof

To prove encoding for a challenged label \(C\) in the replica:

  • Reveal \(Comm_C\) (which must have been stored along with the replica).
  • Open \(Comm_{R_{LAST}}\) from provided \(Comm_R\) by proving that \(H(Comm_C || Comm_{R_{LAST}}) = Comm_R\).
  • Provide a merkle proof \(e_C^{(L)} \rightarrow Comm_{R_{LAST}}\).

Stacked DRG: Offline PoRep Circuit Spec
Stacked DRG Overview

Stacked DRG PoRep is based on layering DRG graphs LAYERS times. The data represented in each DRG layer is a labeling based on previously labeled nodes. The final labeled layer is the SDR key, and the ‘final layer’ of replication the replica, an encoding of the original data using the generated key.

  • ReplicaId is a unique replica identifier (see the Filecoin Proofs spec for details).
  • CommD is the Merkle tree root hash of the input data to the first layer.
  • CommC is the Merkle tree root hash of the SDR column commitments.
  • CommRLast is the Merkle tree root hash of the replica.
  • CommR is the on-chain commitment to the replica, dervied as the hash of the concatenation of CommC and CommRLast.

The (offline) proof size in SDR is too large for blockchain usage (~3MB). We use SNARKs to generate a proof of knowledge of a correct SDR proof. In other words, we implement the SDR proof verification algorithm in an arithmetic circuit and use SNARKs to prove that it was evaluated correctly.

This circuit proves that given a Merkle root CommD, CommRLast, and commRStar, that the prover knew the correct replicated data at each layer.

Spec notation
  • Fr: Field element of BLS12-381
  • UInt: Unsigned integer
  • {0..x}: From 0 (included) to x (not included) (e.g. [0,x) )
  • Check:
    • If there is an equality, create a constraint
    • otherwise, execute the function
  • Inclusion path: Binary representation of the Merkle tree path that must be proven packed into a single Fr element.

Offline PoRep circuit

Public Parameters

Parameters that are embeded in the circuits or used to generate the circuit

  • LAYERS : UInt: Number of DRG layers.
  • LAYER_CHALLENGES : [LAYERS]UInt: Number of challenges per layer.
  • EXPANSION_DEGREE: UInt: Degree of each bipartite expander graph to extend dependencies between layers.
  • BASE_DEGREE: UInt: Degree of each Depth Robust Graph.
  • TREE_DEPTH: UInt: Depth of the Merkle tree. Note, this is (log_2(Size of original data in bytes/32 bytes per leaf)).
  • PARENT_COUNT : UInt: Defined as EXPANSION_DEGREE+BASE_DEGREE.
Public Inputs

Inputs that the prover uses to generate a SNARK proof and that the verifier uses to verify it

  • ReplicaId : Fr: A unique identifier for the replica.
  • CommD : Fr: the Merkle tree root hash of the original data (input to the first layer).
  • CommR : Fr: The Merkle tree root hash of the final replica (output of the last layer).
  • InclusionPath : [LAYERS][]Fr: Inclusion path for the challenged data and replica leaf.
  • ParentInclusionPath : [LAYERS][][PARENT_COUNT]Fr: Inclusion path for the parents of the corresponding InclusionPath[l][c].

Design notes:

  • CommRLast is a private input used during during Proof-of-Spacetime. To enable this, the prover must store CommC and use it to prove that CommRLast is included in CommR [TODO: define ‘included’ language.]
  • InclusionPath and ParentInclusionPath: Each layer l has LAYER_CHALLENGES[l] inclusion paths.
Private Inputs

Inputs that the prover uses to generate a SNARK proof, these are not needed by the verifier to verify the proof

  • CommR : [LAYERS-1]Fr: Commitment of the the encoded data at each layer.

Note: Size is LAYERS-1 since the commitment to the last layer is CommRLast

  • DataProof : [LAYERS][][TREE_DEPTH]Fr: Merkle tree inclusion proof for the current layer unencoded challenged leaf.

  • ReplicaProof : [LAYERS][][TREE_DEPTH]Fr: Merkle tree inclusion proof for the current layer encoded challenged leaves.

  • ParentProof : [LAYERS][][PARENT_COUNT][TREE_DEPTH]Fr: Pedersen hashes of the Merkle inclusion proofs of the parent leaves for each challenged leaf at layer l.

  • DataValue : [LAYERS][]Fr: Value of the unencoded challenged leaves at layer l.

  • ReplicaValue : [LAYERS][]Fr: Value of the encoded leaves for each challenged leaf at layer l.

  • ParentValue : [LAYERS][][PARENT_COUNT]Fr: Value of the parent leaves for each challenged leaf at layer l.

Circuit

In high level, we do 4 checks:

  1. ReplicaId Check: Check the binary representation of the ReplicaId
  2. Inclusion Proofs Checks: Check the inclusion proofs
  3. Encoding Checks: Check that the data has been correctly encoding into a replica
  4. CommRStar Check: Check that CommRStar has been generated correctly

Detailed

// 1: ReplicaId Check - Check ReplicaId is equal to its bit representation
let ReplicaIdBits : [255]Fr = Fr_to_bits(ReplicaId)
assert(Packed(replica_id_bits) == ReplicaId)

let DataRoot, ReplicaRoot Fr

for l in range LAYERS {

  if l == 0 {
    DataRoot = CommD
  } else {
    DataRoot = CommR[l-1]
  }

  if l == LAYERS-1 {
    ReplicaRoot = CommRLast
  } else {
    ReplicaRoot = CommR[l]
  }

  for c in range LAYERS_CHALLENGES[l] {
    // 2: Inclusion Proofs Checks
    // 2.1: Check inclusion proofs for data leaves are correct
    assert(MerkleTreeVerify(DataRoot, InclusionPath[l][c], DataProof[l][c], DataValue[l][c]))
    // 2.2: Check inclusion proofs for replica leaves are correct
    assert(MerkleTreeVerify(ReplicaRoot, InclusionPath[l][c], ReplicaProof[l][c], ReplicaValue[l][c]))
    // 2.3: Check inclusion proofs for parent leaves are correct
    for p in range PARENT_COUNT {
      assert(MerkleTreeVerify(ReplicaRoot, ParentInclusionPath[l][c][p], ParentProof[l][c][p]))
    }

    // 3: Encoding checks - Check that replica leaves have been correctly encoded
    let ParentBits [PARENT_COUNT][255]Fr
    for p in range PARENT_COUNT {
      // 3.1: Check that each ParentValue is equal to its bit representation
      let parent = ParentValue[l][c][p]
      ParentBits[p] = Fr_to_bits(parent)
      assert(Packed(ParentBits[p]) == parent)
    }

    // 3.2: KDF check - Check that each key has generated correctly
    // PreImage = ReplicaIdBits || ParentBits[1] .. ParentBits[PARENT_NODES]
    let PreImage = ReplicaIdBits
    for parentbits in ParentBits {
      PreImage.Append(parentbits)
    }
    let key Fr = Blake2s(PreImage)
    assert(Blake2s(PreImage) == key)

    // 3.3: Check that the data has been encoded to a replica with the right key
    assert(ReplicaValue[l][c] == DataValue[l][c] + key)
  }

  // 4: CommRStar check - Check that the CommRStar constructed correctly
  let hash = ReplicaId
  for l in range LAYERS-1 {
    hash.Append(CommR[l])
  }
  hash.Append(CommRLast)

  assert(CommRStar == PedersenHash(hash))
  // TODO check if we need to do packing/unpacking
}
Verification of offline porep proof
  • SNARK proof check: Check that given the SNARK proof and the public inputs, the SNARK verification outputs true
  • Parent checks: For each leaf = InclusionPath[l][c]:
    • Check that all ParentsInclusionPaths_[l][c][0..PARENT_COUNT} are the correct parent leaves of leaf in the DRG graph, if a leaf has less than PARENT_COUNT, repeat the leaf with the highest label in the graph.
    • Check that the parent leaves are in ascending numerical order.

Proof-of-Spacetime

Rational-PoSt

This document describes Rational-PoSt, the Proof-of-Spacetime used in Filecoin.

High Level API

Fault Detection

Fault detection happens over the course of the life time of a sector. When the sector is for some reason unavailable, the miner is responsible to submit the known faults, before the PoSt challenge begins. (Using the AddFaults message to the chain). Only faults which have been reported at challenge time, will be accounted for. If any other faults have occured the miner can not submit a valid PoSt for this proving period.

The PoSt generation then takes the latest available faults of the miner to generate a PoSt matching the committed sectors and faults.

When a PoSt is successfully submitted all faults are reset and assumed to be recovered. A miner must either (1) resolve a faulty sector and accept challenges against it in the next proof submission, (2) report a sector faulty again if it persists but is eventually recoverable, (3) report a sector faulty and done if the fault cannot be recovered.

If the miner knows that the sectors are permanently lost, they can submit them as part of the doneSet, to ensure they are removed from the proving set.

Note: It is important that all faults are known (i.e submitted to the chain) prior to challenge generation, because otherwise it would be possible to know the challenge set, before the actual challenge time. This would allow a miner to report only faults on challenged sectors, with a gurantee that other faulty sectors would not be detected.
TODO: The penalization for faults is not clear yet.
Fault Penalization

Each reported fault carries a penality with it.

TODO: Define the exact penality structure for this.
Generation

GeneratePoSt generates a Proof of Spacetime over all sealed sectors of a single minerβ€” identified by their commR commitments. This is accomplished by performing a series of merkle inclusion proofs (Proofs of Retrievability). Each proof is of a challenged node in a challenged sector. The challenges are generated pseudo-randomly, based on the provided seed. At each time step, a number of Proofs of Retrievability are performed.

// Generate a new PoSt.
func GeneratePoSt(sectorSize BytesAmount, sectors SectorSet, seed Seed, faults FaultSet) PoStProof {
    // Generate the Merkle Inclusion Proofs + Faults

    challenges := DerivePoStChallenges(seed, faults, sectorSize, SortAsc(GetSectorIds(sectors)))
    challengedSectors := []
    inclusionProofs := []

    for i := 0; i < len(challenges); i++ {
        challenge := challenges[i]

        // Leaf index of the selected sector
        inclusionProof, isFault := GenerateMerkleInclusionProof(challenge.Sector, challenge.Leaf)
        if isFault {
            // faulty sector, need to post a fault to the chain and try to recover from it
            return Fatal("Detected late fault")
        }

        inclusionProofs[n] = inclusionProof
        challengedSectors[i] = sectors[challenge.Sector]
    }

    // Generate the snark
    snarkProof := GeneratePoStSnark(sectorSize, challenges, challengedSectors, inclusionProofs)

    return snarkProof
}
Verification

VerifyPoSt is the functional counterpart to GeneratePoSt. It takes all of GeneratePoSt’s output, along with those of GeneratePost’s inputs required to identify the claimed proof. All inputs are required because verification requires sufficient context to determine not only that a proof is valid but also that the proof indeed corresponds to what it purports to prove.

// Verify a PoSt.
func VerifyPoSt(sectorSize BytesAmount, sectors SectorSet, seed Seed, proof PoStProof, faults FaultSet) bool {
    challenges := DerivePoStChallenges(seed, faults, sectorSize, SortAsc(GetSectorIds(sectors)))
    challengedSectors := []

    // Match up commitments with challenges
    for i := 0; i < len(challenges); i++ {
        challengedSectors[i] = sectors[challenges[i].Sector]
    }

    // Verify snark
    return VerifyPoStSnark(sectorSize, challenges, challengedSectors)
}
Types
// The random challenge seed, provided by the chain.
Seed [32]byte
type Challenge struct {
    Sector SectorID
    Leaf Uint
}
Challenge Derivation
// Derive the full set of challenges for PoSt.
func DerivePoStChallenges(seed Seed, faults FaultSet, sectorSize Uint, sortedSectors []SectorID) [POST_CHALLENGES_COUNT]Challenge {
    challenges := []

    for n := 0; n < POST_CHALLENGES_COUNT; n++ {
        attemptedSectors := {SectorID:bool}
        while challenges[n] == nil {
            challenge := DerivePoStChallenge(seed, n, attempt, sectorSize, sortedSectors)

            // check if we landed in a faulty sector
            if !faults.Contains(challenge.Sector) {
                // Valid challenge
                challenges[n] = challenge
            }

            // invalid challenge, regenerate
            attemptedSectors[challenge.Sector] = true

            if len(attemptedSectors) >= len(sortedSectors) {
                Fatal("All sectors are faulty")
            }
        }
    }

    return challenges
}

// Derive a single challenge for PoSt.
func DerivePoStChallenge(seed Seed, n Uint, attempt Uint, sectorSize Uint, sortedSectors []SectorID) Challenge {
    nBytes := WriteUintToLittleEndian(n)
    data := concat(seed, nBytes, WriteUintToLittleEndian(attempt))
    challengeBytes := blake2b(data)

    sectorChallenge := ReadUintLittleEndian(challengeBytes[0..8])
    leafChallenge := ReadUintLittleEndian(challengeBytes[8..16])

    sectorIdx := sectorChallenge % sectorCount

    return Challenge {
        Sector: sortedSectors[sectorIdx],
        Leaf: leafChallenge % (sectorSize / NODE_SIZE),
    }
}

PoSt Circuit

Public Parameters

Parameters that are embeded in the circuits or used to generate the circuit

  • POST_CHALLENGES_COUNT: UInt: Number of challenges.
  • POST_TREE_DEPTH: UInt: Depth of the Merkle tree. Note, this is (log_2(Size of original data in bytes/32 bytes per leaf)).
  • SECTOR_SIZE: UInt: The size of a single sector in bytes.
Public Inputs

Inputs that the prover uses to generate a SNARK proof and that the verifier uses to verify it

  • CommRs: [POST_CHALLENGES_COUNT]Fr: The Merkle tree root hashes of all replicas, ordered to match the inclusion paths and challenge order.
  • InclusionPaths: [POST_CHALLENGES_COUNT]Fr: Inclusion paths for the replica leafs, ordered to match the CommRs and challenge order. (Binary packed bools)
Private Inputs

Inputs that the prover uses to generate a SNARK proof, these are not needed by the verifier to verify the proof

  • InclusionProofs: [POST_CHALLENGES_COUNT][TREE_DEPTH]Fr: Merkle tree inclusion proofs, ordered to match the challenge order.
  • InclusionValues: [POST_CHALLENGES_COUNT]Fr: Value of the encoded leaves for each challenge, ordered to match challenge order.
Circuit
High Level

In high level, we do 1 check:

  1. Inclusion Proofs Checks: Check the inclusion proofs
Details
for c in range POST_CHALLENGES_COUNT {
  // Inclusion Proofs Checks
  assert(MerkleTreeVerify(CommRs[c], InclusionPath[c], InclusionProof[c], InclusionValue[c]))
}
Verification of PoSt proof
  • SNARK proof check: Check that given the SNARK proof and the public inputs, the SNARK verification outputs true

PoSt Parameters

This section describes parameters for Rational-PoSt, the Proof-of-Spacetime used in Filecoin.

Parameter Type Value Description
POST-CHALLENGE-BLOCKS BLOCKS 480 The time offset before which the actual work of generating the PoSt cannot be started. This is some delta before the end of the Proving Period, and as such less than a single Proving Period.
POST-CHALLENGE-HOURS HOURS 2 PoSt challenge time (see POST_CHALLENGE_BLOCKS).
POST-PROVING-PERIOD BLOCKS 5760 The time interval in which a PoSt has to be submitted
TODO: The above values are tentative and need both backing from research as well as detailed reasoning why we picked them.

Payment Channels

Payment Channels

In order for the Filecoin Markets to work in a timely manner, we need to be able to have off-chain payments. This is a solved problem (at least, for our purposes in v0). Payment channels have been implemented and used in bitcoin, ethereum and many other networks.

The basic premise is this: User A wants to be able to send many small payments to user B. So user A locks up money in a contract that says “this money will only go to user B, and the unclaimed amount will be returned to user A after a set time period”. Once that money is locked up, user A can send user B signed transactions that user B can cash out at any time.

For example:

  • User A locks up 10 FIL to B
  • User B does something for A
  • User A sends SignedVoucher{Channel, 1 FIL} to B
  • User B does something for user A
  • User A sends SignedVoucher{Channel, 2 FIL} to B

At this point, B has two signed messages from A, but the contract is set up such that it can only be cashed out once. So if B decided to cash out, they would obviously select the message with the higher value. Also, once B cashes out, they must not accept any more payments from A on that same channel.

Multi-Lane Payment Channel

The filecoin storage market may require a way to do incremental payments between two parties, over time, for multiple different transactions. The primary motivating usecase for this is to provide payment for file storage over time, for each file stored. An additional requirement is the ability to have less than one message on chain per transaction ‘lane’, meaning that payments for multiple files should be aggregateable (Note: its okay if this aggregation is an interactive process).

Let’s say that A wants to make such an arrangement with B. A should create the payment channel with enough funds to cover all potential transactions. Then A decides to start the first transaction, so they send a signed voucher for the payment channel on ‘lane 1’, for 2 FIL. They can then send more updates on lane 1 as needed. Then, at some point A decides to start another independent transaction to B, so they send a voucher on ‘lane 2’. The voucher for lane 2 can be cashed out independently of lane 1. However, B can ask A to ‘reconcile’ the two payment channels for them into a single update. This update could contain a value, and a list of lanes to close. Cashing out that reconciled update would invalidate the other lanes, meaning B couldnt also cash in those. The single update would be much smaller, and therefore cheaper to close out.

Lane state can be easily tracked on-chain with a compact bitfield.

Payment Channel Reconciliation

In a situation where peers A and B have several different payment channels between them, the scenario may frequently come up where A has multiple payment channel updates from B to apply. Submitting each of these individually would cost a noticeable amount in fees, and put excess unnecessary load on the chain. To remedy this, A can contact B and ask them for a single payment channel update for the combined value of all the updates they have (minus some fee to incent B to actually want to do this). This aggregated update would contain a list of the IDs of the other payment channels that it is superceding so that A cannot also cash out on the originals.

Payment Reconciliation

The filecoin storage market will (likely) have many independent payments between the same parties. These payments will be secured through payment channels, set up initially on chain, but utilized almost entirely off-chain. The point at which they need to touch the chain is when miners wish to cash out their earnings. A naive solution to this problem would have miners perform one on-chain action per file stored for a particular client. This would not scale well. Instead, we need a system where the miner and client can have some additional off-chain communication and end up with the miner submitting only a single message to the chain.

To accomplish this, we introduce the Payment Reconciliation Protocol.

This is a libp2p service run by all participants wanting to participate in payment reconciliation. When Alice has a set of payments from Bob that she is ready to cash out, Alice can send a ReconcileRequest to Bob, containing the following information:

type ReconcileRequest struct {
	vouchers [Vouchers]
	reqVal TokenAmount
}

The Vouchers should all be valid vouchers from Bob to Alice, on the same payment channel, and they should all be ready to be cashed in. ReqVal is a token amount less than or equal to the sum of all the values in the given vouchers. Generally, this value will be between the total sum of the vouchers, and that total sum minus the fees it would cost to submit them all to the chain.

Bob receives this request, and checks that all the fields are correct, and then ensures that the difference between ReqVal and the vouchers sum is sufficient (this is a parameter that the client can set). Then, he sends back a response which either contains the requested voucher, or an error status and message.

type ReconcileResponse struct {
	combined Voucher
	status  Status
	message optional String
}

## TODO: what are the possible status cases?
type Status enum {
    | Success
    | Failure
}

Open Questions:

  • In a number of usecases, this protocol will require the miner look up and connect to a client to propose reconciliation. How does a miner look up and connect to a client over libp2p given only their filecoin address?
  • Without repair miners, this protocol will likely not be used that much. Should that be made clear? Should there be other considerations added to compensate?

BlockSync

  • Name: Block Sync
  • Protocol ID: /fil/sync/blk/0.0.1

The blocksync protocol is a small protocol that allows Filecoin nodes to request ranges of blocks from each other. It is a simple request/response protocol.

The request requests a chain of a given length by the hash of its highest block. The Options allow the requester to specify whether or not blocks and messages to be included.

The response contains the requested chain in reverse iteration order. Each item in the Chain array contains the blocks for that tipset if the Blocks option bit in the request was set, and if the Messages bit was set, the messages across all blocks in that tipset. The MsgIncludes array contains one array of integers for each block in the Blocks array. Each of the arrays in MsgIncludes contains a list of indexes of messages from the Messages array that are in each Block in the blocks array.

type BlockSyncRequest struct {
    ## The TipSet being synced from
	start [&Block]
    ## How many tipsets to sync
	requestLength UInt
    ## Query options
    options Options
}
type Options enum {
    # Include only blocks
    | Blocks 0
    # Include only messages
    | Messages 1
    # Include messages and blocks
    | BlocksAndMessages 2
}

type BlockSyncResponse struct {
	chain [TipSetBundle]
	status Status
}

type TipSetBundle struct {
  blocks [Blocks]
  secpMsgs [SignedMessage]
  secpMsgIncludes [[UInt]]

  blsMsgs [Message]
  blsMsgIncludes [[Uint]]
}

type Status enum {
    ## All is well.
    | Success 0
    ## Sent back fewer blocks than requested.
    | PartialResponse 101
    ## Request.Start not found.
    | BlockNotFound 201
    ## Requester is making too many requests.
    | GoAway 202
    ## Internal error occured.
    | InternalError 203
    ## Request was bad
    | BadRequest 204
}

Example

The TipSetBundle

Blocks: [b0, b1]
secpMsgs: [mA, mB, mC, mD]
secpMsgIncludes: [[0, 1, 3], [1, 2, 0]]

corresponds to:

Block 'b0': [mA, mB, mD]
Block 'b1': [mB, mC, mA]

GossipSub

Messages and block headers along side the message references are propagated using the gossipsub libp2p pubsub router. Every full node must implement and run that protocol. All pubsub messages are authenticated and must be syntactically validated before being propagated further.

Further more, every full node must implement and offer the bitswap protocol and provide all Cid Referenced objects, it knows of, through it. This allows any node to fetch missing pieces (e.g. Message) from any node it is connected to. However, the node should fan out these requests to multiple nodes and not bombard any single node with too many requests at a time. A node may implement throttling and DDoS protection to prevent such a bombardment.

Bitswap

Run bitswap to fetch and serve data (such as blockdata and messages) to and from other filecoin nodes. This is used to fill in missing bits during block propagation, and also to fetch data during sync.

There is not yet an official spec for bitswap, but the protobufs should help in the interim.

Cryptographic Primitives

  • Merkle tree/DAG
  • Vector commitment scheme
  • zkSNARK
  • Verifiable random function (VRF)
  • Verifiable delay function (VDF)
  • Reliable broadcast channel (libp2p)

  • TODO: Add more detail and include references to relevant papers.

Signatures

Signatures are cryptographic functions that attest to the origin of a particular message. In the context of Filecoin, signatures are used to send and receive messages among with the assurance that each message was generated by specific individuals. In other words, it is infeasible for another individual i to generate a signed message that appears to have been generated by j.

We use signatures in filecoin to verify something was done by someone. For example, we use signatures in order to validate deal messages which represent an action like a storage deals. We also use signatures to determine who generated a particular message, determine public keys–which can be recovered from signed data and a signature, and find filecoin addresses which generated from a public key.

  • Messages (From users to the blockchain)
  • Tickets (Signature of proof - Mining)
  • Block signature (Signature over all data in the block - done by block leader)

Interface

Filecoin requires a system that fulfils the following interface to function correctly.

Note: Message is used here as the object being signed, but this interface should also work for other things that need to be signed.

type Signature interface {

	// Sign generates a proof that miner `M` generate message `m`
	//
	// Out:
	//    sig - a series of bytes representing a signature usually `r`|`s`
	//    err - a standard error message indicating any process issues
	// In:
	//    m - a series of bytes representing a message to be signed
	//    sk - a private key which cryptographically links `M` to `sig`
	//
	Sign(m Message, sk PrivateKey) (sig SignatureBytes, err error)

	// Verify validates the statement: only `M` could have generated `sig`
	// given the validator has a message `m`, a signature `sig`, and a
	// public key `pk`.
	//
	// Out:
	//    valid - a boolean value indicating the signature is valid
	//    err - a standard error message indicating any process issues
	// In:
	//    m - a series of bytes representing the signed message
	//    pk - the public key belonging to the signer `M`
	//    sig - a series of bytes representing a signature usually `r`|`s`
	//
	Verify(m Messgage, pk PublicKey, sig SignatureBytes) (valid bool, err error)

	// Recover, as its name implies, recovers a public key associated with a
	// particular signature. In the case of ECDSA signatures, this function can
	// be fulfilled via the 'ECRecover' method. If a different signature scheme
	// is used, then some other mechanism of 'recovering' a message authors
	// public key must be provided.
	//
	// Out:
	//    pk - the public key associated with `M` who signed `m`
	//    err - a standard error message indicating any process issues
	//    **
	// In:
	//    m - a series of bytes representing the signed message
	//    sig - a series of bytes representing a signature usually `r`|`s`
	//
	Recover(m Message, sig SignatureBytes) (pk PublicKey, err error)
}

Secp256k1 Signatures

Currently, Filecoin uses secp256k1 signatures to fulfill the above interface. All signatures on messages, blocks, and tickets currently use the same scheme and format.

Signing Messages

To generate a signature for the Message type, first serialize it, then hash the serialized bytes with blake2b-256. Then, take the 32 byte digest output the hash and compute the secp256k1 signature over it.

Wire Format

We use standard secp256k1 signature serialization, as described below. For more details on how the Filecoin Signature type is serialized, see the relevant section in the data structures spec

Signature

SignatureBytes = [0x30][len][0x02][r][indicator][s][indicator][recovery]

s = Scalar of size 32 bytes

r = Compressed elliptic curve point (x-coordinate) of size 32 bytes

recovery = Information needed to recover a public key from sig.

  • LSB(0) = parity of y-coordinate of r
  • LSB(1) = overflow indicator

indicator = a 2 byte formatting indicator

External References

Listings

Filecoin VM Actors

TODO

Data Structures

Address

An address is an identifier that refers to an actor in the Filecoin state. All actors (miner actors, the storage market actor, account actors) have an address. An address encodes information about:

  • Network this address belongs to
  • Type of data the address contains
  • The data itself
  • Checksum (depending on the type of address)

For more detail, see the full address spec.

Block

A block header contains information relevant to a particular point in time over which the network may achieve consensus. The block header contains:

  • The address of the miner that mined the block
  • A ticket associated to this block’s creation to be used as randomness elsewhere in the protocol (see Secret Leader Election for more details)
  • An electionProof showing this miner was eligible to mine, as well as a Nonce relating the number of rounds over which the block was mined (on expectation 1)
  • The set of parent blocks and aggregate chain weight of the parents
  • This block’s height
  • Merkle root of the state tree (after applying the messages – state transitions – included in this block)
  • Merkle root of the messages (state transitions) in this block
  • Merkle root of the message receipts in this block
  • Timestamp
Note: A block is functionally the same as a block header in the Filecoin protocol. While a block header contains Merkle links to the full system state, messages, and message receipts, a block can be thought of as the full set of this information (not just the Merkle roots, but rather the full data of the state tree, message tree, receipts tree, etc.). Because a full block is quite large, our chain consists of block headers rather than full blocks. We often use the terms block and block header interchangeably.
type Block struct {
    // Miner is the address of the miner actor that mined this block.
    miner Address

    // Tickets is a chain (possibly singleton) of tickets ending with a winning ticket
    // submitted with this block.
    tickets [Ticket]

    // ElectionProof is generated from a past ticket and proves this miner is a leader
    // in this block's round.
    electionProof ElectionProof

    // Parents is an array of distinct CIDs of parents on which this block was based.
    // Typically one, but can be several in the case where there were multiple winning
    // ticket-holders for a given round. The order of parent CIDs is not defined.
    parents [&Block]

    // ParentWeight is the aggregate chain weight of the parent set.
    parentWeight UInt

    // Height is the chain height of this block.
    height UInt

    // StateRoot is a CID pointer to the VM state tree after application of the state
    // transitions corresponding to this block's messages.
    stateRoot &StateTree

    // Messages is the set of messages included in this block. This field is the CID
    // of the TxMeta object that contains the bls and secpk signed message trees.
    messages &TxMeta

    // BLSAggregate is an aggregated BLS signature for all the messages in this block
    // that were signed using BLS signatures.
    blsAggregate Signature

    // MessageReceipts is a set of receipts matching to the sending of the `Messages`.
    // This field is the CID of the root of a sharray of MessageReceipts.
    messageReceipts &[MessageReceipt]

    // The block Timestamp is used to enforce a form of block delay by honest miners.
    // Unix time UTC timestamp (in seconds) stored as an unsigned integer.
    timestamp Timestamp

    // BlockSig is a signature over the hash of the entire block with the miners
    // worker key to ensure that it is not tampered with after creation
    blockSig Signature
}

type TxMeta struct {
    blsMessages &[&Message]<Sharray>
    secpkMessages &[&SignedMessage]<Sharray>
}

Tipset

For more on Tipsets, see the Expected Consensus spec. Implementations may choose not to create a Tipset data structure, instead representing its operations in terms of the underlying blocks.

type TipSet [&Block]

VRF Personalization

We define VRF personalizations as follow, to enable domain separation across operations that make use of the same VRF (e.g. Ticket and ElectionProof).

Type Prefix
Ticket 0x01
ElectionProof 0x02

Ticket

A ticket contains a shared random value referenced by a particular Block in the Filecoin blockchain. Every miner must produce a new Ticket for each ticket used in a leader election attempt.

To produce the ticket values, we use an EC-VRF per Goldberg et al. with Secp256k1 and SHA-256 to obtain a deterministic, pseudorandom output.

type Ticket struct {
    // The VRFProof (pi_string in the RFC) is generated by running our VRF on a past ticket
    // in the ticket chain signed with the miner's keypair. This field is 97 bytes long
    // (may be compressible to 80).
    VRFProof Bytes

    // The VDFResult is derived from the VRFResult of the ticket. It is the value that
    // will be used to generate future tickets or ElectionProofs.
    VDFResult Bytes

    // The VDF proves a delay between tickets generated.
    VDFProof Bytes
} // representation tuple

Ticket Comparison

The ticket is represented concretely by the Ticket data structure. Whenever the Filecoin protocol refers to ticket values (notably in crafting PoSTs or running leader election), what is meant is that the bytes of the VRFResult field in the Ticket struct are used. Specifically, tickets are compared lexicographically, interpreting the bytes of the VRFResult.Output as an unsigned integer value (little-endian).

ElectionProof

An election proof is generated from a past ticket (chosen based on public network parameters) by a miner during the leader election process. Its output value determines whether the miner is elected as one of the leaders, and hence is eligible to produce a block for the current epoch. The inclusion of the ElectionProof in the block allows other network participants to verify that the block was mined by a valid leader. With every leader election attempt for a given ticket, (in cases where no blocks are found in a round) a miner increments that ElectionProof’s associated Nonce, marking increased block height.

type ElectionProof struct {
    // The VRFProof is generated by running our VRF on a past ticket in the ticket chain
    // signed with the miner's keypair. This field is 97 bytes long (may be compressible
    // to 80).
    VRFProof Bytes
}

Message

Message data structures in Filecoin describe operations that can be performed on the Filecoin VM state (e.g., FIL transactions between accounts). To facilitate the process of producing secure protocol implementations, we explicitly distinguish between signed and unsigned Message structures.

type Message union {
    | UnsignedMessage 0
    | SignedMessage 1
} // representation keyed
type UnsignedMessage struct {
    to Address
    from Address

    // When receiving a message from a user account the nonce in the message must match
    // the expected nonce in the "from" actor. This prevents replay attacks.
    nonce UInt
    value UInt

    gasPrice UInt
    gasLimit UInt

    method Uint
    params Bytes  // Serialized parameters to the method.
} // representation tuple
type SignedMessage struct {
    message   UnsignedMessage
    signature Signature
} // representation tuple
type MessageReceipt struct {
    exitCode UInt
    returnValue Bytes
    gasUsed UInt
} // representation tuple

State Tree

The state tree keeps track of the entire state of the VM - Virtual Machine at any given point. It is a map from Address structures to Actor structures, where each Actor may also contain some additional ActorState that is specific to a given actor type.

type StateTree map[Address]Actor

Actor

type Actor struct {
    // CID of the code object for this actor
    code CID

    // Reference to the root of this actor's state
    head &ActorState

    // Counter of the number of messages this actor has sent
    nonce UInt

    // Current Filecoin balance of this actor
    balance UInt
}

Signature

Cryptographic signatures in Filecoin are represented as byte arrays, and come with a tag that signifies what key type was used to create the signature.

type Signature union {
    | Secp256k1Signature 0
    | Bls12_381Signature 1
} // representation byteprefix

type Secp256k1Signature Bytes
type Bls12_381Signature Bytes

FaultSet

FaultSet data structures are used to denote which sectors failed at which block heights.

type FaultSet struct {
    index    UInt
    bitField BitField
}

In order to make the serialization more compact, the index field denotes a block height offset from the start of the corresponding miner’s proving period.

Basic Types

CID

For most objects referenced by Filecoin, a Content Identifier (CID for short) is used. This is effectively a hash value, prefixed with its hash function (multihash) as well as extra labels to inform applications about how to deserialize the given data. For a more detailed specification, we refer the reader to the IPLD repository.

Timestamp

type Timestamp UInt

PublicKey

The public key type is simply an array of bytes.

type PublicKey Bytes

BytesAmount

BytesAmount is just a re-typed Integer.

type BytesAmount UInt

PeerId

The serialized bytes of a libp2p peer ID.

TODO: Spec incomplete; take a look at this PR.

type PeerId Bytes

Bitfield

Bitfields are a set encoded using a custom run length encoding: RLE+.

type Bitfield Bytes

SectorSet

A sector set stores a mapping of sector IDs to the respective commRs.

type SectorSet map[SectorID]Bytes

TODO: Improve on this; see https://github.com/filecoin-project/specs/issues/116.

SealProof

SealProof is an opaque, dynamically-sized array of bytes.

type SealProof Bytes

PoSTProof

PoSTProof is an opaque, dynamically-sized array of bytes.

type PoSTProof Bytes

TokenAmount

A type to represent an amount of Filecoin tokens.

type TokenAmount UInt

SectorID

Uniquely identifies a miner’s sector.

type SectorID uint64

RLE+ Bitset Encoding

RLE+ is a lossless compression format based on RLE. Its primary goal is to reduce the size in the case of many individual bits, where RLE breaks down quickly, while keeping the same level of compression for large sets of contiugous bits.

In tests it has shown to be more compact than RLE itself, as well as Concise and Roaring.

Format

The format consists of a header, followed by a series of blocks, of which there are three different types.

The format can be expressed as the following BNF grammar.

    <encoding> ::= <header> <blocks>
      <header> ::= <version> <bit>
     <version> ::= "00"
      <blocks> ::= <block> <blocks> | ""
       <block> ::= <block_single> | <block_short> | <block_long>
<block_single> ::= "1"
 <block_short> ::= "01" <bit> <bit> <bit> <bit>
  <block_long> ::= "00" <unsigned_varint>
         <bit> ::= "0" | "1"

An <unsigned_varint> is defined as specified here.

The header indicates the very first bit of the bit vector to encode. This means the first bit is always the same for the encoded and non-encoded form.

Blocks

The blocks represent how many bits, of the current bit type there are. As 0 and 1 alternate in a bit vector the inital bit, which is stored in the header, is enough to determine if a length is currently referencing a set of 0s, or 1s.

Block Single

If the running length of the current bit is only 1, it is encoded as a single set bit.

Block Short

If the running length is less than 16, it can be encoded into up to four bits, which a short block represents. The length is encoded into a 4 bits, and prefixed with 01, to indicate a short block.

Block Long

If the running length is 16 or larger, it is encoded into a varint, and then prefixed with 00 to indicate a long block.

Note: The encoding is unique, so no matter which algorithm for encoding is used, it should produce the same encoding, given the same input.

Bit Numbering

For Filecoin, byte arrays representing RLE+ bitstreams are encoded using LSB 0 bit numbering.

Other Considerations

  • The maximum size of an Object should be 1MB (2^20 bytes). Objects larger than this are invalid.
  • Hashes should use a blake2b-256 multihash.

Components

TODO

libp2p Protocols

TODO

Glossary

Updates to definitions

To make any updates to these definitions please submit a pull request with the changes, or open an issue and one of the maintainers will do it for you.

Notes

  • Want to split all repair stuff to separate doc
  • Let’s refer to Filecoin system rather than network. In a sense, the network is an instantiation of the system (this protocol). We can however refer to the Filecoin VM separately which means the system by which we apply changes to the state of the system at a point in time.
  • Asterisks indicate that the definition requires updating by any affected party.

Definitions

Actor

An actor is an on-chain object with its own state and set of methods. An actors state is persisted on-chain in the state tree, keyed by its address. All actors (miner actors, the storage market actor, account actors) have an address. Actors methods are invoked by crafting messages and getting miners to include them in blocks.

Actors are very similar to smart contracts in Ethereum.

Address

An address is an identifier that refers to an actor in the Filecoin state.

Ask

Bid

Block

A block in the Filecoin blockchain is a chunk of data appended to the shared history of the network including transactions, messages, etc. and representing the state of the storage network at a given point in time.

See [Data Structures]()

Bootstrapping

Chain weight

Challenge sampling

Cid

CID is short for Content Identifier, a self describing content address used throughout the ipfs ecosystem. For more detailed information, see the github documentation for it.

Client

A client is any user with an account who wishes to store data with a miner. A client’s account is used to pay for the storage, and helps to prove the clients ability to pay.

Collateral

Collateral is Filecoin tokens pledged by an actor as a commitment to a promise. If the promise is respected, the collateral is returned. If the promise is broken, the collateral is not returned in full. For instance:

  • In becoming a Filecoin storage miner: the miner will put up collateral alongside their SEAL to
  • In a Filecoin deal: both the miner and client put up collateral to ensure their respect of deal terms.

Commitment

See Filecoin Proofs

Confirmation

Consensus

Deal

*** *A deal in a Filecoin market is made when a bid and ask are matched, corresponding to an agreement on a service and price between a miner and client.

Election Proof

An ElectionProof is derived from a past ticket and is included in every block header. The ElectionProof proves that the miner was eligible to mine a block at that height.

Erasure coding

Erasure coding is a strategy through which messages can be lengthened so as to be made recoverable in spite of errors.

See Wikipedia

Fault

A fault occurs when a proof is not posted in the Filecoin system within the proving period, denoting another malfunction such as loss of network connectivity, storage malfunction, malicious miner, etc.

Fair

File

Files are what clients bring to the filecoin system to store. A file is split up into pieces, which are what is actually stored by the network.

Finality

Piece Inclusion Proof

See Filecoin Proofs

Gas, Fees, Prices

Generation Attack Threshold

Security parameter. Number of rounds within which a new Proof-of-Storage must be submitted in order for a miner to retain power in the network (and avoid getting slashed). This number must be be smaller than the minimum time it takes for an adversarial miner to generate a replica of the data (thereby not storing it undetectably for some period of time).

The Generation Attack Threshold is equal to the Polling Time + some Grace Period after which miners get slashed.

GHOST

GHOST is an acronym for Greedy Heaviest Observable SubTree, a class of blockchain structures in which multiple blocks can validly be included in the chain at any given height or round. GHOSTy protocols produce blockDAGs rather than blockchains and use a weighting function for fork selection, rather than simply picking the longest chain.

Height

Height and round are synonymous and used interchangeably in this spec.

Height refers to the number of tickets generated between this TipSet and the genesis block (height 0), counting only the tickets of the block in a TipSet whose final ticket – the one generated alongside the ElectionProof – is the smallest.

If a TipSet contains multiple blocks, each block in the TipSet will have the same height. Put another way, there is a new round of leader election attempts at each height. Typically, such an attempt will find a single leader. If a single leader is found, that leader can generate a single block. If multiple leaders are found, they can each generate multiple blocks in the given round. If no leader is found, no block is generated (but a ticket is).

Leader

A leader, in the context of Filecoin consensus, is a node that is chosen to propose the next block in the blockchain.

Leader election

Leader election is the process by which the Filecoin network agrees who gets to create the next block.

Message

A message is a call to an actor in the Filecoin VM.

Miner

A miner is an actor in the Filecoin system performing a service in the network for a reward.

There are multiple types of miners in Filecoin:

  • Storage miners - storage miners
  • Retrieval miners:
  • Repair miners (to be split out):

Node

*** *A node is a communication endpoint that implements the Filecoin protocol. (also mention IPLD Node?)

On-chain/off-chain

Online/offline

Payment Channel

A payment channel is set up between actors in the Filecoin system to enable off-chain payments with on-chain guarantees, making settlement more efficient.

Piece

A piece is a portion of a file that gets fitted into a sector.

Pledge

****The initial commitment of a storage miner to provide a number of sectors to the system.

Polling Time

Security Parameter. Polling time is the time between two online PoReps in a PoSt proof.

Power

See Power Fraction.

Power Fraction

A miner’s Power Fraction or Power is the ratio of their committed storage as of their last PoSt submission over Filecoin’s total committed storage as of the current block. It is used in leader election.

Power table

The power table is an abstraction provided by the Filecoin storage market that lists the power of every miner in the system.

Protocol

Proving Period

The period of time during which storage miners must compute Proofs of Spacetime. At the end of the period they must submit their PoSt. Put another way, it is the duration of a PoSt.

Proving Set

The elements used as input by a proof of Spacetime to enable a proof to be generated.

**** elements necessary to generate a SEAL, or elements necessary to generate a proof

Proof of Replication

Proof that a unique encoding of data exists in physical storage.

Used in the Filecoin system to generate SEALed sectors through which storage miners prove they hold client data.

Proof of Spacetime

Proof that a given encoding of data existed in physical storage continuously over a period of time.

Used in the Filecoin system by a storage miner to prove that client data was kept over the contract duration.

Random(ness)

****Source of unpredictability used in the Filecoin system to ensure fairness and prevent malicious actors from gaining an advantage over the system.

TODO add a note to distinguish predictability from randomness

Election Randomness Lookback

Security parameter. A number of rounds to sample back from when choosing randomness for use in leader election. A higher number turns a more localized lottery into a more global one since a miner wins or loses on all descendants of a given randomness, but enables miners to look-ahead and know whether they will be elected in the future.

Also referred to as K in consensus settings.

Repair

Repair refers to the processes and protocols by which the Filecoin network ensures that data that is partially lost (by, for example, a miner disappearing) can be re-constructed and re-added to the network.

Round

See Height for definition. They are synonymous.

SEAL/UNSEAL

See Filecoin Proofs

Sector

A sector is a contiguous array of bytes that a miner puts together, seals, and performs Proofs of Spacetime on.

Slashing

Smart contracts

Storage

Storage widely refers to a place in which to store data in a given system.

In the context of:

  • The Filecoin miner: sotrage refers to disk sectors made available to the network.
  • The Filecoin chain: storage refers to the way in which system state is tracked through time on-chain through blocks.
  • Actor: the struct that defines an actor.

State

****Refers to The shared history of the Filecoin system contains actors and their storage, deals, etc. State is deterministically generated from the initial state and the set of messages generated by the system.

Ticket

A ticket is used as a source of randomness in EC leader election. Every block depends on an ElectionProof derived from a ticket. At least one new ticket is produced with every new block. Ticket creation is described here.

Ticket Chain

Each chain in Filecoin can be associated to a given ticket chain. The ticket chain is assembled by taking the tickets (usually one) contained by the block with the smallest final ticket in each of the chain’s TipSets.

Ticket comparison is done by interpreting the tickets’ Bytes as unsigned integers (little endian representation).

TipSet

A TipSet is a set of blocks that have the same parent set and same number of tickets, which implies they will have been mined at the same height. A TipSet can contain multiple blocks if more than one miner successfully mines a block at the same height as another miner.

Verifiable

Something that is verifiable can be checked for correctness by a third party.

VDF

A verifiable function that guarantees a time delay given some hardware assumptions and a small set of requirements. These requirements are efficient proof verification, random output, and strong sequentiality. Verifiable delay functions are formally defined by [[BBBF]](https://eprint.iacr.org/2018/601).

{proof, value} <β€”- VDF(public parameters, seed)

VM

Virtual Machine. The Filecoin VM refers to the system by which changes are applied to the Filecoin system’s state. The VM takes messages as input, and outputs state.

Voucher

Held by an actor as part of a payment channel to complete settlement when the counterparty defaults.

VRF

A verifiable random function that receives {Secret Key (SK), seed} and outputs {proof of correctness, output value}. VRFs must yield a proof of correctness and a unique & efficiently verifiable output.

{proof, value} <-- VRF(SK, seed)

Weight

Every mined block has a computed weight. Together, the weights of all the blocks in a branch of the chain determines the cumulative weight of that branch. Filecoin’s Expected Consensus is a GHOSTy or heaviest-chain protocol, where chain selection is done on the basis of an explicit weighting function. Filecoin’s weight function currently seeks to incentivize collaboration amongst miners as well as the addition of storage to the network. The specific weighting function is defined in Chain Weighting.

zkSNARK

Zero Knowledge Succinct ARguments of Knowledge. A way of producing a small ‘proof’ that convinces a ‘verifier’ that some computation was done correctly.

Appendix

Sharded IPLD Array

The Sharray is an IPLD tree structure used to store an array of items. It is designed for usecases that know all items at the time of creation and do not need insertion or deletion.

IPLD Representation

Each sharray node is represented by an IPLD node of the following schema:

type Node struct {
  height Int
  items [Item]
} representation tuple

Item may be either a direct value, if height == 0, or the Cid of a child node if height > 0.

(For details on IPLD Schemas, see the IPLD Schema Spec (draft))

We use DAG-CBOR for serialization, and blake2b-256 for hashing.

Construction

The tree must not be sparse. Given an array of size N and a fixed width of W. - The left floor(N/W) leaves contain the first N items. - If N % W != 0 the final leaf contains the final remainder. - The tree is perfectly balanced. - The height is the distance from the leaves, not the root. - Leaves (nodes with a height of 0) contain array values. - Inner nodes (nodes with height greater than zero) contain the cids of their child nodes.

Operations

create(items)

Create a sharray from a given set of items

func create(items []Item) Cid {
	var layer cidQueue

	itemQ := queue(items)
	for !itemQ.Empty() {
		// get the next 'Width' items from the input items
		vals := itemQ.PopN(width)

		nd := Node{
			height: 0,
			items:  vals,
		}

		// persist the node to the datastore
		storeNode(nd)

		layer.push(nd.Cid())
	}

	var nextLayer cidQueue
	for height := 1; layer.Len() > 1; height++ {
		for layer.Len() > 0 {
			vals := layer.PopN(width)

			nd := Node{
				height: height,
				items:  vals,
			}

			storeNode(nd)

			nextLayer.append(nd.Cid())
		}
		layer = nextLayer
		nextLayer.ClearItems()
	}

	return nextLayer.First()
}

get(i)

Get the element at index i

func (n node) get(i int) Item {
	if n.Height == 0 {
		return n.Array[i]
	}

	childWidth := Pow(Width, n.Height)

	child := loadNode(n.Array[i/childWidth])
	return child.get(i % childWidth)
}

Address

A Filecoin address is an identifier that refers to an actor in the Filecoin state. All actors (miner actors, the storage market actor, account actors) have an address. This address encodes information about the network to which an actor belongs, the specific type of address encoding, the address payload itself, and a checksum. The goal of this format is to provide a robust address format that is both easy to use and resistant to errors.

Design criteria

  1. Identifiable: The address must be easily identifiable as a Filecoin address.
  2. Reliable: Addresses must provide a mechanism for error detection when they might be transmitted outside the network.
  3. Upgradable: Addresses must be versioned to permit the introduction of new address formats.
  4. Compact: Given the above constraints, addresses must be as short as possible.

Specification

There are 2 ways a filecoin address can be represented. An address appearing on chain will always be formatted as raw bytes. An address may also be encoded to a string, this encoding includes a checksum and network prefix. An address encoded as a string will never appear on chain, this format is used for sharing among humans.

Bytes

When represented as bytes a filecoin address contains the following:

  • A protocol indicator byte that identifies the type and version of this address.
  • The payload used to uniquely identify the actor according to the protocol.

    |----------|---------|
    | protocol | payload |
    |----------|---------|
    |  1 byte  | n bytes |
    

String

When encoded to a string a filecoin address contains the following:

  • A network prefix character that identifies the network the address belongs to.
  • A protocol indicator byte that identifies the type and version of this address.
  • A payload used to uniquely identify the actor according to the protocol.
  • A checksum used to validate the address.

    |------------|----------|---------|----------|
    |  network   | protocol | payload | checksum |
    |------------|----------|---------|----------|
    | 'f' or 't' |  1 byte  | n bytes | 4 bytes  |
    
type Address union {
    | AddressId 0
    | AddressSecp256k1 1
    | AddressActor 2
    | AddressBLS12_381 3
} // representation byteprefix

// ID
type AddressId UInt

// Blake2b-160 Hash
type AddressSecp256k1 Bytes

// Blake2b-160 Hash
type AddressActor Bytes

// 48 byte PublicKey
type AddressBLS12_381 Bytes

Network Prefix

The network prefix is prepended to an address when encoding to a string. The network prefix indicates which network an address belongs in. The network prefix may either be f for filecoin mainnet or t for filecoin testnet. It is worth noting that a network prefix will never appear on chain and is only used when encoding an address to a human readable format.

Protocol Indicator

The protocol indicator byte describes how a method should interpret the information in the payload field of an address. Any deviation for the algorithms and data types specified by the protocol must be assigned a new protocol number. In this way, protocols also act as versions.

  • 0 : ID
  • 1 : SECP256K1 Public Key
  • 2 : Actor
  • 3 : BLS Public Key

An example description in golang:

// Protocol byte
type Protocol = byte

const (
	ID Protocol = iota
	SECP256K1
	Actor
	BLS
)
Protocol 0: IDs

Protocol 0 addresses are simple IDs. All actors have a numeric ID even if they don’t have public keys. The payload of an ID address is base10 encoded. IDs are not hashed and do not have a checksum.

Bytes

|----------|---------------|
| protocol |    payload    |
|----------|---------------|
|    0     | leb128-varint |

String

|------------|----------|---------------|
|  network   | protocol |    payload    |
|------------|----------|---------------|
| 'f' or 't' |    '0'   | leb128-varint |
                  base10[...............]
Protocol 1: libsecpk1 Elliptic Curve Public Keys

Protocol 1 addresses represent secp256k1 public encryption keys. The payload field contains the Blake2b 160 hash of the public key.

Bytes

|----------|---------------------|
| protocol |        payload      |
|----------|---------------------|
|    1     | blake2b-160(PubKey) |

String

|------------|----------|---------------------|----------|
|  network   | protocol |      payload        | checksum |
|------------|----------|---------------------|----------|
| 'f' or 't' |    '1'   | blake2b-160(PubKey) |  4 bytes |
                  base32[................................]
Protocol 2: Actor

Protocol 2 addresses representing an Actor. The payload field contains the Blake2b 160 hash of meaningful data produced as a result of creating the actor.

Bytes

|----------|---------------------|
| protocol |        payload      |
|----------|---------------------|
|    2     | blake2b-160(Random) |

String

|------------|----------|-----------------------|----------|
|  network   | protocol |         payload       | checksum |
|------------|----------|-----------------------|----------|
| 'f' or 't' |    '2'   |  blake2b-160(Random)  |  4 bytes |
                  base32[..................................]
Protocol 3: BLS

Protocol 3 addresses represent BLS public encryption keys. The payload field contains the BLS public key.

Bytes

|----------|---------------------|
| protocol |        payload      |
|----------|---------------------|
|    3     | 48 byte BLS PubKey  |

String

|------------|----------|---------------------|----------|
|  network   | protocol |      payload        | checksum |
|------------|----------|---------------------|----------|
| 'f' or 't' |    '3'   |  48 byte BLS PubKey |  4 bytes |
                  base32[................................]

Payload

The payload represents the data specified by the protocol. All payloads except the payload of the ID protocol are base32 encoded using the lowercase alphabet when seralized to their human readable format.

Checksum

Filecoin checksums are calculated over the address protocol and payload using blake2b-4. Checksums are base32 encoded and only added to an address when encoding to a string. Addresses following the ID Protocol do not have a checksum.

Expected Methods

All implementations in Filecoin must have methods for creating, encoding, and decoding addresses in addition to checksum creation and validation. The follwing is a golang version of the Address Interface:

func New(protocol byte, payload []byte) Address

type Address interface {
	Encode(network Network, a Adress) string
	Decode(s string) Address
	Checksum(a Address) []byte
	ValidateChecksum(a Address) bool
}
New()

New returns an Address for the specified protocol encapsulating corresponding payload. New fails for unknown protocols.

func New(protocol byte, payload []byte) Address {
	if protocol < SECP256K1 || protocol > BLS {
		Fatal(ErrUnknownType)
	}
	return Address{
		Protocol: protocol,
		Payload:  payload,
	}
}
Encode()

Software encoding a Filecoin address must:

  • produce an address encoded to a known network
  • produce an address encoded to a known protocol
  • produce an address with a valid checksum (if applicable)

Encodes an Address as a string, prepending the network prefix, calculating the checksum, and encoding the payload and checksum to base32.

func Encode(network string, a Address) string {
	if network != "f" && network != "t" {
		Fatal("Invalid Network")
	}

	switch a.Protocol {
	case SECP256K1, Actor, BLS:
		cksm := Checksum(a)
		return network + string(a.Protocol) + base32.Encode(a.Payload+cksm)
	case ID:
		return network + string(a.Protocol) + base10.Encode(leb128.Decode(a.Payload))
	default:
		Fatal("invalid address protocol")
	}
}
Decode()

Software decoding a Filecoin address must: * verify the network is a known network. * verify the protocol is a number of a known protocol. * verify the checksum is valid

Decode an Address from a string by removing the network prefix, validating the address is of a know protocol, decoding the payload and checksum, and validating the checksum.

func Decode(a string) Address {
	if len(a) < 3 {
		Fatal(ErrInvalidLength)
	}

	if a[0] != "f" && a[0] != "t" {
		Fatal(ErrUnknownNetwork)
	}

	protocol := a[1]
	raw := a[2:]
	if protocol == ID {
		return Address{
			Protocol: protocol,
			Payload:  leb128.Encode(base10.Decode(raw)),
		}
	}

	raw = base32.Decode(raw)
	payload = raw[:len(raw)-CksmLen]
	if protocol == SECP256K1 || protocol == Actor {
		if len(payload) != 20 {
			Fatal(ErrInvalidBytes)
		}
	}

	cksm := payload[len(payload)-CksmLen:]
	if !ValidateChecksum(a, cksm) {
		Fatal(ErrInvalidChecksum)
	}

	return Address{
		Protocol: protocol,
		Payload:  payload,
	}
}
Checksum()

Checksum produces a byte array by taking the blake2b-4 hash of an address protocol and payload.

func Checksum(a Address) [4]byte {
	blake2b4(a.Protocol + a.Payload)
}
ValidateChecksum()

ValidateChecksum returns true if the Checksum of data matches the expected checksum.

func ValidateChecksum(data, expected []byte) bool {
	digest := Checksum(data)
	return digest == expected
}

Test Vectors

These are a set of test vectors that can be used to test an implementation of this address spec. Test vectors are presented as newline-delimited address/hex fields. The ‘address’ field, when parsed, should produce raw bytes that match the corresponding item in the ‘hex’ field. For example:

address1
hex1

address2
hex2

ID Type Addresses

f00
0000

f0150
009601

f01024
008008

f01729
00c10d

f018446744073709551615
00ffffffffffffffffff01

Secp256k1 Type Addresses

f17uoq6tp427uzv7fztkbsnn64iwotfrristwpryy
01fd1d0f4dfcd7e99afcb99a8326b7dc459d32c628

f1xcbgdhkgkwht3hrrnui3jdopeejsoatkzmoltqy
01b882619d46558f3d9e316d11b48dcf211327026a

f1xtwapqc6nh4si2hcwpr3656iotzmlwumogqbuaa
01bcec07c05e69f92468e2b3e3bf77c874f2c5da8c

f1wbxhu3ypkuo6eyp6hjx6davuelxaxrvwb2kuwva
01b06e7a6f0f551de261fe3a6fe182b422ee0bc6b6

f12fiakbhe2gwd5cnmrenekasyn6v5tnaxaqizq6a
01d1500504e4d1ac3e89ac891a4502586fabd9b417

Actor Type Addresses

f24vg6ut43yw2h2jqydgbg2xq7x6f4kub3bg6as6i
02e54dea4f9bc5b47d261819826d5e1fbf8bc5503b

f25nml2cfbljvn4goqtclhifepvfnicv6g7mfmmvq
02eb58bd08a15a6ade19d0989674148fa95a8157c6

f2nuqrg7vuysaue2pistjjnt3fadsdzvyuatqtfei
026d21137eb4c4814269e894d296cf6500e43cd714

f24dd4ox4c2vpf5vk5wkadgyyn6qtuvgcpxxon64a
02e0c7c75f82d55e5ed55db28033630df4274a984f

f2gfvuyh7v2sx3patm5k23wdzmhyhtmqctasbr23y
02316b4c1ff5d4afb7826ceab5bb0f2c3e0f364053

BLS Type Addresses

To aid in readability, these addresses are line-wrapped. Address and hex pairs are separated by ---.

f3vvmn62lofvhjd2ugzca6sof2j2ubwok6cj4xxbfzz
4yuxfkgobpihhd2thlanmsh3w2ptld2gqkn2jvlss4a
---
03ad58df696e2d4e91ea86c881e938ba4ea81b395e12
797b84b9cf314b9546705e839c7a99d606b247ddb4f9
ac7a3414dd

f3wmuu6crofhqmm3v4enos73okk2l366ck6yc4owxwb
dtkmpk42ohkqxfitcpa57pjdcftql4tojda2poeruwa
---
03b3294f0a2e29e0c66ebc235d2fedca5697bf784af
605c75af608e6a63d5cd38ea85ca8989e0efde9188b
382f9372460d

f3s2q2hzhkpiknjgmf4zq3ejab2rh62qbndueslmsdz
ervrhapxr7dftie4kpnpdiv2n6tvkr743ndhrsw6d3a
---
0396a1a3e4ea7a14d49985e661b22401d44fed402d1
d0925b243c923589c0fbc7e32cd04e29ed78d15d37d
3aaa3fe6da33

f3q22fijmmlckhl56rn5nkyamkph3mcfu5ed6dheq53
c244hfmnq2i7efdma3cj5voxenwiummf2ajlsbxc65a
---
0386b454258c589475f7d16f5aac018a79f6c1169d2
0fc33921dd8b5ce1cac6c348f90a3603624f6aeb91b
64518c2e8095

f3u5zgwa4ael3vuocgc5mfgygo4yuqocrntuuhcklf4
xzg5tcaqwbyfabxetwtj4tsam3pbhnwghyhijr5mixa
---
03a7726b038022f75a384617585360cee629070a2d9
d28712965e5f26ecc40858382803724ed34f2720336
f09db631f074

Filecoin Parameters

All Parameters

Parameter Type Value Description
ACTOR-METHOD BYTES 8 The size required to represent an actor method.
ACTORS-MESSAGES-FRACTION 0.30000004
ACTORS-MESSAGES-PER-BLOCK 32.661842
ADDRESS-SIZE BYTES 35 The size of an address.
ALL-POST-MESSAGES-PER-YEAR 122557560.0
ALL-SEAL-MESSAGES-PER-YEAR 335544320
ALL-SEAL-SIZE-PER-YEAR 335544320
AVG-POSTS-MESSAGES-PER-BLOCK 11.650845
AVG-PROOFS-MESSAGES-PER-BLOCK 43.549118
AVG-SEALS-MESSAGES-PER-BLOCK 31.898273
AVG-TICKETS 1
BLOCK-FRAMING-SIZE BYTES 14709.481 The total amount of block framing.
BLOCK-HEADER-FIXED-SIZE 420
BLOCK-HEADER-SIZE 1427
BLOCK-HEADER-VARIABLE-SIZE 1007
BLOCK-HEIGHT-SIZE 8
BLOCK-SIG-SIZE 96
BLOCK-SIZE 37265.1
BLOCK-SIZE-KIB 36.3917
BLOCK-TIME 15
BLOCKS-IN-A-YEAR 10519200.0
BLOCKS-IN-TEN-YEARS 105192000.0
BLS-AGG-SIZE 96
CHAIN-SIZE-YEAR 391999060000.0
CHAIN-SIZE-YEAR-GIB 365.07758
CID-SIZE BYTES 35 The size of a CID.
COMM-C-CONSTRAINTS 218221140.0
COMM-C-OPENINGS 15
COMM-C-TIME 12924.399
COMM-D-CONSTRAINTS 11011791.0
COMM-D-OPENINGS 1
COMM-D-TIME 652.18604
COMM-P-SIZE 35
COMM-R-CONSTRAINTS 66070744.0
COMM-R-OPENINGS 6
COMM-R-TIME 3913.1165
COMMIT-SIZE 35
COMMIT-TIME 14042.3955
CORES 16
DEGREE 14
DEGREE-BASE 6
DEGREE-EXPANDER 8
DELTA 0.02425
DRG-D 14
DRG-E 0.8
EIX 1152921504606846976
ELECTION-PROOF-SIZE 64
ENCODING-AMAX 2
ENCODING-TIME 6605.1006
ENCODING-TIME-MINS 110.08501
EPSILON 0.0505
EXIT-CODE BYTES 4 The size of an exit code.
EXPECTED-WINNING-MINERS 5
FROM-ADDRESS 35
GAS-LIMIT 8
GAS-PRICE BYTES 8 The size required to represent the gas limit.
GAS-USED BYTES 8 The size required to represent the amount of gas used by a message.
GIB 1073741824
INCLUSION-CIRCUIT-TIME 2.3098202
INCLUSION-CONSTRAINTS 39000.0
KDF-CONTENT 15
KDF-HASH-SIZE 32
KDF-HASH-TIME 4.5608e-08
KIB BYTES 1024 The number of bytes in one KiB.
LAMBDA 10
LAYERS 9.634086
LAYERS-A 6.8276772
LAYERS-B 9.634086
LEAF-CIRCUIT-TIME 0.7417668
LEAF-CONSTRAINTS 12524.312
LEAF-HASH-CIRCUIT-TIME 0.076994
LEAF-HASH-CONSTRAINTS 1300
LEAF-HASH-TIME 4.5608e-08
LEAF-TIME 4.3939139e-07
MALICIOUS-ENCODING 3302.5503
MAX-TICKETS 19.07985
MERKLE-HASH-CONSTRAINTS 1300
MERKLE-HASH-TIME 1.3078e-05
MERKLE-HASH-TIME-CIRCUIT 0.076994
MESSAGE-NONCE BYTES 4 The size of a message’s nonce.
MESSAGE-RECEIPT BYTES 16 The size of one message receipt.
MESSAGE-RECEIPTS-CID BYTES 35 The size of one message receipt’s CID.
MESSAGE-SIZE BYTES 106 The size of a single message.
MESSAGES 108.872795
MESSAGES-ROOT-CID BYTES 35 Size of the CID of the root merkle tree of the messages.
MESSAGES-SIZE BYTES 11540.517 The total size of the messages in a block.
MIB BYTES 1048576 The number of bytes in one EiX.
MIN-TICKETS 0
MINER-ADDRESS-SIZE 35
MINERS 1000
NODE-SIZE 32
NODES 1073741824
OFFLINE-CHALLENGES 282.3536
ONE-BLOCK-IN-TEN-YEARS 9.506427e-09
ONLINE-CHALLENGES 136.53467
OPENING-PER-CHALLENGE 15
OPENINGS 4235.304
P-SIZE 35.0
PARALLEL-SEAL-TIME 8575.856
PARENT-WEIGHT-SIZE 8
PARENTS 5
PARENTS-CIDS 175
PIB 1125899906842624
POLLING-TIME 825.6376
POREP-SNARK-CONSTRAINTS 295303680.0
POREP-SNARK-PARTITIONS 2.9530368
POREP-SNARK-PROOF-SIZE 566.98303
POREP-SNARK-TIME 17489.703
POST-CHALLENGE-BLOCKS BLOCKS 480 The time offset before which the actual work of generating the PoSt cannot be started. This is some delta before the end of the Proving Period, and as such less than a single Proving Period.
POST-CHALLENGE-HOURS HOURS 2 PoSt challenge time (see POST_CHALLENGE_BLOCKS).
POST-CHALLENGE-TIME SECONDS 7200 PoSt challenge time (see POST_CHALLENGE_BLOCKS).
POST-CHALLENGES 136.53467
POST-PROOF-SIZE 192
POST-PROVING-PERIOD BLOCKS 5760 The time interval in which a PoSt has to be submitted
POST-SIZE-PER-BLOCK 2236.9622
POST-SNARK-CIRCUIT 5324852.0
POST-SNARK-PROOF-PARTITIONS 1
POST-SNARK-PROOF-SIZE 192
POSTS-PER-SECTOR-PER-YEAR 365.25
PROOF-MESSAGES-FRACTION 0.4
PROOFGEN-TIME 17489.703
PROOFS-SIZE-PER-BLOCK 22555.621
PROOFS-SIZE-PER-BLOCK-KIB 22.026974
PROVING-PERIOD-HOURS 24
PROVING-PERIOD-SECONDS 86400
RECEIPTS 108.872795
RECEIPTS-SIZE 1741.9647 The total size of all message receipts, in bytes.
REPLICA-COMMIT-TIME 42598.98
RESEAL 0
RETURN BYTES 4 The size of a message’s return value.
RSA-ELEMENT BYTES 256 The size of an RSA element.
SEAL-COMMITMENTS-SIZE 70
SEAL-PROOF-SIZE 636.98303
SEAL-SIZE-PER-BLOCK 20318.658
SEAL-TIME 66693.78
SEALS-PER-SECTOR-PER-YEAR 1
SECTOR-SIZE 34359738368
SECTOR-SIZE-GIB 32
SECTORS-COUNT 335544320
SNARK-MAX-CONSTRAINTS 100000000
SNARK-SINGLE-PROOF-SIZE 192
SPACEGAP 0.1
STATE-ROOT-CID 35
STORAGE-NETWORK-CAPACITY 1.152921504606847e+19
TIB 1099511627776
TICKET-SIZE 832
TICKETS 1
TICKETS-SIZE 832
TIMESTAMP-SIZE 8
TO-ADDRESS BYTES 35 The size of a message’s ‘from address’.
TREE-DEPTH 30.0
TX-MESSAGES-FRACTION 0.3
TX-MESSAGES-PER-BLOCK 32.66184
U64 8 The size of a U64, in bytes.
VALUE BYTES 8 The size of a ‘value’ element.
VARINT BYTES 4 The size of a VarInt.
VDF-OUTPUT-SIZE 0
VDF-PROOF-SIZE 768
YEAR-IN-SECONDS SECONDS 31557600.0 The number of seconds in one year.

Effect of Space Gap and Sector Size on Block Size.

LAMBDA SPACEGAP BLOCK-SIZE-KIB SECTOR-SIZE-GIB
10 0.2 2.0183308 1024
10 0.1 2.1250708 1024
10 0.2 2.2424922 1024
10 0.06 2.2729497 1024
10 0.1 2.5627122 1024
10 0.03 2.6715527 1024
80 0.2 2.7350836 1024
10 0.2 2.847705 256
10 0.06 3.0063488 1024
10 0.1 3.2539802 256
80 0.1 3.589003 1024
10 0.06 3.8169022 256
10 0.2 3.9404943 128
10 0.03 4.2021575 1024
80 0.2 4.3927507 1024
10 0.2 4.730614 256
10 0.1 4.7323604 128
80 0.06 4.7720346 1024
10 0.03 5.3342724 256
10 0.2 5.501524 1024
80 0.2 5.57777 256
10 0.06 5.829611 128
10 0.1 5.94944 256
80 0.1 6.9545097 1024
10 0.1 7.102623 1024
10 0.06 7.638206 256
80 0.03 7.960858 1024
10 0.2 8.008982 128
10 0.03 8.787309 128
80 0.1 8.827972 256
80 0.2 9.263679 128
10 0.06 9.3208065 1024
10 0.1 10.384581 128
10 0.2 10.399412 32
80 0.06 10.503603 1024
10 0.03 12.190317 256
80 0.2 12.920809 256
80 0.06 13.331349 256
10 0.1 13.4014015 32
10 0.06 13.676332 128
10 0.03 15.2998495 1024
80 0.1 15.598608 128
80 0.2 16.252815 1024
10 0.06 17.561655 32
10 0.2 17.942131 256
80 0.03 20.070072 1024
10 0.03 22.549423 128
80 0.1 22.671417 256
80 0.2 23.978535 128
10 0.1 24.036263 256
80 0.06 24.376612 128
80 0.03 25.470312 256
10 0.2 27.385735 32
10 0.03 28.776114 32
80 0.1 29.061607 1024
80 0.2 30.596579 32
10 0.06 32.48009 256
10 0.2 34.33397 128
80 0.06 36.181545 256
10 0.1 36.3917 32
80 0.1 42.983322 128
10 0.1 46.211964 128
80 0.06 46.80707 1024
80 0.03 48.038197 128
10 0.06 48.872463 32
80 0.1 54.612484 32
10 0.03 55.240646 256
80 0.2 58.89311 256
10 0.06 62.670723 128
80 0.06 69.31734 128
80 0.03 72.59843 256
10 0.03 82.51584 32
80 0.06 87.89453 32
80 0.2 87.977234 32
80 0.03 94.63942 1024
10 0.03 107.03619 128
80 0.1 107.64613 256
80 0.2 114.18173 128
10 0.2 131.21773 32
80 0.03 140.30208 128
80 0.1 160.02498 32
80 0.06 175.19678 256
10 0.1 176.24756 32
80 0.03 177.6102 32
80 0.1 209.20566 128
10 0.06 238.65137 32
80 0.06 259.8711 32
80 0.06 340.8757 128
80 0.03 357.2812 256
10 0.03 406.86823 32
80 0.2 434.17523 32
80 0.03 529.018 32
80 0.03 695.79944 128
80 0.1 794.4138 32
80 0.06 1293.6443 32
80 0.03 2639.3792 32

Parameter Definitions

Assumptions:
  CID [CRH]:
    declare(cid_size, bytes)
    describe(cid_size, "The size of a CID.", bytes)
    cid_size = 32 + 1 + 1 + 1

  SLE [VRF]:
    declare(vrf_proof_size) // TODO
    describe(vrf_proof_size, "The size of a VRF proof.", bytes)

Primitives:
  declare(varint, bytes)
  describe(varint, "The size of a VarInt.", bytes)
  varint = 4

  u64 = 8
  describe(u64, "The size of a U64, in bytes.")

  Dimensions:
    Size:
      kib = 1024
      describe (kib, "The number of bytes in one KiB.", bytes)

      mib = 1024 * kib
      describe (mib, "The number of bytes in one MiB.", bytes)

      gib = 1024 * mib
      describe (mib, "The number of bytes in one GiB.", bytes)

      tib = 1024 * gib
      describe (mib, "The number of bytes in one TiB.", bytes)

      pib = 1024 * tib
      describe (mib, "The number of bytes in one PiB.", bytes)

      eix = 1024 * pib
      describe (mib, "The number of bytes in one EiX.", bytes)
    Time:
      year_in_seconds = 365.25 * 24 * 60 * 60
      describe(year_in_seconds, "The number of seconds in one year.", seconds)

VDFRSA:
  declare(rsa_element, bytes)
  describe(rsa_element, "The size of an RSA element.", bytes)
  rsa_element = (2048/8)

Blockchain:
  declare(block_time, seconds)
  Address:
    address_size = cid_size
    describe(address_size, "The size of an address.", bytes)

  Block:
    block_framing_size = block_header_size + messages_size + receipts_size
    describe(block_framing_size, "The total amount of block framing.", bytes)

    Messages:
      // declare(messages, integer) // TODO
      messages_size = messages * message_size
      describe(messages_size, "The total size of the messages in a block.", bytes)

      messages_root_cid = cid_size
      describe(messages_root_cid, "Size of the CID of the root merkle tree of the messages.", bytes)

      Message:
        message_size = to_address + from_address + message_nonce + value + gas_price + gas_limit + actor_method
        describe(message_size, "The size of a single message.", bytes)

        to_address = address_size
        describe(to_address, "The size of a message's 'to address'.", bytes)

        from_address = address_size
        describe(to_address, "The size of a message's 'from address'.", bytes)

        Nonce:
          declare(message_nonce, bytes) // TODO
          describe(message_nonce, "The size of a message's nonce.", bytes)
          message_nonce = varint

        Value:
          declare(value, bytes)
          describe(value, "The size of a 'value' element.", bytes)
          value = u64

        Gas:
          declare(gas_price, bytes)
          describe(gas_price, "The size required to represent the gas price.", bytes)
          gas_price = u64

          declare(gas_limit, bytes)
          describe(gas_price, "The size required to represent the gas limit.", bytes)
          gas_limit = u64

        ActorMethod:
          declare(actor_method, bytes) // TODO: actor_method what is it? how big is it?
          actor_method = u64
          describe(actor_method, "The size required to represent an actor method.", bytes)

    Receipts:
      receipts = messages // TODO check
      // declare(receipts, integer) // TODO
      receipts_size = receipts * message_receipt
      describe(receipts_size, "The total size of all message receipts, in bytes.")
      message_receipts_cid = cid_size
      describe(message_receipts_cid, "The size of one message receipt's CID.", bytes)

      Receipt:
        message_receipt = exit_code + return + gas_used
        describe(message_receipt, "The size of one message receipt.", bytes)

        // declare(exit_code, bytes) // TODO
        exit_code = varint
        describe(exit_code, "The size of an exit code.", bytes)
        // declare(return, bytes) // TODO
        return = varint
        describe(return, "The size of a message's return value.", bytes)

        // declare(gas_used, bytes) // TODO
        gas_used = u64
        describe(gas_used, "The size required to represent the amount of gas used by a message.", bytes)

    BlockHeader (EC):
      declare(block_header_size, bytes)
      assume(block_header_size > 0)
      assume(block_header_size < 1024*1024*10) // assume max is 10MB

      block_header_size = block_header_fixed_size + block_header_variable_size
      block_header_fixed_size = miner_address_size + election_proof_size + parent_weight_size + block_height_size + state_root_cid + messages_root_cid + bls_agg_size + message_receipts_cid + timestamp_size + block_sig_size
      block_header_variable_size = tickets_size + parents_cids

      StateTree:
        state_root_cid = cid_size

      Weight:
        declare(parent_weight_size, bytes) // TODO
        parent_weight_size = u64

      Height:
        declare(block_height_size, bytes) // TODO
        block_height_size = u64

      MinerAddress [CID]:
        miner_address_size = address_size

      ElectionProof (VRFBls) [SLE, BLSSig]:
        declare(election_proof_size, bytes) // TODO
        election_proof_size = 64

      // ElectionProof (VRFSecpk) [SLE, Secpk]:
      //   declare(election_proof_size, bytes) // TODO
      //   election_proof_size = 80

      Parents [CID]:
        // declare(parents, integer) // TODO
        parents = expected_winning_miners
        parents_cids = parents * cid_size

      BLSSignatures (BLSSigAgg) [BLSSigAgg]:
        declare(bls_agg_size, bytes) // TODO
        bls_agg_size = 96

      Timestamp:
        // declare(timestamp_size, integer) // TODO
        timestamp_size = u64

      // BlockSig (BlockSigSecpk):
        // declare(block_sig_size, integer) // TODO
        // block_sig_size = 80

      BlockSig (BlockSigBls):
        declare(block_sig_size, integer) // TODO
        block_sig_size = 96

      Tickets:
        // declare(tickets, integer)
        declare(tickets_size, bytes)
        tickets_size = ticket_size * tickets
        ticket_size = election_proof_size + vdf_proof_size + vdf_output_size

SNARK [SNARKAssumptions]:
  declare(snark_single_proof_size, bytes)
  snark_single_proof_size = 192
  snark_max_constraints = 100000000

// VDF (VDFStorageBased):
//   vdf_proof_size = (vdf_snark_circuit / snark_max_constraints) * snark_single_proof_size
//   vdf_output_size = hash_size

VDF (VDFRSA):
  vdf_proof_size = 3 * rsa_element
  vdf_output_size = 0

Proofs:
  ProofOfReplication:
    Graph:
      // declare(node_size, integer)
      // declare(sector_size, integer)
      // declare(nodes, integer)
      nodes = sector_size / node_size
      degree = degree_base + degree_expander
      sector_size_gib = sector_size / gib

      DRG (DRSample) [DRGAssumption]:
        // declare(degree_base, integer)
        drg_e = 0.80
        drg_d = 1/4

      ExpanderParents (Chung) [ChungAssumption]:
        // declare(degree_expander, integer)

      Layers:
        // declare(layers, integer)
        assume(layers > 0)

    Soundness:
      // declare(lambda, integer)
      assume(soundness > 0)
      assume(soundness < 0.5)
      soundness = 1/(2^lambda)

    SpaceGap:
      assume(spacegap > 0)
      assume(spacegap < 0.5)

    Challenges:
      OfflineChallenges:
        // declare(offline_challenges, integer)
        // declare(offline_challenges_all, integer)
        assume(offline_challenges > 0)

      OnlineChallenges:
        // declare(online_challenges, integer)
        assume(online_challenges > 0)

    Seal:
      Encoding [KDFTiming]:
        assume(kdf_content > 0)
        assume(encoding_time > 0)
        assume(polling_time > 0)
        kdf_content = degree + 1
        encoding_time = layers * nodes * (kdf_content - 1) * kdf_hash_time * (node_size / kdf_hash_size)
        encoding_time_mins = encoding_time / 60
        malicious_encoding = encoding_time / encoding_amax
        polling_time = malicious_encoding * drg_d

    Commitment (ColumnCommitments) [CRH]:
      commit_size = cid_size
      assume(replica_commit_time > 0)
      replica_commit_time = commit_time * 3 + leaf_time * nodes
      seal_commitments_size = commit_size * 2 // 1 commD, 1 CommR

    ProofGeneration (ColumnCommitments):
      assume(opening_time > 0)
      openings = offline_challenges * opening_per_challenge

      Leaf:
        leaf_constraints =  layers * leaf_hash_constraints
        leaf_circuit_time = layers * leaf_hash_circuit_time
        leaf_time = layers * leaf_hash_time

      Inclusion (MerkleVC) [CRH]:
        // declare(tree_depth, integer)
        tree_depth = (log2(nodes))
        inclusion_circuit_time = tree_depth * merkle_hash_time_circuit
        inclusion_constraints = tree_depth * merkle_hash_constraints
        commit_time = nodes * merkle_hash_time

      SNARK [SNARKAssumptions]:
        comm_d_openings = 1
        comm_d_time = offline_challenges * (comm_d_openings * inclusion_circuit_time)
        comm_d_constraints = offline_challenges * (comm_d_openings * inclusion_constraints)

        comm_r_openings = degree_base
        comm_r_time = offline_challenges * (comm_r_openings * inclusion_circuit_time)
        comm_r_constraints = offline_challenges * (comm_r_openings * inclusion_constraints)

        comm_c_openings = opening_per_challenge
        comm_c_time = offline_challenges * (comm_c_openings * (inclusion_circuit_time + leaf_circuit_time))
        comm_c_constraints = offline_challenges * (comm_c_openings * (inclusion_constraints + leaf_constraints))

        porep_snark_time = comm_d_time + comm_r_time + comm_c_time
        porep_snark_constraints = comm_d_constraints + comm_r_constraints + comm_c_constraints

        porep_snark_partitions = porep_snark_constraints / snark_max_constraints
        porep_snark_proof_size = porep_snark_partitions * snark_single_proof_size

        proofgen_time = porep_snark_time

    Size:
      seal_proof_size = porep_snark_proof_size + seal_commitments_size

    Time:
      seal_time = replica_commit_time + proofgen_time + encoding_time
      parallel_seal_time = (porep_snark_time + commit_time)/cores + encoding_time
      declare(unseal_time) // TODO

    Cost:
      seal_cost = seal_time * (cpu_cost_per_second + memory_cost_per_second)
      declare(unseal_cost) // TODO

  ProofOfReplication (SDR):
    Graph:
      Layers:
        layers = layers_b // TODO max(layers_a, layers_b) + 1
        layers_a = (0.68 - epsilon + delta) / (0.12 - delta)
        layers_b = (log2(1 / (3 * (epsilon - 2 * delta)))) + 0.12 / (0.12 - delta) + 1

    SpaceGap:
      assume(epsilon <= 0.24)
      // delta < epsilon/2
      // delta = epsilon/2 + 0.001
      // spacegap = epsilon + 2 * delta
      spacegap = 2 * epsilon - 0.001
      delta = epsilon/2 - 0.001

    Challenges:
      OfflineChallenges:
        offline_challenges = (- lambda) / (log2(1 - delta))

      OnlineChallenges:
        online_challenges = (- lambda) / (log2(2 - epsilon - 2 * delta) - 1)

    ProofGeneration (ColumnCommitments):
      opening_per_challenge = degree_base + degree_expander + 1

  ProofOfSpacetime:
    Randomness [RandomBeacon]:
      declare(post_randomness_lookback)

    Parameters:
      declare(proving_period_hours)
      declare(max_proving_sectors)

      declare(post_challenge_blocks)
      describe(post_challenge_blocks, "The time offset before which the actual work of generating the PoSt cannot be started. This is some delta before the end of the Proving Period, and as such less than a single Proving Period.", blocks)

      describe(post_challenge_time, "PoSt challenge time (see POST_CHALLENGE_BLOCKS).", seconds)
      post_challenge_time = post_challenge_blocks * block_time
      post_challenge_time = post_challenge_hours * 60 * 60
      describe(post_challenge_hours "PoSt challenge time (see POST_CHALLENGE_BLOCKS).", hours)

  ProofOfSpacetime (RationalPoSt):
    Parameters:
      post_challenges = online_challenges
      post_proving_period = proving_period_seconds / block_time
      describe(post_proving_period, "The time interval in which a PoSt has to be submitted", blocks)

    Cost:
      declare(post_proving_cost)

    SNARK:
      post_snark_circuit = online_challenges * inclusion_constraints

    Size:
      post_proof_size = post_snark_proof_size
      post_snark_proof_partitions = 1 // TODO post_snark_circuit / snark_max_constraints
      post_snark_proof_size = post_snark_proof_partitions * snark_single_proof_size


Consensus [ProofOfReplication, ProofOfSpacetime]:
  // declare(expected_winning_miners, integer)
  // declare(finality_height, integer)
  Tickets:
    tickets = avg_tickets
    avg_tickets = 1
    min_tickets = 0
    max_tickets = log(one_block_in_ten_years)/log(0.36) + 1 // 0.36^(max_tickets-1) = one_block_in_ten_years
    blocks_in_a_year = (year_in_seconds / block_time) * expected_winning_miners
    blocks_in_ten_years = blocks_in_a_year * 10
    one_block_in_ten_years = 1/blocks_in_ten_years

StorageMarket:
  declare(min_storing_time)
  Deals (OnChainDeals):
    comm_p_size = cid_size
    declare(p_size, 1)
    p_size = (log2(sector_size))
    sector_manifest_size = pieces * comm_p_size + p_size
    max_sector_manifest_hashes = sector_size / min_piece_size


Mining:
  proving_period_seconds = proving_period_hours * 60 * 60

  seals_per_sector_per_year = reseal+1
  posts_per_sector_per_year = year_in_seconds / proving_period_seconds

ScalingRequirements:
  storage_network_capacity = 10 * eix

ChainBandwidth:
  sectors_count = storage_network_capacity / sector_size
  all_seal_size_per_year = all_seal_messages_per_year
  all_seal_messages_per_year = sectors_count * seals_per_sector_per_year
  all_post_messages_per_year = sectors_count * posts_per_sector_per_year / miners

  Chain Size:

  Block Size:
    chain_size_year = block_size * blocks_in_a_year
    chain_size_year_gib = chain_size_year / gib
    block_size_kib = block_size / kib

  Block Content:
    block_size = block_framing_size + proofs_size_per_block
    messages = avg_proofs_messages_per_block + tx_messages_per_block + actors_messages_per_block // TODO messages dont include deals

    Proofs:
      avg_proofs_messages_per_block = avg_seals_messages_per_block + avg_posts_messages_per_block
      avg_seals_messages_per_block = all_seal_messages_per_year / blocks_in_a_year
      avg_posts_messages_per_block = all_post_messages_per_year / blocks_in_a_year

      seal_size_per_block = avg_seals_messages_per_block * seal_proof_size
      post_size_per_block = avg_posts_messages_per_block * post_proof_size
      proofs_size_per_block = seal_size_per_block + post_size_per_block
      proofs_size_per_block_kib = proofs_size_per_block / kib

    Composition:
      avg_proofs_messages_per_block = proof_messages_fraction * messages
      tx_messages_per_block = tx_messages_fraction * messages
      actors_messages_per_block = actors_messages_fraction * messages

Parsed Parameter Definitions

{
  "subsystems": [
    {
      "name": "Assumptions",
      "subsystems": [
        {
          "name": "cid",
          "schema": {
            "description": null,
            "parameters": [
              {
                "name": "cidSize",
                "description": "The size of a CID.",
                "type": "bytes"
              }
            ]
          },
          "constraints": {
            "cidSize.tmp1%": "(+ 32 1)",
            "cidSize.tmp2%": "(+ cid-size.tmp1% 1)",
            "cidSize": "(+ cid-size.tmp2% 1)"
          }
        },
        {
          "name": "sle",
          "schema": {
            "description": null,
            "parameters": [
              {
                "name": "vrfProofSize",
                "description": "The size of a VRF proof.",
                "type": "bytes"
              }
            ]
          }
        }
      ]
    },
    {
      "name": "Primitives",
      "schema": {
        "description": null,
        "parameters": [
          {
            "name": "u64",
            "description": "The size of a U64, in bytes."
          },
          {
            "name": "varint",
            "description": "The size of a VarInt.",
            "type": "bytes"
          }
        ]
      },
      "constraints": {
        "varint": "(== 4)",
        "u64": "(== 8)"
      },
      "subsystems": [
        {
          "name": "Dimensions",
          "subsystems": [
            {
              "name": "Size",
              "schema": {
                "description": null,
                "parameters": [
                  {
                    "name": "mib",
                    "description": "The number of bytes in one EiX.",
                    "type": "bytes"
                  },
                  {
                    "name": "mib",
                    "description": "The number of bytes in one PiB.",
                    "type": "bytes"
                  },
                  {
                    "name": "mib",
                    "description": "The number of bytes in one TiB.",
                    "type": "bytes"
                  },
                  {
                    "name": "mib",
                    "description": "The number of bytes in one GiB.",
                    "type": "bytes"
                  },
                  {
                    "name": "mib",
                    "description": "The number of bytes in one MiB.",
                    "type": "bytes"
                  },
                  {
                    "name": "kib",
                    "description": "The number of bytes in one KiB.",
                    "type": "bytes"
                  }
                ]
              },
              "constraints": {
                "kib": "(== 1024)",
                "mib": "(* 1024 kib)",
                "gib": "(* 1024 mib)",
                "tib": "(* 1024 gib)",
                "pib": "(* 1024 tib)",
                "eix": "(* 1024 pib)"
              }
            },
            {
              "name": "Time",
              "schema": {
                "description": null,
                "parameters": [
                  {
                    "name": "yearInSeconds",
                    "description": "The number of seconds in one year.",
                    "type": "seconds"
                  }
                ]
              },
              "constraints": {
                "yearInSeconds.tmp1%": "(* 365.25 24)",
                "yearInSeconds.tmp2%": "(* year-in-seconds.tmp1% 60)",
                "yearInSeconds": "(* year-in-seconds.tmp2% 60)"
              }
            }
          ]
        }
      ]
    },
    {
      "name": "vdfrsa",
      "schema": {
        "description": null,
        "parameters": [
          {
            "name": "rsaElement",
            "description": "The size of an RSA element.",
            "type": "bytes"
          }
        ]
      },
      "constraints": {
        "rsaElement": "(/ 2048 8)"
      }
    },
    {
      "name": "Blockchain",
      "subsystems": [
        {
          "name": "Address",
          "schema": {
            "description": null,
            "parameters": [
              {
                "name": "addressSize",
                "description": "The size of an address.",
                "type": "bytes"
              }
            ]
          },
          "constraints": {
            "addressSize": "(== cid-size)"
          }
        },
        {
          "name": "Block",
          "schema": {
            "description": null,
            "parameters": [
              {
                "name": "blockFramingSize",
                "description": "The total amount of block framing.",
                "type": "bytes"
              }
            ]
          },
          "constraints": {
            "blockFramingSize.tmp1%": "(+ block-header-size messages-size)",
            "blockFramingSize": "(+ block-framing-size.tmp1% receipts-size)"
          },
          "subsystems": [
            {
              "name": "Messages",
              "schema": {
                "description": null,
                "parameters": [
                  {
                    "name": "messagesRootCid",
                    "description": "Size of the CID of the root merkle tree of the messages.",
                    "type": "bytes"
                  },
                  {
                    "name": "messagesSize",
                    "description": "The total size of the messages in a block.",
                    "type": "bytes"
                  }
                ]
              },
              "constraints": {
                "messagesSize": "(* messages message-size)",
                "messagesRootCid": "(== cid-size)"
              },
              "subsystems": [
                {
                  "name": "Message",
                  "schema": {
                    "description": null,
                    "parameters": [
                      {
                        "name": "toAddress",
                        "description": "The size of a message's 'from address'.",
                        "type": "bytes"
                      },
                      {
                        "name": "toAddress",
                        "description": "The size of a message's 'to address'.",
                        "type": "bytes"
                      },
                      {
                        "name": "messageSize",
                        "description": "The size of a single message.",
                        "type": "bytes"
                      }
                    ]
                  },
                  "constraints": {
                    "messageSize.tmp1%": "(+ to-address from-address)",
                    "messageSize.tmp2%": "(+ message-size.tmp1% message-nonce)",
                    "messageSize.tmp3%": "(+ message-size.tmp2% value)",
                    "messageSize.tmp4%": "(+ message-size.tmp3% gas-price)",
                    "messageSize.tmp5%": "(+ message-size.tmp4% gas-limit)",
                    "messageSize": "(+ message-size.tmp5% actor-method)",
                    "toAddress": "(== address-size)",
                    "fromAddress": "(== address-size)"
                  },
                  "subsystems": [
                    {
                      "name": "Nonce",
                      "schema": {
                        "description": null,
                        "parameters": [
                          {
                            "name": "messageNonce",
                            "description": "The size of a message's nonce.",
                            "type": "bytes"
                          }
                        ]
                      },
                      "constraints": {
                        "messageNonce": "(== varint)"
                      }
                    },
                    {
                      "name": "Value",
                      "schema": {
                        "description": null,
                        "parameters": [
                          {
                            "name": "value",
                            "description": "The size of a 'value' element.",
                            "type": "bytes"
                          }
                        ]
                      },
                      "constraints": {
                        "value": "(== u64)"
                      }
                    },
                    {
                      "name": "Gas",
                      "schema": {
                        "description": null,
                        "parameters": [
                          {
                            "name": "gasPrice",
                            "description": "The size required to represent the gas limit.",
                            "type": "bytes"
                          },
                          {
                            "name": "gasPrice",
                            "description": "The size required to represent the gas price.",
                            "type": "bytes"
                          }
                        ]
                      },
                      "constraints": {
                        "gasPrice": "(== u64)",
                        "gasLimit": "(== u64)"
                      }
                    },
                    {
                      "name": "ActorMethod",
                      "schema": {
                        "description": null,
                        "parameters": [
                          {
                            "name": "actorMethod",
                            "description": "The size required to represent an actor method.",
                            "type": "bytes"
                          }
                        ]
                      },
                      "constraints": {
                        "actorMethod": "(== u64)"
                      }
                    }
                  ]
                }
              ]
            },
            {
              "name": "Receipts",
              "schema": {
                "description": null,
                "parameters": [
                  {
                    "name": "messageReceiptsCid",
                    "description": "The size of one message receipt's CID.",
                    "type": "bytes"
                  },
                  {
                    "name": "receiptsSize",
                    "description": "The total size of all message receipts, in bytes."
                  }
                ]
              },
              "constraints": {
                "receipts": "(== messages)",
                "receiptsSize": "(* receipts message-receipt)",
                "messageReceiptsCid": "(== cid-size)"
              },
              "subsystems": [
                {
                  "name": "Receipt",
                  "schema": {
                    "description": null,
                    "parameters": [
                      {
                        "name": "gasUsed",
                        "description": "The size required to represent the amount of gas used by a message.",
                        "type": "bytes"
                      },
                      {
                        "name": "return",
                        "description": "The size of a message's return value.",
                        "type": "bytes"
                      },
                      {
                        "name": "exitCode",
                        "description": "The size of an exit code.",
                        "type": "bytes"
                      },
                      {
                        "name": "messageReceipt",
                        "description": "The size of one message receipt.",
                        "type": "bytes"
                      }
                    ]
                  },
                  "constraints": {
                    "messageReceipt.tmp1%": "(+ exit-code return)",
                    "messageReceipt": "(+ message-receipt.tmp1% gas-used)",
                    "exitCode": "(== varint)",
                    "return": "(== varint)",
                    "gasUsed": "(== u64)"
                  }
                }
              ]
            },
            {
              "name": "BlockHeader",
              "constraints": {
                "blockHeaderSize": "(+ block-header-fixed-size block-header-variable-size)",
                "blockHeaderFixedSize.tmp1%": "(+ miner-address-size election-proof-size)",
                "blockHeaderFixedSize.tmp2%": "(+ block-header-fixed-size.tmp1% parent-weight-size)",
                "blockHeaderFixedSize.tmp3%": "(+ block-header-fixed-size.tmp2% block-height-size)",
                "blockHeaderFixedSize.tmp4%": "(+ block-header-fixed-size.tmp3% state-root-cid)",
                "blockHeaderFixedSize.tmp5%": "(+ block-header-fixed-size.tmp4% messages-root-cid)",
                "blockHeaderFixedSize.tmp6%": "(+ block-header-fixed-size.tmp5% bls-agg-size)",
                "blockHeaderFixedSize.tmp7%": "(+ block-header-fixed-size.tmp6% message-receipts-cid)",
                "blockHeaderFixedSize.tmp8%": "(+ block-header-fixed-size.tmp7% timestamp-size)",
                "blockHeaderFixedSize": "(+ block-header-fixed-size.tmp8% block-sig-size)",
                "blockHeaderVariableSize": "(+ tickets-size parents-cids)"
              },
              "subsystems": [
                {
                  "name": "StateTree",
                  "constraints": {
                    "stateRootCid": "(== cid-size)"
                  }
                },
                {
                  "name": "Weight",
                  "constraints": {
                    "parentWeightSize": "(== u64)"
                  }
                },
                {
                  "name": "Height",
                  "constraints": {
                    "blockHeightSize": "(== u64)"
                  }
                },
                {
                  "name": "MinerAddress",
                  "constraints": {
                    "minerAddressSize": "(== address-size)"
                  }
                },
                {
                  "name": "ElectionProof",
                  "constraints": {
                    "electionProofSize": "(== 64)"
                  }
                },
                {
                  "name": "Parents",
                  "constraints": {
                    "parents": "(== expected-winning-miners)",
                    "parentsCids": "(* parents cid-size)"
                  }
                },
                {
                  "name": "blsSignatures",
                  "constraints": {
                    "blsAggSize": "(== 96)"
                  }
                },
                {
                  "name": "Timestamp",
                  "constraints": {
                    "timestampSize": "(== u64)"
                  }
                },
                {
                  "name": "BlockSig",
                  "constraints": {
                    "blockSigSizeInteger%": "(integer block-sig-size)",
                    "blockSigSize": "(== 96)"
                  }
                },
                {
                  "name": "Tickets",
                  "constraints": {
                    "ticketsSize": "(* ticket-size tickets)",
                    "ticketSize.tmp1%": "(+ election-proof-size vdf-proof-size)",
                    "ticketSize": "(+ ticket-size.tmp1% vdf-output-size)"
                  }
                }
              ]
            }
          ]
        }
      ]
    },
    {
      "name": "snark",
      "constraints": {
        "snarkSingleProofSize": "(== 192)",
        "snarkMaxConstraints": "(== 100000000)"
      }
    },
    {
      "name": "vdf",
      "constraints": {
        "vdfProofSize": "(* 3 rsa-element)",
        "vdfOutputSize": "(== 0)"
      }
    },
    {
      "name": "Proofs",
      "subsystems": [
        {
          "name": "ProofOfReplication",
          "subsystems": [
            {
              "name": "Graph",
              "constraints": {
                "nodes": "(/ sector-size node-size)",
                "degree": "(+ degree-base degree-expander)",
                "sectorSizeGib": "(/ sector-size gib)"
              },
              "subsystems": [
                {
                  "name": "drg",
                  "constraints": {
                    "drgE": "(== 0.8)",
                    "drgD": "(/ 1 4)"
                  }
                },
                {
                  "name": "ExpanderParents"
                },
                {
                  "name": "Layers"
                }
              ]
            },
            {
              "name": "Soundness",
              "constraints": {
                "soundness": "(/ 1 soundness.tmp1%)"
              }
            },
            {
              "name": "SpaceGap"
            },
            {
              "name": "Challenges",
              "subsystems": [
                {
                  "name": "OfflineChallenges"
                },
                {
                  "name": "OnlineChallenges"
                }
              ]
            },
            {
              "name": "Seal",
              "subsystems": [
                {
                  "name": "Encoding",
                  "constraints": {
                    "kdfContent": "(+ degree 1)",
                    "encodingTime.tmp1%": "(- kdf-content 1)",
                    "encodingTime.tmp2%": "(/ node-size kdf-hash-size)",
                    "encodingTime.tmp4%": "(* layers nodes)",
                    "encodingTime.tmp5%": "(* encoding-time.tmp4% encoding-time.tmp1%)",
                    "encodingTime.tmp6%": "(* encoding-time.tmp5% kdf-hash-time)",
                    "encodingTime": "(* encoding-time.tmp6% encoding-time.tmp2%)",
                    "encodingTimeMins": "(/ encoding-time 60)",
                    "maliciousEncoding": "(/ encoding-time encoding-amax)",
                    "pollingTime": "(* malicious-encoding drg-d)"
                  }
                }
              ]
            },
            {
              "name": "Commitment",
              "constraints": {
                "commitSize": "(== cid-size)",
                "replicaCommitTime.tmp1%": "(* commit-time 3)",
                "replicaCommitTime.tmp2%": "(* leaf-time nodes)",
                "replicaCommitTime": "(+ replica-commit-time.tmp1% replica-commit-time.tmp2%)",
                "sealCommitmentsSize": "(* commit-size 2)"
              }
            },
            {
              "name": "ProofGeneration",
              "constraints": {
                "openings": "(* offline-challenges opening-per-challenge)"
              },
              "subsystems": [
                {
                  "name": "Leaf",
                  "constraints": {
                    "leafConstraints": "(* layers leaf-hash-constraints)",
                    "leafCircuitTime": "(* layers leaf-hash-circuit-time)",
                    "leafTime": "(* layers leaf-hash-time)"
                  }
                },
                {
                  "name": "Inclusion",
                  "constraints": {
                    "treeDepth": "(log2 nodes)",
                    "inclusionCircuitTime": "(* tree-depth merkle-hash-time-circuit)",
                    "inclusionConstraints": "(* tree-depth merkle-hash-constraints)",
                    "commitTime": "(* nodes merkle-hash-time)"
                  }
                },
                {
                  "name": "snark",
                  "constraints": {
                    "commDOpenings": "(== 1)",
                    "commDTime.tmp1%": "(* comm-d-openings inclusion-circuit-time)",
                    "commDTime": "(* offline-challenges comm-d-time.tmp1%)",
                    "commDConstraints.tmp2%": "(* comm-d-openings inclusion-constraints)",
                    "commDConstraints": "(* offline-challenges comm-d-constraints.tmp2%)",
                    "commROpenings": "(== degree-base)",
                    "commRTime.tmp3%": "(* comm-r-openings inclusion-circuit-time)",
                    "commRTime": "(* offline-challenges comm-r-time.tmp3%)",
                    "commRConstraints.tmp4%": "(* comm-r-openings inclusion-constraints)",
                    "commRConstraints": "(* offline-challenges comm-r-constraints.tmp4%)",
                    "commCOpenings": "(== opening-per-challenge)",
                    "commCTime.tmp6%": "(+ inclusion-circuit-time leaf-circuit-time)",
                    "commCTime.tmp5%": "(* comm-c-openings comm-c-time.tmp6%)",
                    "commCTime": "(* offline-challenges comm-c-time.tmp5%)",
                    "commCConstraints.tmp8%": "(+ inclusion-constraints leaf-constraints)",
                    "commCConstraints.tmp7%": "(* comm-c-openings comm-c-constraints.tmp8%)",
                    "commCConstraints": "(* offline-challenges comm-c-constraints.tmp7%)",
                    "porepSnarkTime.tmp9%": "(+ comm-d-time comm-r-time)",
                    "porepSnarkTime": "(+ porep-snark-time.tmp9% comm-c-time)",
                    "porepSnarkConstraints.tmp10%": "(+ comm-d-constraints comm-r-constraints)",
                    "porepSnarkConstraints": "(+ porep-snark-constraints.tmp10% comm-c-constraints)",
                    "porepSnarkPartitions": "(/ porep-snark-constraints snark-max-constraints)",
                    "porepSnarkProofSize": "(* porep-snark-partitions snark-single-proof-size)",
                    "proofgenTime": "(== porep-snark-time)"
                  }
                }
              ]
            },
            {
              "name": "Size",
              "constraints": {
                "sealProofSize": "(+ porep-snark-proof-size seal-commitments-size)"
              }
            },
            {
              "name": "Time",
              "constraints": {
                "sealTime.tmp3%": "(+ replica-commit-time proofgen-time)",
                "sealTime": "(+ seal-time.tmp3% encoding-time)",
                "parallelSealTime.tmp2%": "(+ porep-snark-time commit-time)",
                "parallelSealTime.tmp1%": "(/ parallel-seal-time.tmp2% cores)",
                "parallelSealTime": "(+ parallel-seal-time.tmp1% encoding-time)"
              }
            },
            {
              "name": "Cost",
              "constraints": {
                "sealCost.tmp1%": "(+ cpu-cost-per-second memory-cost-per-second)",
                "sealCost": "(* seal-time seal-cost.tmp1%)"
              }
            }
          ]
        },
        {
          "name": "ProofOfReplication",
          "subsystems": [
            {
              "name": "Graph",
              "subsystems": [
                {
                  "name": "Layers",
                  "constraints": {
                    "layers": "(== layers-b)",
                    "layersA.tmp2%": "(- 0.68 epsilon)",
                    "layersA.tmp1%": "(+ layers-a.tmp2% delta)",
                    "layersA.tmp3%": "(- 0.12 delta)",
                    "layersA": "(/ layers-a.tmp1% layers-a.tmp3%)",
                    "layersB.tmp8%": "(* 2 delta)",
                    "layersB.tmp7%": "(- epsilon layers-b.tmp8%)",
                    "layersB.tmp6%": "(* 3 layers-b.tmp7%)",
                    "layersB.tmp5%": "(/ 1 layers-b.tmp6%)",
                    "layersB.tmp4%": "(log2 layers-b.tmp5%)",
                    "layersB.tmp10%": "(- 0.12 delta)",
                    "layersB.tmp9%": "(/ 0.12 layers-b.tmp10%)",
                    "layersB.tmp15%": "(+ layers-b.tmp4% layers-b.tmp9%)",
                    "layersB": "(+ layers-b.tmp15% 1)"
                  }
                }
              ]
            },
            {
              "name": "SpaceGap",
              "constraints": {
                "spacegap.tmp1%": "(* 2 epsilon)",
                "spacegap": "(- spacegap.tmp1% 0.001)",
                "delta.tmp2%": "(/ epsilon 2)",
                "delta": "(- delta.tmp2% 0.001)"
              }
            },
            {
              "name": "Challenges",
              "subsystems": [
                {
                  "name": "OfflineChallenges",
                  "constraints": {
                    "offlineChallenges.tmp1%": "(- 0 lambda)",
                    "offlineChallenges.tmp3%": "(- 1 delta)",
                    "offlineChallenges.tmp2%": "(log2 offline-challenges.tmp3%)",
                    "offlineChallenges": "(/ offline-challenges.tmp1% offline-challenges.tmp2%)"
                  }
                },
                {
                  "name": "OnlineChallenges",
                  "constraints": {
                    "onlineChallenges.tmp1%": "(- 0 lambda)",
                    "onlineChallenges.tmp5%": "(* 2 delta)",
                    "onlineChallenges.tmp4%.tmp6%": "(- 2 epsilon)",
                    "onlineChallenges.tmp4%": "(- online-challenges.tmp4%.tmp6% online-challenges.tmp5%)",
                    "onlineChallenges.tmp3%": "(log2 online-challenges.tmp4%)",
                    "onlineChallenges.tmp2%": "(- online-challenges.tmp3% 1)",
                    "onlineChallenges": "(/ online-challenges.tmp1% online-challenges.tmp2%)"
                  }
                }
              ]
            },
            {
              "name": "ProofGeneration",
              "constraints": {
                "openingPerChallenge.tmp1%": "(+ degree-base degree-expander)",
                "openingPerChallenge": "(+ opening-per-challenge.tmp1% 1)"
              }
            }
          ]
        },
        {
          "name": "ProofOfSpacetime",
          "subsystems": [
            {
              "name": "Randomness"
            },
            {
              "name": "Parameters",
              "schema": {
                "description": null,
                "parameters": [
                  {
                    "name": "postChallengeHours",
                    "description": "PoSt challenge time (see POST_CHALLENGE_BLOCKS).",
                    "type": "hours"
                  },
                  {
                    "name": "postChallengeTime",
                    "description": "PoSt challenge time (see POST_CHALLENGE_BLOCKS).",
                    "type": "seconds"
                  },
                  {
                    "name": "postChallengeBlocks",
                    "description": "The time offset before which the actual work of generating the PoSt cannot be started. This is some delta before the end of the Proving Period, and as such less than a single Proving Period.",
                    "type": "blocks"
                  }
                ]
              },
              "constraints": {
                "postChallengeTime": "(* post-challenge-time.tmp1% 60)",
                "postChallengeTime.tmp1%": "(* post-challenge-hours 60)"
              }
            }
          ]
        },
        {
          "name": "ProofOfSpacetime",
          "subsystems": [
            {
              "name": "Parameters",
              "schema": {
                "description": null,
                "parameters": [
                  {
                    "name": "postProvingPeriod",
                    "description": "The time interval in which a PoSt has to be submitted",
                    "type": "blocks"
                  }
                ]
              },
              "constraints": {
                "postChallenges": "(== online-challenges)",
                "postProvingPeriod": "(/ proving-period-seconds block-time)"
              }
            },
            {
              "name": "Cost"
            },
            {
              "name": "snark",
              "constraints": {
                "postSnarkCircuit": "(* online-challenges inclusion-constraints)"
              }
            },
            {
              "name": "Size",
              "constraints": {
                "postProofSize": "(== post-snark-proof-size)",
                "postSnarkProofPartitions": "(== 1)",
                "postSnarkProofSize": "(* post-snark-proof-partitions snark-single-proof-size)"
              }
            }
          ]
        }
      ]
    },
    {
      "name": "Consensus",
      "subsystems": [
        {
          "name": "Tickets",
          "constraints": {
            "tickets": "(== avg-tickets)",
            "avgTickets": "(== 1)",
            "minTickets": "(== 0)",
            "maxTickets.tmp2%": "(log one-block-in-ten-years)",
            "maxTickets.tmp3%": "(log 0.36)",
            "maxTickets.tmp1%": "(/ max-tickets.tmp2% max-tickets.tmp3%)",
            "maxTickets": "(+ max-tickets.tmp1% 1)",
            "blocksInAYear.tmp4%": "(/ year-in-seconds block-time)",
            "blocksInAYear": "(* blocks-in-a-year.tmp4% expected-winning-miners)",
            "blocksInTenYears": "(* blocks-in-a-year 10)",
            "oneBlockInTenYears": "(/ 1 blocks-in-ten-years)"
          }
        }
      ]
    },
    {
      "name": "StorageMarket",
      "subsystems": [
        {
          "name": "Deals",
          "constraints": {
            "commPSize": "(== cid-size)",
            "pSize": "(log2 sector-size)",
            "sectorManifestSize.tmp1%": "(* pieces comm-p-size)",
            "sectorManifestSize": "(+ sector-manifest-size.tmp1% p-size)",
            "maxSectorManifestHashes": "(/ sector-size min-piece-size)"
          }
        }
      ]
    },
    {
      "name": "Mining",
      "constraints": {
        "provingPeriodSeconds.tmp1%": "(* proving-period-hours 60)",
        "provingPeriodSeconds": "(* proving-period-seconds.tmp1% 60)",
        "sealsPerSectorPerYear": "(+ reseal 1)",
        "postsPerSectorPerYear": "(/ year-in-seconds proving-period-seconds)"
      }
    },
    {
      "name": "ScalingRequirements",
      "constraints": {
        "storageNetworkCapacity": "(* 10 eix)"
      }
    },
    {
      "name": "ChainBandwidth",
      "constraints": {
        "sectorsCount": "(/ storage-network-capacity sector-size)",
        "allSealSizePerYear": "(== all-seal-messages-per-year)",
        "allSealMessagesPerYear": "(* sectors-count seals-per-sector-per-year)",
        "allPostMessagesPerYear.tmp1%": "(* sectors-count posts-per-sector-per-year)",
        "allPostMessagesPerYear": "(/ all-post-messages-per-year.tmp1% miners)"
      },
      "subsystems": [
        {
          "name": "Chain"
        },
        {
          "name": "Block",
          "constraints": {
            "chainSizeYear": "(* block-size blocks-in-a-year)",
            "chainSizeYearGib": "(/ chain-size-year gib)",
            "blockSizeKib": "(/ block-size kib)"
          }
        },
        {
          "name": "Block",
          "constraints": {
            "blockSize": "(+ block-framing-size proofs-size-per-block)",
            "messages.tmp1%": "(+ avg-proofs-messages-per-block tx-messages-per-block)",
            "messages": "(+ messages.tmp1% actors-messages-per-block)"
          },
          "subsystems": [
            {
              "name": "Proofs",
              "constraints": {
                "avgProofsMessagesPerBlock": "(+ avg-seals-messages-per-block avg-posts-messages-per-block)",
                "avgSealsMessagesPerBlock": "(/ all-seal-messages-per-year blocks-in-a-year)",
                "avgPostsMessagesPerBlock": "(/ all-post-messages-per-year blocks-in-a-year)",
                "sealSizePerBlock": "(* avg-seals-messages-per-block seal-proof-size)",
                "postSizePerBlock": "(* avg-posts-messages-per-block post-proof-size)",
                "proofsSizePerBlock": "(+ seal-size-per-block post-size-per-block)",
                "proofsSizePerBlockKib": "(/ proofs-size-per-block kib)"
              }
            },
            {
              "name": "Composition",
              "constraints": {
                "avgProofsMessagesPerBlock": "(* proof-messages-fraction messages)",
                "txMessagesPerBlock": "(* tx-messages-fraction messages)",
                "actorsMessagesPerBlock": "(* actors-messages-fraction messages)"
              }
            }
          ]
        }
      ]
    }
  ]
}