Skip to content

Latest commit

 

History

History
240 lines (138 loc) · 18.5 KB

0051-safecoin-alternative-implementation.md

File metadata and controls

240 lines (138 loc) · 18.5 KB

Safecoin Alternative Implementation

  • Status: postponed
  • Type: new feature
  • Related components: SAFE Vault, SAFE Client Libs
  • Start Date: 15-09-2018
  • Discussion:
  • Supersedes: N.A.
  • Superseded by: N.A.

Summary

Implementation details of safecoin; the currency of the SAFE Network. This RFC describes a different approach to implementing safecoin compared to that proposed in RFC #0012.

Conventions

  • The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.

Motivation

The motivation for implementing safecoin is well described in RFC #0012. The motivation for this particular RFC is to present an alternative approach which has several advantages over the other:

  • Simpler
  • Fairer for farmers
  • Faster completion of transactions
  • Less overhead for the network to manage (less data, smaller account structures, fewer messages)
  • Coin divisibility can be handled trivially
  • Doesn't require deletion of data (hence adhering to Network Fundamentals) when recycling coins
  • Simpler to implement pre-farmed state for network startup

Detailed design

Overview

The crux of this proposal is that safecoin don't exist as units of data representing individual coins (or parts of coins), but rather only balances of safecoin exist. These balances will be in the form of values in client-owned CoinAccounts and PaymentAccounts and as section-wide values representing the sections' farmed totals.

First, let's define the form of these balances:

struct Coin {
    units: u32,
    parts: u32,
}

The units field will represent whole safecoins, and since the defined upper limit of issuable safecoin is 2^32, this need be no bigger than a u32.

The parts field represents a multiple of "250 pico-safecoins", ie the number of 250 * 10^-12-th parts of a single safecoin. The total value of the parts field will be required to be less than a single safecoin, i.e. it will always be less than 4 billion.

To represent client-owned accounts for holding safecoin, the following will be used:

struct CoinAccount {
    id: PublicSigningKey,
    balance: Coin,
}

As with other network addressable entities, the id will represent the account's address on the network. It will be managed by the close group of Vaults for that address, using a new persona: CoinManager. The ID is also an asymmetric public signing key.

A single client can create many CoinAccounts if desired, and there is no need to associate any of these with its main MAID account. (In this context, MAID stands for MaidSafe Anonymous ID, and any user wishing to send requests to the network which require payment must first create a MAID account. This is not a CoinAccount, but rather a record held by the network used to hold e.g. the MAID public key and the public keys of authorised apps). For example, a user may want to run multiple farmer Vaults and allocate a different CoinAccount for each to receive the farming rewards.

When a client creates a new CoinAccount on the network, the request need only specify the id, since the initial balance will always be 0. It is up to the client to create and manage the key pairs of all CoinAccounts it owns. These could be stored on the network as part of its encrypted MAID account, or managed by a standalone wallet application.

To pay for network resources (e.g. putting data) a PaymentAccount must exist where the account's ID is the same as the MAID's public key:

struct PaymentAccount {
    details: CoinAccount,
    app_rules: BTreeMap<PublicSigningKey, AppRules>,
}

enum AppRules {
    Unrestricted,
    SpendLimit {
        remaining: Coin,
    }
}

The details of the PaymentAccount will be explained below at "Paying for Network Resources". Until we reach that point, just consider this as a wrapped CoinAccount and when CoinAccount is mentioned it means either of these types.

Farming

Each section of the network will be responsible for a proportion of the total issuable safecoin equal to the proportion of the network address space it manages. By "responsible for", we mean responsible for managing the amount of farmed coin from that address space by ensuring it doesn't exceed the allotted amount or drop below zero. For instance, at the start when the network has a single section covering the full address space, that section will be responsible for all 2^32 safecoin. When the section splits, each half will be responsible for 2^31 safecoin.

This means that for a section with Prefix length n, it will be responsible for 2^(32-n) safecoin.

Each section's CoinManagers will maintain a record farmed (of type Coin) of the amount of safecoin farmed at that section. Any changes to this total will come as a result of an action having passed through Parsec in order to ensure that all managers maintain an eventually consistent record.

The section's farmed value will never be allowed to exceed the amount of coins for which that section is responsible. Ideally no section should ever get close to "running out" of farmable coins; getting the farming rate algorithm correct should ensure that. However, in the case that a section has farmed all of its coins, it will stop issuing any more until the farmed value reduces again.

The actual algorithm defining the farming rate is somewhat orthogonal to this RFC and worthy of its own RFC, so the details will be omitted, but it would seem fairly critical that it ensures sections don't become fully farmed or fully unfarmed.

One benefit of providing coin divisibility is that on a successful farming attempt, it is trivial to pay all farmers in that section rather than the lottery approach taken in the alternative proposal whereby only one farmer is paid at a time, and only if the chosen coin is unfarmed. Indeed, they could be paid proportional to the value they have provided to the network. They could also be paid smaller amounts more frequently to encourage new Vault owners to continue to run their Vaults.

