Overview

qointum-logo-large.png
Qointum Logo - depiction of a coin quantum tunneling through a barrier

Qointum is the Quantum-Secure Trustless Smart Money powering Web 3.0. Secure and trustless digital money like Qointum is known as cryptocurrency.

Qointum is trustless because it is decentralized, anyone with a home computer can join the Qointum network to help verify transactions of digital money, and all computers operate using the same rules written in machine code. Qointum is also transparent, anyone can read the machine code rules that govern it and everyone knows the total amount of money in existence, ensuring that money can not be arbitrarily created at the whim of officials behind closed doors. With a Qointum wallet you are in full control, your account can never be frozen by authorities or become unavailable due to bank insolvency.

Today, research laboratories around the world are racing to build the first practical quantum computer, and experimental 16-qubit processors have already been constructed. With cryptocurrencies that use elliptic curve cryptography (ECC) such as Bitcoin, a transaction from an account will expose that account’s public key in the signature, which can be used in theory by a quantum computer to derive the associated private key, giving an attacker control over the account. A significant percentage of Bitcoins are currently held in accounts with exposed public keys and are thus vulnerable to quantum attack. Qointum employs “post-quantum” cryptography that is designed to be secure against quantum computers as well as classical computers, thus future-proofing the security of stored value owned by Qointum users from all theorized cryptographic attacks.

Another challenge facing many cryptocurrencies like Bitcoin is the cost of securing their decentralized networks through “proof-of-work” schemes, which require unbounded amounts of processing and thus consume limitless electricity. The Qointum network provides light-weight decentralized security through Queued Delegated Proof-of-Stake which consumes a limited and negligible amount of electricity, so Qointum users can enjoy a high-frequency value exchange with low transaction fees. Qointum is also ready to handle everyday mobile payments as the network provides instant transaction confirmation by means of a consensus heuristic based on delegate voting.

Qointum is also the first and only cryptocurrency to be scripted in the popular language Python which is used by millions of skilled developers worldwide. It’s truly programmable money capable of smart contracts, which are trustless automated rules that govern relationships between parties. Together with full web browser integration and Qointum’s new web-scalable Entangled Chains technology which can be described as decentralized interconnected databases, Qointum is powering the emerging privacy-centric and cryptocurrency-native Web 3.0.

Features

qoin.png
depiction of a ℚoin - the qoinel asset of Qointum’s main chain

  • Qointum is developed from scratch in the world-class blazing-fast language C++
  • the first and only cryptocurrency based on Post-Quantum Cryptography, provides 128-bit level security against quantum computer attacks
  • Qointum is a decentralized shared-state Turing machine controlled by Transaqoins and Qointracts which are trustless automated rules that govern relationships between parties
  • the first and only cryptocurrency scripted in the popular language Python, the Qointum SDK enables developers to use their preferred Python IDE to create and distribute transaqoins and qointracts
  • web browser-based Qoinapps with a JavaScript API enables web developers to create decentralized applications
  • web browser plugin QHTTP serves entire websites over a decentralized peer-to-peer blockchain network
  • new light-weight decentralized security algorithm Queued Delegated Proof-of-Stake that schedules delegates randomly ahead of time enabling network optimization
  • new light-weight consensus heuristic Maximally Vetted Delegate Chain that enables light clients to perform Simplified State Verification
  • Quorum Transaction Confirmation provides instant confirmation by means of a consensus heuristic based on delegate voting
  • new web-scalable Entangled Chains that enable Web 3.0 decentralized encrypted services such as a social network, or redundant storage, or government-issued digital currency
  • assets such as ℚoin use arbitrary precision arithmetic so they can be divided into infinitesimal pieces
  • stakeholders rewarded with 5% annual stake interest and benefit from payments made for the Qointum Pro License
  • decentralized governance enables adaptive inflation, and polls make binding decisions within the Qointum community
  • new blockdag secure data structure, a graph of blocks enables branch merging and faster convergence of consensus
  • new decentralized messaging network Ensemble using post-quantum cryptography to enable private communication and stealth payments

Qoin Distribution

qoin_distribution.png
initial distribution of ℚoins

In order to bootstrap the distributed Qointum network, we, NewGamePlus Inc. (“the Qointum team”), must create tokens (“ℚoins”) that operate the network in the first block (“genesis block”) and distribute them to participants (“stakeholders”). ℚoins will be created and distributed in the genesis block at the launch of the official client (Church’s Calculus).

Stakeholders are rewarded with 5% annual stake interest and benefit from the Qointum Pro License as large-scale developers may acquire qoins to pay the Qointum team for their subscription.

In order to facilitate a quick market turnaround for our crowdfunding backers, we will create tokens (“ℚoinXs”) that operate a temporary pre-release network derived from Bitcoin, these tokens can be later exchanged for ℚoins at a fixed conversion rate. All ℚoinXs that will ever be issued will be created in the genesis block at the launch of the temporary pre-release client “Qointum-X”. Unlike Bitcoin, there will be no mining fees on the Qointum-X network, only transaction fees.

80.1% of qoins will be made available to stakeholders through 3 rounds of crowdfunding with periods that correspond to our client release roadmap: Initial, Alpha and Beta. After the Initial Round, a fixed conversion rate between qoin and qoinX will be determined. At the launch of Qointum-X, backers will receive a corresponding amount of qoinXs to trade on markets. Then later, at the launch of our official client, the state of the Qointum-X blockchain will be captured and only holders of qoinXs will be given the opportunity to claim qoins at the conversion rate. Backers can thus only claim qoins if they do not trade away their qoinXs during the intervening period.

Anyone with selected cryptocurrencies such as Bitcoin (“BTC”), Ethereum or Litecoin may obtain qoins during our crowdfunding rounds. In order to receive qoins and participate in pre-launch releases, backers must agree to the Terms of Crowdfunding and send cryptocurrency to the address displayed on our crowdfunding page. For the Initial Round, the pledge reward is ℚ20000 per 1 BTC, with no limit to the number of qoins available. Due to volatility in the market, pledges denominated in other cryptocurrencies may have a non-fixed reward that is determined daily. The Initial Round ends on Aug-14-2017-00:00-PDT, after which we will no longer accept pledges until the next round. As our thanks to early backers, pledges made before July-17-2017-00:00-PDT and July-31-2017-00:00-PDT will receive qoin bonuses of 75% and 30% respectively. As our thanks to strong backers, backers who pledge a total of over 1 BTC and 0.2 BTC (or equivalent summated over all cryptocurrencies) will receive additional qoin bonuses of 75% and 30% respectively.

19.9% of qoins will be allocated to the Qointum team. These qoins will be allocated incrementally in a manner similar to the backers so that they become available to the Qointum team as qoinXs during the pre-release period. After each crowdfunding round, 24.843945% of qoins rewarded in that round will be allocated to the Qointum team (bringing ownership to 19.9% of the total). Also, a large number of qoinXs will be held out of circulation in publicly designated addresses to be later distributed in the Alpha and Beta rounds as the pledge rewards. It is expected that most of these qoinXs will never enter circulation and thus will not be converted to qoins.

The total number of qoins will be normalized at launch of the official client so that there will be at least ℚ1 billion in circulation. This means that there will be an extra factor in the conversion rate from qoinX to qoin determined after the last crowdfunding round. For example, if ℚ10 million total are rewarded then backers who hold onto all their qoinXs would end up claiming 100x more qoins than originally rewarded due to the normalization.

Software Licensing

The Qointum client software is available under the following license types:

  • a Personal License which is free but limited by revenue restrictions
  • a Pro License which requires a subscription (see Pricing)

Source Code Policy

We believe that transparency of source code is essential for decentralization, however until Qointum has achieved a huge network effect we believe that protecting our intellectual property will also serve to protect the value of the ℚoin and its stakeholders. We plan to allow the Qointum community to elect independent entities (via decentralized governance) to perform an annual source code audit in order to verify its security as a cryptocurrency platform. These entities will be bound to a non-disclosure agreement that protects our intellectual property.

Any modifications to open source projects used in Qointum can be found in our repositories.

The Qointum source code is licensed on a per-case basis via special arrangements made by our team. As this can be quite expensive, we do not generally license source code to smaller operations, educational institutions, nor to companies in countries which do not have adequate legal intellectual property protection.

\( %latex shortcuts \def\concat{\mathbin{\|}} \def\hash{\mathcal{H}} \)

Post-Quantum Cryptography

The quantum-secure cryptography required by Qointum consists of two main components: digital signatures for transactions to prove identity and asset ownership; and encryption to communicate privately over the Ensemble messaging network, or store private data on the public blockchain.

Merkle Signature Scheme

MerkleTree2.png
Merkle Signature - auth path for i=2

For digital signatures Qointum uses the Merkle Signature Scheme which is a signature scheme based on hash trees, and gives practical ~32 byte public key sizes (quantum security analysis – security depends on the hashing algorithm and there are no known practical quantum attacks against hashing algorithms in general). The signature scheme is optimized using a fractal tree representation and the Winternitz One-Time Signature Scheme to reduce signature size down to a practical ~1.7 KB for a public key that can verify up to 1 million signatures.

For hashing in the signature scheme Qointum uses the BLAKE2 256-bit hashing algorithm (detailed security analysis).

McEliece Cryptosystem

For encryption Qointum uses the McEliece cryptosystem which is an asymmetric key encryption scheme (quantum security article, presentation slides, and detailed analysis). The cryptosystem is optimized using quasi-dyadic Goppa codes to reduce public key sizes down to a practical ~4 KB. Also, encryption is supplemented by a Fujisaki-Okamoto transformation using the ChaCha symmetric cipher to ensure that ciphertexts are indistinguishable and therefore secure.

Queued Delegated Proof-of-Stake

qdpos.png
Qointum’s decentralized security algorithm

qdpos_violations.png
QDPoS violation - double block creation

Proof-of-Stake is a decentralized security algorithm where “stakeholders”, individuals or groups, who own the blockchain’s scarce asset (eg. ℚoins) are granted the authority to approve transactions of that asset. The amount of authority granted is proportional to the amount of the asset that a stakeholder owns. A stakeholder proves ownership of an asset amount by using their private key to cryptographically sign a message, this signature can be matched to a public address (a string of bytes) and its associated asset amount found on the blockchain. The security comes from the underlying distribution of the asset as it is spread among many stakeholders, thus many individuals partake in transaction approval, and furthermore they have an economic incentive to properly approve transactions as it preserves the value of their proven asset holdings.

Delegated Proof-of-Stake is an improvement where stakeholders may assign “delegates”, individuals or groups, to approve transactions in their place by voting with their stake. Delegates are represented on the network by a public address (a string of bytes) and prove their delegation by similarly signing a message with their private key. Delegates create blocks of transactions, earn interest for their stakeholders, and collect transaction fees. Delegation may be revoked at any time if their stakeholder does not find their service adequate. This is important for network optimization as delegates, catering to stakeholders, will typically have powerful well-connected servers which can handle higher transaction rates, ultimately providing a better service to Qointum users.

In Qointum’s new “Queued Delegated Proof-of-Stake” (QDPoS), delegates register into a queue scheduled randomly using blockchain entropy. While the scheduling method is random in nature it also ensures a more even round-robin ordering, so it scales well from a few delegates to thousands. The queue is scheduled a number of minutes ahead of time, and queue positions can’t change. This short fixed queue enables network optimization while maintaining security, as opposed to a long fixed order where consecutive delegates would have a greater ability to collude and attack by building their own branch.

Stake assigned to a delegate is held as a security deposit so that game-theoretic incentives can be applied by automatically rewarding or penalizing both stakeholders (who may vote maliciously) and delegates. For example, double block creation by a delegate can be penalized to minimize branching (in addition to loss of voter confidence). Also, short ephemeral registrations by stakeholders can be penalized to minimize block overhead.

The number of delegates is limited by the required security deposit amount. Only a small percentage of qoins are expected to be tied up in security deposits earning interest, as qoins must be kept liquid for operational fees and economic activity. The main chain is expected to be operated by ~100 delegates at any given time, however it can support many more.

Stakeholder Registration

Stakeholder registration is performed by publishing blocks. This block-level design (as opposed to transaction-level) allows for operation without any delegate authorization. Eg. a dead branch where all delegates have dropped out due to inactivity can be bootstrapped by registering as its only delegate.

To register as (or assign) a delegate one must provide a unique ID, a method of authorization such as a signable address, and a stake proof transaqoin that extracts a balance of at least \(min\_reg\_stake = ℚ50K\). This stake is held as a security deposit for the duration of registration. To be scheduled for block creation a delegate must be assigned at least \(min\_stake = min\_reg\_stake \cdot 50 = ℚ2.5M\). This gives an upper bound of \(ℚ1B\ /\ min\_stake = 400\) possible delegates and growing with inflation. However, only ~25% of qoins are expected to be tied up in security deposits, resulting in ~100 delegates.

