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