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?