Registrants may optionally assign stake to (vote for) a delegate who they trust, and in return earn interest at up to a 5% nominal annual rate. In this way the majority of stakeholders may participate in chain security without always being online. Voters specify the % of interest return to give to the delegate, and the % of fees to receive from the delegate (the delegate must also specify agreeable parameters). Delegates are elected in a process similar to single transferable vote, so a stakeholder may vote for multiple delegates in order of preference. To aid in voting the client tracks delegate performance info.

Details

  • a delegate will on average be selected to produce a block every \(1\ hour \approx 100\ delegates\ /\ 120\ blocks\ per\ hour\)
  • registrant must also include a transaqoin specifying how to refund the security deposit and reward any collected interest and fees
  • registration transaqoins must complete within a short runtime length of ℚ0.1
  • registration size limits: block - 10 KB (enough for a few signatures), handlers - 1 KB, ID - 128 B, about - 1 KB, votes - 10 delegates
  • registration ID is a Unicode name followed by a 4-character base58-encoded checksum derived from the first 23 bits of its hash (1 in ~10 million chance of a false positive)
  • registration IDs are protected for a period of 3 months after unregistration, where the original registrant can reclaim their ID by providing authorization
  • stakeholder receives interest on assigned stake for each delegate block
    • nominal annual interest rate adjusted in range [0.5, 5]% through decentralized governance
    • \(ℚ1B \cdot 5\%\ /\ 1M\ blocks\ per\ year = ℚ50\ reward\ per\ block\ average\)
    • interest calculated from time of registration block or last delegate block
    • interest rewarded after 1 round wait (after all delegates have validated the block)
  • the client tracks the following delegate performance info:
    • blocks produced/missed
    • early/late block latency
    • % of known registrations/transactions per block
    • violation counts:
      • double block creation
      • entropy commitments opened early
      • double/false-positive QTC votes

Secret Registration

secret_registration.png
Secret Registration

Registration is performed secretly through a commitment scheme in order to mitigate vote cherry-picking by delegates and collusion among voters/delegates.

The registrant first publishes a registration commitment block consisting of a ℚ250 registration fee payment and the hash of their secret registration block. The commitment is then linked into the blockdag by a delegate, and the registration block is then implicitly scheduled to be linked in 1 round later. The registrant then reveals their block by publishing it at some configurable time up to 90 seconds prior to the scheduled time.

Details

  • The active delegate may link in commitments at their own discretion and from each collect half of the fee. Without any foreknowledge of a registrant’s action (vote for whom, unregister, etc.), the active delegate is thus incentivized to link in all known commitments indiscriminately.
  • The active delegate must also link in any scheduled registration blocks and from each collect the other half of the fee. If the active delegate refuses to link one in (eg. because it reveals unfavourable votes) then the next delegate is incentivized to link it in and collect the fee, an action that also requires ignoring the previous block due to the scheduling. Thus, refusing a registration carries a risk of missing one’s block and requires collusion among the majority.
  • A registrant who fails to reveal their block in time will have their fee payment forfeited and split between the relevant delegates. Without any foreknowledge of who will be scheduled due to random selection, a registrant is thus incentivized to publish honest commitments or else risk losing at least half of their fee payment.
  • A dishonest registrant can’t cause a consensus split unless the delegate scheduled to link in their block is also dishonest. Given a dishonest registrant who reveals exactly 1 minute prior to the scheduled time, and a dishonest delegate who refuses to link it in:
    • both registrant and delegate are honest: all nodes see the registration
    • registrant is honest, delegate is dishonest: all nodes see that the reveal had enough time (over 30 seconds prior to block start) to propagate across the network, the delegate must have refused to link it in so their block is ignored
    • registrant is dishonest, delegate is honest: the reveal still had enough time (30 seconds prior to block start) to propagate across the network, the delegate would have seen it and linked it in
    • both registrant and delegate are dishonest: consensus is split as some nodes see the reveal 1 minute prior and ignore the delegate’s block which doesn’t link it in, while others see the reveal too late and accept the delegate’s block

API

module qointum.stakeholder

authorize_stakeholder(id, auth)

  • authorize a stakeholder to prove ownership of a block or QTC vote

    id – a unique registration ID
    auth – object passed into the authorization handler

pay_reg_fee(balance)

reg_id(name, sep=’:’)

  • generates a registration ID from a name and optional separator by appending a 4-character checksum of <name><sep>, returning <name><sep><checksum>

register_delegate(stake, id, auth_handler, reward_handler, interest_request, fee_offer, about, auth=None)

  • register a delegate on the blockchain

    stake – A balance of qoinel asset to be held as a security deposit for the duration of registration. Must be at least a minimum amount as specified in the qoinel.
    id – a unique registration ID
    auth_handler – A handler to authorize full access to this registration. The handler takes an argument auth which is an authorization object and returns true if authorization is successful.
    reward_handler – A handler called by the system periodically to reward the stakeholder for creating blocks, or refund the stake deposit. The handler takes arguments (balance, info) where balance holds the reward amount of qoinel asset, and info is a dict of information about the reward.
    interest_request – % of interest return that delegate requests from voters
    fee_offer – % of fees that delegate is offering voters
    about – info for public display
    auth – object passed into the current authorization handler to reclaim this ID if it was previously registered

register_voter(stake, id, auth_handler, reward_handler, votes, max_interest_offer, min_fee_request, auth=None)

  • register a voter on the blockchain

    stake – see register_delegate
    id – see register_delegate
    auth_handler – see register_delegate
    reward_handler – see register_delegate
    votes – vote for (assign stake to) a list of delegates in order of preference
    max_interest_offer – Max % of interest return that voter is offering delegate. Must agree with the delegate for the vote to be accepted.
    min_fee_request – Min % of fees that voter requests from delegate. Must agree with the delegate for the vote to be accepted.
    auth – see register_delegate

Example - Registration

Transaqoin to pay registration fee, in a commitment block:

from qointum.stakeholder import pay_reg_fee
from qointum.qointract.accounts import BasicAccount

account = BasicAccount('__sign__ Q1JvcReZAHTjaUj4pNG6KLzwqJ7SLtJq5JVaJGGvpeVF9Po8AucML')
pay_reg_fee(account.withdraw(250))

Transaqoin to perform action, in a registration block:

from functools import partial
from qointum.stakeholder import register_delegate, register_voter, reg_id
from qointum.qointract import Qointract
from qointum.qointract.accounts import BasicAccount

#handler defined below for example
#from qointum.qointract.accounts import account_authorized
def account_authorized(instance_path, account):
    return (isinstance(account, Qointract) and account.instance_path == instance_path and
            account.authorized)

#When registering the scope is "main".
#When the handlers are later unpickled this transaqoin is imported,
#so we must check the scope to avoid registering again in error.
if __name__ == '__main__':
    account = BasicAccount('__sign__ Q1JvcReZAHTjaUj4pNG6KLzwqJ7SLtJq5JVaJGGvpeVF9Po8AucML')
    stake = account.withdraw(50000)

    #example of registering a delegate
    register_delegate(stake,
        reg_id('Mr. FooBar'), #generates id 'Mr. FooBar:VeZ7'
        partial(account_authorized, account.instance_path),
        account.deposit,
        interest_request=0.1,
        fee_offer=0.1,
        about="Mr. FooBar - vote for me! - https://foo.bar",
        #auth=account
        )

    #example of registering a voter
    register_voter(stake,
        reg_id(account.instance_id),
        partial(account_authorized, account.instance_path),
        account.deposit,
        votes=['Mr. FooBar:VeZ7'],
        max_interest_offer=0.1,
        min_fee_request=0.1,
        #auth=account
        )

Example - Authorization

Transaqoin to authorize for unregistration, in a registration block:

from qointum.stakeholder import authorize_stakeholder, reg_id
from qointum.qointract.accounts import BasicAccount

account = BasicAccount('__sign__ Q1JvcReZAHTjaUj4pNG6KLzwqJ7SLtJq5JVaJGGvpeVF9Po8AucML')
#example of authorizing a delegate
authorize_stakeholder(reg_id('Mr. FooBar'), auth=account)
#example of authorizing a voter
authorize_stakeholder(reg_id(account.instance_id), auth=account)

Delegate Scheduling

Delegates are scheduled in randomized rounds with a block generation rate of 30 seconds. They are queued up to 30 minutes (60 delegates) ahead of time.

Details

  • selection is weighted by \(stake\_weight \cdot (6 - count\ of\ missed\ blocks\ in\ last\ 6\ scheduled)\), where each delegate has \(stake\_weight = \lfloor assigned\ stake\ /\ min\_stake \rfloor = usually\ 1\)
  • Selection is randomized by gathering \(\log{N}\) bits of blockchain entropy, where \(N\) is the total weight of all delegates. If the bit string is not less than \(N\) then it is shifted right with an entropy bit concatenated on the left and tested again, this is repeated until success. If out of entropy bits (negligible chance \(2^{-256}\)) then the result is taken as \(bit\ string \mod{N}\).
  • Each round consists of \(2 \cdot total\_stake\_weight\) blocks and ensures that every delegate registered at round start has a chance to participate at least once and at most 4 times. To ensure a more even round-robin ordering, selected delegates are removed from the candidate list for a quarter-round wait period. A delegate’s selection weight is doubled for each quarter-round elapsed since their last scheduling (weights carry into the next round). In the final quarter-round, if the number of yet unselected delegates is equal to the number of block positions left, then only those delegates will be considered for selection. The chance of falling back to such a forced selection given ~100 delegates is roughly \((1 - 2^3\ /\ 50)^{50} \approx 0.02\%\) per delegate, or \(1 - (1 - 0.02\%)^{100} \approx 2\%\) per round.

Do not miss blocks

If a delegate fails to create a valid block on schedule 3 times successively then they will be forcefully unregistered and their stake (not including assigned stake) will be forfeited to the next delegate.

No double block creating

A delegate will be penalized if they are caught double block creating, that is creating a block on another branch with an equal or later time than a block created on the current branch. A whistleblowing delegate may embed a violating signed block header into their block header, the violator will be forcefully unregistered and their stake (not including assigned stake) will be forfeited to the whistleblower. The whistleblower must also embed the chain of headers that connect the violating block to an ancestor no more than 1 round behind.

  • to facilitate parallel network processing a delegate must stream out signed incremental block updates:
    • the first message consists of a prototype block header (parent hashes, nonce, tx/state merkle root, tx count), and a list of transactions and their post-state merkle roots
    • prototype update messages consist of a header (the first prototype hash, sequence number, updated tx/state merkle root, new tx count), and a list of new transactions and their post-state merkle roots
    • the final message consists of a complete block header
    • the final merkle root represents the state after performing all implicit finalization duties such as distributing collected fees or rewarding stake interest
  • A delegate in queue must prove that they are ready between 60-90 seconds prior to becoming active by broadcasting their transaction preferences (used for transaction forwarding priority). If the delegate fails to provide proof then the position is considered vacant. QTC quorum members then broadcast their preferences, the member earliest in queue to respond within 30 seconds becomes the substitute and will begin their block early, however all blocks from the vacant position until the substitute member are considered missed.
  • to facilitate QTC and quick hand-off of block delegation, 60-90 seconds prior to becoming active each delegate will broadcast its signed node ID so that QTC quorum members can connect to neighbouring nodes via the Kademlia protocol
  • A delegate is limited to 5 transactions/sec (150 per block) and a block size of 1 MB, and provides up to 1 update/sec. If a block is missed then the next block is given increased data limits to compensate for the backlog of transactions. Average allowed transaction size is ~7 KB, enough for a few signatures and a few KB of transaqoin code + data. Peak network throughput given a steady update stream is \(1\ MB\ block\ /\ 30sec + 3\ KB\ update \approx 40\ KB/sec\).
  • Block time is stored as 4 bytes. First bit denotes absolute or relative time. Absolute time is relative to Jan 1, 2016, 31 bit seconds (max 68 years). Relative time is relative to last block, 15 bit seconds (max 9 hours) and 16 bit microseconds (min 15 μs).

Blockdag

blockdag.png
blockdag example

Qointum’s blockchain is structured as a “blockdag”, a directed acyclic graph of blocks. A block may have multiple parents and children so that branches may be merged. All branches are broadcast to the network, old or improbable branches are pruned as decided by the consensus algorithm.

Details

  • the number of parents is limited to 2, and the secondary subtree is limited to at most 10 blocks and all links must lead to common ancestors
  • the primary parent is always a delegate block, registration blocks must be merged in from the secondary branch
  • the secondary branch contributes its transactions only (no entropy, delegate queue, finalization, etc.)
  • To merge in a secondary branch the active delegate first verifies that its branches are not in conflict, that is transactions in one branch do not access state written to in the other branch. The delegate then attempts to run the sequence of secondary branch transactions before their own, and provides a list of valid transaction hashes and their post-state merkle roots. The collected fees are implicitly distributed to the delegate.

Branch merging is a feature planned for a future release

Branch merging is a complex scalability feature, however initially the block generation rate will be set low enough so that consensus will be stable.

Blockchain Entropy

Blockchain entropy is derived from the hash of “opened commitments”, that is, revealed secrets where delegates can prove that they selected their secret at an earlier date. Each delegate block gives a new 256-bit random number generated from the opened commitments of previous delegates.