To send a payment, the CoinManagers will send a Credit message to the destination CoinAccount's section (the destination address defining the particular CoinAccount::id):

struct Credit {
    amount: Coin,
    transaction_id: Option<u64>,
}

(The transaction_id will be explained in the "Transferring to a Different CoinAccount" section below, but when sending farming rewards, it will always be None.)

The CoinManager persona will be responsible for managing all aspects of farming within their section. This will include among other things:

  • calculating the farming rate
  • maintaining a mapping of Vault to CoinAccount::id for each Vault in their section (i.e. where to pay successful farming attempts)
  • sending Credits to these CoinAccounts when a farming attempt is successful, and increasing the farmed total for the section
  • receiving Credits from other sections and updating the corresponding CoinAccounts

When a section X splits into X0 and X1, each new section will start with half of X's final farmed value. If farmed.parts is odd, rather than rounding, X1 will be allocated the extra 1 part.

When two sections merge, the new section's farmed value will be the total of their final farmed values.

When handling a received Credit, if the specified CoinAccount doesn't exist, the coin will be recycled by decreasing that destination section's farmed value by the specified Credit::amount. It would perhaps seem more intuitive to return a failure message to the source section, since that's the farmed value which was increased, and hence it seems fairer to recycle the coin back into that source section. However, handling this would involve more traffic and more code, and such "unfairness" is likely to become fair overall when applied equally across all sections.

When a farming vault first starts, the user must specify an associated CoinAccount::id. This will be persisted by the network when the vault gets relocated. The client can supply a new ID if such a request is signed by the old ID. If no initial ID is provided, that vault will never earn farmed coin (its proportion will never be sent and will remain unfarmed at the source).

Other Rewards

CoinManagers will also be responsible for paying other rewards, i.e. those for application developers and maintainer developers. The details of how these actual CoinAccounts are managed are left for a future RFC, but the IDs of these accounts will be known to the section's CoinMangers so that rewards can be paid into them using Credit messages.

As with farming, when a reward is paid, the section from which it is paid will increase its farmed total accordingly.

Paying for Network Resources

When a client wants to consume network resources which requires payment (e.g. when putting data), it will send such a request to its MaidManagers (as is currently implemented). However, before approving this request, the MaidManager Vaults will deduct appropriate payment from the PaymentAccount with the same ID as the MAID. Since the client's MAID account and PaymentAccount have the same ID, each vault which is a MaidManager will also be a CoinManager for that account; so no extra network messages will be needed to take the payment.

Furthermore, to handle the case where a client sends multiple such requests, but doesn't have sufficient balance to cover them all, the receiving Vaults will pass the requests through Parsec in order to get an agreed order of these requests. In that way, there will be eventual agreement amongst them about which transactions are valid and which aren't.

As per the existing Vault implementation, once approved, the MaidManagers will forward the request to the appropriate DataManagers. This will continue to be the case, but the request will also now include a Coin field specifying the amount which the client paid.

When handling these requests from MaidManagers, the DataManager Vaults decide whether the request can be actioned or not. If it can, these Vaults will have their CoinManager personas deduct the paid amount from their section's farmed total, effectively recycling the coin and making it farmable again. If the request can't be actioned, the DataManagers send a failure response back to the MaidManagers (as per the existing implementation). On receipt of such a failure message, the client's CoinManagers will refund the amount to its PaymentAccount.

When recycling the coin, the farmed total will not be allowed to drop below zero. If the amount to be deducted exceeds the farmed value, then the excess coin will be sent by this section to a randomly-chosen neighbouring section for recycling there. As with farming, we should ensure that the farming rate algorithm makes such a case of having a section which is fully unfarmed exceptionally unlikely. Nonetheless, having a farmed value of zero isn't reason for DataManagers to reject client requests to pay for network resources at that section.

The MaidManagers currently already handle a single MAID account being associated with multiple "app authentication keys", allowing different applications different rights with regards to data mutation. This notion will be extended to include spend limits on the Client's PaymentAccount from which it will make payments.

When an app makes a payable request, the client's PaymentAccount will be checked to see if the app has the required balance available to it. If there is an entry corresponding to that app's keys in PaymentAccount::app_rules then:

  • for AppRules::Unrestricted, the app has permission to use the full balance of PaymentAccount::details::balance, and that balance is reduced accordingly
  • for AppRules::SpendLimit { remaining: Coin }, the app has permission to use only remaining from PaymentAccount::details::balance. If there is enough in both, then both values are reduced accordingly.

These rules can be extended if required to increase the flexibility of managing spend-limits for apps.

Transferring to a Different CoinAccount

(Note, when mentioning CoinAccount from this point onwards, we mean both CoinAccount and PaymentAccount unless otherwise indicated.)

