taler-docs

Documentation for GNU Taler components, APIs and protocols
Log | Files | Refs | README | LICENSE

092-incremental-backup-sync.rst (110717B)


      1 ===========================================
      2  DD 92: Incremental Wallet Backup and Sync
      3 ===========================================
      4 
      5 :Design status: Accepted
      6 :Implementation status: Prototype
      7 :DD shepherd: TBD
      8 :Historical contributors: Iván Ávalos, Christian Grothoff
      9 :First published: 2026-03-26
     10 :Last substantive change: 2026-08-18
     11 :Implementation evidence: ``taler-typescript-core`` (2026-08-12); ``taler-android`` (2026-08-07; 2026-08-09); not merged into the reviewed HEADs
     12 :Normative references: ``core/api-sync.rst`` (vBACKUP is upcoming; the current v2 API says no component uses Sync)
     13 
     14 Summary
     15 =======
     16 
     17 This design document describes an incremental, CRDT-based, encrypted wallet
     18 backup and sync protocol that addresses the limitations of previous solutions.
     19 
     20 Motivation
     21 ==========
     22 
     23 An encrypted backup and sync protocol for wallets was the subject of three
     24 design documents (`DD05`_, `DD09`_ and `DD19`_), in which considerations for
     25 different aspects of backup and sync, as well as limitations of the proposed
     26 designs, were discussed and documented, ultimately resulting in a
     27 proof-of-concept server and wallet implementation.
     28 
     29 .. _DD05: https://docs.taler.net/design-documents/005-wallet-backup-sync.html
     30 .. _DD09: https://docs.taler.net/design-documents/009-backup.html
     31 .. _DD19: https://docs.taler.net/design-documents/019-wallet-backup-merge.html
     32 
     33 In the original design, an object containing a set of data entities managed by
     34 the wallet is serialized, gzip-compressed, kilobyte-padded and encrypted using
     35 libsodium's `secretbox`_ function using a symmetric key derived from the
     36 wallet's root key and a salt.
     37 
     38 .. _secretbox: https://libsodium.gitbook.io/doc/secret-key_cryptography/secretbox
     39 
     40 The resulting block is then uploaded to a sync server configured in the
     41 wallet, where it can be later recovered by another wallet and decrypted. It is
     42 at this point where conflicts with the existing database are resolved on a
     43 last-write-wins CRDT fashion, favoring deletion in concurrent, conflicting
     44 insert/delete operations.
     45 
     46 Since the data entities contained in the backup represent the state of the
     47 entire database at a given timestamp, the backup and restore operations
     48 described are not incremental and therefore not practical for synchronization
     49 between multiple devices, as the database can grow in size indefinitely,
     50 slowing down backup and restore operations over time.
     51 
     52 The revised solution proposed in this design document aims to address the
     53 limitations of the previous design by introducing an incremental, CRDT-based,
     54 end-to-end-encrypted wallet backup and sync protocol that is robust,
     55 efficient, reliable, and suitable for use between multiple devices.
     56 
     57 Requirements
     58 ============
     59 
     60 * **Confidenciality/E2EE:** No information about the contents of the wallets
     61   should be accessible or derivable by any third-party who lacks control over
     62   the wallet, including the backup service. Any potential metadata
     63   leakage—such as backup file sizes, upload frequencies, or timing
     64   patterns—should be minimized to the highest extent possible.
     65 * **Incrementality:** The solution should minimize network usage and bandwidth
     66   by incrementally uploading and fetching updates to the global state when
     67   possible, limiting the situations where a full backup or restore is
     68   required.
     69 * **Plausible deniability:** The solution should ensure that no information
     70   can be decrypted or retrieved from the backup after its deletion, including
     71   the evidence that such information was deleted.
     72 
     73 .. _threat-model:
     74 
     75 Threat model
     76 ============
     77 
     78 The design protects the confidentiality of the wallet's backup contents
     79 against any party that does not hold the wallet's backup encryption key,
     80 including the backup service itself. Blocks and blobs are end-to-end encrypted
     81 with keys derived from secrets that only the user's wallets know, so neither a
     82 passive network observer nor the operator of the backup service can learn
     83 anything about the contents of a backup from the data they can access.
     84 
     85 Within this model, the backup service is trusted to honor deletion requests
     86 and to not retain deleted blocks nor previous versions of updated blocks. The
     87 protocol does **not** defend against a service that fails to do so: while such
     88 a service still cannot decrypt the retained data, it can defeat the plausible
     89 deniability requirement by preserving evidence that certain information once
     90 existed in the backup, and countering this would be impractical for an
     91 incremental, multi-device protocol. Users must therefore trust the sync server
     92 operator in such cases, as well as to refrain from misusing the metadata that
     93 the protocol necessarily exposes to it (see :ref:`limitations`).
     94 
     95 Proposed solution
     96 =================
     97 
     98 Backup and synchronization service
     99 ----------------------------------
    100 
    101 Insertions and updates to objects in the wallet database are collected in a
    102 temporary buffer. Certain events in schedules in the wallet trigger the
    103 incremental backup process, where this buffer is serialized, encrypted into a
    104 kilobyte-padded block, assigned a random UUID, and finally uploaded to the
    105 backup service, along with the UUIDs of the previous and next block (when
    106 applicable), and the hashes of all the large binary objects (blob) that are
    107 referenced in the batch, which are expected to be encrypted and uploaded
    108 beforehand to a separate hash-indexed object store.
    109 
    110 .. graphviz::
    111 
    112    digraph G {
    113        subgraph block {
    114            {
    115                rank = same
    116                "Block 0" [shape=box]
    117                "Block 1" [shape=box]
    118                "Block 2" [shape=box]
    119            }
    120 
    121            "Block 0" -> "Block 1"
    122            "Block 1" -> "Block 0"
    123            "Block 1" -> "Block 2"
    124            "Block 2" -> "Block 1"
    125 
    126            {
    127                rank = same
    128                first [shape=plaintext]
    129                last [shape=plaintext]
    130            }
    131 
    132            first -> "Block 0"
    133            last -> "Block 2"
    134        }
    135 
    136        node [shape=record]
    137        hash [label="{<f0> 197d605 | <f1> 409f945 | <f2> 8103756} | {<g0> 1 | <g1> 0 | <g2> 2} | {<h0> \<blob\> | <h1> \<blob\> | <h2> \<blob\>}"]
    138 
    139        edge [style=dotted]
    140        "Block 0" -> hash:f0 [constraint=false]
    141        "Block 1" -> hash:f2 [constraint=false]
    142        "Block 2" -> hash:f2 [constraint=false]
    143    }
    144 
    145 Double-linked list block store
    146 ------------------------------
    147 
    148 The sync server maintains a double-linked list in its database, as well as
    149 references to the global first and last block (useful for full restores). Via
    150 INSERT, DELETE and REPLACE operations, as well as a signature to authenticate
    151 the operation, wallets can upload blocks and manipulate the linked list in
    152 accordance with their internal CRDT logic.
    153 
    154 The sync server itself makes no decisions based on the content of the blocks,
    155 since it can only see them in their encrypted form. Wallets must therefore
    156 maintain a local, unencrypted version of the block store by fetching missing
    157 blocks from the server and assembling them in the correct order, verifying
    158 block signatures in the process in order to detect tampering or corruption.
    159 
    160 Furthermore, wallets are responsible of ensuring that all deletion operations
    161 provide plausible deniability by retroactively redacting the deleted objects
    162 from all the blocks where they appear or are referenced, and uploading the
    163 changes to the sync server, which is in turn trusted (see :ref:`threat-model`)
    164 to honor deletion requests and not retain any deleted blocks nor previous
    165 versions of updated blocks.
    166 
    167 During the synchronization process, wallets can either download the entirety
    168 of the linked list (full sync), or fetch only the missing and updated blocks
    169 by comparing their contents with the ones in the sync server by means of a
    170 reconciliation mechanism (read :ref:`sync-data-structures`).
    171 
    172 Block format
    173 ~~~~~~~~~~~~
    174 
    175 Each block consists of a 2-byte version number, a random 24-byte nonce, an
    176 8-byte serial, and a gzip-compressed JSON object with its length. The block is
    177 be padded up to the next whole kilobyte for privacy reasons. A block whose
    178 length is already a multiple of a kilobyte is not padded further.
    179 
    180 The nonce is 24 bytes because that is exactly what `secretbox`_ takes, which
    181 lets a block be encrypted under its own nonce.
    182 
    183 The serial is only ever seen by the wallets: it sits inside the encrypted
    184 payload, so the sync server knows nothing about it.  Wallets assign it on
    185 every content write (append or in-place update) as the account's maximum
    186 known serial plus one; relinking a block never changes its data and therefore
    187 never its serial.  A wallet checks the serial when it decrypts a block and
    188 refuses to apply a block whose serial is lower than the last one it saw for
    189 that block, which makes a rolled-back (replayed) block detectable.
    190 
    191 Encryption is performed on the block using symmetric authenticated encryption
    192 via libsodium's `secretbox`_ function, with a 32-byte key derived from the
    193 wallet's backup encryption key and the nonce of the block, which in the final
    194 implementation should be shareable between any wallets that the user wishes to
    195 add to the synchronization group.
    196 
    197 .. note::
    198 
    199    The key is derived from the *nonce* rather than from the hash of the
    200    plaintext block: the nonce travels with the block, whereas the plaintext
    201    hash is only known to whoever can already decrypt it, so deriving from it
    202    would make the block undecryptable.
    203 
    204 .. code-block:: text
    205 
    206    +----------------------------+
    207    | version number (2 byte)    |
    208    +----------------------------+
    209    | nonce (24 byte)            |
    210    +----------------------------+
    211    | serial (8 byte)            |
    212    +----------------------------+
    213    | JSON length n (4 byte)     |
    214    +----------------------------+
    215    | gzipped JSON (n byte)      |
    216    +----------------------------+
    217    | padding (to next full KB)  |
    218    +----------------------------+
    219 
    220 Block store API
    221 ~~~~~~~~~~~~~~~
    222 
    223 The account key is the base32-encoded Crockford representation of an EdDSA
    224 public key that identifies the backup account.  All upload requests must be
    225 signed by the corresponding private key; the signature is transmitted in the
    226 request body.
    227 
    228 Binary values in URLs, headers and JSON bodies (nonces, UIDs, hashes,
    229 signatures and the encrypted payloads themselves) are all base32-encoded in
    230 Crockford representation, as is usual for Taler.
    231 
    232 Signatures use EdDSA with the account private key.  Each signature payload
    233 follows the common Taler signing structure with a ``purpose`` field (see
    234 :ref:`Signatures` in the API common conventions for the general format).  The
    235 specific payloads are:
    236 
    237 .. sourcecode:: c
    238 
    239    /**
    240     * Purpose: TALER_SIGNATURE_SYNC_BLOCK_UPLOAD (1452)
    241     * Authorizes the append or in-place update of a block.
    242     * For appends, old_hash is all-zeros.
    243     */
    244    struct SyncBlockUploadSignaturePS {
    245      struct GNUNET_CRYPTO_SignaturePurpose purpose;
    246      struct SYNC_BlockNonce prev_nonce;  ///< all-zeros if first block
    247      struct SYNC_BlockNonce next_nonce;  ///< all-zeros if last block
    248      struct SYNC_BlockNonce nonce;
    249      struct GNUNET_HashCode old_hash;    ///< all-zeros for appends
    250      struct GNUNET_HashCode new_hash;
    251      struct GNUNET_HashCode refs_hash;   ///< over object_refs, see below
    252    };
    253 
    254    /**
    255     * Purpose: TALER_SIGNATURE_SYNC_BLOCK_DELETE (1453)
    256     * Authorizes the deletion of a block.
    257     */
    258    struct SyncBlockDeleteSignaturePS {
    259      struct GNUNET_CRYPTO_SignaturePurpose purpose;
    260      struct SYNC_BlockNonce nonce;
    261      struct SYNC_BlockNonce prev_nonce;  ///< all-zeros if first block
    262      struct SYNC_BlockNonce next_nonce;  ///< all-zeros if last block
    263      struct GNUNET_HashCode hash;
    264      struct GNUNET_HashCode refs_hash;   ///< over object_refs, see below
    265    };
    266 
    267    /**
    268     * Purpose: TALER_SIGNATURE_SYNC_OBJECT_UPLOAD (1454)
    269     * Authorizes the upload of a blob object.
    270     */
    271    struct SyncObjectUploadSignaturePS {
    272      struct GNUNET_CRYPTO_SignaturePurpose purpose;
    273      struct SYNC_ObjectUID uid;
    274      struct GNUNET_HashCode hash;
    275    };
    276 
    277 Absent optional nonces (``prev_nonce`` / ``next_nonce``) are treated as
    278 all-zeros in the signed data.
    279 
    280 The ``refs_hash`` field covers the ``object_refs`` of the request, so that the
    281 reference-count adjustments cannot be altered in transit.  It is the SHA-512
    282 hash over a canonical *binary* encoding of the references — not over their
    283 JSON representation.
    284 
    285 Each reference is laid out as the 64 raw UID bytes followed by the adjustment
    286 as a signed 16-bit integer in network byte order, and the resulting 66-byte
    287 records are concatenated in ascending order of UID:
    288 
    289 .. code-block:: text
    290 
    291    +----------------------------+
    292    | uid (64 byte)              |
    293    +----------------------------+
    294    | adjustment (2 byte, int16) |
    295    +----------------------------+
    296 
    297 Sorting by UID is required because ``object_refs`` travels as a JSON object,
    298 whose member order is not preserved.  A request without any references hashes
    299 the empty byte string.
    300 
    301 A UID may appear at most once, since the wire format keys the references by
    302 UID and could not otherwise transmit them faithfully.
    303 
    304 The server stores the ``upload_sig`` with the block, together with the rest of
    305 the signed context (``old_hash`` and ``refs_hash``), and returns them in the
    306 block list.  A wallet therefore verifies every block's stored signature
    307 against the account key before applying it; a block whose signature does not
    308 verify must not be applied.
    309 
    310 Operations that rewrite the links of an existing block (an append relinks the
    311 previous tail, a delete relinks both of its neighbours) require that block's
    312 *new* signature to be uploaded along with the operation.  This is an ordinary
    313 ``TALER_SIGNATURE_SYNC_BLOCK_UPLOAD`` signature over the relinked block's new
    314 nonces, carried in the ``relink_prev`` / ``relink_next`` fields of the
    315 request.  The server verifies it against the current state of the relinked
    316 block and stores it in the block's row; relinking never changes the block's
    317 data, so the signature's ``old_hash`` and ``new_hash`` are both the block's
    318 stored hash.
    319 
    320 .. http:get:: /config
    321 
    322    Return the server's protocol version and terms.  Requires no account
    323    and no signature.
    324 
    325    **Response**
    326 
    327    :http:statuscode:`200 OK`:
    328      The body is a ``SyncConfig`` object.
    329 
    330    .. code-block:: typescript
    331 
    332       interface SyncConfig {
    333         name: "sync";
    334         implementation: string;
    335         storage_limit_in_megabytes: number;
    336         liability_limit: AmountString;
    337         annual_fee: AmountString;
    338         version: string;
    339       }
    340 
    341    ``storage_limit_in_megabytes`` is the per-upload limit enforced for both
    342    blocks and objects; exceeding it yields ``413``.  ``version`` follows the
    343    Taler ``current:revision:age`` convention.
    344 
    345 .. http:get:: /backups/${ACCOUNT_KEY}
    346 
    347    Report the state of the account: when it expires, and how much of the
    348    storage allowance its backup uses.  Requires no signature, like the other
    349    read endpoints -- the account public key is the capability, and the stored
    350    data is client-encrypted.
    351 
    352    This is the only endpoint that answers for an expired account rather than
    353    demanding payment: when the account expires is precisely what the caller is
    354    asking, so a ``402`` here would be useless.  Wallets use it to tell the
    355    user how long the backup is paid for without waiting for the next write to
    356    fail.
    357 
    358    **Response**
    359 
    360    :http:statuscode:`200 OK`:
    361      The body is a ``SyncAccountStatus`` object.  Returned even when
    362      ``expiration_date`` lies in the past.
    363    :http:statuscode:`404 Not found`:
    364      The server does not know this account at all.  It has never been
    365      paid for, so there is no expiry to report.
    366 
    367    .. code-block:: typescript
    368 
    369       interface SyncAccountStatus {
    370         // When the account expires, or expired.  Every other endpoint
    371         // answers 402 past this point.
    372         expiration_date: Timestamp;
    373 
    374         // Total size of the account's stored blocks, in bytes.
    375         storage_used_bytes: number;
    376 
    377         // Number of blocks in the account's linked list.
    378         block_count: number;
    379       }
    380 
    381 .. http:get:: /backups/${ACCOUNT_KEY}/blocks
    382 
    383    List blocks from the account's linked list with pagination.
    384 
    385    **Request**
    386 
    387    :query limit:
    388      *Required.* Maximum number of blocks to return.  Must be a positive
    389      count (int16).
    390    :query start_nonce:
    391      Optional nonce of the block from which to start listing.  If omitted,
    392      listing starts from the first block.
    393 
    394    **Response**
    395 
    396    :http:statuscode:`200 OK`:
    397      The body is a JSON array of ``BlockEntry`` objects.  The array is
    398      empty if the account has no blocks.
    399    :http:statuscode:`400 Bad request`:
    400      The ``limit`` parameter is missing, malformed, given without a
    401      value, or not positive; or ``start_nonce`` is malformed or given
    402      without a value.
    403    :http:statuscode:`402 Payment required`:
    404      The account has expired and requires payment.
    405    :http:statuscode:`404 Not found`:
    406      The ``start_nonce`` block was not found in the linked list.
    407    :http:statuscode:`500 Internal server error`:
    408      A database error occurred.
    409 
    410    .. code-block:: typescript
    411 
    412       interface BlockEntry {
    413         nonce: BlockUuid;
    414         block_hash: HashCodeString;
    415         prev_nonce?: BlockUuid;
    416         next_nonce?: BlockUuid;
    417         data: string;
    418         upload_sig: EddsaSignatureString;
    419         old_hash: HashCodeString;
    420         refs_hash: HashCodeString;
    421       }
    422 
    423    ``data`` is the encrypted block payload as it was uploaded, and hashes to
    424    ``block_hash``.  ``prev_nonce`` and ``next_nonce`` are absent for the first
    425    and last block of the linked list respectively.  ``upload_sig`` is the
    426    signature stored with the block, and ``old_hash`` / ``refs_hash`` the
    427    remainder of the signed context; the wallet verifies the signature before
    428    applying the block.
    429 
    430 .. http:post:: /backups/${ACCOUNT_KEY}/blocks/${NONCE}
    431 
    432    Upload a new block and append it at the end of the account's linked list.
    433    If a block with the same nonce already exists, the content hash is
    434    compared: if it matches, a ``304 Not modified`` is returned; if it differs,
    435    the client should use ``PUT`` instead.
    436 
    437    The request must include an ``If-None-Match`` header containing the quoted
    438    base32-encoded SHA-512 hash of the encrypted block data.  This hash is used
    439    by the server to detect duplicates, and the server rejects the upload if
    440    the ``data`` in the body does not hash to it.
    441 
    442    **Request**
    443 
    444    :query fresh:
    445      Optional. Force the server to issue a fresh payment order even if a
    446      pending one already exists for this account.
    447    :query pay:
    448      Optional. Any non-empty value (e.g. ``y``) signals that the client
    449      wants to pay before uploading.
    450    :query paying:
    451      Optional. An existing order identifier.  The client is promising
    452      that it is already paying on a related order.  This will cause the
    453      server to delay processing until the respective payment has arrived
    454      (if the operation requires a payment).  Useful if the server
    455      previously returned a ``402 Payment required`` and the client wants
    456      to proceed as soon as the payment went through.
    457 
    458    The request body is a JSON object:
    459 
    460    .. code-block:: typescript
    461 
    462       interface UploadBlockRequest {
    463         upload_sig: EddsaSignatureString;
    464         prev_nonce?: BlockUuid;
    465         next_nonce?: BlockUuid;
    466         data: string;
    467         object_refs?: { [uid: BlobUid]: number };
    468         relink_prev?: { upload_sig: EddsaSignatureString };
    469       }
    470 
    471    ``upload_sig``
    472      EdDSA signature over the block nonce, ``prev_nonce``,
    473      ``next_nonce``, old data hash (for updates, all-zeros for appends),
    474      new data hash and the hash over ``object_refs``, signed with the
    475      account's private key
    476      (``TALER_SIGNATURE_SYNC_BLOCK_UPLOAD``).
    477 
    478    ``prev_nonce``
    479      Nonce of the preceding block in the DLL.
    480      Must be omitted for the first block.
    481 
    482    ``next_nonce``
    483      Must be omitted; inserts into the middle of the linked list are not
    484      supported, so an append never has a succeeding block.
    485 
    486    ``data``
    487      The encrypted block contents (binary, base32-encoded).
    488 
    489    ``object_refs``
    490      Optional object whose keys are blob UIDs and whose values are
    491      16-bit signed integer reference-count deltas.  Any objects
    492      referenced here must have been uploaded *beforehand* via
    493      ``POST /backups/${ACCOUNT_KEY}/objects/${UID}``, and each UID may
    494      appear at most once.  The adjustments are applied in the same
    495      transaction as the block operation: if any of them names an object
    496      the account does not have, or would take a reference count below
    497      zero, the entire request is rejected and nothing is modified.
    498 
    499    ``relink_prev``
    500      Required when ``prev_nonce`` is present.  The new signature of the
    501      block at ``prev_nonce`` (the previous tail), covering its new
    502      ``next`` link after this append.  The server verifies it against
    503      the tail's current state and stores it with the block.
    504 
    505    **Response**
    506 
    507    :http:statuscode:`204 No content`:
    508      The block was stored successfully.
    509    :http:statuscode:`304 Not modified`:
    510      A block with the same nonce and data hash already exists.
    511    :http:statuscode:`400 Bad request`:
    512      Malformed parameters, bad hash, or missing required headers.
    513    :http:statuscode:`402 Payment required`:
    514      The account has expired and requires payment.  The response includes
    515      a ``Taler`` header with a ``taler://pay/...`` URI.
    516    :http:statuscode:`403 Forbidden`:
    517      The signature is invalid or does not match the request.
    518    :http:statuscode:`409 Conflict`:
    519      The request does not fit the state the server holds, and retrying
    520      it unchanged will not help.  Either the write is outdated (the
    521      linked list has been modified by another device since the caller
    522      last fetched it), the nonce is already in use, or ``object_refs``
    523      names an object the account does not have or would take a
    524      reference count below zero.  Nothing was modified.
    525    :http:statuscode:`413 Request entity too large`:
    526      The upload exceeds the server's configured upload limit.
    527    :http:statuscode:`500 Internal server error`:
    528      A database error occurred.
    529 
    530 .. http:put:: /backups/${ACCOUNT_KEY}/blocks/${NONCE}
    531 
    532    Replace an existing block's content in-place.  Semantics are identical to
    533    ``POST`` on the same endpoint, with one addition: the ``If-Match`` header
    534    must contain the quoted base32-encoded SHA-512 hash of the old block data
    535    that is being replaced.  The server rejects the request with ``409
    536    Conflict`` if the old hash, ``prev_nonce`` or ``next_nonce`` do not match
    537    the stored block.
    538 
    539    The ``upload_sig`` must also cover the old data hash (from ``If-Match``) in
    540    addition to the new data hash (from ``If-None-Match``).
    541 
    542    .. note::
    543 
    544       ``PUT`` stands in for ``PATCH``, which the update operation would
    545       otherwise use, until the HTTP server library supports it.
    546 
    547    **Response**
    548 
    549    Same status codes as ``POST``, plus:
    550 
    551    :http:statuscode:`404 Not found`:
    552      The specified block does not exist (cannot update a missing block).
    553 
    554 .. http:delete:: /backups/${ACCOUNT_KEY}/blocks/${NONCE}
    555 
    556    Delete an existing block from the linked list.  The request must include an
    557    ``If-Match`` header containing the quoted base32-encoded SHA-512 hash of
    558    the block data to delete, which the server uses to detect concurrent
    559    modifications.
    560 
    561    **Request**
    562 
    563    The request body is a JSON object:
    564 
    565    .. code-block:: typescript
    566 
    567       interface DeleteBlockRequest {
    568         delete_sig: EddsaSignatureString;
    569         prev_nonce?: BlockUuid;
    570         next_nonce?: BlockUuid;
    571         object_refs?: { [uid: BlobUid]: number };
    572         relink_prev?: { upload_sig: EddsaSignatureString };
    573         relink_next?: { upload_sig: EddsaSignatureString };
    574       }
    575 
    576    ``delete_sig``
    577      EdDSA signature over the block nonce, ``prev_nonce``,
    578      ``next_nonce``, block hash (from ``If-Match``) and the hash over
    579      ``object_refs``, signed with the account's private key
    580      (``TALER_SIGNATURE_SYNC_BLOCK_DELETE``).
    581 
    582    ``prev_nonce``
    583      Nonce of the preceding block in the DLL.
    584      Must be omitted if the block being deleted is the first block.
    585 
    586    ``next_nonce``
    587      Nonce of the succeeding block in the DLL.
    588      Must be omitted if the block being deleted is the last block.
    589 
    590    ``object_refs``
    591      Optional object whose keys are blob UIDs and whose values are
    592      16-bit signed integer reference-count deltas (typically negative,
    593      to decrement the refcount of objects that were referenced by the
    594      deleted block).  The same rules as for block uploads apply: each
    595      UID may appear at most once, and the whole request is rejected if
    596      an adjustment names an unknown object or would take a reference
    597      count below zero.
    598 
    599    ``relink_prev``
    600      Required when ``prev_nonce`` is present.  The new signature of the
    601      block at ``prev_nonce``, covering its new ``next`` link.
    602 
    603    ``relink_next``
    604      Required when ``next_nonce`` is present.  The new signature of the
    605      block at ``next_nonce``, covering its new ``prev`` link.
    606 
    607    **Response**
    608 
    609    :http:statuscode:`204 No content`:
    610      The block was deleted successfully.
    611    :http:statuscode:`400 Bad request`:
    612      Malformed parameters or missing ``If-Match`` header.
    613    :http:statuscode:`402 Payment required`:
    614      The account has expired and requires payment.
    615    :http:statuscode:`403 Forbidden`:
    616      The signature is invalid or does not match the request.
    617    :http:statuscode:`404 Not found`:
    618      The specified block does not exist (or was already deleted).
    619    :http:statuscode:`409 Conflict`:
    620      The ``If-Match`` hash, ``prev_nonce`` or ``next_nonce`` do not
    621      match the stored block (concurrent modification detected), or
    622      ``object_refs`` names an object the account does not have or would
    623      take a reference count below zero.  Nothing was modified.
    624    :http:statuscode:`500 Internal server error`:
    625      A database error occurred.
    626 
    627 Hash-indexed object store
    628 -------------------------
    629 
    630 All static large binary objects (blobs) referenced in a new block generated by
    631 the wallet are required to be uploaded separately to the sync server in
    632 encrypted form before the actual referencing block is uploaded.
    633 
    634 Blobs are stored in a hash-indexed object store with a reference count of
    635 zero, which increases with every referencing block that is uploaded to the
    636 block store. Any blobs with a reference count of zero will be deleted from the
    637 server after a preconfigured expiration period.
    638 
    639 Uploads are keyed by UID and are idempotent: re-uploading a UID that the
    640 account already holds is accepted and changes nothing, so a wallet that is
    641 unsure whether a blob is already present can simply upload it again.  The
    642 stored contents of an existing UID are never replaced.
    643 
    644 Blob format
    645 ~~~~~~~~~~~
    646 
    647 Similar to blocks, each blob consists of 2-byte version number, the 4-byte
    648 data length, the gzipped data, and a padding to the next whole kilobyte. The
    649 blob is then encrypted using a key derived from the wallet's backup encryption
    650 key and the hash of the unencrypted file:
    651 
    652 .. code-block:: text
    653 
    654    key = KDF(32, backup_key, "taler-sync-blob-secret-salt", H(plaintext))
    655    uid = H(key)
    656 
    657 Every blob therefore has its own key. The 64-byte ``uid``, which is the
    658 SHA-512 hash of that key, is what indexes the object in the store and is the
    659 only one of the two the sync server ever learns; the key itself is stored
    660 *inside the blocks* that reference the blob, where it doubles as the reference
    661 to the object that has to be fetched.
    662 
    663 The key is thus all a wallet needs to both locate and decrypt a blob, which is
    664 the only thing a block carries. The `secretbox`_ nonce is consequently derived
    665 from the key as well, as the first 24 bytes of ``H(key)``. Nonce reuse cannot
    666 occur, because distinct plaintexts derive distinct keys.
    667 
    668 Because the key is derived from the plaintext, blobs are content-addressed:
    669 identical contents yield the same key, UID and ciphertext, so an unchanged
    670 blob is only ever uploaded once.
    671 
    672 .. code-block:: text
    673 
    674    +----------------------------+
    675    | version number (2 byte)    |
    676    +----------------------------+
    677    | data length n (4 byte)     |
    678    +----------------------------+
    679    | gzipped data (n byte)      |
    680    +----------------------------+
    681    | padding (to next full KB)  |
    682    +----------------------------+
    683 
    684 Object store API
    685 ~~~~~~~~~~~~~~~~
    686 
    687 Objects are scoped to the account: a UID is only ever visible to the account
    688 that uploaded it.
    689 
    690 .. http:get:: /backups/${ACCOUNT_KEY}/objects/${UID}
    691 
    692    Retrieve an existing blob by its UID.
    693 
    694    **Response**
    695 
    696    :http:statuscode:`200 OK`:
    697      The body is an ``ObjectEntry`` object.
    698    :http:statuscode:`400 Bad request`:
    699      The ``$UID`` is malformed.
    700    :http:statuscode:`404 Not found`:
    701      The account has no object under that UID.  This is also the answer
    702      for an account that does not exist.
    703    :http:statuscode:`500 Internal server error`:
    704      A database error occurred.
    705 
    706    .. code-block:: typescript
    707 
    708       interface ObjectEntry {
    709         uid: BlobUid;
    710         data: string;
    711       }
    712 
    713 .. http:post:: /backups/${ACCOUNT_KEY}/objects/${UID}
    714 
    715    Upload an encrypted blob and store it in the hash-indexed object store.
    716    The ``$UID`` is the object's unique identifier.
    717 
    718    The object is stored with a reference count of zero; it only becomes
    719    referenced once a block naming it in ``object_refs`` is uploaded.  Until
    720    then it is subject to expiry, so blobs should be uploaded shortly before
    721    the block that references them.
    722 
    723    **Request**
    724 
    725    The request body is a JSON object:
    726 
    727    .. code-block:: typescript
    728 
    729       interface UploadObjectRequest {
    730         object_sig: EddsaSignatureString;
    731         data: string;
    732       }
    733 
    734    ``object_sig``
    735      EdDSA signature over the ``$UID`` and the hash of ``data``, signed
    736      with the account's private key
    737      (``TALER_SIGNATURE_SYNC_OBJECT_UPLOAD``).
    738 
    739    ``data``
    740      The encrypted blob contents (binary, base32-encoded).
    741 
    742    **Response**
    743 
    744    :http:statuscode:`204 No content`:
    745      The object was stored.  This is also the answer when the account
    746      already holds an object under that UID, in which case the stored
    747      contents are left as they are.
    748    :http:statuscode:`400 Bad request`:
    749      The ``$UID`` or the request body is malformed.
    750    :http:statuscode:`402 Payment required`:
    751      The account has expired and requires payment.
    752    :http:statuscode:`403 Forbidden`:
    753      The signature is invalid or does not match the request.
    754    :http:statuscode:`413 Request entity too large`:
    755      The upload exceeds the server's configured upload limit.
    756    :http:statuscode:`500 Internal server error`:
    757      A database error occurred.
    758 
    759 .. TODO: synchronization primitive
    760 
    761 Backup schema
    762 -------------
    763 
    764 Local operations on the wallet database are collected into a temporary buffer,
    765 called an “increment set”. Each top-level key in this set holds a list of
    766 insertion operations (“increments”) for a particular database entity
    767 (e.g. exchanges) or event (e.g. payments).
    768 
    769 .. code-block:: typescript
    770 
    771    interface IncrementSet {
    772      addExchangeIncs?: AddExchangeInc[];
    773      setGlobalExchangeTrustIncs?: SetGlobalExchangeTrustInc[];
    774      addBankAccountIncs?: AddBankAccountInc[];
    775      // ...
    776    }
    777 
    778 When a backup operation is triggered, this buffer is processed into a block
    779 and subsequently emptied. The resulting block gets assigned a random UUID,
    780 appended to the local linked-list, and uploaded to the backup service.
    781 
    782 Since the operations in a given wallet may conflict with operations in the
    783 backup with matching primary keys, a state-based CRDT “merge” strategy was
    784 carefuly devised for every top-level operation type in the block, so that
    785 wallets can deterministically agree on a consistent global state.
    786 
    787 One rule cuts across all of the transaction families: **a transaction only
    788 ever moves towards its end.** The wallets of a group work on the same
    789 transactions at the same time, so an increment that would take a record back
    790 to a state it has already moved past is describing an older view of it, and
    791 only its origin block is recorded.  The terminal states are ranked rather than
    792 simply frozen, so that two wallets which reached *different* ones both settle
    793 on the same one:
    794 
    795 .. code-block:: text
    796 
    797    done  >  failed  >  aborted  >  expired  >  (not terminal)
    798 
    799 Preferring ``done`` is deterministic, which is what convergence needs, and it
    800 is also the truthful answer: a transaction that finished actually moved the
    801 money.  Without the rule, a wallet that completed a withdrawal would pull in
    802 the abort another device had issued against the copy it restored, and end up
    803 showing an abandoned transaction while holding the coins it produced.
    804 
    805 Add or update an exchange
    806 ~~~~~~~~~~~~~~~~~~~~~~~~~
    807 
    808 User accepts ToS for a new or existing exchange.
    809 
    810 Exchanges without an accepted ToS are not included in the backup.
    811 
    812 .. code-block:: typescript
    813 
    814    interface AddExchangeInc {
    815      type: "add-exchange";
    816      exchangeBaseUrl: string;
    817      tosAcceptedEtag: string;
    818      tosAcceptedEtagTimestamp: Timestamp;
    819    }
    820 
    821 * **Primary key:** ``[exchangeBaseUrl]``
    822 * **Deletion groups:** ``[exchanges]``
    823 
    824 Merge strategy
    825 ++++++++++++++
    826 
    827 Favor the operation with the largest ``tosAcceptedEtagTimestamp``. If two
    828 timestamps are equal, favor the operation with the largest ``tosAcceptedEtag``
    829 in lexicographical order.
    830 
    831 Set exchange to global trust
    832 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    833 
    834 User sets an exchange to global trust.
    835 
    836 .. code-block:: typescript
    837 
    838    interface SetGlobalExchangeTrustInc {
    839      type: "set-global-exchange-trust";
    840      exchangeBaseUrl: string;
    841      exchangeMasterPub: EddsaPublicKey;
    842    }
    843 
    844 * **Primary key:** ``[exchangeBaseUrl, exchangeMasterPub]``
    845 * **Deletion groups:** ``[global-exchange-trust]``
    846 
    847 Merge strategy
    848 ++++++++++++++
    849 
    850 No merge is required.
    851 
    852 Add or update a bank account
    853 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    854 
    855 User adds (or updates) a known bank account.
    856 
    857 .. code-block:: typescript
    858 
    859    interface AddBankAccountInc {
    860      type: "add-bank-account";
    861      bankAccountId: string;
    862      paytoUri: string;
    863      label: string;
    864    }
    865 
    866 * **Primary key:** ``[bankAccountId]``
    867 * **Deletion groups:** ``[bank-accounts]``
    868 
    869 Merge strategy
    870 ++++++++++++++
    871 
    872 Last write wins.
    873 
    874 Set Donau info
    875 ~~~~~~~~~~~~~~
    876 
    877 User sets info for tax-deductible donations.
    878 
    879 .. code-block:: typescript
    880 
    881    interface SetDonauInfoInc {
    882      type: "set-donau-info";
    883      donauBaseUrl: string;
    884      taxPayerId: string;
    885    }
    886 
    887 * **Primary key:** ``[info]``
    888 * **Deletion groups:** ``[donau-info]``
    889 
    890 Merge strategy
    891 ++++++++++++++
    892 
    893 Last write wins.
    894 
    895 Add a denomination
    896 ~~~~~~~~~~~~~~~~~~
    897 
    898 A denomination is stored in the wallet.
    899 
    900 .. code-block:: typescript
    901 
    902    interface AddDenominationInc {
    903      type: "add-denomination";
    904      denomPub: DenominationPubKey;
    905      value: AmountString;
    906      fees: DenomFees;
    907      stampStart: TalerProtocolTimestamp;
    908      stampExpireWithdraw: TalerProtocolTimestamp;
    909      stampExpireLegal: TalerProtocolTimestamp;
    910      stampExpireDeposit: TalerProtocolTimestamp;
    911      masterSig: EddsaSignature;
    912      exchangeBaseUrl: string;
    913      exchangeMasterPub: EddsaPublicKey;
    914    }
    915 
    916 * **Primary key:** ``[exchangeBaseUrl, denomPub]``
    917 * **Deletion groups:** ``[denominations]``
    918 
    919 Merge strategy
    920 ++++++++++++++
    921 
    922 No merge is required, a denomination is expected to always remain constant, so
    923 later additions of the same denomination can be safely discarded.
    924 
    925 Add a coin
    926 ~~~~~~~~~~
    927 
    928 A coin comes into the wallet (withdrawn or refreshed) and is signed by the
    929 exchange.
    930 
    931 The wallet database stores per-coin key material, so the increment carries the
    932 coin **as it stands** -- key, blinding key, signature and status -- rather
    933 than deriving it from a seed as earlier designs did.  The wallet records an
    934 ``add-coin`` when the coin is created and a ``spend-coin`` when it is spent;
    935 the full collection pass emits the ``add-coin`` form for any coin the backup
    936 has never seen, whatever state it is in.  The ``spend-coin`` section is
    937 applied after the ``add-coin`` section, so a coin that was spent before a
    938 cycle ran restores in its spent state.
    939 
    940 Restoring the coin also recomputes the wallet's *coin availability* rows (the
    941 counts the balance reads) from the restored coins, so a restored wallet shows
    942 the same balance as the wallet that made the backup.  The counts are always
    943 derived and never carried, which is what makes the restore idempotent; only a
    944 coin that is spendable (status ``fresh``) counts, matching what the wallet's
    945 own bookkeeping does with a suspended one.
    946 
    947 For the two balances to agree, *every* change to whether a coin counts has to
    948 reach the other wallets, not only spending: a coin melted into a refresh,
    949 recouped from a revoked denomination, written off with its denomination, or
    950 suspended by the user is reported with a ``spend-coin`` increment carrying its
    951 new status.  The section is the coin's terminal update, whatever brought it
    952 about.  A change that is not reported is the one way the two devices can end
    953 up disagreeing about how much money the user has, since a coin that is already
    954 backed up is never offered again by the full collection pass.
    955 
    956 The reserves (and with them the ability to recoup a restored coin) are backed
    957 up by the ``add-reserve`` family, and the withdrawal family
    958 (``withdrawal-start`` / ``withdrawal-abort`` / ``withdrawal-done`` /
    959 ``withdrawal-fail``, referencing the reserve by ``[exchangeBaseUrl,
    960 reservePub]``, and carrying the ``wgInfo`` with the ``taler://withdraw`` URI
    961 that identifies the bank's operation) restores the withdrawal transactions
    962 themselves and lets a restored wallet continue a pending one -- the bank's
    963 operation is keyed by that URI, and the reserve key pair and the coin seed are
    964 in the backup too; only an expired bank operation cannot be resumed.  A
    965 refreshed coin's melt is backed up by the refresh family below, so a restored
    966 coin can be recouped-refreshed as well as recouped (see the recoup discussion
    967 under "Add a reserve").
    968 
    969 ``exchangeWithdrawValues`` carries the blinding values the exchange
    970 contributed to the withdraw, which a recoup has to replay.  For an RSA coin
    971 they are the constant ``{"cipher": "RSA"}``; for a Clause-Schnorr coin they
    972 are the R-values, which nothing can re-derive, so they have to travel in the
    973 increment.  The field is optional because it was added after the increment was
    974 first released: a coin from a wallet that predates it is treated as RSA.
    975 
    976 .. code-block:: typescript
    977 
    978    interface AddCoinInc {
    979      type: "add-coin";
    980      coinSource: CoinSource;
    981      sourceTransactionId?: string;
    982      coinPub: string;
    983      coinPriv: string;
    984      denomPubHash: string;
    985      denomSig: UnblindedDenominationSignature;
    986      exchangeBaseUrl: string;
    987      exchangeMasterPub: string;
    988      blindingKey: string;
    989      coinEvHash: string;
    990      status: CoinStatus;
    991      visible?: number;
    992      maxAge: number;
    993      ageCommitmentProof?: AgeCommitmentProof;
    994      exchangeWithdrawValues?: ExchangeWithdrawValue;
    995    }
    996 
    997 .. code-block:: typescript
    998 
    999    type CoinSource =
   1000      | WithdrawalCoinSource
   1001      | RefreshCoinSource;
   1002 
   1003 .. code-block:: typescript
   1004 
   1005    interface WithdrawalCoinSource {
   1006      type: "withdrawal";
   1007      withdrawalGroupId: string;
   1008      coinNumber: number;
   1009      reservePub: string;
   1010    }
   1011 
   1012 .. code-block:: typescript
   1013 
   1014    interface RefreshCoinSource {
   1015      type: "refresh";
   1016      refreshGroupId: string;
   1017      oldCoinPub: string;
   1018    }
   1019 
   1020 * **Primary key:** ``[coinPub]``
   1021 * **Deletion groups:** ``[coins]``
   1022 
   1023 Merge strategy
   1024 ++++++++++++++
   1025 
   1026 Last write wins: a coin is unique and its parameters never change, so the
   1027 latest copy wins.
   1028 
   1029 Spend a coin
   1030 ~~~~~~~~~~~~
   1031 
   1032 A signed coin is spent by the user.
   1033 
   1034 .. code-block:: typescript
   1035 
   1036    interface SpendCoinInc {
   1037      type: "spend-coin";
   1038      coinSource: CoinSource;
   1039      sourceTransactionId?: string;
   1040      coinPub: string;
   1041      coinPriv: string;
   1042      denomPubHash: string;
   1043      denomSig: UnblindedDenominationSignature;
   1044      exchangeBaseUrl: string;
   1045      exchangeMasterPub: string;
   1046      blindingKey: string;
   1047      coinEvHash: string;
   1048      status: CoinStatus;
   1049      visible?: number;
   1050      maxAge: number;
   1051      ageCommitmentProof?: AgeCommitmentProof;
   1052      exchangeWithdrawValues?: ExchangeWithdrawValue;
   1053    }
   1054 
   1055 * **Primary key:** ``[coinPub]``
   1056 * **Deletion groups:** ``[coins]``
   1057 
   1058 Add a token
   1059 ~~~~~~~~~~~
   1060 
   1061 A token is generated by the wallet but not yet signed by the merchant (the
   1062 wallet database calls this a *slate*).
   1063 
   1064 Like coins, tokens were originally designed as seed-derived: the increment
   1065 carried ``[secretSeed, choiceIndex, outputIndex]`` and the wallet re-derived
   1066 the key pair from it.  The wallet database stores per-token key material
   1067 instead, so the increments carry the token as it stands, and the token's *use*
   1068 public key is the primary key of the family.  The three increments share one
   1069 body, ``TokenIncBase``:
   1070 
   1071 .. code-block:: typescript
   1072 
   1073    interface TokenIncBase {
   1074      // Purchase the token belongs to, and the position within its
   1075      // contract that produced it.
   1076      purchaseId: string;
   1077      transactionId?: string;
   1078      choiceIndex?: number;
   1079      outputIndex?: number;
   1080      repeatIndex?: number;
   1081 
   1082      merchantBaseUrl: string;
   1083      kind: MerchantContractTokenKind;
   1084      slug: string;
   1085      name: string;
   1086      description: string;
   1087      descriptionI18n?: InternationalizedString;
   1088      extraData: MerchantContractTokenDetails;
   1089 
   1090      tokenIssuePub: TokenIssuePublicKey;
   1091      tokenIssuePubHash: string;
   1092      tokenFamilyHash?: string;
   1093      validAfter: TalerProtocolTimestamp;
   1094      validBefore: TalerProtocolTimestamp;
   1095 
   1096      // The key material the wallet holds for this token.  Nothing can
   1097      // reconstruct it, so it travels in the increment.
   1098      tokenUsePub: string;
   1099      tokenUsePriv: string;
   1100      tokenUseSig?: TokenUseSig;
   1101      tokenEv: TokenEnvelope;
   1102      tokenEvHash: string;
   1103      blindingKey: string;
   1104    }
   1105 
   1106 .. code-block:: typescript
   1107 
   1108    interface AddTokenInc extends TokenIncBase {
   1109      type: "add-token";
   1110    }
   1111 
   1112 * **Primary key:** ``[tokenUsePub]``
   1113 * **Deletion groups:** ``[tokens]``
   1114 
   1115 Merge strategy
   1116 ++++++++++++++
   1117 
   1118 No merge is required, new tokens are unique.
   1119 
   1120 Sign a token
   1121 ~~~~~~~~~~~~
   1122 
   1123 A token is signed by the merchant.  Applying this increment also removes the
   1124 slate the token was issued from, the same way the wallet's own issuance flow
   1125 does.
   1126 
   1127 .. code-block:: typescript
   1128 
   1129    interface SignTokenInc extends TokenIncBase {
   1130      type: "sign-token";
   1131      tokenIssueSig: UnblindedDenominationSignature;
   1132    }
   1133 
   1134 * **Primary key:** ``[tokenUsePub]``
   1135 * **Deletion groups:** ``[tokens]``
   1136 
   1137 Merge strategy
   1138 ++++++++++++++
   1139 
   1140 No merge is required, only one signature for a given token can be issued by
   1141 the merchant, further attempts to sign it will fail.
   1142 
   1143 Spend a token
   1144 ~~~~~~~~~~~~~
   1145 
   1146 A signed token is spent by the user.  Only the fields the spend changes
   1147 travel; the increment updates a token that is already there and is skipped
   1148 when it is not.
   1149 
   1150 .. code-block:: typescript
   1151 
   1152    interface SpendTokenInc {
   1153      type: "spend-token";
   1154      tokenUsePub: string;
   1155      transactionId?: string;
   1156      tokenUseSig?: TokenUseSig;
   1157    }
   1158 
   1159 * **Primary key:** ``[tokenUsePub]``
   1160 * **Deletion groups:** ``[tokens]``
   1161 
   1162 Merge strategy
   1163 ++++++++++++++
   1164 
   1165 No merge is required, each token can only be spent once, further attempts at
   1166 spending the token will fail.
   1167 
   1168 Start a withdrawal
   1169 ~~~~~~~~~~~~~~~~~~
   1170 
   1171 User initiates a withdrawal.
   1172 
   1173 The increment references the reserve by ``[exchangeBaseUrl, reservePub]`` (see
   1174 the "Add a reserve" section): the restored wallet takes the reserve's key pair
   1175 from the reserve record.  It also carries the ``wgInfo`` -- for a
   1176 bank-integrated withdrawal, the ``taler://withdraw`` URI that identifies the
   1177 bank's withdrawal operation.  That URI, the reserve key pair and the coin seed
   1178 (all in the backup) are everything a restored wallet needs to continue a
   1179 withdrawal that was still pending on the other device; the only thing that
   1180 cannot be resumed is a bank operation the bank has already expired or deleted.
   1181 
   1182 .. code-block:: typescript
   1183 
   1184    interface WithdrawalStartInc {
   1185      type: "withdrawal-start";
   1186      withdrawalGroupId: string;
   1187      exchangeBaseUrl: string;
   1188      reservePub: EddsaPublicKey;
   1189      secretSeed: string;
   1190      timestampStart: TalerPreciseTimestamp;
   1191      restrictAge?: number;
   1192      instructedAmount?: AmountString;
   1193      wgInfo: WgInfo;
   1194    }
   1195 
   1196 * **Primary key:** ``[withdrawalGroupId]``
   1197 * **Deletion groups:** ``[withdrawals]``
   1198 
   1199 Merge strategy
   1200 ++++++++++++++
   1201 
   1202 No merge is required, all withdrawals are independent from each other.
   1203 
   1204 Abort a withdrawal
   1205 ~~~~~~~~~~~~~~~~~~
   1206 
   1207 User aborts a withdrawal.
   1208 
   1209 .. code-block:: typescript
   1210 
   1211    interface WithdrawalAbortInc {
   1212      type: "withdrawal-abort";
   1213      withdrawalGroupId: string;
   1214      abortReason?: TalerErrorDetail;
   1215    }
   1216 
   1217 * **Primary key:** ``[withdrawalGroupId]``
   1218 * **Deletion groups:** ``[withdrawals]``
   1219 
   1220 Merge strategy
   1221 ++++++++++++++
   1222 
   1223 Store all ``abortReason`` in the database.
   1224 
   1225 Withdrawal done
   1226 ~~~~~~~~~~~~~~~
   1227 
   1228 A withdrawal started by the user completes successfully.
   1229 
   1230 .. code-block:: typescript
   1231 
   1232    interface WithdrawalDoneInc {
   1233      type: "withdrawal-done";
   1234      withdrawalGroupId: string;
   1235      timestampFinish: TalerPreciseTimestamp;
   1236      rawWithdrawalAmount: AmountString;
   1237      effectiveWithdrawalAmount: AmountString;
   1238    }
   1239 
   1240 * **Primary key:** ``[withdrawalGroupId]``
   1241 * **Deletion groups:** ``[withdrawals]``
   1242 
   1243 Merge strategy
   1244 ++++++++++++++
   1245 
   1246 No merge is required, a withdrawal can only succeed once.
   1247 
   1248 Withdrawal failed
   1249 ~~~~~~~~~~~~~~~~~
   1250 
   1251 A withdrawal started by the user fails.
   1252 
   1253 .. code-block:: typescript
   1254 
   1255    interface WithdrawalFailInc {
   1256      type: "withdrawal-fail";
   1257      withdrawalGroupId: string;
   1258      failReason: TalerErrorDetail;
   1259    }
   1260 
   1261 * **Primary key:** ``[withdrawalGroupId]``
   1262 * **Deletion groups:** ``[withdrawals]``
   1263 
   1264 Merge strategy
   1265 ++++++++++++++
   1266 
   1267 Store all ``failReason`` in the database.
   1268 
   1269 .. TODO: withdrawal (soft) deletion as increment?
   1270    (can't be easily deleted because of coin references)
   1271 
   1272 Set the reserve seed
   1273 ~~~~~~~~~~~~~~~~~~~~
   1274 
   1275 The wallet derives every reserve key pair from a single wallet-level seed (32
   1276 random bytes), so that the backup carries no per-reserve key material: the
   1277 private key of reserve ``i`` is re-derived as
   1278 
   1279 .. code-block:: text
   1280 
   1281    reservePriv_i = KDF(32, reserveSeed, "taler-reserve-key-salt", i)
   1282 
   1283 and the public key from the private one (``eddsa_get_public``).  The seed
   1284 itself is wallet state and travels in the backup like the wallet root key;
   1285 this increment is what the backup carries it as.  It is created lazily at the
   1286 first reserve created after this feature ships, so wallets that predate it do
   1287 not grow a seed until they create their next reserve.  Reserves created before
   1288 the seed existed keep their random key pairs and are backed up with the
   1289 ``reservePriv`` fallback of ``add-reserve`` below.
   1290 
   1291 .. code-block:: typescript
   1292 
   1293    interface SetReserveSeedInc {
   1294      type: "set-reserve-seed";
   1295      seed: string;
   1296    }
   1297 
   1298 * **Primary key:** ``[]`` (a singleton, like ``set-donau-info``)
   1299 * **Deletion groups:** ``[reserve-seed]``
   1300 
   1301 Merge strategy
   1302 ++++++++++++++
   1303 
   1304 Last write wins.
   1305 
   1306 The ``set-reserve-seed`` section of an increment set is applied before the
   1307 ``add-reserve`` section, so that a wallet deriving a reserve key pair on
   1308 restore already has the seed.
   1309 
   1310 Add a reserve
   1311 ~~~~~~~~~~~~~
   1312 
   1313 A reserve is created by the wallet for every withdrawal and for the merge
   1314 capability of P2P payments, and its key pair lives in the wallet's
   1315 ``reserves`` object store (see the ``WalletReserve`` record in ``db.ts``).
   1316 The increment carries the record's identity -- the exchange and the reserve's
   1317 derivation index -- and, for the reserves that predate the seed, the private
   1318 key.
   1319 
   1320 .. code-block:: typescript
   1321 
   1322    interface AddReserveInc {
   1323      type: "add-reserve";
   1324      exchangeBaseUrl: string;
   1325      reserveIndex: number;
   1326      // Only for reserves created before the reserve seed existed, whose
   1327      // keys are random and cannot be re-derived.
   1328      reservePriv?: EddsaPrivateKey;
   1329    }
   1330 
   1331 * **Primary key:** ``[exchangeBaseUrl, reserveIndex]``
   1332 * **Deletion groups:** ``[reserves]``
   1333 
   1334 Merge strategy
   1335 ++++++++++++++
   1336 
   1337 Last write wins: the identity of a reserve never changes, and a re-recorded
   1338 increment (e.g. by the full collection pass) carries the same index and the
   1339 same key material.
   1340 
   1341 The public key of the reserve is *not* carried: it is derived from the private
   1342 key on restore (``eddsa_get_public``), whether the private key was re-derived
   1343 from the seed or restored from ``reservePriv``.  The restored record therefore
   1344 has the same ``reservePub`` as the wallet that created the reserve, which is
   1345 what the other increments reference it by (see below).  The ``WalletReserve``
   1346 record gains ``exchangeBaseUrl``, ``reserveIndex`` and the
   1347 ``reserveSeedDerived`` marker (which decides whether the full collection pass
   1348 emits the index-only form or the index-plus-private-key form); the exchange
   1349 base URL is required by the increment and was missing from the record (see the
   1350 ``FIXME: Should reference exchange.`` comment in ``db.ts`` and the redundant
   1351 ``exchangeBaseUrl`` of ``WithdrawalGroupRecord``).
   1352 
   1353 The remaining fields of ``WalletReserve`` (``status``, the KYC thresholds,
   1354 ``kycAccessToken``, ``amlReview``) are all derivable by querying the exchange
   1355 and are deliberately not backed up, so that a restored wallet re-derives them
   1356 instead of trusting stale state.
   1357 
   1358 Recoup
   1359 ++++++
   1360 
   1361 The reserve increment is what keeps recoup working on a restored wallet.  The
   1362 recoup request itself is signed by the *coin*: the coin record (``add-coin``)
   1363 carries the coin private key, the blinding key and the denomination signature
   1364 the request needs, and the request names the reserve only by its public key,
   1365 which the coin source carries.  After the exchange confirms the recoup, the
   1366 wallet queries the reserve's balance and withdraws it back into coins; that
   1367 re-withdrawal needs the reserve *private* key, which is exactly what
   1368 ``add-reserve`` restores.  The recoup of a refreshed coin (``recoup-refresh``)
   1369 likewise needs only the coin records -- the refreshed coin plus the old coin
   1370 the refresh source names -- so no refresh-group data is involved.
   1371 
   1372 The upcoming batch recoup protocol (``vRECOUP``, see ``api-exchange.rst``)
   1373 adds, per coin, the Clause-Schnorr blinding data (``cs_session_nonce`` and the
   1374 ``cs_r_pubs`` of the exchange's ``/blinding-prepare``) for post-quantum
   1375 denominations.  The wallet does not store that data anywhere yet; when it
   1376 does, the ``add-coin`` increment must carry it (as optional fields).  That is
   1377 a coin-family extension; the reserve side of a post-quantum recoup stays as
   1378 described above.
   1379 
   1380 Why the schema matters to the other increment types
   1381 ++++++++++++++++++++++++++++++++++++++++++++++++++++
   1382 
   1383 The ``reserves`` store is referenced, directly or through its row id, by the
   1384 withdrawal groups (``reservePub``/``reservePriv``), the coin sources
   1385 (``WithdrawCoinSource.reservePub``, used for recouping), the exchange entries
   1386 (``currentMergeReserveRowId``) and the peer-pull-credit records
   1387 (``mergeReserveRowId``):
   1388 
   1389 * ``withdrawal-start`` is the most obvious case: the wallet's
   1390   ``WithdrawalGroupRecord`` embeds the reserve key pair and the exchange base
   1391   URL.  With ``add-reserve``, a ``withdrawal-start`` increment can reference
   1392   the reserve by ``[exchangeBaseUrl, reservePub]`` instead of carrying the key
   1393   pair, avoiding duplication.
   1394 * ``add-coin`` / ``spend-coin`` reference the reserve through the withdrawal
   1395   coin source's ``reservePub``; the restored reserve record is what makes the
   1396   restored coin recoupable (see above).
   1397 * The exchange entries and the peer-pull-credit records reference the merge
   1398   reserve by a *row id* into the ``reserves`` store, which is not portable
   1399   across wallets.  The ``add-exchange`` increment does not carry the
   1400   ``currentMergeReserveRowId`` pointer, so a restored exchange entry starts
   1401   without one; the merge reserve remains findable by its public key, and
   1402   re-linking the pointer on restore is a follow-up.
   1403 
   1404 Every increment family in this document is implemented; see the "Definition of
   1405 done" section for what remains.
   1406 
   1407 Start a deposit
   1408 ~~~~~~~~~~~~~~~
   1409 
   1410 .. code-block:: typescript
   1411 
   1412    interface DepositStartInc {
   1413      type: "deposit-start";
   1414      depositGroupId: string;
   1415      currency: string;
   1416      amount: AmountString;
   1417      wireTransferDeadline: TalerProtocolTimestamp;
   1418      merchantPub: EddsaPublicKey;
   1419      merchantPriv: EddsaPrivateKey;
   1420      noncePub: EddsaPublicKey;
   1421      noncePriv: EddsaPrivateKey;
   1422      wire: {payto_uri: string, salt: string};
   1423      contractTermsHash: HashCode; // blob
   1424      totalPayCost: AmountString;
   1425      timestampCreated: TalerPreciseTimestamp;
   1426      infoPerExchange: {[exchangeBaseUrl: string]: DepositInfoPerExchange};
   1427    }
   1428 
   1429 * **Primary key:** ``[depositGroupId]``
   1430 * **Deletion groups:** ``[deposits]``
   1431 
   1432 Merge strategy
   1433 ++++++++++++++
   1434 
   1435 No merge is required, all deposits are independent from each other.
   1436 
   1437 Abort a deposit
   1438 ~~~~~~~~~~~~~~~
   1439 
   1440 User aborts a deposit.
   1441 
   1442 .. code-block:: typescript
   1443 
   1444    interface DepositAbortInc {
   1445      type: "deposit-abort";
   1446      depositGroupId: string;
   1447      abortReason?: TalerErrorDetail;
   1448    }
   1449 
   1450 * **Primary key:** ``[depositGroupId]``
   1451 * **Deletion groups:** ``[deposits]``
   1452 
   1453 Merge strategy
   1454 ++++++++++++++
   1455 
   1456 Store all ``abortReason`` in the database.
   1457 
   1458 Deposit done
   1459 ~~~~~~~~~~~~
   1460 
   1461 A deposit started by the user completes successfully.
   1462 
   1463 .. code-block:: typescript
   1464 
   1465    interface DepositDoneInc {
   1466      type: "deposit-done";
   1467      depositGroupId: string;
   1468      timestampFinished: TalerPreciseTimestamp;
   1469    }
   1470 
   1471 * **Primary key:** ``[depositGroupId]``
   1472 * **Deletion groups:** ``[deposits]``
   1473 
   1474 Merge strategy
   1475 ++++++++++++++
   1476 
   1477 No merge required, a deposit can only succeed once.
   1478 
   1479 Deposit fail
   1480 ~~~~~~~~~~~~
   1481 
   1482 A deposit started by the user fails.
   1483 
   1484 .. code-block:: typescript
   1485 
   1486    interface DepositFailInc {
   1487      type: "deposit-fail";
   1488      depositGroupId: string;
   1489      failReason: TalerErrorDetail;
   1490    }
   1491 
   1492 * **Primary key:** ``[depositGroupId]``
   1493 * **Deletion groups:** ``[deposits]``
   1494 
   1495 Merge strategy
   1496 ++++++++++++++
   1497 
   1498 Store all ``failReason`` in the database.
   1499 
   1500 Start a merchant payment
   1501 ~~~~~~~~~~~~~~~~~~~~~~~~
   1502 
   1503 User initiates a payment to a merchant.
   1504 
   1505 .. code-block:: typescript
   1506 
   1507       interface PaymentStartInc {
   1508         type: "payment-start";
   1509         proposalId: string;
   1510         // Not in the original design, but needed to reconstruct the
   1511         // `taler://pay/...' URI and re-download the proposal on restore:
   1512         merchantBaseUrl: string;
   1513         orderId: string;
   1514         claimToken?: string;
   1515         downloadSessionId?: string;
   1516         repurchaseProposalId?: string;
   1517         noncePub: EddsaPublicKey;
   1518         noncePriv: EddsaPrivateKey;
   1519         secretSeed: string;
   1520         exchanges?: string[];
   1521         // Hash of the contract terms (a blob).  Unknown until the
   1522         // proposal has been downloaded.
   1523         contractTermsHash?: string;
   1524         timestamp: TalerPreciseTimestamp;
   1525 
   1526         // Donau
   1527         donauOutputIndex?: number;
   1528         donauBaseUrl?: string;
   1529         donauAmount?: AmountString;
   1530         donauTaxIdHash?: string;
   1531         donauTaxIdSalt?: string;
   1532         donauTaxId?: string;
   1533         donauYear?: number;
   1534       }
   1535 
   1536 * **Primary key:** ``[proposalId]``
   1537 * **Deletion groups:** ``[payments]``
   1538 
   1539 Merge strategy
   1540 ++++++++++++++
   1541 
   1542 No merge is required, all payments are independent from each other.
   1543 
   1544 Confirm a merchant payment
   1545 ~~~~~~~~~~~~~~~~~~~~~~~~~~
   1546 
   1547 User confirms a payment to a merchant.
   1548 
   1549 .. code-block:: typescript
   1550 
   1551    interface PaymentConfirmInc {
   1552      type: "payment-confirm";
   1553      proposalId: string;
   1554      choiceIndex?: number;
   1555      timestampAccept: TalerPreciseTimestamp;
   1556    }
   1557 
   1558 * **Primary key:** ``[proposalId]``
   1559 * **Deletion groups:** ``[payments]``
   1560 
   1561 Merge strategy
   1562 ++++++++++++++
   1563 
   1564 No merge is required, a payment can only succeed once.
   1565 
   1566 Abort a merchant payment
   1567 ~~~~~~~~~~~~~~~~~~~~~~~~
   1568 
   1569 User aborts a payment to a merchant.
   1570 
   1571 .. code-block:: typescript
   1572 
   1573    interface PaymentAbortInc {
   1574      type: "payment-abort";
   1575      proposalId: string;
   1576      abortReason?: TalerErrorDetail;
   1577    }
   1578 
   1579 * **Primary key:** ``[proposalId]``
   1580 * **Deletion groups:** ``[payments]``
   1581 
   1582 Merge strategy
   1583 ++++++++++++++
   1584 
   1585 Store all ``abortReason`` in the database.
   1586 
   1587 Merchant purchase done
   1588 ~~~~~~~~~~~~~~~~~~~~~~
   1589 
   1590 A payment started by the user completes successfully.
   1591 
   1592 .. code-block:: typescript
   1593 
   1594    interface PaymentDoneInc {
   1595      type: "payment-done";
   1596      proposalId: string;
   1597    }
   1598 
   1599 * **Primary key:** ``[proposalId]``
   1600 * **Deletion groups:** ``[payments]``
   1601 
   1602 Merchant purchase fail
   1603 ~~~~~~~~~~~~~~~~~~~~~~
   1604 
   1605 A payment started by the user fails.
   1606 
   1607 .. code-block:: typescript
   1608 
   1609    interface PaymentFailInc {
   1610      type: "payment-fail";
   1611      proposalId: string;
   1612      failReason: TalerErrorDetail;
   1613    }
   1614 
   1615 * **Primary key:** ``[proposalId]``
   1616 * **Deletion groups:** ``[payments]``
   1617 
   1618 Merge strategy
   1619 ++++++++++++++
   1620 
   1621 Store all ``failReason`` in the database.
   1622 
   1623 Start peer-push-credit
   1624 ~~~~~~~~~~~~~~~~~~~~~~
   1625 
   1626 User receives an incoming push payment.
   1627 
   1628 .. code-block:: typescript
   1629 
   1630    interface PeerPushCreditStartInc {
   1631      type: "peer-push-credit-start";
   1632      peerPushCreditId: string;
   1633      exchangeBaseUrl: string;
   1634      pursePub: EddsaPublicKey;
   1635      mergePriv: EddsaPrivateKey;
   1636      contractPriv: EddsaPrivateKey;
   1637      timestamp: TalerPreciseTimestamp;
   1638      estimatedAmountEffective: AmountString;
   1639      contractTermsHash: HashCode; // blob
   1640      currency: string;
   1641    }
   1642 
   1643 * **Primary key:** ``[peerPushCreditId]``
   1644 * **Deletion groups:** ``[peer-push-credit]``
   1645 
   1646 Merge strategy
   1647 ++++++++++++++
   1648 
   1649 Last write wins, since the parameters of a peer-push-credit transaction are
   1650 expected to always remain constant. However, ``peerPushCreditId`` must be
   1651 derived from the ``exchangeBaseUrl`` and ``pursePub``.
   1652 
   1653 Abort peer-push-credit
   1654 ~~~~~~~~~~~~~~~~~~~~~~
   1655 
   1656 User aborts an incoming push payment.
   1657 
   1658 .. code-block:: typescript
   1659 
   1660    interface PeerPushCreditAbortInc {
   1661      type: "peer-push-credit-abort";
   1662      peerPushCreditId: string;
   1663      abortReason?: TalerErrorDetail;
   1664    }
   1665 
   1666 * **Primary key:** ``[peerPushCreditId]``
   1667 * **Deletion groups:** ``[peer-push-credit]``
   1668 
   1669 Merge strategy
   1670 ++++++++++++++
   1671 
   1672 Store all ``abortReason`` in the database.
   1673 
   1674 Peer-push-credit done
   1675 ~~~~~~~~~~~~~~~~~~~~~
   1676 
   1677 An incoming push payment received by the user completes successfully.
   1678 
   1679 .. code-block:: typescript
   1680 
   1681    interface PeerPushCreditDoneInc {
   1682      type: "peer-push-credit-done";
   1683      peerPushCreditId: string;
   1684    }
   1685 
   1686 * **Primary key:** ``[peerPushCreditId]``
   1687 * **Deletion groups:** ``[peer-push-credit]``
   1688 
   1689 Merge strategy
   1690 ++++++++++++++
   1691 
   1692 No merge is required, a peer-push-credit payment can only succeed once.
   1693 
   1694 Peer-push-credit fail
   1695 ~~~~~~~~~~~~~~~~~~~~~
   1696 
   1697 An incoming push payment received by the user fails.
   1698 
   1699 .. code-block:: typescript
   1700 
   1701    interface PeerPushCreditFailInc {
   1702      type: "peer-push-credit-fail";
   1703      peerPushCreditId: string;
   1704      failReason: TalerErrorDetail;
   1705    }
   1706 
   1707 * **Primary key:** ``[peerPushCreditId]``
   1708 * **Deletion groups:** ``[peer-push-credit]``
   1709 
   1710 Merge strategy
   1711 ++++++++++++++
   1712 
   1713 Store all ``failReason`` in the database.
   1714 
   1715 Start peer-push-debit
   1716 ~~~~~~~~~~~~~~~~~~~~~
   1717 
   1718 User initiates an outgoing push payment.
   1719 
   1720 .. code-block:: typescript
   1721 
   1722    interface PeerPushDebitStartInc {
   1723      type: "peer-push-debit-start";
   1724      exchangeBaseUrl: string;
   1725      instructedAmount: AmountString;
   1726      effectiveAmount: AmountString;
   1727      contractTermsHash: HashCode; // blob
   1728      pursePub: EddsaPublicKey;
   1729      pursePriv: EddsaPrivateKey;
   1730      mergePub: EddsaPublicKey;
   1731      mergePriv: EddsaPrivateKey;
   1732      contractPub: EddsaPublicKey;
   1733      contractPriv: EddsaPrivateKey;
   1734      contractEncNonce: string;
   1735      purseExpiration: TalerProtocolTimestamp;
   1736      timestampCreated: TalerPreciseTimestamp;
   1737    }
   1738 
   1739 * **Primary key:** ``[pursePub]``
   1740 * **Deletion groups:** ``[peer-push-debit]``
   1741 
   1742 Merge strategy
   1743 ++++++++++++++
   1744 
   1745 No merge is required, all peer-push-debit payments are independent from each
   1746 other.
   1747 
   1748 Abort peer-push-debit
   1749 ~~~~~~~~~~~~~~~~~~~~~
   1750 
   1751 User aborts an outgoing push payment.
   1752 
   1753 .. code-block:: typescript
   1754 
   1755    interface PeerPushDebitAbortInc {
   1756      type: "peer-push-debit-abort";
   1757      pursePub: EddsaPublicKey;
   1758      abortReason?: TalerErrorDetail;
   1759    }
   1760 
   1761 * **Primary key:** ``[pursePub]``
   1762 * **Deletion groups:** ``[peer-push-debit]``
   1763 
   1764 Merge strategy
   1765 ++++++++++++++
   1766 
   1767 Store all ``abortReason`` in the database.
   1768 
   1769 Peer-push-debit done
   1770 ~~~~~~~~~~~~~~~~~~~~
   1771 
   1772 An outgoing push payment initiated by the user completes successfully.
   1773 
   1774 .. code-block:: typescript
   1775 
   1776    interface PeerPushDebitDoneInc {
   1777      type: "peer-push-debit-done";
   1778      pursePub: EddsaPublicKey;
   1779    }
   1780 
   1781 * **Primary key:** ``[pursePub]``
   1782 * **Deletion groups:** ``[peer-push-debit]``
   1783 
   1784 Merge strategy
   1785 ++++++++++++++
   1786 
   1787 No merge is required, a peer-push-debit payment can only succeed once.
   1788 
   1789 Peer-push-debit fail
   1790 ~~~~~~~~~~~~~~~~~~~~
   1791 
   1792 An outgoing push payment initiated by the user fails.
   1793 
   1794 .. code-block:: typescript
   1795 
   1796    interface PeerPushDebitFailInc {
   1797      type: "peer-push-debit-fail";
   1798      pursePub: EddsaPublicKey;
   1799      failReason: TalerErrorDetail;
   1800    }
   1801 
   1802 * **Primary key:** ``[pursePub]``
   1803 * **Deletion groups:** ``[peer-push-debit]``
   1804 
   1805 Merge strategy
   1806 ++++++++++++++
   1807 
   1808 Store all ``failReason`` in the database.
   1809 
   1810 Start peer-pull-debit
   1811 ~~~~~~~~~~~~~~~~~~~~~
   1812 
   1813 User confirms a payment request from another wallet.
   1814 
   1815 .. code-block:: typescript
   1816 
   1817    interface PeerPullDebitDoneInc {
   1818      type: "peer-pull-debit-start";
   1819      peerPullDebitId: string;
   1820      pursePub: EddsaPublicKey;
   1821      exchangeBaseUrl: string;
   1822      amount: AmountString;
   1823      contractTermsHash: HashCode; // blob
   1824      timestampCreated: TalerPreciseTimestamp;
   1825      contractPriv: EddsaPrivateKey;
   1826      totalCostEstimated: AmountString;
   1827    }
   1828 
   1829 * **Primary key:** ``[peerPullDebitId]``
   1830 * **Deletion groups:** ``[peer-pull-debit]``
   1831 
   1832 Merge strategy
   1833 ++++++++++++++
   1834 
   1835 Last write wins, since the parameters of a peer-pull-debit transaction are
   1836 expected to always remain constant. However, ``peerPullDebitId`` must be
   1837 derived from the ``exchangeBaseUrl`` and ``pursePub``.
   1838 
   1839 Abort peer-pull-debit
   1840 ~~~~~~~~~~~~~~~~~~~~~
   1841 
   1842 User aborts a payment to another wallet.
   1843 
   1844 .. code-block:: typescript
   1845 
   1846    interface PeerPullDebitAbortInc {
   1847      type: "peer-pull-debit-abort";
   1848      peerPullDebitId: string;
   1849      abortReason?: TalerErrorDetail;
   1850    }
   1851 
   1852 * **Primary key:** ``[peerPullDebitId]``
   1853 * **Deletion groups:** ``[peer-pull-debit]``
   1854 
   1855 Merge strategy
   1856 ++++++++++++++
   1857 
   1858 Store all ``abortReason`` in the database.
   1859 
   1860 Peer-pull-debit done
   1861 ~~~~~~~~~~~~~~~~~~~~
   1862 
   1863 A payment to another wallet completes successfully.
   1864 
   1865 .. code-block:: typescript
   1866 
   1867    interface PeerPullDebitDoneInc {
   1868      type: "peer-pull-debit-done";
   1869      peerPullDebitId: string;
   1870    }
   1871 
   1872 * **Primary key:** ``[peerPullDebitId]``
   1873 * **Deletion groups:** ``[peer-pull-debit]``
   1874 
   1875 Merge strategy
   1876 ++++++++++++++
   1877 
   1878 No merge is required, a peer-pull-debit payment can only succeed once.
   1879 
   1880 Peer-pull-debit fail
   1881 ~~~~~~~~~~~~~~~~~~~~
   1882 
   1883 A payment to another wallet fails.
   1884 
   1885 .. code-block:: typescript
   1886 
   1887    interface PeerPullDebitFailInc {
   1888      type: "peer-pull-debit-fail";
   1889      peerPullDebitId: string;
   1890      failReason: TalerErrorDetail;
   1891    }
   1892 
   1893 * **Primary key:** ``[peerPullDebitId]``
   1894 * **Deletion groups:** ``[peer-pull-debit]``
   1895 
   1896 Merge strategy
   1897 ++++++++++++++
   1898 
   1899 Store all ``failReason`` in the database.
   1900 
   1901 Start peer-pull-credit
   1902 ~~~~~~~~~~~~~~~~~~~~~~
   1903 
   1904 User requests money to another wallet.
   1905 
   1906 .. code-block:: typescript
   1907 
   1908    interface PeerPullCreditStartInc {
   1909      type: "peer-pull-credit-start";
   1910      exchangeBaseUrl: string;
   1911      amount: AmountString;
   1912      estimatedAmountEffective: AmountString;
   1913      pursePub: EddsaPublicKey;
   1914      pursePriv: EddsaPrivateKey;
   1915      contractTermsHash: HashCode; // blob
   1916      mergePub: EddsaPublicKey;
   1917      mergePriv: EddsaPrivateKey;
   1918      contractPub: EddsaPublicKey;
   1919      contractPriv: EddsaPrivateKey;
   1920      contractEncNonce: string;
   1921      mergeTimestamp: TalerPreciseTimestamp;
   1922      mergeReserveRowId: number;
   1923      withdrawalGroupId?: string;
   1924    }
   1925 
   1926 * **Primary key:** ``[pursePub]``
   1927 * **Deletion groups:** ``[peer-pull-credit]``
   1928 
   1929 Merge strategy
   1930 ++++++++++++++
   1931 
   1932 No merge is required, all peer-pull-credit payments are independent from each
   1933 other.
   1934 
   1935 Abort peer-pull-credit
   1936 ~~~~~~~~~~~~~~~~~~~~~~
   1937 
   1938 User aborts request to another wallet.
   1939 
   1940 .. code-block:: typescript
   1941 
   1942    interface PeerPullCreditAbortInc {
   1943      type: "peer-pull-credit-abort";
   1944      pursePub: EddsaPublicKey;
   1945      abortReason?: TalerErrorInfo;
   1946    }
   1947 
   1948 * **Primary key:** ``[pursePub]``
   1949 * **Deletion groups:** ``[peer-pull-credit]``
   1950 
   1951 Merge strategy
   1952 ++++++++++++++
   1953 
   1954 Store all ``failReason`` in the database.
   1955 
   1956 Peer-pull-credit done
   1957 ~~~~~~~~~~~~~~~~~~~~~
   1958 
   1959 A request to another wallet completes successfully (i.e. money is received).
   1960 
   1961 .. code-block:: typescript
   1962 
   1963    interface PeerPullCreditDoneInc {
   1964      type: "peer-pull-credit-done";
   1965      pursePub: EddsaPublicKey;
   1966    }
   1967 
   1968 * **Primary key:** ``[pursePub]``
   1969 * **Deletion groups:** ``[peer-pull-credit]``
   1970 
   1971 Merge strategy
   1972 ++++++++++++++
   1973 
   1974 No merge is required, a peer-pull-credit payment can only succeed once.
   1975 
   1976 Peer-pull-credit fail
   1977 ~~~~~~~~~~~~~~~~~~~~~
   1978 
   1979 A request to another wallet fails.
   1980 
   1981 .. code-block:: typescript
   1982 
   1983    interface PeerPullCreditFailInc {
   1984      type: "peer-pull-credit-fail";
   1985      pursePub: EddsaPublicKey;
   1986      failReason: TalerErrorInfo;
   1987    }
   1988 
   1989 * **Primary key:** ``[pursePub]``
   1990 * **Deletion groups:** ``[peer-pull-credit]``
   1991 
   1992 Merge strategy
   1993 ++++++++++++++
   1994 
   1995 Store all ``failReason`` in the database.
   1996 
   1997 Start a refresh
   1998 ~~~~~~~~~~~~~~~
   1999 
   2000 The wallet melts the remainder of one or more coins into fresh ones -- as
   2001 change after a payment, or to renew a coin whose denomination is about to
   2002 expire.
   2003 
   2004 The group carries the plan; how far it has got lives in the per-coin sessions
   2005 below.  A restored group is what lets a wallet that melted a coin and then
   2006 lost the device still collect the change: the exchange holds the first melt
   2007 commitment, and a wallet that re-melted with a fresh seed could not reveal
   2008 against it.
   2009 
   2010 .. code-block:: typescript
   2011 
   2012    interface RefreshStartInc {
   2013      type: "refresh-start";
   2014      refreshGroupId: string;
   2015      currency: string;
   2016      reason: string;
   2017      originatingTransactionId?: string;
   2018      oldCoinPubs: string[];
   2019      inputPerCoin: AmountString[];
   2020      expectedOutputPerCoin: AmountString[];
   2021      timestampCreated: TalerPreciseTimestamp;
   2022    }
   2023 
   2024 * **Primary key:** ``[refreshGroupId]``
   2025 * **Deletion groups:** ``[refreshes]``
   2026 
   2027 Merge strategy
   2028 ++++++++++++++
   2029 
   2030 Last write wins: the plan of a refresh group never changes.
   2031 
   2032 Refresh session
   2033 ~~~~~~~~~~~~~~~
   2034 
   2035 The melt of one coin of a refresh group.
   2036 
   2037 Everything the reveal step needs -- the fresh coins' key material included --
   2038 is derived from ``sessionPublicSeed`` together with the old coin and the
   2039 chosen denominations, all of which travel here, so this is the part of a
   2040 refresh that has to be backed up.
   2041 
   2042 .. code-block:: typescript
   2043 
   2044    interface RefreshSessionInc {
   2045      type: "refresh-session";
   2046      refreshGroupId: string;
   2047      coinIndex: number;
   2048      sessionPublicSeed?: string;
   2049      refreshProtocolVersion?: number;
   2050      amountRefreshOutput: AmountString;
   2051      newDenoms: { denomPubHash: string; count: number }[];
   2052      norevealIndex?: number;
   2053    }
   2054 
   2055 * **Primary key:** ``[refreshGroupId, coinIndex]``
   2056 * **Deletion groups:** ``[refreshes]``
   2057 
   2058 Merge strategy
   2059 ++++++++++++++
   2060 
   2061 Last write wins: the session is written once, when the coin is melted.
   2062 
   2063 Refresh done
   2064 ~~~~~~~~~~~~
   2065 
   2066 Every coin of the group has been melted and the fresh coins collected.
   2067 
   2068 .. code-block:: typescript
   2069 
   2070    interface RefreshDoneInc {
   2071      type: "refresh-done";
   2072      refreshGroupId: string;
   2073      timestampFinished: TalerPreciseTimestamp;
   2074    }
   2075 
   2076 * **Primary key:** ``[refreshGroupId]``
   2077 * **Deletion groups:** ``[refreshes]``
   2078 
   2079 Refresh failed
   2080 ~~~~~~~~~~~~~~
   2081 
   2082 The refresh could not be completed.
   2083 
   2084 .. code-block:: typescript
   2085 
   2086    interface RefreshFailInc {
   2087      type: "refresh-fail";
   2088      refreshGroupId: string;
   2089      failReason: TalerErrorDetail;
   2090    }
   2091 
   2092 * **Primary key:** ``[refreshGroupId]``
   2093 * **Deletion groups:** ``[refreshes]``
   2094 
   2095 Derived operations: refunds, recoups and denomination losses
   2096 ------------------------------------------------------------
   2097 
   2098 The three families below differ from every other one in this document: the
   2099 wallet does not start them, it *learns* about them.  A refund is the
   2100 merchant's answer to a refund query, a recoup is forced by an exchange
   2101 revoking a denomination, and a denomination loss is what the wallet has to
   2102 write off when a denomination expires or is withdrawn from circulation.
   2103 
   2104 Any wallet holding the coins can ask the same question and get the same
   2105 answer, which is what decides how they are backed up: **only a finished one
   2106 travels, and it restores as finished.** Backing up a pending one would hand
   2107 the second device work on an operation it cannot see the whole of -- it would
   2108 go and query a merchant about a refund that is already settled on the first
   2109 device -- and would leave the user looking at an operation that is long over
   2110 elsewhere but "pending" here.  A pending one is simply not collected, and
   2111 keeps no origin block, so a later pass offers it up once it has finished.
   2112 
   2113 Refund
   2114 ~~~~~~
   2115 
   2116 A refund the merchant granted, as it finally stood.
   2117 
   2118 The refund *items* (one per coin) are deliberately not carried: nothing
   2119 outside the refund query itself reads them, the transaction is rendered
   2120 entirely from the group, and their identity is the merchant's
   2121 (``coin_pub``/``rtransaction_id``), so a wallet that does query gets the same
   2122 ones back.
   2123 
   2124 .. code-block:: typescript
   2125 
   2126    interface RefundInc {
   2127      type: "refund";
   2128      refundGroupId: string;
   2129      // The purchase this refunds; restored as the transaction it points
   2130      // at, and not applied at all when that purchase is not there.
   2131      proposalId: string;
   2132      outcome: DerivedOutcome;
   2133      amountRaw: AmountString;
   2134      amountEffective: AmountString;
   2135      timestampCreated: TalerPreciseTimestamp;
   2136    }
   2137 
   2138 .. code-block:: typescript
   2139 
   2140    // How one of the derived operations ended.  A wire string rather than
   2141    // the wallet's numeric status enum, which is a database detail.
   2142    type DerivedOutcome = "done" | "failed" | "aborted" | "expired";
   2143 
   2144 * **Primary key:** ``[refundGroupId]``
   2145 * **Deletion groups:** ``[refunds, payments]``
   2146 
   2147 Merge strategy
   2148 ++++++++++++++
   2149 
   2150 Last write wins: the increment describes one finished operation, and there is
   2151 nothing to reconcile field by field.
   2152 
   2153 Recoup
   2154 ~~~~~~
   2155 
   2156 Coins reclaimed from an exchange that revoked their denomination.
   2157 
   2158 What the recoup *did* to the coins reaches the other wallets as coin
   2159 increments; this is what makes the operation itself appear.  Its per-coin
   2160 progress is not carried -- it describes a run the other wallet did not make --
   2161 and a restored recoup is marked finished for every coin, so that the second
   2162 device does not go and re-submit somebody else's recoup.
   2163 
   2164 .. code-block:: typescript
   2165 
   2166    interface RecoupInc {
   2167      type: "recoup";
   2168      recoupGroupId: string;
   2169      exchangeBaseUrl: string;
   2170      outcome: DerivedOutcome;
   2171      // The coins that were recouped, in the order the group listed them.
   2172      coinPubs: string[];
   2173      timestampStarted: TalerPreciseTimestamp;
   2174      timestampFinished?: TalerPreciseTimestamp;
   2175    }
   2176 
   2177 * **Primary key:** ``[recoupGroupId]``
   2178 * **Deletion groups:** ``[recoups, coins]``
   2179 
   2180 Merge strategy
   2181 ++++++++++++++
   2182 
   2183 Last write wins.
   2184 
   2185 Denomination loss
   2186 ~~~~~~~~~~~~~~~~~
   2187 
   2188 A denomination the wallet had to write off, with the coins it cost.
   2189 
   2190 Unlike the two above this one is not merely history: until the other wallets
   2191 learn of it they keep the affected coins in their balance, and the two devices
   2192 disagree about how much money the user has.  The coins themselves carry the
   2193 same news -- their status becomes ``denom-loss`` -- and this is what makes the
   2194 transaction appear.
   2195 
   2196 ``denomLossEventId`` is **derived from the loss** rather than drawn at random.
   2197 Both wallets notice the same expiry on their own, each updating the exchange
   2198 and seeing the same denominations go; with random identifiers the user would
   2199 end up with the same loss listed twice.
   2200 
   2201 .. code-block:: text
   2202 
   2203    denom_loss_event_id = SHA512(exchange_base_url || 0 || event_type || 0 ||
   2204                                 sorted(denom_pub_hashes) each || 0)[0:32]
   2205 
   2206 .. code-block:: typescript
   2207 
   2208    interface DenomLossInc {
   2209      type: "denom-loss";
   2210      denomLossEventId: string;
   2211      currency: string;
   2212      exchangeBaseUrl: string;
   2213      denomPubHashes: string[];
   2214      // "denom-expired", "denom-vanished", "denom-revoked",
   2215      // "denom-unoffered".
   2216      eventType: string;
   2217      // "aborted" when the loss turned out to be reversible.
   2218      outcome: "done" | "aborted";
   2219      amount: AmountString;
   2220      timestampCreated: TalerPreciseTimestamp;
   2221    }
   2222 
   2223 * **Primary key:** ``[denomLossEventId]``
   2224 * **Deletion groups:** ``[denom-losses, denominations]``
   2225 
   2226 Merge strategy
   2227 ++++++++++++++
   2228 
   2229 Last write wins.
   2230 
   2231 Item deletion
   2232 -------------
   2233 
   2234 Due to privacy considerations within our use case, rather than using classical
   2235 CRDT-style tombstones to encode deletion operations into blocks, a novel
   2236 approach was conceived, whereby each item (e.g. an exchange) in the local
   2237 wallet database to be included in the backup keeps a list of UUIDs of the
   2238 "origin" blocks that have inserted or updated it.
   2239 
   2240 .. code-block:: typescript
   2241 
   2242    originBlocks: Set<BlockUuid>;
   2243 
   2244 Using this approach, a deletion of an item would simply consist of locating
   2245 the origin blocks referenced in its UUID list, and deleting the corresponding
   2246 insertion/update operations from all of them.
   2247 
   2248 In order to prevent wallets from mistakenly reinserting an item into the
   2249 backup that was previously deleted by another wallet, an item is deemed
   2250 deleted iff it no longer appears in any of its origin blocks, allowing it to
   2251 be safely removed from the local database as well.
   2252 
   2253 Mechanically, a wallet deletes an item by scrubbing its increments out of the
   2254 pending buffer and rewriting every origin block that still carries them: a
   2255 block that keeps other content is replaced in place (``PUT``, under its
   2256 original nonce), one that becomes empty is removed from the linked list
   2257 (``DELETE``, relinking its neighbours).  A block rewritten in place keeps its
   2258 nonce, so the other wallets detect the change only by noticing that the
   2259 block's hash no longer matches their local copy; a deleted block shows up as a
   2260 gap in the linked list.  On either signal a wallet re-applies the whole linked
   2261 list and drops every item that no longer appears in any origin block, which is
   2262 what makes deletions propagate across the sync group.
   2263 
   2264 Deletion groups
   2265 ~~~~~~~~~~~~~~~
   2266 
   2267 A resource within its deletion group is identified by its primary key. When
   2268 the resource in question is deleted, all references to this resource within
   2269 the resource group must also be deleted from the blocks listed in the
   2270 ``originBlocks`` field of its database record.
   2271 
   2272 For example, when deleting a denomination, all the coin insertions of that
   2273 denomination must also be deleted from the backup, since they are in the
   2274 ``denominations`` deletion group and thus contain a reference to a
   2275 denomination. In turn, all the sign and spend operations of the deleted coins
   2276 must also be deleted, since they are in the ``coins`` deletion group and thus
   2277 contain a reference to a coin.
   2278 
   2279 Backup process
   2280 --------------
   2281 
   2282 Collecting increments
   2283 ~~~~~~~~~~~~~~~~~~~~~
   2284 
   2285 Recording runs inside the very transaction that performs the withdrawal, the
   2286 payment or the deposit, which is what makes wallet state and backup state
   2287 commit together -- and also means that anything the recording throws takes
   2288 that operation down with it.  It must therefore be impossible for the backup
   2289 to fail an operation: the eager recording is an *optimisation*, not the
   2290 guarantee.  A record whose increment never made it keeps its ``originBlocks``
   2291 unset, which is exactly what the full collection pass looks for, so a failure
   2292 costs a delay and nothing else.  Recording, waking the cycle and queueing a
   2293 deletion all log and swallow; the critical-point hold fails open.
   2294 
   2295 The same applies to key material the wallet *derives* for an operation.  A
   2296 reserve key pair comes from the reserve seed, so a seed the wallet cannot
   2297 decode would otherwise block every withdrawal, permanently, since the seed is
   2298 stored.  An unusable seed instead falls back to a random reserve key pair,
   2299 which the backup carries as ``reservePriv`` the way it does for reserves that
   2300 predate the seed, and the seed itself is left untouched -- reserves already
   2301 derived from it are named by their index, so replacing it would make them
   2302 underivable elsewhere.
   2303 
   2304 Stored key material is checked before it is decoded, because the two Crockford
   2305 base32 decoders a wallet may run on do not agree: the JavaScript one ignores
   2306 trailing padding bits that are not zero, while the native (qtart) one rejects
   2307 the string outright.  A value decoded unchecked therefore works in a browser
   2308 extension and throws on a phone.  Re-encoding the decoded bytes and comparing
   2309 settles it on either runtime, and is what the restore path uses to refuse a
   2310 malformed seed rather than store one.
   2311 
   2312 Wallet transactions record what they changed by appending increments to a
   2313 pending buffer, held in the wallet's backup configuration record. The
   2314 recording happens **within the same database transaction that performs the
   2315 change**, so that the change and the increment describing it commit together.
   2316 A wallet can therefore never end up in a state that its backup does not know
   2317 about, however abruptly it is shut down.
   2318 
   2319 A wallet that has not set up backup yet has no encryption key to protect the
   2320 increments with, so recording is a no-op rather than an error.
   2321 
   2322 An increment that another record depends on must not reach the group later
   2323 than the record itself.  The denomination of a coin is the case that
   2324 matters: a restored coin only counts towards the balance once the
   2325 denomination it names is in the database, since that is where the
   2326 availability row takes its currency and value from.  Denominations are not
   2327 written by a transaction of their own, so recording a coin records its
   2328 denomination with it -- once per denomination, however many coins of it a
   2329 withdrawal makes -- and the two travel in the same block, where the
   2330 denomination section is applied before the coin section.  Leaving the
   2331 denomination to the full collection pass instead would let a coin reach
   2332 the other wallets of the group up to a day ahead of it.
   2333 
   2334 The backup cycle
   2335 ~~~~~~~~~~~~~~~~
   2336 
   2337 One cycle takes whatever increments have accumulated, packs them into a block,
   2338 and appends that block to the account's linked list:
   2339 
   2340 1. In a single database transaction, move the pending increments out of the
   2341    buffer and into an *in-flight block*, storing its nonce, hash, contents and
   2342    the nonce of the block it is to be appended after.
   2343 2. Upload any blobs the block references, then the block itself.
   2344 3. Once the provider has acknowledged the block, discard the in-flight block
   2345    and advance the pointer to the last acknowledged block.
   2346 
   2347 The hand-over in step 1 is what makes the cycle resilient: the increments are
   2348 never absent from both the buffer and a block. A wallet that dies at any point
   2349 either finds increments still pending, or finds an in-flight block and retries
   2350 it — under its **original nonce**, which the server answers with ``304 Not
   2351 modified`` if the upload did in fact land. Increments are thus neither lost
   2352 nor backed up twice, and a cycle that has packed a block always retries it
   2353 before packing new increments, so the linked list stays ordered.
   2354 
   2355 A cycle packs at most one block, and bounds its size.  The server refuses
   2356 an upload beyond its ``storage_limit_in_megabytes`` with ``413``, and a
   2357 block over that limit is not a transient failure: the wallet would re-upload
   2358 the very same block on every cycle and never get past it.  The pack
   2359 therefore stops well below any plausible server limit and leaves whatever
   2360 does not fit in the pending buffer, which the next cycle takes -- a wallet
   2361 handing over a long history (the full collection pass on a well-used
   2362 device) sends it as a run of blocks rather than as one oversized one, and
   2363 reports progress rather than backing off between them.  A ``413`` that
   2364 happens anyway is answered by putting the block's increments back and
   2365 packing the next one smaller, since retrying it unchanged can never
   2366 succeed.
   2367 
   2368 A cycle also pulls the account's linked list before packing new increments,
   2369 applying any blocks it has not seen before (see "Restore process" below), so
   2370 that new blocks are appended after the current end of the list.
   2371 
   2372 An account that has not been paid for yet answers every request with ``402
   2373 Payment required``, and only the upload endpoints carry the ``Taler:`` header
   2374 with a ``taler://pay/...`` URI.  A cycle that is answered this way while
   2375 pulling therefore pushes whatever it has pending, so the payment is settled —
   2376 automatically when the annual fee is zero — and subsequent writes are
   2377 accepted.
   2378 
   2379 Backup schedule
   2380 ---------------
   2381 
   2382 A backup runs at *critical points* of wallet operations, and on a schedule
   2383 otherwise.
   2384 
   2385 A critical point is one past which losing the device loses money or user data
   2386 that cannot be reconstructed. The canonical example is a withdrawal: coin
   2387 secrets are derived from the withdrawal group's seed, so a backup is triggered
   2388 once every planchet has been generated and persisted but **before** the
   2389 exchange is asked to sign them. Past that point the exchange considers the
   2390 coins withdrawn while a wallet restored from an older backup could no longer
   2391 reconstruct them.
   2392 
   2393 A cycle is triggered after the recording transaction commits; if the wallet
   2394 stops before it runs, the increments simply stay pending until the next cycle.
   2395 Independently, a periodic task runs a cycle every hour, covering increments
   2396 whose trigger never fired, e.g. because the wallet was offline or the
   2397 operation has no critical point.  A cycle that could not reach the provider is
   2398 retried after five minutes, and one that is waiting for the account payment to
   2399 be prepared after thirty seconds -- the payment is what unlocks every upload,
   2400 so it is worth retrying as soon as the provider's merchant backend recovers.
   2401 
   2402 Waking the cycle is not always enough. Past a critical point the wallet has
   2403 already revealed key material to somebody else -- the exchange has signed the
   2404 planchets, the purse exists and can be paid into -- and the cycle runs
   2405 concurrently, so the operation would go ahead regardless.  Those points
   2406 therefore *hold*: the task returns to the scheduler and is retried, and only
   2407 proceeds once the pending buffer has reached the provider.  The hold is
   2408 skipped when the account is unpaid, since no cycle can drain the buffer until
   2409 the user pays and freezing every such transaction would be the worse failure.
   2410 
   2411 Each request for a cycle names how much is at stake, and the most urgent
   2412 reason asked for since the last cycle that reached the provider is what
   2413 decides how hard a *failing* cycle retries:
   2414 
   2415 * ``irrecoverable-secret`` -- key material a lost device would turn into lost
   2416   money.  Retried after fifteen seconds: the transaction that produced it is
   2417   held until the buffer drains, so a longer wait is also how long that
   2418   transaction sits still.
   2419 * ``transaction-milestone`` -- a state the user would notice losing, but one
   2420   that can be reconstructed.
   2421 * ``account-payment`` -- the sync account's own payment moved; nothing of the
   2422   user's is at stake.
   2423 
   2424 The last two fall back to the ordinary five-minute retry.  The urgency is not
   2425 persisted: after a restart the pending increments are still there and the
   2426 critical points ask again on their next retry, so it re-establishes itself
   2427 rather than having to be reconstructed.
   2428 
   2429 Full collection pass
   2430 ~~~~~~~~~~~~~~~~~~~~
   2431 
   2432 Eager recording covers every transaction family, but a record can still exist
   2433 that no transaction ever reported: one that predates the backup, or one of a
   2434 kind whose creation path bypasses the record handle.  A periodic *full
   2435 collection pass* is the safety net: it walks every record kind the backup
   2436 manages (the ``backupSources`` of ``sources.ts``) and turns the records that
   2437 have never been backed up into "start" increments.
   2438 
   2439 The pass is expensive -- it reads every denomination, exchange, bank account
   2440 and transaction the wallet holds -- so it does not run on every cycle.  It
   2441 runs when a watermark, ``lastFullCollection`` in the wallet's backup
   2442 configuration record, is older than 24 hours (or absent, i.e. never run).  A
   2443 cycle that woke from a critical point therefore stays cheap while still
   2444 backing up whatever the transactions themselves reported.
   2445 
   2446 A forced cycle (see ``runBackupCycle`` in the wallet-core API below) bypasses
   2447 the watermark and runs the pass regardless.  This is the tool for developer
   2448 diagnostics: everything the pass would collect is reported by
   2449 ``getBackupDiagnostics`` before the cycle runs, so the two requests together
   2450 show exactly what is waiting to be backed up and what a forced cycle would
   2451 add.
   2452 
   2453 Restore process
   2454 ---------------
   2455 
   2456 Restoring a wallet on a (fresh) device is the pull half of the backup cycle,
   2457 driven by a recovery document from ``getBackupRecovery``:
   2458 
   2459 1. ``loadBackupRecovery`` installs the recovery's root key and providers, and
   2460    drops the wallet's own block pointers, so the device starts from nothing.
   2461 2. Once the user activates a recovered provider (``addBackupProvider`` with
   2462    ``activate``), the backup cycle downloads the account's linked list,
   2463    decodes each block it has not seen before, CRDT-applies its increments to
   2464    the local database -- recording the block's nonce in the ``originBlocks``
   2465    of every record it touched -- and stores the blocks locally.
   2466 
   2467 Because the same root key derives the same per-provider account keys, a
   2468 recovering wallet sees exactly the blocks any other wallet in the group
   2469 uploaded and applies them with the same merge rules, so all devices converge
   2470 on the same state.
   2471 
   2472 Two things a restored record cannot simply carry are worked out again on
   2473 the restoring device:
   2474 
   2475 * A coin that arrives before the denomination it names cannot be counted,
   2476   because the availability row cannot be written without it.  Applying a
   2477   denomination therefore recounts the coins of that denomination that are
   2478   already in the database, so a coin whose denomination travels in a later
   2479   block -- or in a block written by another wallet -- still reaches the
   2480   balance instead of being dropped from it for good.
   2481 * A pending withdrawal's transfer instructions -- the exchange's credit
   2482   accounts, and the transfer options the user actually pays with -- are
   2483   derived from the exchange, the instructed amount and the reserve key
   2484   pair, and an option registered with a prepared-transfer service carries
   2485   an expiry.  A restoring wallet derives them again whenever the ones it
   2486   restored are absent or expired, and does so *before* it queries the
   2487   reserve: until the transfer has been made the reserve does not exist at
   2488   the exchange yet, so a wallet that waited for the reserve status would
   2489   never get as far as showing the user something to pay with.
   2490 
   2491 Restore schedule
   2492 ----------------
   2493 
   2494 Restoring happens on demand: it starts when a recovery document is loaded and
   2495 the recovered provider is activated.  Afterwards the restored wallet is kept
   2496 up to date by the same periodic backup task as every other wallet -- the pull
   2497 half runs on every cycle, so changes made by other devices are picked up at
   2498 the cycle interval.
   2499 
   2500 Wallet-core API
   2501 ---------------
   2502 
   2503 Backup providers and the wallet's backup key are managed through the
   2504 wallet-core API. All requests below are available on every platform.  The
   2505 request handlers described here are implemented; the collection and scheduling
   2506 mechanisms described above drive them.
   2507 
   2508 .. code-block:: typescript
   2509 
   2510    interface AddBackupProviderRequest {
   2511      backupProviderBaseUrl: string;
   2512 
   2513      name: string;
   2514 
   2515      // Activate the provider.  Should only be done after
   2516      // the user has reviewed the provider.
   2517      activate?: boolean;
   2518    }
   2519 
   2520 The cycle never *waits* for the account payment.  Downloading the provider's
   2521 proposal and paying it are the purchase's own task, so the cycle only ever
   2522 looks at where that purchase has got to -- confirming it when it is waiting
   2523 for a decision, and otherwise leaving it alone -- and comes back when the
   2524 purchase transitions, or on its retry interval.  Every step is therefore
   2525 idempotent and survives a wallet that stops in the middle.
   2526 
   2527 ``addBackupProvider`` registers a sync server: it stores a provider record and
   2528 -- when ``activate`` is set -- makes it the active sync target and wakes the
   2529 backup cycle.  The request itself does not talk to the provider and returns as
   2530 soon as the record is written; an unreachable provider, or one that is not a
   2531 sync server, therefore shows up as a failing (and retrying) cycle rather than
   2532 as an error from this request.
   2533 
   2534 The first cycle is what learns the provider's terms (it fetches ``/config``
   2535 and reports the result with the ``terms-fetched`` phase of the
   2536 ``backup-status`` notification) and what settles the account payment: a sync
   2537 account only exists once it has been paid for, and the server rejects every
   2538 upload (even at a zero annual fee) until then.  A zero-fee account is paid
   2539 automatically; any other account produces a payment transaction that the user
   2540 confirms from the wallet, and the ``payment-required`` phase of the
   2541 notification carries its ``taler://pay/...`` URI.  Clients follow all of this
   2542 through the notifications, not through this request's response:
   2543 
   2544 .. code-block:: typescript
   2545 
   2546    interface AddBackupProviderResponse {
   2547      status: "ok";
   2548    }
   2549 
   2550 ``removeBackupProvider`` takes a ``RemoveBackupProviderRequest`` naming the
   2551 provider by base URL and returns an empty object.
   2552 
   2553 .. code-block:: typescript
   2554 
   2555    interface RemoveBackupProviderRequest {
   2556      backupProviderBaseUrl: string;
   2557    }
   2558 
   2559 ``getBackupInfo`` reports the wallet's backup identity and the state of each
   2560 known provider, including its terms, payment status and the outcome of the
   2561 last backup attempt.
   2562 
   2563 .. code-block:: typescript
   2564 
   2565    interface BackupInfo {
   2566      walletRootPub: string;
   2567      providers: ProviderInfo[];
   2568    }
   2569 
   2570 ``ProviderInfo`` describes one known provider and the state of the wallet's
   2571 account on it:
   2572 
   2573 .. code-block:: typescript
   2574 
   2575    interface ProviderInfo {
   2576      active: boolean;
   2577      backupProviderBaseUrl: string;
   2578      name: string;
   2579      terms?: BackupProviderTerms;
   2580 
   2581      // Why the last cycle failed, when it did.  Only for the active
   2582      // provider: the cycle statistics describe the wallet's last cycle,
   2583      // and that ran against the provider it syncs to.
   2584      lastError?: TalerErrorDetail;
   2585      lastSuccessfulBackupTimestamp?: TalerPreciseTimestamp;
   2586      lastAttemptedBackupTimestamp?: TalerPreciseTimestamp;
   2587 
   2588      // Payment transactions opened for this account, most recent last.
   2589      paymentTransactionIds: string[];
   2590      // Deprecated alias of paymentTransactionIds, with the same contents,
   2591      // for user interfaces built against an older wallet-core.
   2592      paymentProposalIds: string[];
   2593      paymentStatus: ProviderPaymentStatus;
   2594 
   2595      // What the provider reports it holds for the account, from the
   2596      // account status lookup.  Absent until a cycle has managed to ask,
   2597      // and for providers older than sync protocol v4.
   2598      storageUsedBytes?: number;
   2599      blockCount?: number;
   2600    }
   2601 
   2602 .. code-block:: typescript
   2603 
   2604    interface BackupProviderTerms {
   2605      supportedProtocolVersion: string;
   2606      annualFee: AmountString;
   2607      storageLimitInMegabytes: number;
   2608    }
   2609 
   2610 The provider's ``paymentStatus`` reflects how far the account payment has
   2611 gotten, based on the payment transaction the wallet opened for it:
   2612 
   2613 .. code-block:: typescript
   2614 
   2615    type ProviderPaymentStatus =
   2616      | { type: "unpaid" }
   2617      | { type: "pending"; talerUri?: string }
   2618      | { type: "insufficient-balance"; amount: AmountString }
   2619      | { type: "paid"; paidUntil: AbsoluteTime }
   2620      | { type: "terms-changed";
   2621          paidUntil: AbsoluteTime;
   2622          oldTerms: BackupProviderTerms;
   2623          newTerms: BackupProviderTerms };
   2624 
   2625 ``getBackupRecovery`` returns the secret needed to restore the wallet on
   2626 another device, along with the providers to fetch the blocks from. It is what
   2627 the user backs up out of band, and what a restoring wallet is fed.
   2628 
   2629 .. code-block:: typescript
   2630 
   2631    interface BackupRecovery {
   2632      walletRootPriv: string;
   2633      providers: {
   2634        name: string;
   2635        url: string;
   2636      }[];
   2637 
   2638      // The same data as a self-contained plain text, for writing down by
   2639      // hand or saving to a file.  Produced here and *not* consumed by
   2640      // loadBackupRecovery, which reads the structured fields above.
   2641      paperKey?: string;
   2642    }
   2643 
   2644 The paper key is line-oriented, so that a line is the unit to copy, parse and
   2645 transpose:
   2646 
   2647 .. code-block:: text
   2648 
   2649    TALER-PAPERKEY:1
   2650    KEY: GXDG VQKT ...            (the root key, grouped in fours)
   2651    CHECK: a1b2c3d4               (first 8 hex digits of SHA-512(root key))
   2652    PROVIDER: https://sync.example.com/
   2653    URI: taler://restore/...      (the machine-readable form, LSD0006 5.7)
   2654 
   2655 The ``URI`` line is the canonical machine form: a device restoring from a scan
   2656 or a file needs nothing but that line.  The ``KEY`` / ``PROVIDER`` lines are
   2657 the human form, and the checksum catches a transcription error before it
   2658 silently restores a different -- empty -- sync group.
   2659 
   2660 ``loadBackupRecovery`` feeds such a recovery document into a wallet, which is
   2661 how a second (or replacing) device joins the sync group.  The wallet adopts
   2662 the recovery's root key -- the key every per-provider account key is derived
   2663 from, so adopting it *is* what joining the group means -- and adds the
   2664 recovery's providers.  There is no "keep my own key" variant: a wallet that
   2665 kept its own key would derive different account keys and so would not be in
   2666 the group at all.
   2667 
   2668 Adopting another root key also detaches the wallet from the group it was in:
   2669 the blocks it stored are encrypted under a key it no longer has, and the
   2670 ``originBlocks`` lists that reference them are meaningless.  Both are cleared.
   2671 That deliberately leaves the wallet's own records looking "never backed up",
   2672 which is what they are with respect to the group being joined: the full
   2673 collection pass then offers them up, instead of the pull's "deleted iff absent
   2674 from all origin blocks" sweep removing them for not appearing in the new
   2675 group's linked list.
   2676 
   2677 The providers are registered but not activated; the client activates one with
   2678 ``addBackupProvider`` (``activate: true``), and that is what starts the cycle
   2679 which pulls the backup.
   2680 
   2681 .. code-block:: typescript
   2682 
   2683    interface RecoveryLoadRequest {
   2684      recovery: BackupRecovery;
   2685    }
   2686 
   2687 ``runBackupCycle`` runs a backup cycle now, instead of waiting for the
   2688 periodic task.  This is the dedicated "back up now" request; earlier
   2689 implementations triggered a cycle by re-adding the active provider.
   2690 
   2691 The request only *wakes* the cycle and returns an empty object immediately:
   2692 the cycle runs asynchronously (and is serialized against any other cycle),
   2693 reports its progress and outcome through the ``backup-status`` notifications,
   2694 and persists its statistics for ``getBackupDiagnostics``.  Clients track the
   2695 cycle through those, not through this request's response.
   2696 
   2697 .. code-block:: typescript
   2698 
   2699    interface RunBackupCycleRequest {
   2700      // Run the full-collection pass even when its periodic watermark
   2701      // (24h since the last pass) has not elapsed.  Harmless -- the pass
   2702      // only reads the wallet database -- and user interfaces are
   2703      // expected to only expose it in developer mode.
   2704      force?: boolean;
   2705    }
   2706 
   2707 The statistics are persisted by the wallet after every cycle, whatever
   2708 triggered it, and are reported by ``getBackupDiagnostics`` as the "last cycle"
   2709 outcome.  The ``outcome`` field says how the cycle ended: ``"ok"`` (including
   2710 idle cycles with nothing to push), ``"payment-required"`` (the account is
   2711 unpaid) or ``"error"``.
   2712 
   2713 .. code-block:: typescript
   2714 
   2715    interface BackupCycleStats {
   2716      timestamp: TalerPreciseTimestamp;
   2717      // How the cycle ended: "ok", "payment-required" or "error".
   2718      outcome: "ok" | "payment-required" | "error";
   2719      // Why it failed, when the outcome is "error"; the same detail the
   2720      // notification carried, kept for a client that was not listening.
   2721      lastError?: TalerErrorDetail;
   2722 
   2723      // What the cycle pushed to the provider.
   2724      pushed: {
   2725        // Whether the full-collection pass ran in this cycle.
   2726        fullCollectionRan: boolean;
   2727        // Nonce of the block uploaded, if there was anything to upload.
   2728        blockNonce?: string;
   2729        incrementCount: number;
   2730        // Number of increments per increment type, keyed by the
   2731        // increment type's wire string (e.g. "payment-start").
   2732        incrementsByType: { [type: string]: number };
   2733        blobRefCount: number;
   2734      };
   2735 
   2736      // What the cycle's pull applied from the provider.
   2737      pulled: {
   2738        blocksApplied: number;
   2739        blocksSkipped: number;
   2740        incrementCount: number;
   2741        incrementsByType: { [type: string]: number };
   2742        blobRestoreCount: number;
   2743      };
   2744    }
   2745 
   2746 ``getBackupDiagnostics`` reports aggregated statistics about what the backup
   2747 holds: what a cycle would back up right now, and what the last cycle restored.
   2748 It is intended for developer tooling; user interfaces are expected to only
   2749 expose it in developer mode, but the request itself is harmless and available
   2750 on every platform.
   2751 
   2752 .. code-block:: typescript
   2753 
   2754    interface BackupDiagnostics {
   2755      // The increments waiting in the eager pending buffer: what a normal
   2756      // (unforced) cycle would push right now.
   2757      pending: IncrementStatSummary;
   2758 
   2759      // The records the backup has never seen, which only the periodic
   2760      // full-collection pass picks up: what a forced cycle would add.
   2761      fullCollectionCandidates: IncrementStatSummary;
   2762 
   2763      // Outcome of the last backup cycle, when at least one has run.
   2764      lastCycle?: BackupCycleStats;
   2765    }
   2766 
   2767 .. code-block:: typescript
   2768 
   2769    interface IncrementStatSummary {
   2770      incrementCount: number;
   2771      // Number of increments per increment type, keyed by the increment
   2772      // type's wire string.
   2773      incrementsByType: { [type: string]: number };
   2774      // Number of distinct blob references the increments carry.
   2775      blobRefCount: number;
   2776    }
   2777 
   2778 Account keys are not part of any of these payloads: they are derived from the
   2779 wallet root key and the provider's base URL, so each provider sees an
   2780 unlinkable account public key and only the root key has to be preserved.
   2781 
   2782 .. code-block:: text
   2783 
   2784    account_priv = KDF(32, wallet_root_priv,
   2785                       "taler-sync-account-key-salt", provider_base_url)
   2786 
   2787 Backup notifications
   2788 --------------------
   2789 
   2790 The wallet pushes a ``backup-status`` notification to its clients
   2791 (``NotificationType.BackupStatus``) as a backup cycle runs, through the
   2792 regular wallet notification listener.  Clients should use it instead of
   2793 polling ``getBackupInfo`` to track a cycle: it reports the phase the cycle is
   2794 in and, on the terminal phases, the outcome and the relevant counters.
   2795 
   2796 .. code-block:: typescript
   2797 
   2798    interface BackupStatusNotification {
   2799      type: "backup-status";
   2800      providerBaseUrl: string;
   2801      // "started", "pulling", "pushing" and "terms-fetched" are progress
   2802      // phases; the cycle ends in exactly one of "completed", "error" and
   2803      // "payment-required".
   2804      phase: "started" | "pulling" | "pushing" | "terms-fetched" |
   2805             "completed" | "error" | "payment-required";
   2806      // Number of increments packed into the block being pushed
   2807      // (at "pushing").
   2808      pendingIncrementCount?: number;
   2809      // Number of blocks the pull applied (at "completed").
   2810      pulledBlocks?: number;
   2811      // Nonce of the block pushed (at "completed").
   2812      pushedBlockNonce?: string;
   2813      // Reason of the failure (at "error").
   2814      error?: TalerErrorDetail;
   2815      // taler://pay/... URI of the prepared account payment (at
   2816      // "payment-required"); absent when the provider answered a bare
   2817      // 402 without a pay URI.
   2818      talerUri?: string;
   2819      timestamp: TalerPreciseTimestamp;
   2820    }
   2821 
   2822 The wallet emits ``started`` when a cycle begins, ``pulling`` before the
   2823 linked list is fetched, ``pushing`` with the increment count before the packed
   2824 block (and its blobs) is uploaded, ``terms-fetched`` when it has read the
   2825 provider's ``/config`` (which is where a newly added provider's terms come
   2826 from, so a client showing them refreshes on it), and a terminal phase when the
   2827 cycle ends:
   2828 
   2829 * ``completed`` -- the cycle ran without error and without requiring payment
   2830   (``pulledBlocks`` / ``pushedBlockNonce`` carry the counters);
   2831 * ``payment-required`` -- the account is unpaid; a payment transaction may
   2832   already have been prepared, and the UI should take the user to it;
   2833 * ``error`` -- the cycle failed (with ``error`` as the reason); the wallet
   2834   retries on its own schedule, so the notification is only for the user
   2835   interface.  The reason is also persisted, and reported by ``getBackupInfo``
   2836   as the active provider's ``lastError``, so a client that was not listening
   2837   at the time still sees it.
   2838 
   2839 A cycle whose pull applied anything additionally emits a ``balance-change``
   2840 notification.  The apply path writes coins and transactions straight into the
   2841 database, so none of the transaction state machines report them; the
   2842 ``backup-status`` notification says a cycle finished, not that the wallet's
   2843 contents changed, and a client that refreshed on it alone would show a
   2844 restoring wallet as empty until something else happened.
   2845 
   2846 An earlier ``backup-error`` notification type (``BackupOperationError``) was
   2847 part of a legacy backup proof of concept and has been removed in favor of the
   2848 ``error`` phase of ``backup-status``.
   2849 
   2850 .. _limitations:
   2851 
   2852 Limitations
   2853 ===========
   2854 
   2855 While the design minimizes the metadata that the backup service is exposed to,
   2856 some leakage is inherent to the protocol and cannot be avoided in a practical
   2857 way. The service necessarily learns how many blocks and blobs an account
   2858 holds, how much data is uploaded and downloaded, and when these operations
   2859 take place. Kilobyte padding ensures that the size of an individual block or
   2860 blob reveals little about the contents it carries, but it cannot conceal the
   2861 overall volume of activity, the number of operations performed, nor their
   2862 distribution in time. In particular, the number of blocks in an account grows
   2863 with every performed operation, so the block count itself is a lower bound on
   2864 the amount of activity that cannot be disguised by padding.
   2865 
   2866 Timing patterns are particularly hard to hide. Backups run at critical points
   2867 of wallet operations and on a periodic schedule, and some of these critical
   2868 points correlate with user behavior in ways a curious service could exploit:
   2869 for example, a backup forced right before a withdrawal hints that a withdrawal
   2870 is about to occur, and one taken right after a payment hints that a payment
   2871 just happened. The frequency of periodic backups can be reduced and their
   2872 timing jittered to make such inferences harder, which also limits the amount
   2873 of metadata that accumulates over time. The backups that critical points
   2874 mandate, however, cannot be dropped without risking the loss of funds or data
   2875 and therefore remain observable. Where such behavioral patterns are
   2876 unavoidable, the user must trust the service not to misuse them -- an
   2877 assumption already made in the :ref:`threat-model`.
   2878 
   2879 Definition of done
   2880 ==================
   2881 
   2882 The checked implementation items below describe prototype feature branches,
   2883 not the reviewed main branches.  The normative Sync API still labels backup
   2884 support as upcoming.
   2885 
   2886 * [x] Design backup schema.
   2887 * [ ] Design incremental sync.
   2888 * [x] Design backup/restore schedules.
   2889 * [x] Design wallet-core API.
   2890 * [x] Wallet-core implementation.  The machinery -- block and blob encoding,
   2891   CRDT merge, the sync protocol client and its signatures, increment
   2892   collection, the scheduled backup cycle with its pull/merge/apply half, the
   2893   API request handlers, the account payment flow, and item deletion
   2894   (retro-redaction of the ``originBlocks`` plus the pull-side "deleted iff
   2895   absent from all origin blocks" sweep) -- is done, and so is **every
   2896   increment family in this document**: the exchange, global-trust,
   2897   bank-account, donau and denomination entities; the reserve family
   2898   (``set-reserve-seed`` / ``add-reserve``, with the seed-derived key pairs and
   2899   the ``reservePriv`` fallback for reserves that predate the seed), which is
   2900   what makes a restored coin recoupable; the withdrawal, deposit,
   2901   merchant-payment, peer-push-credit, peer-push-debit, peer-pull-debit and
   2902   peer-pull-credit transaction families; the refresh family, whose per-coin
   2903   session seed lets a restored wallet finish a melt instead of losing the
   2904   change; and the coin and token families, which carry the per-record key
   2905   material the wallet database stores (the seed-derived modelling of earlier
   2906   drafts is gone from both).
   2907 
   2908   Contract terms travel as blobs -- uploaded ahead of the blocks that
   2909   reference them, with their reference counts adjusted, and fetched and stored
   2910   back into the contract-terms store on the pull side; a transaction whose
   2911   terms are not available is shown in a reduced form instead of failing the
   2912   transaction listing.  Restoring a coin recomputes the coin-availability
   2913   rows, so a restored wallet shows the same balance as the wallet that made
   2914   the backup, and a restored wallet can continue a pending withdrawal (only an
   2915   expired bank operation cannot be resumed).  ``runBackupCycle`` and
   2916   ``getBackupDiagnostics``, the per-cycle statistics, the forced
   2917   full-collection pass and the ``backup-status`` notifications are all in
   2918   place, on both database backends: the native (sqlite) schema stores the
   2919   backup providers and blocks and the ``originBlocks`` of every backup-managed
   2920   record, and a wallet migrating from the IndexedDB backend carries all three
   2921   across.
   2922 
   2923   The three *derived* families -- refund, recoup and denomination loss -- are
   2924   implemented as finished facts, and every change to whether a coin counts
   2925   towards the balance (spend, refresh, recoup, denomination loss, suspend) is
   2926   reported as a coin increment, so two wallets converge on the same balance
   2927   rather than only on the same coins.  A transaction can no longer be taken
   2928   back out of a terminal state by an increment describing an older view of it.
   2929 
   2930   Known gaps, none of which loses money: refund *items* are not carried
   2931   (nothing outside the refund query reads them, and the merchant hands back
   2932   the same ones); the exchange entries and peer-pull-credit records do not
   2933   restore their ``currentMergeReserveRowId`` pointer, since it is a row id
   2934   local to one database; recoup transactions are backed up and restored but
   2935   the wallet does not yet render them as transactions; and a wallet cannot
   2936   join a sync group written by a *newer* wallet -- it refuses the blocks
   2937   rather than re-uploading a truncated view of them.
   2938 * [x] Design sync API (+ auth).
   2939 * [ ] Server-side implementation (partial: block GET/POST/PUT/DELETE, object
   2940   store GET/POST with reference counting, /config and payments done;
   2941   reconciliation mechanism still missing).
   2942 * [x] UI/UX for backup and sync, in the Android wallet: adding and removing a
   2943   provider, the account payment prompt, the recovery as a QR code and as a
   2944   paper key (written down or saved to a file) with its import counterpart,
   2945   "back up now" through ``runBackupCycle`` with a force-full-backup control
   2946   and a diagnostics card in developer mode, and a progress display driven by
   2947   the ``backup-status`` notifications.  The web extension shows the cycle in
   2948   its wallet-activity view, but has no provider management user interface yet.
   2949 
   2950 Alternatives
   2951 ============
   2952 
   2953 .. _sync-data-structures:
   2954 
   2955 Synchronization data structures
   2956 -------------------------------
   2957 
   2958 In order to perform incremental restores (i.e. synchronization) and converge
   2959 towards the global state (a.k.a. reconciliation), wallets need to keep track
   2960 (in real time) of all the changes in the backup that occurred after the last
   2961 incremental restore, resolve any resulting conflicts, and apply the changes to
   2962 the local database, all while preserving the requirements of incrementality
   2963 and plausible deniability.
   2964 
   2965 So far, two strategies to achieve this have been discussed:
   2966 
   2967 * Invertible bloom filter.
   2968 * Event-driven message queue.
   2969 
   2970 Invertible bloom filter
   2971 ~~~~~~~~~~~~~~~~~~~~~~~
   2972 
   2973 In this approach, a invertible bloom filter of dynamic size is calculated by
   2974 the wallet and server across all known blocks, and used by the wallets to
   2975 compare their local contents with the ones in the server and only fetch the
   2976 inserted and updated blocks, deleting the ones missing from the server.
   2977 
   2978 Wallets would use additional information stored in the server, such as total
   2979 number of blocks, to decide based on the number of the number of differences
   2980 with the server up to a specified threshold, whether to perform an incremental
   2981 backup using the bloom filter or simply perform a full backup.
   2982 
   2983 In order to reduce the rate of false positives, the bloom filter would be
   2984 doubled in size and recalculated as the total number of blocks increases. In
   2985 the rare event of a false positive, both the wallets and the server would
   2986 recalculate the bloom filter by adding a special prefix to the blocks before
   2987 hashing, rate-limited by the theoretical probability of false positives to
   2988 prevent denial-of-service attacks.
   2989 
   2990 Each bucket in the bloom filter (format below) would be 32 bits in size (for
   2991 optimal byte alignment) and have the following structure:
   2992 
   2993 .. code-block:: text
   2994 
   2995    +-----------------------+
   2996    | Bloom filter (10 bit) |
   2997    +-----------------------+
   2998    | Counter (4 bit)       |
   2999    +-----------------------+
   3000    | Hash (12-16 bit)      |
   3001    +-----------------------+
   3002    | Checksum (4-8 bit)    |
   3003    +-----------------------+
   3004 
   3005 Event-driven message queue
   3006 ~~~~~~~~~~~~~~~~~~~~~~~~~~
   3007 
   3008 Another proposed solution is to use a message queue used mainly to stream
   3009 blocks operations (INSERT, DELETE, UPDATE) to other wallets in the
   3010 synchronization group.
   3011 
   3012 In order to provide "eventual" plausible deniability, events in the message
   3013 queue would be permanently deleted as soon as all the active wallets in the
   3014 synchronization group have consumed them, meaning that the server would need
   3015 to keep track of all the "subscribed" wallets.
   3016 
   3017 Inactive wallets would be automatically "unsubscribed" from the message queue
   3018 after a predefined period of time (e.g. 2 weeks), or after being manually
   3019 deleted by the user (similarly to e.g. Signal). Upon coming back online or
   3020 being added back to the synchronization group, a wallet would need to perform
   3021 a full backup.
   3022 
   3023 .. TODO:
   3024    Drawbacks
   3025    =========
   3026 
   3027 Discussion / Q&A
   3028 ================
   3029 
   3030 * How to manage (add/rm) linked devices? Do they ever expire? Is there a
   3031   *master* device with permissions to manage linked devices?
   3032 
   3033 * How to safely delete a withdrawal operation? Instead of storing the keypair
   3034   for each coin, we derive coins from a secret seed and the coin index within
   3035   a withdrawal group. Coins in the backup thus contain a reference to the
   3036   originating withdrawal operation, which in the event of being deleted will
   3037   prevent coins from being restored from backup.
   3038 
   3039 * Should the wallets always keep a full copy of the linked list?