The number will be random unless all delegates collude together, so the scheme requires only one honest delegate. To mitigate collusion, a delegate will be penalized if their commitment is opened early by a whistleblowing delegate, this also encourages secrets to be unguessably random. A delegate could also opt to miss their block and not reveal their secret (eg. to avoid a loss in a lottery draw), however the next number would remain random and such an action would incur a loss of voter confidence.

Details

  • In their block header each delegate commits the hash of a random secret \(\hash(b_i)\) and also opens their previously committed secret \(\hash(a_i)\) by revealing \(a_i\). The opened commitments of the current and last round of delegates are then hashed together in sequence \(a = \hash(a \concat a_i)\) to give the random number \(a\).
  • delegates publish their first commitment in their registration block
  • a block is invalid if the revealed secret does not match its commitment

No opening commitments early

A whistleblowing delegate may reveal another delegate’s secret early by embedding it in their block header. The penalty is the same as for double block creation.

Blockchain Consensus

The consensus chain is the longest delegate block chain found by the consensus heuristic Maximally Vetted Delegate Chain. This heuristic is efficient as it only requires block headers and a select number of signatures, making it suitable for SSV.

Maximally Vetted Delegate Chain

consensus_algorithm.png
Qointum’s consensus heuristic

This consensus heuristic follows the actions of delegates starting from the genesis block. Delegates vet for the integrity of a chain by appending blocks, which implicitly vets new delegates so that integrity can be assumed even if older delegates drop out of the chain. Chains with the largest amounts of vetted registered stake are favoured.

Details

  • new delegates begin with a score of 0 and when fully vetted attain the maximum score of their \(stake\_weight\) (usually 1)
  • a branch score is also calculated by adding up the scores of registered delegates, thus a greater amount of registered stake allows for a higher score
  • When a delegate who existed prior to a new registrant appends a block, the new registrant receives \(vetting\_factor = prior\ delegate\ score\ at\ reg\ time\ /\ total\_scores\_at\_reg\_time\). The registrant’s delegate score is then added to by \(vetting\_factor \cdot (assigned\ stake\ /\ min\_stake)\). Thus if all prior delegates append a block then a registrant and their delegate become fully vetted.
  • the genesis block begins with \(total\_scores\_at\_reg\_time = 0\), so the first delegate is immediately fully vetted
  • Unregistration has the same vetting effect as a delegate block. The registrant’s delegate score is then subtracted from by \(vetting\_factor \cdot (assigned\ stake\ /\ min\_stake)\), this amount is also removed from the branch score and \(total\_scores\_at\_reg\_time\) of future registrants.
  • a missed block has its delegate score temporarily removed from the branch score, the score may be later re-added if the delegate succeeds in creating a block
  • the blockdag is climbed in a breadth-first search as block headers of branches are incrementally downloaded, the branch to step next is determined by \(branch\ score\ /\ total\ branch\ scores\) – a highly vetted branch may be stepped many times before another gets a single step
  • If the end of a branch is reached and it is the current consensus (largest delegate block count), and the next best branch is over 1 round shorter or all branches have been exhausted, then a candidate consensus has been found.
  • A candidate consensus must pass a sanity check of select block signatures, this is done by both full nodes and SSV clients. If valid then full nodes proceed to download and verify its blocks, otherwise the search proceeds with the next branch.
  • Signatures eligible for the sanity check are those of the last blocks created by all current and past registrants. The parent block hash embedded in each block contains entropy from all previous blocks creating a secure chain, so a single signature from a registrant can cover all of their previous blocks. Given an estimated 5K current registrants and an estimated 5K ephemerals per year, nodes must store \((5K\ current + 5K\ ephemeral\ /\ year) \cdot 3\ KB\ signature\ \approx 15\ MB + 15\ MB\ /\ year\). All current delegates (~100) are verified. Current voters (~5K) are verified with a random 10% (500). Past registrants are verified with a random 10% spread evenly across 1-month periods. The amount of signature data required to verify 1 year is therefore \(100+500+5K \cdot 10\% \approx 3\ MB\).
  • once a node or SSV client has decided upon consensus it begins tracking that branch
  • In principle there should never be any large sudden shifts in consensus, so all sibling branches stemming from an ancestor more than 1 round behind are pruned. This gives all delegates a chance to validate the tracking branch.
  • If the active delegate attempts to split consensus by providing duplicate/conflicting prototypes within a 10 second span, then nodes who catch this attack will consider the current block potentially missed and allow the next delegate to backtrack one block if required.

Security Analysis

  • attackers without stake are unable to fool SSV clients as branches with fake balances and registrations will be slowed by lack of vetting and inactive delegates, resulting in a lower delegate block count
  • A group of 20 sequentially queued delegates could collude to double-spend by spending in one branch then creating a longer branch that undoes that spend. Recipients of large transactions over ℚ10K should wait up to 1 hour for confirmation (120 delegates) as delegates beyond 30 minutes are not yet known or queued, and consensus becomes more difficult to shift as more delegates become involved in creating blocks.
  • an attacker could buy up 50% of all stake on the market and create their own branch, however the cost of this attack would be astronomical as market prices rise with demand
  • To fool SSV clients, assuming the consensus branch has \(x\%\) of stake registered, a smaller percentage of vetted delegates \(x/(x+100)\%\) could spin off their own branch and attain a similar score by registering and vetting the remaining \(100-x\%\) of stake using fake balances. The fake branch would be slowed for a day as inactive delegates drop out, so to catch up the attackers could try to regularly miss their consensus branch blocks, however they risk their reputation as their stakeholders could revoke delegation. To remedy a successful attack, a new checkpoint would be included in SSV clients.
  • To fool new nodes/SSV clients, assuming the consensus branch has \(x\%\) of stake registered, an attacker could compromise \(x\%\) of unused private keys (through hacking or purchase) of earlier delegates and create a fake branch. New nodes would then accept payment from the compromised historical balances. Such an attack would be rare given the substantial number of keys required which all must share an active block range, and that there is an unregistration fee to penalize unused keys, and that the reputation of delegates is tied to their keys which may stay in use for months or years. To remedy a successful attack, a new checkpoint would be included in all clients.

Simplified State Verification

ssv.png
Simplified State Verification

Simplified State Verification (SSV) is a protocol used by light clients such as smartphones and entangled chains which do not have the resources to perform full verification, they instead rely on a weaker form of verification that requires no prior transaction or state knowledge.

The SSV client uses the consensus heuristic to determine the consensus branch, block headers and signatures are checked but the heuristic enables the client to forgo downloading and verifying full blocks. Once the consensus has been decided, the SSV client can then make requests to an SSV-providing node and verify its responses.

Details

  • the SSV client can publish a transaction
  • the SSV client can request any transaction within the past week by its hash which returns:
    • the including block hash, or signed prototype header if the request supports prototypes
    • the post-state merkle root
    • a tx/state merkle path verified against the tx/state merkle root in the header, which proves the tx is in the block
  • the SSV client can request any state (eg. account balance) within the past 10 minutes by a key which returns:
    • the block hash, or signed prototype header if the request supports prototypes
    • the tx hash and its post-state merkle root
    • a tx/state merkle path verified against the tx/state merkle root in the header, which proves the post-state is in the block
    • a value merkle path verified against the post-state merkle root, which proves the value is part of the post-state
    • the value
  • The SSV client can additionally request QTC votes along with a transaction request provided that the transaction was processed within the past 10 minutes and was confirmed. This request additionally returns:
    • the signed prototype header, all information is verified against it
    • the votes

Quorum Transaction Confirmation

instant_tx_confirm.png
Quorum Transaction Confirmation

itc_voting_violations.png
QTC voting violations - double and false-positive

Quorum Transaction Confirmation (QTC) provides a trade-off between less security and faster turn-around time. It is explicitly requested in transactions and can be trusted for small transactions of under ℚ10K.

QTC is performed by a quorum composed of the next 20 queued delegates (10 minutes worth). Quorum members vote to confirm consensus on a transaction so that the involved parties can have confidence that the transaction will not be undone. Consensus is unlikely to shift as quorum members are held to their vote by the risk of penalization and loss of voter confidence (similar to double block creation).

Details

  • Once the active delegate provides a block prototype containing an QTC, quorum members process it and vote by signing the prototype hash, transaction hash, and post-state merkle root. The transaction is confirmed as soon as a strong majority of at least 80% vote the same prototype and state.

No double or false-positive voting

Quorum members will be penalized if they are caught double voting (voting for two different states), or false-positive voting (voting in the majority then creating a block that is not a descendant). The method of penalization is similar to double block creation.

  • confirmation is expected to take 5-10 seconds as quorum members are connected to neighbouring nodes of the active delegate, and even unoptimized crytocurrency networks show ~3 second transaction propagation latency for the 90th percentile of nodes
  • The transaction recipient waits to receive votes for up to 30 seconds. Unresponsive quorum members are likely to miss their block, so unreceived votes are removed from the total when calculating the decision.
  • the transaction fee is implicitly split among the active delegate and any quorum members who produce a downstream block
  • A delegate is limited to 1 QTC/sec (30 per block) and QTCs are prioritized by transaction fee. Peak network throughput given a steady update stream is \(20\ delegates \cdot 3\ KB\ vote \cdot 1\ QTC/sec \approx 60\ KB/sec\).

Checkpoints

Checkpoints are block hashes that serve as consensus guideposts for new nodes joining the network. New nodes examine block headers to determine a candidate consensus branch before downloading and verifying full blocks, and a candidate must contain all checkpoints. Any blocks may serve as checkpoints, they are defined in a plaintext JSON file and are acquired from sources that the user trusts (eg. bundled with the client or downloaded from a community website).

Consensus state (registrations, signatures, scores) is included in the storage state, so new nodes only need to download a storage state snapshot corresponding to a checkpoint in order to determine the delegate queue.

Note

The Qointum team will provide a checkpoint file shortly after launch once all initial stakeholders have registered.

Nodes that have been offline and not tracking the consensus branch for a period of weeks to months are susceptible to rare attacks and may require a recent checkpoint. As chains mature through game-theoretic incentives and become operated by reputable long-standing delegates, older checkpoints can be relied upon.

Blockchain Compression

blockchain_compression.png
blockchain and storage compression

In order to maintain sane node hardware requirements (given the large quantum-secure signatures), the blockchain is pruned and compressed daily. By leveraging the consensus algorithm we target a blockchain size upper bound of 20 GB, growing by 1 GB per year. Nodes have the option of archiving old blockchain data and automatically distributing it to other nodes that demand a higher level of blockchain validation.

There is also qointract storage with its state snapshots and deltas, which varies between chains and typically uses ~20 GB.

Details

  • Data older than a week that is not required for consensus security is discarded. This gives a blockchain size upper bound of \(1\ MB\ block \cdot 20K\ blocks\ /\ week \cdot 1\ week \approx 20\ GB\).
  • Since old blockchain transactions are pruned it is not possible to determine whether a transaction has already been processed by simply looking up its hash. To ensure that transactions are processed only once, we require that they include an epoch counter that must match the current epoch. To smooth the transition between epochs, transactions from both the current and last epochs are accepted, and blockchain transactions from last epoch are not pruned until after the current epoch. This scheme gives an epoch time of half a week.
  • As per the consensus algorithm, we require all block headers, and signatures of only the last blocks created by all current and past registrants. This gives a growing blockchain size upper bound of \(0.2\ KB\ block\ header \cdot 1M\ blocks\ /\ year + 15\ MB\ signatures\ /\ year \approx 0.2\ GB\ /\ year\).

Sharding

sharding.png
sharding

Sharding is a scalability feature that allows for the transaction processing and state storage of a single chain to be divided among all nodes. The chain qoinel specifies the total number of shards, and delegates specify the number of shards they wish to administer. All delegates participate in the master chain which coordinates transactions and rollbacks, and also participate in their shard chains which are randomly assigned upon registration using blockchain entropy. The storage state is divided into shards by evenly partitioning the space of instance path hashes so that each qointract instance and system state group will reside on a single shard. Delegates then only need to process and verify transactions that read or write to their shard partition, and state from other partitions is embedded into transactions as SSV proofs verifiable against the master chain.

Sharding is a feature planned for a future release

Sharding is a complex scalability feature, however entangled chains already provide scalability as a kind of read-only sharding. Also, much of the machinery developed for entangled chains can be later shared with sharding.

