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