taler-docs

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

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


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