Details

  • SSV requests must be made directly to the relevant shards (they are not routed internally). Clients are able to hash the keys themselves then request the total number of shards and a random list of relevant shard addresses.
  • Prior to publishing a transaction, the client preprocesses it performing SSV requests for every state read and logs every state write, and then embeds the SSV proofs and write log as transaction data. Nodes can then efficiently verify the proofs against block headers on the master chain.
  • The master active delegate verifies the fee payment using the embedded SSV proofs and prioritizes the transaction. The transaction is then streamed out in the next master block prototype update in an “unprocessed” list.
  • Upon receiving the master update, each shard active delegate examines the data of each transaction to determine if there are any reads or writes to its partition. Each delegate then processes the relevant transactions, verifying that state reads to its partition have embedded SSV values equal to its latest state (old proofs are valid as long as the value remains unchanged), and verifying that state reads to other partitions have valid embedded SSV proofs, and performing any state writes to its partition. Each delegate then streams out a shard block prototype update containing a “processed” list of each transaction hash, whether it’s valid, and if valid its post-state merkle root and fee amount.
  • Upon receiving the shard updates, the master active delegate verifies for each transaction that all relevant shard delegates (from the master’s perspective of shard consensus) have reported it to be valid with the same fee amount, and then creates a complete post-state merkle root from the latest shard states. Each transaction is then again streamed out in the next master block prototype update which contains an updated tx/state merkle root and lists of new valid/invalid/unprocessed transactions and their shard state deltas. Nodes can then apply the shard state deltas to their complete merkle tree so that they can respond to SSV requests with proofs verifiable against the master chain.
  • Upon receiving the master update, each shard active delegate rolls back any invalid relevant transactions, and then the process repeats for the “unprocessed” list. Each delegate then streams out a shard block prototype update which contains an updated tx/state merkle root and lists of new relevant valid/processed transactions.
  • Prior to publishing its block, the master active delegate must create a finalization transaction to perform any duties that are usually implicit. Here the fee amount of each transaction is distributed evenly to the master and relevant shard delegates. The master delegate must perform SSV requests and embed the proofs and write log. This transaction is then processed like the others.
  • Shard consensus shifts are handled on the master chain by creating a purge block to cut the master chain, then invalidating the cut transactions by examining their dependencies generated from SSV proofs and write logs, and then reprocessing the rest (excluding finalizations). The chain is cut at the earliest block containing a transaction invalidated by any shard consensus shifts. Each shard active delegate then creates their own purge block that contains the relevant reprocessed transactions.
  • A master consensus shift is handled on the shard chains by rolling back blocks until a common state is found and then branching. At this point the shard chain is stalled (all relevant transactions will be reported as invalid) until the master chain detects this consensus shift and creates a purge block.
  • Given 5 transactions/sec and 1 update/sec, total peak network throughput per shard given a steady update stream is \(5 \cdot 100\ B\ tx + 3\ KB\ update \approx 4\ KB/sec\).

Transaction Scripting

Qointum transactions are scripted via Transaqoins and Qointracts which are trustless automated rules written in Python that govern relationships between parties. Qointracts are “smart contracts” written by developers and published on the blockchain like reusable persistent code libraries. These contracts are invoked by transaqoins which are “smart transactions” or single-use ephemeral instructions. Qointum is designed so that transactions and contracts can be specified entirely within Python code and then operated on through the OS command-line from the developer’s preferred Python IDE.

Scripts are published as Python bytecode, and the human-readable source code may optionally be included. Whether the source code compiles to its corresponding bytecode is not verified by delegates due to the resource strain, however it can be verified independently through a command-line utility.

Python Language

Python-logo-notext.png
Python - Qointum’s language of choice for smart contracts

Smart transactions/contracts are simply Turing machines, there are many pre-existing languages that are adaptable to our needs and are already adept at interfacing humans with Turing machines.

We must restrict our language choices to those that provide a sandboxed virtual machine that ensures security and determinism when executing untrusted code. Each node must be able execute the same sequence of transactions and result in the same state, without crashing the client or compromising its host device. Also, as a shared-state Turing machine the virtual machine must be able to restrict access to code and data protected by the individual developers and users. Python, specifically its implementation PyPy, can meet these requirements.

Qointum is leveraging Python’s significant network effect – according to the TIOBE Index Python is in the top 5 virtual-machine languages and there are an estimated millions of Python developers world-wide. Qointum is also leveraging millions of man-hours of labour on the core Python language and third-party libraries, allowing us to focus on just developing a cryptocurrency, not a language.

PyPy provides bindings into native and efficient C code, Qointum’s API makes use of this feature when performing computationally intensive cryptographic methods. Also, PyPy provides just-in-time (JIT) compilation of interpreted Python bytecode to native machine code for improved performance.

Transaqoins

Transaqoins are Python scripts that specify transactions, they are compiled and published on the blockchain as single-use ephemeral instructions.

Invalid transactions

A transaction is invalid if:

Do not forward invalid transactions

Forwarding transactions where the fee payment can’t be verified against a recent state may result in a ban from peers.

No fee refund for invalid transactions

If an invalid transaction error occurs then the state will be rolled back to just after the fee and deposit payment. In this way delegates are guaranteed payment for processing transactions. However, if the error is handled by a runtime context then a refund on the deposit is still possible.

Nodes prioritize transactions for forwarding and block inclusion by the fee per KB, weighted by the transaction preferences broadcast by each delegate prior to becoming active.

Details

  • When a node first verifies a transaction it is forwarded along with its verified-against state and fee amounts for prioritization (if this info is invalid then the node may be peer banned). Peers then check that the payment is valid and unchanged against their current state before forwarding, and then the active delegate checks again before including it in their block.
  • any transactions received over 30 seconds prior which are still valid but not included in the current delegate block count as a mark against that delegate’s performance of % known transactions
  • the transaction priority is \((base\ fee\ /\ tx\ size + data\ fee) \cdot data\ weight + (runtime\ factor\ /\ tx\ size) \cdot runtime\ weight\)
  • the runtime weight should be based on the average runtime length for transaqoins on the chain
    • as a future consideration a machine learning algorithm such as a neural network may be employed to estimate each runtime length based on an examination of qointract method calls and arguments
  • In order to meet the 5 transactions/sec performance target, transaqoins on the main chain are limited to a runtime length of ℚ3 (~30 CPU-milliseconds). Transaqoins are also limited to a memory usage of 8 MB which fits into the shared cache of typical CPUs.

API

module qointum.transaqoin

pay_tx_fee(balance, base_fee, runtime_deposit, runtime_factor, data_fee=1)

  • Pay the transaction fee. Payment must be a balance of qoinel asset.

    This function must be called at the start of a transaqoin within a short runtime length of ℚ1, this length allows for the verification of several signatures.

    This function also builds a rollback state by committing all active qointracts.

refund_tx_runtime()

  • Determine the final runtime fee and refund the unused runtime deposit. Returns a balance of qoinel asset.

    This function must be called at the end of a transaqoin and the returned refund must be deposited within a short runtime length of ℚ0.1 (suggesting that the receiving account should already be authorized).

    This function also commits all active qointracts.

Transaction Fee

In order to maintain sane node hardware requirements, the Qointum network bounds transaqoin runtime length and memory usage through hard limits and fees. These hard limits and fees are allowed to vary between entangled chains in order to maintain separate economies.

The transaction fee consists of a base fee, a data fee per KB of transaction size, and a runtime factor that applies to the runtime instruction fees. An upfront runtime deposit must also be made since the runtime length of the transaqoin is unknown. The transaqoin specifies the fee and runtime deposit amounts, how to extract the fee and deposit, and how to refund any unused deposit.

Failure to pay transaction fee

A transaction is invalid if:

  • the fee and deposit payment is not made within a short runtime length
  • its runtime length exceeds its deposit
  • the refund is not deposited within a short runtime length

Bytecode Instruction Fees

Unless otherwise noted, all Python bytecode instructions have a runtime fee of µℚ2.5, which is based on a 3 GHz CPU processing ~40 million bytecode instructions per second at a fee of ℚ1 per ~10 CPU-milliseconds.

IMPORT_NAME(namei)

  • Fee amount depends on whether the import is a core Python/Qointum module cached in memory, a third-party library, or a qointract from the blockchain or entangled chain. Fee amount is pending benchmarking.

Native Function Fees

module qointum.utils

  • compress(data)
    uncompress(data)

    • Compress/uncompress data using Snappy. Fee amount grows with the size of data. Fee amount is pending benchmarking.

    hash(data)

    • Hashes data using BLAKE2. Fee amount grows with the size of data. Fee amount is pending benchmarking.

    verify_sig(sig)

    • Verify that the signature is correct. sig must be the result of a __sign__ postprocessor command. Fee amount is ℚ0.1.

Transaqoin Postprocessor

After compiling a transaqoin, the client needs to postprocess the resulting bytecode before it can be published on the blockchain in order to append data such as signatures or other bytecode. The client scans the transaqoin bytecode for string constants of the form __command__ [args…] , and replaces them with the processed result. Arguments are specified in the familiar GNU command-line format.

Commands

__package__ [options] <src>

  • Create a package from all pathnames matching src which is a glob relative to the current working directory. The result is a byte string of a pickled and compressed tree of directories (dict) and files (byte string). Any .py modules found in the glob are compiled and the resulting .pyc is included instead. The package __init__.py is optional. All directories found in the glob are recursed so that a package may contain subpackages importable via import <package path>.<subpackage>.... When recursing, dot-prefixed (.) hidden dirs/files are ignored.

    All parties signing the transaction must have a copy of the files being packaged as they must all arrive at the same transaqoin bytecode after compiling and postprocessing its source code.

    options

    --include-hidden
    -h

    • when recursing include any dot-prefixed (.) hidden dirs/files

    --include-src
    -s

    • include the .py module source files

    --uncompressed
    -u

    • do not compress the resulting pickled file tree

    --working-dir=<dir>
    -w=<dir>

    • Set the working directory so that src is relative to dir, the resulting file tree will then have all paths relative to dir. dir is itself relative to the current working directory.

__sign__ <address>

  • Sign the transaction using the corresponding private key of the public key address. The result is a byte string of the form <sign command> <signature>. If the client wallet does not have the private key then no action is taken.

    The client supports multi-party transactions by providing methods to export signatures from transaqoin bytecode and merge signatures into transaqoin bytecode.

    The transaction hash is taken prior to appending any signatures, and as signatures include their original commands they can be later replaced with their originals in order to verify the hash. This ensures that the hash can’t be changed in transit (avoiding the “transaction malleability” problem) as it represents all data covered by the signatures.

Example - Simple Transaction

from qointum.transaqoin import pay_tx_fee, refund_tx_runtime
from qointum.qointract.accounts import BasicAccount

#authorize a pre-existing account for withdrawal
my_account = BasicAccount('__sign__ Q1JvcReZAHTjaUj4pNG6KLzwqJ7SLtJq5JVaJGGvpeVF9Po8AucML')

#Withdraw 10 qoins to pay tx fee and recipient.
#The returned object is a conserved balance of asset type Qoin.
balance = my_account.withdraw(10)

#this must be called early within a short runtime length
pay_tx_fee(balance, base_fee=1, runtime_deposit=1, runtime_factor=1)

#Pay recipient 5 qoins, may be existing account or a new one.
#Crediting a balance by an amount transfers the amount into a new balance and returns the new balance.
recipient_account = BasicAccount('Q1K3LRJSg3xYB8V2APZB2Ch2YDLPTZ71bVNsQAmwV37cyV5UMgakH')
recipient_account.deposit(balance.credit(5))

#Refund unused runtime deposit, must be deposited within a short runtime length.
#Also deposit any unused balance.
#Debiting a balance transfers the other balance into itself and returns itself.
my_account.deposit(balance.debit(refund_tx_runtime()))

#printing a qointract outputs a json representation of its key/value store
print(my_account)
#"qointum.qointract.accounts.BasicAccount:Q1JvcReZAHTjaUj4pNG6KLzwqJ7SLtJq5JVaJGGvpeVF9Po8AucML": {
#   "balance": "ℚ3.75"
#}

Qointracts

Qointracts are Python classes that specify contracts, they are compiled and published on the blockchain as reusable persistent code libraries.

API

module qointum.qointract

class Qointract(instance_id=None, life_term=timedelta(days=90)) abstract extends Model, AttrProtect

  • a contract with storage access

    ctor

    __sys_init__(instance_id) protected

    • called by the system to initialize the qointract instance in order to perform maintenance such as collecting storage fees

    create_asset(asset) protected final

    • Create an asset type from asset which is a subclass of Asset. Returns the new asset type which is an instance of asset. This qointract will govern the new asset type, storing any garbage collected or discarded asset amounts.

    instance_id final

    • an ID associated with this qointract, ensures that this qointract is unique

    instance_path final

    is_new protected final

    • returns true if this qointract instance does not yet exist in storage

    pay_storage_fee(amount) protected

    • Called by the system periodically to collect storage fees. Must return a payment balance of qoinel asset within a runtime length of ℚ0.1 (suggesting that this method should be authorized for withdrawal without a signature). If this method itself allocates more storage (such as when a balance requires greater precision), then the system may call it again.

    qualname final

    • the fully qualified qointract classname, includes the containing chain, package and module names in the format: chain.<chain>.<package>.<module>.<classname>

Storage

qointum_storage.png
Qointum’s key/value storage

All qointract data is stored in a LevelDB key/value database and protected through a storage abstraction model in Python.

For SSV we require that the key/value store holds a single root state hash as well as a merkle path of hashes leading to the value to prove it is part of the state. The keys and values must be structured within the database in a way that these hashes can be efficiently updated when a single value is changed. For this purpose we store a radix tree in the database.