When a client wants to transfer some coins from a CoinAccount it owns to some other one it may or may not own, it will send a CoinTransfer to the CoinManagers of its source CoinAccount (the address defining the particular source CoinAccount::id):

struct CoinTransfer {
    destination: PublicSigningKey,
    credit: Credit,
}

The Credit::transaction_id is an optional value which can be agreed beforehand by the owners of the two CoinAccounts and can be used by either to confirm that the transfer has been completed (details below).

As with paying for network resources, before acting upon such client requests, the CoinManagers must pass the requests through the same instance of Parsec in order to ensure that they agree which requests to accept or reject.

On receipt of a CoinTransfer request (after passing out of Parsec), the CoinManagers will deduct the amount specified in credit from the source account. If the account's balance doesn't permit this, the request will be silently dropped.

These source CoinManagers will then send the Credit to the destination CoinManagers, where it will be run through Parsec to cover the case where that owner is concurrently trying to spend coin from that CoinAccount.

If the account to be credited doesn't exist, the destination CoinManagers will refund the coin to the sender by sending another CoinTransfer with the same credit value as the original. The client will receive no notification that the refund happened. Well-designed client applications should be able to reduce the risk of accidentally transferring coins to invalid addresses to zero, so multiple refunds to an individual account should be seen as malicious behaviour.

Each CoinAccount will have an associated fixed-length FIFO queue for holding the most recent transaction_ids and amounts of Credits made to that account. When crediting the destination CoinAccount, the CoinManagers will push the transaction_id and amount onto that queue if the transaction_id is not None. At this stage, the transaction is complete.

Any client will be able to query for the existence of such a completed transaction by sending a GetTransaction message to the destination CoinAccount's section (the destination address defining the particular CoinAccount::id) and expecting to receive a Transaction in response:

struct GetTransaction {
    transaction_id: u64,
}

enum Transaction {
    Success {
        value: Coin,
        transaction_id: u64,
    },
    NoSuchTransaction,
    NoSuchCoinAccount,
}

A CoinManager receiving a GetTransaction will always return a Transaction. If the specified CoinAccount doesn't exist, NoSuchCoinAccount is returned. If the CoinAccount does exist, but the requested transaction_id doesn't exist in its FIFO queue, NoSuchTransaction is returned. Otherwise the appropriate Success value is returned. As this message is signed by the Vaults comprising the CoinManagers, a Transaction::Success can be used as proof by any client, (in particular the sender) that the transaction was successful.

This has the benefit of requiring neither the sender nor the receiver to stay connected while the payment is being processed, as would be the case for example if the sender needed to receive a response in order to prove payment had been made. It also serves to further anonymise the sender, as the recipient will likely not be aware of the actual CoinAccount used by the sender to credit its own; only the transaction_id is visible.

Drawbacks

  • Loses marketing difference of "physical" coins vs adjusting a number in a record

Alternatives

The original proposal for a safecoin implementation is detailed in RFC #0012.

Unresolved questions

  • We may need to handle the case where a section is receiving payment (e.g. for a Put request), but its farmed amount is less than the payment, meaning the coins can't all be recycled by that section. This would seem to imply a failure of the farming rate, but potentially we'd need to cover that situation, e.g. by having that section forward a specific new type of message to a neighbour section which can handle recycling that amount of coins.

  • If charges for consuming network resources weren't variable from section to section, we could omit the Coin field from the requests which MaidManagers send to DataManagers. (For example, if we have a fixed, constant rate of x safecoin per TB for Put, then the amount of safecoin could be calculated independently at the MaidManagers and the DataManagers.)

  • Instead of GetTransaction and Transaction, we could possibly use push notifications to notify the sending and receiving clients of a completed transaction.

  • It's unclear at the moment how to discourage spamming the network with CoinTransfer requests for tiny amounts without doing something like charging a minimal amount for handling transfers. It's also similarly unclear how to discourage the creation of excessive numbers CoinAccounts by a malicious client. This could potentially be handled by the proxy nodes. A further spam scenario (possibly less likely to be detectable by the proxies unless they're also the MaidManagers) is sending many CoinTransfer requests to non-existent addresses in order to get refunds.

  • The original proposal suggested allowing new MAID accounts to be given a small amount of safecoin for free to be used exclusively for paying for network resources. This may still be required, and would probably require special handling of such CoinAccounts. However, with the approach described in this RFC, pre-farming may be enough to obviate the need for such special handling.

  • Using Parsec should ensure that a malicious proxy can't replay a client's CoinTransfer or other chargeable request, since Parsec observations need to be unique. However should this not be the case (e.g. in light of Parsec pruning), then an alternative mechanism would need to be found to prevent this attack. Examples could be requiring clients to send requests via a quorum of proxy nodes, or to specify a sequence/version number which would be held in the CoinAccount.