Skip to content

Commit

Permalink
Merge #2618
Browse files Browse the repository at this point in the history
2618: Multi-Asset Migration Algorithm r=jonathanknowles a=jonathanknowles

# Issue Number

ADP-839

# Overview

This PR adds an algorithm for performing migrations of multi-asset UTxO sets from one wallet to another.

It consists of four main parts:

1. Module `Migration`, which provides a top-level public API, defined in terms of concrete wallet types.
2. Module `Migration.Planning`, which contains an algorithm for planning migrations at a high
level. It determines how to partition a UTxO set into entries of different types, and in which order to add UTxO entries to selections.
3. Module `Migration.Selection`, which provides functions for incrementally constructing a
selection to be included in a migration plan. (A selection is the basis for an individual transaction.)
4. The `TxConstraints` type, which provides an abstract cost model for transactions, allowing parts of a transaction can be costed individually. This allows the incremental cost of extending a selection to be calculated without having to recalculate the cost of the entire selection.

# Performance

For pure ada wallets, we can process around 30,000 UTxO entries per second. 

We can process around 3,000 UTxO entries per second with wallets of the following composition:

| Percentage | UTxO Entry Type |
| -- | -- |
| 10% | Pure ada entries below the minimum ada quantity |
| 40% | Pure ada entries with at least the minimum ada quantity |
| 40% | Multi-asset token bundles with precisely the minimum ada quantity |
| 10% | Multi-asset token bundles with more than the minimum ada quantity |

# Design

## UTxO partitioning

The algorithm starts by partitioning the UTxO set into three types of UTxO entry:

| Type | Definition |
| -- | -- |
| **_supporter_** | an entry that is capable of paying for itself |
| **_freerider_** | an entry that is **not** capable of paying for itself |
| **_ignorable_** | a dust ada entry that should not be included because its value is less than the marginal cost of an input. |

## Adding entries to transactions

The algorithm uses a **supporter** entry to initialize each transaction, but then gives priority to adding **freerider** entries until the transaction is full. It adds a **supporter** entry only when there's not enough ada available to pay for an additional **freerider** entry.

<img src="https://user-images.githubusercontent.com/206319/115514178-6c521b80-a2b6-11eb-808f-92b4c679b99a.png" alt="extending a selection" width="300px">

## Creating outputs within transactions

The algorithm minimizes the number of outputs for each transaction (and thus minimizes the fee required) by merging together all value provided by the inputs. Due to the nature of our transaction constraints (which limit the size of an output), the algorithm usually produces transactions with between 1 and 4 outputs.

# Requirements

The algorithm in this PR is designed to satisfy the following requirements:

-  **REQ-1**
  The algorithm _must_ succeed at selecting all pure ada entries greater than `txBaseFee` + `minAdaQuantity` + `marginalFeeForInputAndOutput`.
- **REQ-2**
  The algorithm _must_ succeed at selecting all multi-asset entries whose ada quantities are greater than or equal to `txBaseFee` + `minAdaQuantity` + `marginalFeeForInputAndOutput`.
- **REQ-3**
  The algorithm _should_ make a best effort to select entries whose ada quantities are less than `txBaseFee` + `minAdaQuantity` + `marginalFeeForInputAndOutput`, but is permitted to omit such entries if it cannot find a viable strategy to select them.
- **REQ-4**
  The algorithm _must_ produce **T**: a set of transactions for which fees are balanced, and **R**: the remainder of the UTxO set that could not be selected, such that the inputs of **T** and the entries in **R** are disjoint, and such that the union of inputs in **T** and entries in **R** is equal to the original UTxO set.
- **REQ-5**
  The algorithm _must_ preserve the balance of every non-ada asset, so that for any given asset **a**, the total quantity of asset **a** in the selected inputs is exactly equal to the total quantity of asset **a** in the generated outputs.
- **REQ-6**
  The algorithm _must not_ produce transactions with dependencies between them; it should be possible to broadcast the generated transactions in any order.
- **REQ-7**
  The algorithm _must not_ produce outputs with token quantities that exceed the maximum allowed in an output `(2^64 − 1)`.
- **REQ-8**
  The algorithm _must not_ produce outputs whose serialized length exceeds the maximum allowed length (`4000` bytes).
- **REQ-9**
  The algorithm _must not_ produce transactions that exceed the transaction size limit (`16384` bytes).
- **REQ-10**
  The algorithm _must_ move the reward balance in its entirety.
- **REQ-11**
  The algorithm _must_ be deterministic: if run twice on the same UTxO set, it must generate exactly the same migration plan. (This is necessary to support an API where a non-hardware wallet user can generate a plan, inspect the plan, and then initiate a migration without having to resubmit the plan.)

# Non-Requirements

- **NON-REQ-1**
  Preserving the UTxO distribution. 