To facilitate bringing new full nodes up to speed on the chain, a compressed week-old storage snapshot is maintained as well as hourly state deltas. These snapshots and deltas are automatically distributed to new nodes. To catch up, new nodes simply apply the deltas to the snapshot and then process the remaining transactions over the past hour.

Details

Radix Tree
  • The tree uses a radix of 16. The keys are split up into nibbles (4 bits) and each nibble indexes into a node in the tree. Each node is labeled by either a single nibble or a string of common nibbles, as nodes with only one child are merged into their parent. When adding a key/value to the tree, if the key shares only part of a node’s label then that node must be split. When there are no more nodes to traverse, a leaf node is created which contains the value and is labeled by the remaining nibbles. A node is referenced by the BLAKE2 hash of its contents, which is used as a key into the database. To ensure performant leaf-splitting, a value is only embedded into a leaf node if it is smaller than its hash (32 B), otherwise it is referenced by hash like a node.
  • each node stores the size of its subtree, used to quickly determine storage fees
  • Each node has a child occupancy table with child elements of max size 32 bytes. To descend the tree, sum the sizes of children before the key nibble and seek to the child element.

Storage Fee

storage_fee.png
storage fee scaling

In order to maintain sane node hardware requirements, the Qointum network bounds qointract storage through hard limits and fees. These hard limits and fees are allowed to vary between entangled chains in order to maintain separate economies.

The storage fee is payed by qointract instances on a KB-day basis. Every qointract specifies a life term in days of up to 1 year, and how to extract the storage fee. To ensure a tight upper bound on storage size, the fee must be paid upfront upon storage allocation. At the end of a storage term the fee is distributed to all registered delegates. This effectively locks away qoins for the term allowing for long-term storage discounts while maintaining a fixed upper bound on storage size. Storage is automatically expanded and contracted as expired storage terms are renewed only if necessary.

Failure to pay storage fee

If a qointract fails to pay its storage fee when expanding storage then the transaction is deemed invalid. If a qointract fails to pay its storage fee when automatically renewing an expired storage term then the qointract instance is garbage collected and all stored data is lost.

For the storage fee on the main chain we target a storage size upper bound of 250 GB with a typical usage of ~10 GB as we only expect ~5% of qoins per year will be used for storage. Given 1 billion qoins and a minimum life term of 2 weeks, the maximum storage fee is then ℚ0.3 per KB-day (ℚ2 per KB-week), and we give a 5x scaling discount down to ℚ0.06 per KB-day (ℚ0.4 per KB-week) for a 1 year term. A typical account balance is ~0.1 KB so storage on a 1 year term would cost upfront ℚ2.

Details

  • The storage fee scales as \(min\ fee\ \cdot (term\ /\ max\ term)^{-1/2}\) clamped between the min/max fees. This structure gives a KB-day fee discount to longer terms while maintaining a lower upfront cost for shorter terms. This is the default chain setting and may be changed to any equation in the qoinel.
Storage Term Table
  • every storage expansion is a new entry in the qointract’s storage term table
  • storage is expanded in powers of two to limit the size of the term table
  • changes to the life term setting are effective for subsequent expansions and renewals

Storage Object Model

Qointum provides an abstraction layer to the underlying key/value store. This layer conveniently binds code to storage data, protects qointract metadata, and enforces asset conservation.

Qointracts are instantiated with IDs to become qointract instances, each referenced in the key/value store by a unique instance path <qointract>:<instance id>. Qointracts may require that their instance IDs include a Qointum address (eg. Q1Jvc...) for authorization, in this way a user may hold a large variety of account types and asset balances under one address.

A qointract is a model, which is a class that defines a data layout in storage. A model holds a set of fields which may in turn hold other models recursively. In this way a key path can be generated that looks like <field>.<field>... which is appended to the instance data path. A model may also hold field lists/dicts in order to utilize the underlying key/value store to its fullest extent.

Details

  • In a transaqoin only one qointract per instance path may be active at a time, this is to ensure that each instance remains in sync with its storage. Active qointracts are tracked within the runtime context stack. Active qointracts are committed to storage automatically on context change (usually on transaqoin exit).
  • A qointract can be saved (pickled) in an ObjectField which preserves all non-model attributes and permissions. Eg. an authorized account can be saved so that withdrawals may be performed in a later transaction without a signature. It is an error to load a qointract if a qointract with the same instance path is already active.

Qointract pickle support is disabled by default

Pickling often results in unexpected behaviour as only non-model attributes are preserved and the underlying storage can completely change between saving/loading. After thorough testing one may enable pickle support by setting a class attribute picklable = True.

  • qointracts may directly get/set/delete their instance data keys for debugging and cleanup
  • qointract instance data is prefixed with the key path <instance path>:data
  • key path size limits: qointract - 128 B, instance id - 128 B
  • key path character sets:
  • Qointum address specification:
    • \(address = \text{'Q'} \concat type \concat base58(data)\)
    • type - base58 string
    • for type 1:
      • \(data = public\ key \concat checksum\)
      • \(public\ key = merkle\ root\ (32\ B) \concat \log{max\ sigs}\ (1\ B)\)
      • \(checksum = \hash(public\ key)[:4\ B]\)
      • \(length = 1 + 1 + \lceil 37\ B \cdot \log{256}\ /\ \log{58} \rceil = 53\ characters\)
      • address example: Q1JvcReZAHTjaUj4pNG6KLzwqJ7SLtJq5JVaJGGvpeVF9Po8AucML
    • type z is reserved to extend the type space (eg. z11, z12zz1111)
Model Committing
  • models and field lists/dicts keep track of loaded objects, dirty flag, and dirty list
  • to commit we crawl the model hierarchy and write all loaded object/any fields unless dirty is false
  • asset fields keep track of dirty internally and are committed from their runtime context list

Model

module qointum.qointract

class Model abstract

  • a model holds a set of fields to define a data layout in storage

Fields

module qointum.qointract.fields

class Field(perms=None) abstract

  • Defines the value of a key in storage. perms is a (list of) access specifiers to be applied when this is a top-level field of a qointract.

class ObjectField(**kwargs) extends Field

  • Holds any Python object, which is pickled into storage. kwargs is forwarded to the base class.

    An object field resolves to the key <field>.

class AssetField(**kwargs) extends Field

  • Holds a balance of any asset type. If no balance is explicitly set into the field then by default it holds an empty balance of the qoinel asset. kwargs is forwarded to the base class.

    All balance objects are tracked in a special list <instance path>:assets.<field paths>. This list is managed privately by the system so that assets remain conserved.

class ModelField(model, **kwargs) extends Field

  • Holds an instance of the class model. This field does not directly store a value, instead its name becomes part of the field path for fields in the held model. kwargs is forwarded to the base class.

    A model field resolves to the keys <field>.<fields of held model>

class FieldList(field, **kwargs) extends Field

  • Maintains a list of field in the underlying storage, providing a performance advantage over storing a list in an ObjectField. kwargs is forwarded to the base class.

    Items resolve to the keys <field>.items.<indices>, the indices are stored in byte form. List size is also available at <field>.length.

class FieldDict(field, **kwargs) extends Field

  • Maintains a dictionary of field in the underlying storage, providing a performance advantage over storing a dict in an ObjectField. kwargs is forwarded to the base class.

    Keys are available in a list at <field>.keys.<hashes>, the hashes are stored in byte form. Values resolve to the keys <field>.values.<hashes>. Dict size is also available at <field>.length.

class AnyField(**kwargs) extends Field

  • Automatically selects the field type depending on the object set into it, allowing for heterogeneous lists and dicts, and recursive models like tree nodes. kwargs is forwarded to the base class.

    The disadvantage of the any field is that it must store its field type alongside the value. The value resolves to the key <field>.value. The field type is pickled at <field>.type.

Example - Fields

from qointum.qointract import Qointract, Model
from qointum.qointract.fields import (  ObjectField, AssetField, ModelField,
                                        FieldList, FieldDict, AnyField)
from qointum.asset import Asset

class Account(Qointract):
    #ObjectField
    name = ObjectField()
    #AssetField
    balance = AssetField()
    #ModelField
    class Address(Model):
        street = ObjectField()
        city = ObjectField()
    address = ModelField(Address)
    #FieldList
    address_history = FieldList(ModelField(Address))
    #FieldDict
    investments = FieldDict(AssetField())
    #AnyField
    class BitUsd(Asset): pass
    class BitEur(Asset): pass
    foreign_currencies = FieldDict(AnyField())
    class HeapNode(Model):
        value = ObjectField()
        left = AnyField()
        right = AnyField()
    exchange_bids = ModelField(HeapNode)
    exchange_asks = ModelField(HeapNode)

    def test_fields(self, payment):
        #ObjectField
        self.name = "John Smith"
        #AssetField
        self.balance.debit(payment)
        #ModelField
        city = self.address.city
        #FieldList
        self.address_history.append(self.Address("Main Street", "Capital City"))
        last_street = self.address_history[0].street
        #FieldDict
        self.investments['tax free savings'] = self.balance.credit(100)
        #AnyField
        self.foreign_currencies['bitusd'] = self.create_asset(self.BitUsd).mint(100) 
        self.foreign_currencies['biteur'] = self.create_asset(self.BitEur).mint(100)
        self.exchange_bids.value = 42
        self.exchange_bids.left = self.HeapNode()
        self.exchange_bids.left.value = 41
        self.exchange_bids.left.right = self.HeapNode()
        self.exchange_bids.left.right.value = 40

Accounts

An account is any qointract that is picklable and supports the following methods:

authorized

  • whether this account has been provided with the credentials (such as a signature) necessary for access to withdraw

deposit(balance, info=None)

  • Transfer out the entire asset amount held by balance into this account. info is a dict of information about the deposit request.

withdraw(amount, info=None)

  • Transfer a number amount of an asset out of the account, returning a new balance that holds the asset amount. info is a dict of information about the withdraw request.

API

module qointum.qointract.accounts

account_authorized(instance_path, account)

  • Simple handler to check if account is the correct instance and has been authorized (eg. by a signature)

class BasicAccount(address, account_id=None, **kwargs) extends Qointract

  • A simple account holding a balance of qoinel asset. The instance ID is constructed from <address>_<account_id>. If address is signed then this account will be authorized. kwargs is forwarded to the base class.

    balance protected

    • Returns the amount of asset held in this account as a Fraction. This method is publicly accessible with authorization.

Example - Basic Account

from qointum.qointract import Qointract
from qointum.qointract.fields import AssetField
from qointum.utils import sig_address, verify_sig

class BasicAccount(Qointract):
    perms = {'super': 'public'}
    picklable = True

    _balance = AssetField(perms='protected')

    @public
    def __init__(self, address, account_id=None, **kwargs):
        self.address = sig_address(address)
        self.account_id = account_id
        super().__init__('_'.join(filter(None, [self.address, self.account_id])), **kwargs)
        if verify_sig(address): self.authorize()

    @public
    @property
    def authorized(self):
        return self.has_perms({'withdraw': 'public'})

    @public
    def deposit(self, balance, info=None):          
        self._balance += balance

    @protected
    def authorize(self):
        self.grant_perms({'balance': 'public', 'withdraw': 'public'})

    @protected
    @property
    def balance(self):
        return self._balance.value

    @protected                      
    def withdraw(self, amount, info=None):
        return self._balance.credit(amount)

    @protected
    def pay_storage_fee(self, amount):
        return self.withdraw(amount)

Assets

An asset is a conserved value that can only be created or destroyed in a manner defined by its governing qointract. Assets can be transferred in a conserved manner from one balance object to another across both runtime memory and persistent storage. Qointracts may access their balances through the storage object model or enumerate them in a list.

Transactions must conserve assets

A transaction is invalid if it results in a non-zero balance being garbage collected. Non-zero balances must be explicitly discarded which transfers control of the balance to its governing qointract.

Each chain has its own “qoinel asset” (“ℚoin” on the main chain) accessed via from chain.self.qoinel import Asset.

API

module qointum.asset

class Asset abstract extends AttrProtect

  • An asset type can be created by defining a subclass and then instantiating it from its governing qointract via Qointract.create_asset. The asset should be defined within its governing qointract so that the qointract has access to its protected methods.

    An asset type is uniquely defined by its combination of qualname, instance_path, and instance_id.

    destroy(balance) protected

    • destroy an amount of this asset held by balance, leaving it with zero value

    discarded protected final

    • returns a balance that holds any garbage collected or discarded amounts of this asset

    instance_id final

    • a generated ID associated with this asset, ensures that this asset is unique

    instance_path final

    • the instance path of the governing qointract

    mint(amount) protected

    • create a number amount of this asset, returning a balance that holds the new amount

    prefix

    • Prepended to the value when representing an amount of this asset as a string. Eg. prefix results in ℚ3.75.

    qualname final

    • the fully qualified asset classname

    suffix

    • Appended to the value when representing an amount of this asset as a string. Eg. suffix ' FOO' results in 3.75 FOO.

class balance(asset) extends AttrProtect

  • Holds an amount of asset and ensures its conservation. The constructor creates an empty zero-value balance of type asset, which is an instance of Asset. The balance may be debited with another non-empty balance of the same asset type (such as the result of asset.mint) to increase its value, and credited with an amount to decrease its value.

    The amount is represented internally as a Fraction of arbitrary precision, qointracts that hold their assets in higher precision have to pay higher storage fees.

    Balances support all comparison ops (==, <, …) and arithmetic ops (+, -, …) with numbers and other balances, however these ops are unable to modify asset amounts, instead the balance is just converted to a number. Balances can also be converted to the standard number types (int, long and float) with some precision loss.

    +=(balance)

    asset

    • the asset type, an instance of Asset

    credit(amount)

    • transfers an amount out of this balance and returns a new balance holding the amount

    debit(balance, amount=None)

    • Transfers an amount (or all if none) from balance into this balance and returns this balance. balance must have the same asset type as this balance.

    discard()

    • empty this balance (leaving it with zero value) and return control of the asset amount to its governing qointract

    value

    • returns the amount of asset that this balance holds as a Fraction

Example - Company Shares

from qointum.qointract import Qointract
from qointum.qointract.fields import AssetField, ObjectField
from qointum.qointract.accounts import account_authorized
from qointum.asset import Asset

class FooCompany(Qointract):
    perms = {'super': 'public'}

    class Share(Asset):
        perms = {'super': 'public'}

        @public
        @property
        def suffix(self):
            return ' FOO'

    account = ObjectField()
    account_path = ObjectField()
    share = ObjectField()
    share_pool = AssetField()

    @public
    def __init__(self, account=None, **kwargs):
        super().__init__(**kwargs) #no instance id, FooCompany is a singleton
        if self.is_new:
            if not isinstance(account, Qointract) or not account.authorized:
                raise TypeError("new FooCompany must have authorized account")
            self.account = account
            self.account_path = account.instance_path
            self.share = self.create_asset(self.Share)
            self.share_pool = self.share.mint(100)
            self.authorize()
        else:
            if account_authorized(self.account_path, account):
                #the stored account is not accessible because an instance is already active,
                #so we must use the passed in instance
                self.account = account
                self.set_dirty('account', False)
                self.authorize()

    @public
    def buy_share(self, payment):
        if payment != 10: raise ValueError("share costs ℚ10")
        if not self.share_pool: raise RuntimeError("no more shares")
        self.account.deposit(payment)
        return self.share_pool.credit(1)

    def authorize(self):
        self.grant_perms({'maintenance': 'public'})

    def maintenance(self):
        #recover any garbage-collected shares
        self.share_pool += self.share.discarded

    def pay_storage_fee(self, amount):
        return self.account.withdraw(amount)

Attribute Permissions

Attribute permissions allow developers to protect certain code pathways and the data they modify. For example, an account qointract could require that the user provide a valid address signature in order to access the “withdraw qoins” method.

Any class that inherits from AttrProtect (such as Qointract) is provided with contextual access protection for its attributes. Permissions are specified as dict items ’<target>’: ‘<access specifier>’ using the familiar C++ style access specifiers and friend classes. Permissions can also be granted or revoked dynamically at runtime.

Targets

The permission target may be set to:

  • an instance or class attribute, which includes member/static variables and methods, and nested classes
  • a parent class or super (controls access to its inherited attributes)
  • the current class or self (controls access to all attributes)

Access Specifiers

By default all attributes have private access, making them only accessible within the context of their class or any nested / outer classes.

For attributes that reference methods or nested classes, only read/call access can be granted. These attributes can only be changed within the private context or be overridden by subclasses.

public

  • grant access in all contexts

protected

  • grant access in subclass contexts and only with instances of the same subclass as the context

<class or function>

  • grant access in the context of a fully qualified class or function name (eg. qointum.qointract.accounts.BasicAccount)

final

  • The attribute can not be changed after initialization. Once granted this specifier can not be revoked.

    If the target is a method then it can not be overridden by a subclass. Private methods are automatically declared as final.

    If the target is the current class then it can not be subclassed.

API

module qointum.qointract

class AttrProtect abstract

  • any class that inherits from this class is provided with contextual access protection for its attributes

    grant_perms(perms) protected

    has_perms(perms) protected

    • Returns true if all the specified permissions have been granted. See grant_perms.

    revoke_perms(perms) protected

Packages

In order to publish a qointract on the blockchain a package name must be reserved by registration. This reserved name allows the package to be imported via import chain.self.<package name>. There is a package registration fee to discourage squatters from reserving a large number of names. The package registration fee is ℚ100 per 3 months.

Package terms are automatically renewed and may be cancelled through unregistration. However, unregistration does not take effect for 2 weeks in order to protect users from unexpected changes, and during this period the original registrant can reclaim their name by providing authorization. Users are warned when importing a package that is undergoing unregistration.

Failure to pay package fee

A package will be unregistered if it fails to pay its fee.

Details

  • the package fee handler has the same runtime limit as the storage fee handler
  • The transaqoin containing the handlers/packager (if applicable) is stored without signatures. The storage fee is added to the package fee.
  • the package fee is distributed to delegates in the same way as the storage fee
  • package name size is limited to 32 B
  • package name character set is restricted to that of the Python identifier

API

module qointum.package

register_package(name, auth_handler, pay_fee_handler, packager, auth=None)

  • register a package on the blockchain

    name – a unique package name
    auth_handler – A handler to authorize full access to this package. The handler takes an argument auth which is an authorization object and returns true if authorization is successful.
    pay_fee_handler – A handler called by the system periodically to collect package fees. The handler takes arguments (amount, info) where amount is the required number of qoinel asset, and info is a dict of information about the fee. The handler must return a payment balance.
    packager – instance of the packager to initialize storage
    auth – object passed into the current authorization handler to reclaim this name if it was previously registered

unregister_package(name, auth)

  • unregister a package

    name – the package name
    auth – object passed into the authorization handler

Packagers

The packager is similar to a filesystem, it handles qointract publishing and importing and provides guarantees about how its qointracts are allowed to be changed. The packager is decided upon package registration and can only be changed by re-registering the package which requires a wait period. The registered packager can be imported via a special module path from <package path>.packager import Packager.

Failure to pay packager storage fee

If the packager fails to pay its storage fee when automatically renewing an expired storage term then the package itself will be unregistered.

Users of a qointract should review its packager and its package __init__.py

The packager has the ability to completely change the underlying qointract at any time, so users should review it and prefer one that guards against nexpected changes such as ImmutablePackager. Users should also review the optional package __init__.py (if it exists) which is executed upon import.

Built-in Packagers

module qointum.package.packagers

class Packager(**kwargs) abstract extends Qointract

  • Handles qointract publishing and importing. kwargs is forwarded to the base class.

class ImmutablePackager(account=None, **kwargs) extends Packager

  • This packager ensures that its qointracts can not be changed once published. For new instances account must be valid and authorized. For publishing access account must be authorized. kwargs is forwarded to the base class.

    isdir(path)

    • returns true if path is an existing directory

    isfile(path)

    • returns true if path is an existing file

    listdir(path)

    • list directory contents at path, returns list of entry names

    read(path)

    • read file contents at path, returns file bytes

    write(filetree, dirpath=None) protected

    • Write a tree of files to a directory path. Files are immutable, so this method raises FileExistsError if a file path already exists. This method is publicly accessible with authorization.

      filetree – a tree of directories (dict) and files (byte string), or the output of a __package__ postprocessor command
      dirpath – directory in which to write, will be created if it doesn’t exist

class MutablePackager(account=None, **kwargs) extends ImmutablePackager

  • This packager allows its qointracts to be changed at any time using the authorized account. For new instances account must be valid and authorized. kwargs is forwarded to the base class.

    write(filetree, dirpath=None) protected

    • write a tree of files to a directory path, overwriting any existing files

Example - Package Registration

from functools import partial
from qointum.transaqoin import pay_tx_fee, refund_tx_runtime
from qointum.qointract import Qointract
from qointum.qointract.accounts import BasicAccount
from qointum.package import register_package

#handler defined below for example
#from qointum.qointract.accounts import account_authorized
def account_authorized(instance_path, account):
    return (isinstance(account, Qointract) and account.instance_path == instance_path and
            account.authorized)

#we could define a custom packager class here
#class MyPackager(Packager): ...
from qointum.package.packagers import ImmutablePackager

#When registering the scope is "main".
#When the handlers and packager class are later unpickled this transaqoin is imported,
#so we must check the scope to avoid registering again in error.
if __name__ == '__main__':
    account = BasicAccount('__sign__ Q1JvcReZAHTjaUj4pNG6KLzwqJ7SLtJq5JVaJGGvpeVF9Po8AucML')
    pay_tx_fee(account.withdraw(2), base_fee=1, runtime_deposit=1, runtime_factor=1)

    register_package('foobar',
        partial(account_authorized, account.instance_path),
        account.withdraw,
        packager=ImmutablePackager(account),
        #auth=account
        )

    account.deposit(refund_tx_runtime())

Example - Qointract Publishing and Usage

from chain.self.foobar.packager import Packager

account = BasicAccount('__sign__ Q1JvcReZAHTjaUj4pNG6KLzwqJ7SLtJq5JVaJGGvpeVF9Po8AucML')
pay_tx_fee(account.withdraw(2), base_fee=1, runtime_deposit=1, runtime_factor=1)

#switch working dir to 'foobar' and create a package from its contents, publish as subpackage 'v1'
Packager(account).write('__package__ -w foobar .', 'v1')

account.deposit(refund_tx_runtime())
#import our published qointract
from chain.self.foobar.v1 import Foo

account = BasicAccount('__sign__ Q1JvcReZAHTjaUj4pNG6KLzwqJ7SLtJq5JVaJGGvpeVF9Po8AucML')
pay_tx_fee(account.withdraw(2), base_fee=1, runtime_deposit=1, runtime_factor=1)

#use our qointract
foo = Foo(account)
foo.bar('new data')

account.deposit(refund_tx_runtime())

Example - Immutable Packager

import pickle
from qointum.package.packagers import Packager
from qointum.qointract import Qointract, Model
from qointum.qointract.fields import ObjectField, ModelField, FieldDict, AnyField
from qointum.qointract.accounts import account_authorized
from qointum.utils import uncompress

class ImmutablePackager(Packager):
    perms = {'super': 'public'}

    @protected
    class Directory(Model):
        entries = FieldDict(AnyField())

    account = ObjectField(perms='protected')
    account_path = ObjectField(perms='protected')
    root = ModelField(Directory, perms='protected')

    @public
    def __init__(self, account=None, **kwargs):
        super().__init__(**kwargs)
        if self.is_new:
            if not isinstance(account, Qointract) or not account.authorized:
                raise TypeError("new packager must have authorized account")
            self.account = account
            self.account_path = account.instance_path
            self.authorize()
        else:
            if account_authorized(self.account_path, account):
                #the stored account is not accessible because an instance is already active,
                #so we must use the passed in instance
                self.account = account
                self.set_dirty('account', False)
                self.authorize()

    @public
    def isdir(self, path):
        cur = root
        for name in filter(None, path.split('/')):
            cur = cur.entries.get(name)
            if not isinstance(cur, self.Directory): return False
        return True

    @public
    def isfile(self, path):
        cur = root
        names = list(filter(None, path.split('/')))
        for name in names[:-1]:
            cur = cur.entries.get(name)
            if not isinstance(cur, self.Directory): return False
        file = cur.entries.get(names[-1])
        return isinstance(file, bytes)

    @public
    def listdir(self, path):
        cur = root
        for name in filter(None, path.split('/')):
            cur = cur.entries.get(name)
            if not isinstance(cur, self.Directory): raise FileNotFoundError('No such directory: ' + path)
        return list(cur.entries.keys())

    @public
    def read(self, path):
        cur = root
        names = list(filter(None, path.split('/')))
        for name in names[:-1]:
            cur = cur.entries.get(name)
            if not isinstance(cur, self.Directory): raise FileNotFoundError('No such file: ' + path)
        file = cur.entries.get(names[-1])
        if not isinstance(file, bytes): raise FileNotFoundError('No such file: ' + path)
        return file

    @protected
    def authorize(self):
        self.grant_perms({'write': 'public'})

    @protected
    def write(self, filetree, dirpath=None):
        if isinstance(filetree, bytes): filetree = pickle.loads(uncompress(filetree))
        cur = root
        cur_path = []
        for name in filter(None, (dirpath or '').split('/')):
            cur = cur.entries.setdefault(name, self.Directory())
            cur_path += name
            if not isinstance(cur, self.Directory):
                raise FileExistsError("Unable to create directory: " + '/'.join(cur_path))
        self.__write(filetree, cur, cur_path)

    def __write(self, filetree, cur, cur_path):
        for name, entry in filetree.items():
            if isinstance(entry, dict):
                next = cur.entries.setdefault(name, self.Directory())
                cur_path += name
                if not isinstance(next, self.Directory):
                    raise FileExistsError("Unable to create directory: " + '/'.join(cur_path))
                self.__write(entry, next, cur_path)
                cur_path.pop()
            else if isinstance(entry, bytes):
                if cur.entries.get(name) != None:
                    raise FileExistsError("Unable to create file: " + '/'.join(cur_path + [name]))
                cur.entries[name] = entry
            else
                raise TypeError("Unknown entry type")

    @protected
    def pay_storage_fee(self, amount):
        return self.account.withdraw(amount)