Co-authored-by: Jonathan Knowles <[email protected]>
  • Loading branch information
iohk-bors[bot] and jonathanknowles authored Apr 22, 2021
2 parents dbc752e + 2445551 commit 99a6ff9
Show file tree
Hide file tree
Showing 10 changed files with 3,118 additions and 1 deletion.
8 changes: 8 additions & 0 deletions lib/core/cardano-wallet-core.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ library
, persistent
, persistent-sqlite
, persistent-template
, pretty-simple
, profunctors
, quiet
, random
Expand Down Expand Up @@ -175,6 +176,9 @@ library
Cardano.Wallet.Primitive.AddressDiscovery.SharedState
Cardano.Wallet.Primitive.SyncProgress
Cardano.Wallet.Primitive.CoinSelection.MA.RoundRobin
Cardano.Wallet.Primitive.Migration
Cardano.Wallet.Primitive.Migration.Planning
Cardano.Wallet.Primitive.Migration.Selection
Cardano.Wallet.Primitive.Model
Cardano.Wallet.Primitive.Types
Cardano.Wallet.Primitive.Types.Address
Expand Down Expand Up @@ -284,6 +288,7 @@ test-suite unit
, network
, network-uri
, persistent
, pretty-simple
, regex-pcre-builtin
, OddWord
, ouroboros-consensus
Expand Down Expand Up @@ -357,6 +362,9 @@ test-suite unit
Cardano.Wallet.Primitive.AddressDiscovery.SharedStateSpec
Cardano.Wallet.Primitive.AddressDiscoverySpec
Cardano.Wallet.Primitive.CoinSelection.MA.RoundRobinSpec
Cardano.Wallet.Primitive.MigrationSpec
Cardano.Wallet.Primitive.Migration.PlanningSpec
Cardano.Wallet.Primitive.Migration.SelectionSpec
Cardano.Wallet.Primitive.ModelSpec
Cardano.Wallet.Primitive.Slotting.Legacy
Cardano.Wallet.Primitive.SlottingSpec
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ data SelectionResult change = SelectionResult
-- ^ An optional extra source of ada.
, outputsCovered
:: ![TxOut]
-- ^ A list of ouputs covered.
-- ^ A list of outputs covered.
-- FIXME: Left as a list to allow to work-around the limitation of
-- 'performSelection' which cannot run for no output targets (e.g. in
-- the context of a delegation transaction). This allows callers to
Expand Down
77 changes: 77 additions & 0 deletions lib/core/src/Cardano/Wallet/Primitive/Migration.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedLabels #-}

-- |
-- Copyright: © 2021 IOHK
-- License: Apache-2.0
--
-- This module provides a public API for planning wallet migrations.
--
-- Use 'createPlan' to create a migration plan.
--
module Cardano.Wallet.Primitive.Migration
(
-- * Creating a migration plan
createPlan
, MigrationPlan (..)
, RewardWithdrawal (..)
, Selection (..)
, TxSize (..)

) where

import Prelude

import Cardano.Wallet.Primitive.Migration.Selection
( RewardWithdrawal (..), Selection (..), TxSize (..) )
import Cardano.Wallet.Primitive.Types.Coin
( Coin )
import Cardano.Wallet.Primitive.Types.Tx
( TxConstraints (..), TxIn, TxOut )
import Cardano.Wallet.Primitive.Types.UTxO
( UTxO )
import Data.Generics.Internal.VL.Lens
( view )
import Data.Generics.Labels
()
import GHC.Generics
( Generic )

import qualified Cardano.Wallet.Primitive.Migration.Planning as Planning

-- | Represents a plan for migrating a 'UTxO' set.
--
-- See 'createPlan' to create a migration plan.
--
data MigrationPlan size = MigrationPlan
{ selections :: ![Selection (TxIn, TxOut) size]
-- ^ A list of generated selections: each selection is the basis for a
-- single transaction.
, unselected :: !UTxO
-- ^ The portion of the UTxO that was not selected.
, totalFee :: !Coin
-- ^ The total fee payable: equal to the sum of the fees of the
-- individual selections.
}
deriving (Eq, Generic, Show)

-- | Creates a migration plan for the given UTxO set and reward withdrawal
-- amount.
--
-- See 'MigrationPlan'.
--
createPlan
:: TxSize size
=> TxConstraints size
-> UTxO
-> RewardWithdrawal
-> MigrationPlan size
createPlan constraints utxo reward = MigrationPlan
{ selections = view #selections plan
, unselected = Planning.uncategorizeUTxO (view #unselected plan)
, totalFee = view #totalFee plan
}
where
plan = Planning.createPlan
constraints (Planning.categorizeUTxO constraints utxo) reward
Loading

0 comments on commit 99a6ff9

Please sign in to comment.