Tasks and Signals

Tasks and signals allow for endogenic transactions, that is transactions generated by internal processes as opposed to exogenic transactions received over the network. Endogenic transactions are treated exactly the same as exogenic transactions, delegates prioritize them by their fees and they are not guaranteed to be included in a block.

Tasks are used to schedule transactions at specific times or periodically, and signals are used to notify listeners about a state change. Sometime after a task is due or a signal has been triggered, at the discretion of the active delegate, the task/listener handler is executed as a transaqoin. Similar to an exogenic transaction, the handler must pay its transaction fee within a short runtime length so that delegates can efficiently verify the payment and prioritize it.

Entangled chains may also listen to signals on their partner chains. The active delegate polls its partner chains using SSV to detect signals, if a listener has been signalled then the delegate provides an SSV proof of the signal along with the listener handler.

The task/listener fee scales the same as the storage fee and has the same min/max life term with fees ranging from ℚ1 - ℚ0.2 per day. A task/listener may be cancelled at any time, and after cancelling the life term is protected where the original owner can reclaim their term by providing their ID and authorization.

Failure to pay task/listener fee

A task/listener will be cancelled if it fails to pay its fee.

Failure to pay task/listener handler tx fee

If a task/listener handler fails to pay its tx fee then the active delegate may include it in their block in which case the task/listener itself will be cancelled and its term fees distributed.

Details

  • Each triggered signal is stored for 1 hour along with a timestamp so that it can be detected using SSV. Signals may be triggered with arbitrary data arguments, however these arguments require a storage fee which is added to the current runtime fee.
  • the task/listener fee handler has the same runtime limit as the storage fee handler
  • The transaqoin containing the handlers (if applicable) is stored without signatures. The storage fee is added to the task/listener fee.
  • the task/listener fee is distributed to delegates in the same way as the storage fee
  • task/listener IDs may be generated automatically and are restricted to the same size and character set as instance IDs

API

Tasks

module qointum.task

class crontab(minute=’*‘, hour=’*‘, day_of_week=’*‘, day_of_month=’*‘, month_of_year=’*‘)

  • provides detailed cron-like scheduling

    minute – A (list of) integers in range [0,59] that represent the minutes of an hour. May also be a crontab pattern (eg. every half hour minute='*/30', or on the hour and every two minutes of the last half hour minute='0,30-59/2').
    hour – A (list of) integers in range [0,23] that represent the hours of a day. May also be a crontab pattern (eg. every two hours hour='*/2', or at midnight and every two office hours hour='0,9-17/2').
    day_of_week – A (list of) integers in range [Sunday=0, Saturday=6] that represent the days of a week. May also be a crontab pattern (eg. every weekday day_of_week='mon-fri', or every even numbered day day_of_week='*/2').
    day_of_month – A (list of) integers in range [1,31] that represent the days of the month. May also be a crontab pattern (eg. every even numbered day day_of_month='*/2', or the first and third weeks of the month day_of_month='1-7,15-21').
    month_of_year – A (list of) integers in range [1,12] that represent the months of the year. May also be a crontab pattern (eg. the first month of every quarter month_of_year='*/3', or every even numbered month month_of_year='*/2').

class Task(id=None)

  • Holds a task ID. If id is none then one will be generated when schedule is first called.

    cancel(auth)

    • cancel the scheduled task

      auth – object passed into the authorization handler

    schedule(handler, auth_handler, pay_fee_handler, period=None, delay=None, life_term=timedelta(days=90), auth=None)

    • schedule a task for execution

      handler – the function to execute when the task is due, must be a transaqoin (pay tx fees, etc.)
      auth_handler – A handler to authorize full access to this task. The handler takes an argument auth which is an authorization object and returns true if authorization is successful.
      pay_fee_handler – A handler called by the system periodically to collect task fees. The handler takes arguments (amount, info) where amount is the required number of qoinel asset, and info is a dict of information about the fee. The handler must return a payment balance.
      period – Execute task every period amount of time. If none then the task will execute only once. The recurring period is specified as a timedelta or for finer control a crontab.
      delay – Delay first execution for an amount of time specified as a timedelta. If none then the task will be delayed for the amount of time specified by period.
      life_term – reserve object for term (determines fees)
      auth – object passed into the current authorization handler to reclaim the ID if it is in use

Signals

module qointum.signal

class Listener(id=None)

  • Holds a listener ID. If id is none then one will be generated when listen is first called.

    listen(signal, instance_id, handler, auth_handler, pay_fee_handler, life_term=timedelta(days=90), auth=None)

    • execute a function when a signal is triggered

      signal – the signal to listen to, must be a type
      instance_id – the ID of the object to listen to, as provided when triggered
      handler – the function to execute when triggered, must be a transaqoin (pay tx fees, etc.) and accept all args provided by the signal
      auth_handler – A handler to authorize full access to this listener. The handler takes an argument auth which is an authorization object and returns true if authorization is successful.
      pay_fee_handler – A handler called by the system periodically to collect listener fees. The handler takes arguments (amount, info) where amount is the required number of qoinel asset, and info is a dict of information about the fee. The handler must return a payment balance.
      life_term – reserve object for term (determines fees)
      auth – object passed into the current authorization handler to reclaim the ID if it is in use

    unlisten(auth)

    • stop listening to the current signal

      auth – object passed into the authorization handler

class Signal(instance_id, *args, **kwargs) abstract protected extends AttrProtect

  • A signal can be triggered to notify listeners about a state change.

    A signal can be created by defining an empty subclass. The signal should be defined within the class (eg. a qointract) that requires access to its trigger. The signal should specify an attribute args as a list of argument names that will be provided to listeners (for self-documentation only). Eg. class foo_done(Signal): args=['name']. Listeners need access to the signal type object, so the signal should be decorated with the appropriate access specifier (eg. @public).

    A signal can be triggered by instantiating it which notifies listeners of the object with ID instance_id. args and kwargs are forwarded to the listener handlers. Eg. self.foo_done(self.instance_id, 'bar').

Example Task - Lottery

from functools import partial
from datetime import timedelta
from random import randrange
from qointum.qointract import Qointract, Model
from qointum.qointract.fields import ObjectField, ModelField, FieldList
from qointum.qointract.accounts import account_authorized
from qointum.task import Task, crontab
from qointum.signal import Signal
from qointum.transaqoin import pay_tx_fee, refund_tx_runtime, RuntimeContext

class Lottery(Qointract):
    perms = {'super': 'public'}

    account = ObjectField()
    account_path = ObjectField()
    task = ObjectField()
    pot = AssetField()
    class Ticket(Model):
        reward_account = ObjectField()
    tickets = FieldList(ModelField(Ticket))

    @public
    class draw_done(Signal): args=['winner']

    @public
    def __init__(self, instance_id, account=None, **kwargs):
        super().__init__(instance_id, **kwargs)
        if self.is_new:
            if not isinstance(account, Qointract) or not account.authorized:
                raise TypeError("new lottery must have authorized account")
            self.account = account
            self.account_path = account.instance_path
            self.task = Task()
            self.authorize()
        else:
            if account_authorized(self.account_path, account):
                #the stored account is not accessible because an instance is already active,
                #so we must use the passed in instance
                self.account = account
                self.set_dirty('account', False)
                self.authorize()

    @public
    def buy_ticket(self, payment, reward_account):
        if payment != 10: raise ValueError("ticket costs ℚ10")
        if not isinstance(reward_account, Qointract): raise TypeError("invalid account")
        if not self.task.scheduled: self.start_draw() #we have a player so start the draw
        if self.task.due: raise RuntimeError("drawing in progress, please try again later")
        self.pot += payment
        self.tickets.append(self.Ticket(reward_account))

    def authorize(self):
        self.grant_perms({'cancel_draw': 'public'})

    def start_draw(self):
        self.task.schedule(self.draw,
            partial(account_authorized, self.account.instance_path),
            self.account.withdraw,
            #period=timedelta(hours=1),     #draw every hour
            #draw every 4 hours and every office hour on weekdays
            period=crontab(minute=0, hour='*/4,9-17', day_of_week='mon-fri'),
            delay=timedelta(hours=1),       #delay first draw for at least an hour
            auth=self.account
            )

    def cancel_draw(self):
        #refund payments
        for e in self.tickets:
            refund = self.pot.credit(10)
            try:
                with RuntimeContext(max_length=0.1):
                    e.reward_account.deposit(refund)
            except Exception as e:
                self.account.deposit(refund)
                print("problem with account: ", e)
        del self.tickets[:]
        self.task.cancel(auth=self.account)

    def draw(self):
        #begin task handler, pay tx fee
        pay_tx_fee(self.account.withdraw(2), base_fee=1, runtime_deposit=1, runtime_factor=1)
        #reward 70% to winner, 30% to lottery owner
        winner = self.tickets[randrange(len(self.tickets))]
        winner_id = "Unknown"
        prize = self.pot.credit(self.pot * 0.7)
        try:
            with RuntimeContext(max_length=0.1):
                if not isinstance(winner.reward_account, Qointract): raise TypeError("invalid account")
                winner_id = winner.reward_account.instance_path
                winner.reward_account.deposit(prize)
        except Exception as e:
            self.account.deposit(prize)
            print("problem with account: ", e)
        self.account.deposit(self.pot) 
        #restart lottery
        del self.tickets[:]
        self.task.cancel(auth=self.account)
        #trigger signal
        self.draw_done(self.instance_id, winner_id)
        #end task handler, refund tx runtime
        self.account.deposit(refund_tx_runtime())

    def pay_storage_fee(self, amount):
        return self.account.withdraw(amount)

Example Listener - Lottery Logger

from functools import partial
from datetime import datetime
from qointum.transaqoin import pay_tx_fee, refund_tx_runtime
from qointum.qointract import Qointract
from qointum.qointract.accounts import BasicAccount, account_authorized
from qointum.signal import Listener
from chain.self.lottery import Lottery
from chain.self.logger import Logger

#we assume there is some logger qointract that just appends log entries to a FieldList
def log_draw(account, logger, winner):
    #begin listener handler, pay tx fee
    pay_tx_fee(account.withdraw(2), base_fee=1, runtime_deposit=1, runtime_factor=1)
    #log the winner
    logger.log('{}: {}'.format(datetime.now(), winner))
    #end listener handler, refund tx runtime
    account.deposit(refund_tx_runtime())

#When setting up the listener the scope is "main".
#When the handler is later unpickled this transaqoin is imported,
#so we must check the scope to avoid setting up again in error.
if __name__ == '__main__':
    account = BasicAccount('__sign__ Q1JvcReZAHTjaUj4pNG6KLzwqJ7SLtJq5JVaJGGvpeVF9Po8AucML')
    pay_tx_fee(account.withdraw(2), base_fee=1, runtime_deposit=1, runtime_factor=1)

    #log the lottery with instance id 'foo'
    logger = Logger('Lottery_foo', account)
    listener = Listener()
    listener.listen(Lottery.draw_done, 'foo',
        partial(log_draw, account, logger),
        partial(account_authorized, account.instance_path),
        account.withdraw,
        #auth=account
        )

    account.deposit(refund_tx_runtime())

Runtime Contexts

Runtime contexts allow for invalid transaction errors to be localized and handled, and also allow for the runtime length of arbitrary code to be limited. For example, an account deposit method may error because the account can’t pay its storage fees or it gets stuck in an infinite loop, and if left unhandled the entire transaction would be rolled back with tx fees wasted. By wrapping the deposit method in a new runtime context, these errors can be handled.

When an invalid transaction error occurs all changes to qointracts, assets, and storage made within the runtime context are rolled back. The transaction may then continue after handling the error.

A runtime context is created by wrapping a code block in a with statement that instantiates transaqoin.RuntimeContext.

Details

  • runtime contexts can be nested and form a stack of contexts
  • Each context tracks a list of qointracts activated and assets created within it (tracked references are kept private as otherwise they could be exploited). Upon exiting a context its qointracts are deactivated and its assets are garbage collected, any dangling references will point to a None object.
  • to build a restore state on error, prior to entering a context all active qointracts are committed to storage, and all active qointracts and assets are pickled
  • upon exiting a context all active qointracts are again committed to storage, this allows the context to handle errors and limit the runtime length of arbitrary storage processes
  • When an invalid transaction error occurs, the prior state is restored and the call stack is rolled back to the stack frame containing the context block and an exception is raised. If an error is encountered when restoring state then the error recurses to the parent context.

To enter a runtime context all active qointracts must support pickling

All active qointracts must be able to have their state reverted and be reused without unexpected behaviour. Not all qointracts support pickling due to its complexity. If a qointract does not support pickling then if possible move its instantiation into the body of the context block (it will be garbage collected on context exit).

Test qointracts for partial execution cases

Unexpected behaviour may result when a qointract method is run again after erroring out mid-execution due to an imposed runtime length limit. Qointracts must not write to global attributes as they are not restored after an error.

API

module qointum.transaqoin

class RuntimeContext(max_length=None)

  • Create a new runtime context and push it onto the stack. max_length is the max allowed runtime length of all execution within the context measured in units of the qoinel asset.

Example - Runtime Deposit Guard

A transaqoin that uses a runtime context to guard against losing its runtime deposit on error:

from qointum.transaqoin import RuntimeContext

my_account = BasicAccount(...)
pay_tx_fee(my_account.withdraw(2), base_fee=1, runtime_deposit=1, runtime_factor=1)

try:
    #localize any errors of account load/save and deposit, and limit runtime length
    with RuntimeContext(max_length=0.1):
        recipient_account = BasicAccount(...)
        recipient_account.deposit(my_account.withdraw(10))
except Exception as e:
    print("problem with account: ", e)

my_account.deposit(refund_tx_runtime())

Qointum HTTP

qhttp.png
Qointum HTTP (Web 3.0 Protocol)

Qointum HTTP (QHTTP) is a next-generation “Web 3.0” protocol that serves entire websites over a decentralized peer-to-peer blockchain network. It is implemented as a web browser plugin and activated for URLs that begin with the protocol identifier qhttp://.

The website contents are stored on-chain and served via SSV requests distributed over multiple peers. The retrieved HTML / Javascript documents may contain qoinapps which in turn make further SSV requests to dynamically populate the page and provide user interaction.

A Web 3.0 model-view-controller (MVC) Python framework is included in order to handle HTTP requests entirely on the client side. When a user visits a website, their QHTTP client downloads and runs MVC code from the website’s corresponding chain. This MVC code routes any requested URLs to views, which in turn generate HTTP responses containing either HTML rendered from on-chain templates and qointract data, or on-chain media (eg. CSS, images). This framework can be understood as a client-side Web 3.0 variant of Django (the server-side framework that powers this website).

Entangled Chains

entangled_chains.png
Entangled Chains example

Entangled chains enable Qointum to scale up across the web providing a vast array of services. Entangled chains are like separate databases with connected value, or filters on the Qointum network, nodes can selectively process transactions and store data of only desired services. Through this selective ability, entangled chains can operate with separate transaction and storage fees depending on the economic value of data transferred and stored on each chain.

Each chain is initialized with a qoinel that generates its qoinel asset used for operational fees and specifies a list of entangled chains and other parameters. A chain may bootstrap its qoinel asset from any number of entangled partner chains using SSV.

SSV proofs can be embedded into transactions enabling entangled chains to communicate with each other and exchange value. For example:

  • A user first sends qoins to an exodus qointract on the main chain which tracks the sender address/balance. The user then initiates a generation transaction on the partner which fetches the stored address/balance via SSV and embeds the address signature and merkle paths. The partner qoinel then verifies the signature against the fetched address, and merkle paths against the main chain block headers, before finally generating a qoinel asset for the user.

Note

The main chain does not initially have any entangled chains, it permits SSV requests from other chains but not transaction SSV proofs due to the resource strain on nodes. However, the main chain qoinel will enable the Qointum community to change the list of entangled chains as “core” chains may become established (such as a redundant storage service).

Details

  • while verifying a tx fee payment, if the transaqoin imports a package from a partner chain then it is marked as an SSV transaction
  • The client preprocesses SSV transactions against the current state, performing any necessary SSV requests, and embeds the SSV proofs as transaction data. Nodes can then efficiently verify the proofs against block headers on partner chains.
    • qointracts have access to metadata about the proofs and may accept proofs of a less secure but short wait recent partner state, or proofs of a more secure but long wait old partner state
    • transactions do not support proofs containing prototype headers as they are not linked into the partner chain and so provide weak verification
  • delegates preprocess endogenic SSV transactions in the background up to 2 minutes prior to becoming active and later embed the proofs as transaction data
    • proofs embedded in endogenic transactions must be from a recent (within the last 2 minutes) partner state

The active delegate does not perform SSV requests for missing values

The local state can change between the time that a transaction was preprocessed and the time that the active delegate processes it, potentially changing the required set of SSV values. For this reason entangled qointracts must operate in epochs where for each input the corresponding set of SSV values remains unchanged within an epoch.

Qoinels

The chain qoinel is a core kernel composed of founding smart contracts that define the chain, govern its operations, and govern its interactions with entangled chains. The genesis block of each chain contains an initial revision of its qoinel. More specifically, the qoinel governs the creation and destruction of its qoinel asset that is used for operational fees, specifies chain parameters such as stake interest rate, restricts delegate and package registration, and restricts packages imported off entangled chains. The qoinel may include decentralized governance such as polls to allow for portions of the qoinel itself to be updated.

Note

Prospective stakeholders should carefully review a chain’s qoinel before taking a position in its qoinel asset. Distribution of the asset may be complex as it may be bootstrapped from other assets on entangled chains.

Entanglement Modes

weak_entanglement.png
weak entanglement example

Chains have 2 modes of entanglement to partner chains, strong and weak (default).

In strong entanglement when a consensus shift occurs on the partner, nodes always re-verify past transaction SSV proofs (up to 4 hours prior). This securely ties value to the partner, however a misbehaving partner with frequent consensus shifts may invalidate blocks on the chain causing collateral damage.

In weak entanglement nodes only re-verify proofs with an age lower than some limit (default 10 minutes), reducing collateral damage. Proofs older than the age limit are just assumed to be valid and may be re-verified only to warn the user about misbehaving partners.

Purge Blocks

purge_blocks.png
purge block example

In order to handle a consensus shift on an entangled partner chain gracefully with minimal collateral damage, the active delegate may additionally create a purge block. Purge blocks cut, squash, and branch the chain in order to remove the invalidated SSV proof and any dependent transactions. In this way the consensus score for the branch (vetting and length) and most transactions are kept intact, as opposed to a trivial rollback where all progress would be lost.

Details

  • a purge block is attached to the chain as a sibling to the earliest block containing a transaction with an invalid SSV proof
  • nodes only seek a purge block if they detect that the current branch contains an SSV proof not permitted by the entanglement settings
  • a purge block contains a full copy of the cut chain, a list of transaction hashes that contain an invalid SSV proof, and a list of valid transaction hashes and their post-state merkle roots
  • Nodes revert state and attempt to run the sequence of cut chain transactions. Nodes verify that each transaction with an invalid SSV proof is in the provided list, then mark as invalid all state key writes performed in the transaction. Any other transactions that read from these keys become invalidated themselves and their state writes are marked as invalid. In this way a dependency graph of invalidated transactions is built and then purged, giving a new consensus state with minimal changes. The collected fees are implicitly distributed to the delegate who created the purge block.

Entangled Third-Party Services

Miscellaneous Examples

  • a decentralized encrypted social network where only friends can decrypt and view your profile
  • a government-issued digital currency: authorities grant currency amounts and account creation privileges to authorized dealers such as banks, and currency flow into other anonymous chains is restricted
  • a decentralized asset exchange: enables derivatives and hedging
  • a decentralized prediction market: users bet on the outcomes of external events, the outcomes are determined by a consensus of user reports
  • a decentralized news website: earned karma can be exchanged for assets
  • a decentralized auction website: users maintain a web of trust and where trust is lacking the buyer and seller put up collateral (eg. ℚ1 each) into a contract – if both parties fail to agree then the collateral is automatically sent to an arbitrator or released at a later date
  • Decentralized Autonomous Corporations
  • Smart Oracles

Decentralized Redundant Storage

Details

Select an encrypted file to store and generate at least \(24 \cdot 365\) random suffixes (one for every hour for a year) and hashes \(\hash(file \concat suffix)\). Build a merkle tree out of alternating leaves where one leaf is \(\hash(file \concat suffix)\) and the next leaf is \(null\), and publish the merkle root in a qointract while distributing over BitTorrent the file + merkle tree without leaves + partial suffixes. Load up the qointract balance with \(24 \cdot 365\) qoins and the qointract rewards ℚ1 to anyone who provides a merkle path which includes a missing leaf. Provide enough bits in the partial suffixes so that on average, the network of nodes hashing the file will guess a full suffix once per hour. The qointract keeps track of which leaves have already been guessed, and adjusts the reward according to the hash rate moving average.

The owner must monitor the blockchain in case the hash rate drops to low levels, at that point the owner should retrieve the file via direct download from any available nodes using qointracts that parties update for each MB transferred.

Ensemble Messaging Network

Ensemble.png
Ensemble messaging network

Qointum’s messaging network “Ensemble” is used for private communication that doesn’t require state consensus or long-term data storage. Ensemble is a decentralized peer-to-peer network that provides post-quantum encrypted communication and plausible deniability as to the recipient identity. Ensemble supports asynchronous communication as messages are given a time to live (TTL), so a message sent earlier can be received at any later time. Python and Javascript APIs are provided for developer access to Ensemble.

Nodes connect to random peers and prefer peers who deliver messages on topics of interest, while underperforming peers are dropped. Nodes advertise topics that their peer neighbourhood is interested in, and this advertisement becomes more fuzzy as hop distance to the neighbourhood increases. The more general the topic, the less recipient identity revealed. Routing requires only a logarithmic number of hops with respect to the size of the network, similar to the Kademlia protocol. Ensemble may be accessed from Tor to provide physical routing privacy.

For private communication messages may be optionally encrypted, and to identify the sender messages may be signed. An Ensemble ID consists of an encryption key pair and a signature key pair, messages may be sent to and from IDs. To facilitate recovery of lost IDs, the hierarchical deterministic (HD) wallet holds a tree of IDs generated from a mnemonic seed.

To mitigate spam each message must include a proof of stake, this ensures incentive for an operational service. Due to this asset dependence each chain operates its own Ensemble network.

Each message is assigned a priority which determines the amount of bandwidth and storage allocated to transmit it. The priority is based on peer neighbourhood interest in the topic, the TTL, and the amount of stake proven relative to other messages in queue. Messages with short TTLs are prioritized, so as nodes reach their storage limits longer TTLs are bumped off the network.

Details

  • Topic privacy can be customized in a percentile range. A bit mask is applied to the topic based on the privacy value. Both the masked topic and the mask itself are advertised to peers and used to match against messages.
  • Before advertising topics collected from peers, their bit precision is halved by zeroing out bits in the mask. Thus every hop exponentially reduces topic precision (128, 64, 32…).
  • Every node independently specifies its max bandwidth and storage, with defaults of 50 KB/s (~10 messages per second), and 100 MB (~20K high TTL messages). These limits are shared across all Ensemble networks.

Stake proof requires unique asset sources

Proven stake is tracked by its asset sources (ie. asset keys), an asset source can’t be used in more than one proof.

  • on the main Ensemble network the minimum amount of stake that must be proven is ℚ100
  • Ensemble IDs are 4 KB so they require 2 QR codes (version 40-M) or ~700 base58 characters.

Security Analysis

  • sender identity may be revealed through stake proofs as they’re linked to the blockchain
  • a spammer could move their proven stake in a transaction and then provide a new proof of the same stake to double spam, so proofs must be periodically verified

API

module qointum.ensemble

class Ensemble(chain=None)

  • Provides access to the Ensemble network on chain. If chain is none then the default chain (specified in the client config) is used.

    class Message

    • a message passed to the listener handler

      data – the payload
      topics – a list of associated topics
      from – source ID
      to – destination ID
      ttl – time to live in seconds

    join()

    • wait for the internal thread to complete its execution

    listen(handler, topics=None, to=None)

    • Execute handler when a new message arrives from the network with any matching topics (or if none then accept all topics) and decrypt it using destination ID to. The handler takes an argument msg which is the new Message.

    post(data, topics=None, from=None, to=None, ttl=None)

    • post a message to the network

      data – the payload
      topics – a list of associated topics
      from – source ID, sign the message using this ID
      to – destination ID, encrypt the message using this ID
      ttl – Time to live in seconds. Nodes will attempt to store the message for this amount of time and prefer shorter ttls.

Example - Stealth Payments

An Ensemble ID can be advertised as a public payment address (eg. on a website), yet when recorded on the blockchain all payments are made to new addresses that have no discernible link to the advertised address. The payer uses the ID to discreetly request a single-use address from the recipient over Ensemble.

Note

Qointum stealth payments just provide convenient operational security rather than a new cryptographic algorithm.

Stealth payee service, run by the client qointumd:

from qointum.ensemble import Ensemble
from qointum.wallet import Wallet, IdBank

my_id = IdBank.get('my web shop')

ensemble = Ensemble()

def on_message(msg):
    if not msg.from: return   #the payer must include their generated id (encryption key)
    #creates a quantum-secure address quickly but it can only be signed a small number of times
    single_use_address = Wallet.create_address(parent='stealth payments', max_sigs=256)
    ensemble.post(single_use_address, topics=['stealth payments'], from=my_id, to=msg.from, ttl=5)

ensemble.listen(on_message, topics=['stealth payments'], to=my_id)
ensemble.join()