commit 6fd5dd13117c2aeb4900eb82763dbba33a942ccb
parent ee0c431687c3cb8f0d8f51fa9f5acf48d42de680
Author: Iván Ávalos <avalos@disroot.org>
Date: Mon, 3 Aug 2026 15:00:57 +0200
dd92: document implemented backup APIs, restore flow and payment
Diffstat:
1 file changed, 137 insertions(+), 43 deletions(-)
diff --git a/design-documents/092-incremental-backup-sync.rst b/design-documents/092-incremental-backup-sync.rst
@@ -291,7 +291,7 @@ by UID and could not otherwise transmit them faithfully.
.. http:get:: /backups/${ACCOUNT_KEY}/blocks
- List blocks from the account's block chain with pagination.
+ List blocks from the account's linked list with pagination.
**Request**
@@ -314,7 +314,7 @@ by UID and could not otherwise transmit them faithfully.
:http:statuscode:`402 Payment required`:
The account has expired and requires payment.
:http:statuscode:`404 Not found`:
- The ``start_nonce`` block was not found in the chain.
+ The ``start_nonce`` block was not found in the linked list.
:http:statuscode:`500 Internal server error`:
A database error occurred.
@@ -330,12 +330,12 @@ by UID and could not otherwise transmit them faithfully.
``data`` is the encrypted block payload as it was uploaded, and
hashes to ``block_hash``. ``prev_nonce`` and ``next_nonce`` are
- absent for the first and last block of the chain respectively.
+ absent for the first and last block of the linked list respectively.
.. http:post:: /backups/${ACCOUNT_KEY}/blocks/${NONCE}
- Upload a new block and append (or insert) it into the account's block
- chain. If a block with the same nonce already exists, the content
+ Upload a new block and append (or insert) it into the account's linked
+ list. If a block with the same nonce already exists, the content
hash is compared: if it matches, a ``304 Not modified`` is returned;
if it differs, the client should use ``PUT`` instead.
@@ -416,7 +416,7 @@ by UID and could not otherwise transmit them faithfully.
:http:statuscode:`409 Conflict`:
The request does not fit the state the server holds, and retrying
it unchanged will not help. Either the write is outdated (the
- block chain has been modified by another device since the caller
+ linked list has been modified by another device since the caller
last fetched it), the nonce is already in use, or ``object_refs``
names an object the account does not have or would take a
reference count below zero. Nothing was modified.
@@ -452,7 +452,7 @@ by UID and could not otherwise transmit them faithfully.
.. http:delete:: /backups/${ACCOUNT_KEY}/blocks/${NONCE}
- Delete an existing block from the block chain. The request must
+ Delete an existing block from the linked list. The request must
include an ``If-Match`` header containing the quoted base32-encoded
SHA-512 hash of the block data to delete, which the server uses to
detect concurrent modifications.
@@ -1674,11 +1674,6 @@ change**, so that the change and the increment describing it commit together.
A wallet can therefore never end up in a state that its backup does not know
about, however abruptly it is shut down.
-Increments are recorded as a flat list; which section of the
-``IncrementSet`` an increment lands in is derived from its
-``type``, so that transaction code never names a section itself and a new
-increment type cannot be added without giving it a home.
-
A wallet that has not set up backup yet has no encryption key to protect the
increments with, so recording is a no-op rather than an error.
@@ -1686,7 +1681,7 @@ The backup cycle
~~~~~~~~~~~~~~~~
One cycle takes whatever increments have accumulated, packs them into a
-block, and appends that block to the account's chain:
+block, and appends that block to the account's linked list:
1. In a single database transaction, move the pending increments out of the
buffer and into an *in-flight block*, storing its nonce, hash, contents
@@ -1701,7 +1696,18 @@ point either finds increments still pending, or finds an in-flight block and
retries it — under its **original nonce**, which the server answers with
``304 Not modified`` if the upload did in fact land. Increments are thus
neither lost nor backed up twice, and a cycle that has packed a block always
-retries it before packing new increments, so the chain stays ordered.
+retries it before packing new increments, so the linked list stays ordered.
+
+A cycle also pulls the account's linked list before packing new increments,
+applying any blocks it has not seen before (see "Restore process" below), so
+that new blocks are appended after the current end of the list.
+
+An account that has not been paid for yet answers every request with
+``402 Payment required``, and only the upload endpoints carry the ``Taler:``
+header with a ``taler://pay/...`` URI. A cycle that is answered this way
+while pulling therefore pushes whatever it has pending, so the payment is
+settled — automatically when the annual fee is zero — and subsequent writes
+are accepted.
Backup schedule
---------------
@@ -1717,31 +1723,49 @@ the exchange is asked to sign them. Past that point the exchange considers
the coins withdrawn while a wallet restored from an older backup could no
longer reconstruct them.
-Triggering is only a nudge, and deliberately carries no guarantee: it happens
-after the recording transaction has committed, and nothing depends on it
-having run. Independently, a periodic task runs a cycle at a fixed interval,
-which is what catches increments whose trigger never fired — because the
-wallet was killed, was offline, or because the operation that produced them
-has no critical point of its own.
+A cycle is triggered after the recording transaction commits; if the wallet
+stops before it runs, the increments simply stay pending until the next
+cycle. Independently, a periodic task runs a cycle at a fixed interval,
+covering increments whose trigger never fired, e.g. because the wallet was
+offline or the operation has no critical point.
-.. TODO:
- Restore process
- ---------------
+Restore process
+---------------
-.. TODO:
- Restore schedule
- ----------------
+Restoring a wallet on a (fresh) device is the pull half of the backup
+cycle, driven by a recovery document from ``getBackupRecovery``:
+
+1. ``loadBackupRecovery`` installs the recovery's root key and providers,
+ and drops the wallet's own block pointers, so the device starts from
+ nothing.
+2. Once the user activates a recovered provider (``addBackupProvider`` with
+ ``activate``), the backup cycle downloads the account's linked
+ list, decodes each block it has not seen before, CRDT-applies its
+ increments to the local database -- recording the block's nonce in the
+ ``originBlocks`` of every record it touched -- and stores the blocks
+ locally.
+
+Because the same root key derives the same per-provider account keys, a
+recovering wallet sees exactly the blocks any other wallet in the group
+uploaded and applies them with the same merge rules, so all devices
+converge on the same state.
+
+Restore schedule
+----------------
+
+Restoring happens on demand: it starts when a recovery document is loaded
+and the recovered provider is activated. Afterwards the restored wallet is
+kept up to date by the same periodic backup task as every other wallet --
+the pull half runs on every cycle, so changes made by other devices are
+picked up at the cycle interval.
Wallet-core API
---------------
Backup providers and the wallet's backup key are managed through the
-wallet-core API. All requests below are available on every platform.
-
-.. note::
-
- The request handlers are currently declared but not yet implemented; the
- collection and scheduling mechanisms described above are.
+wallet-core API. All requests below are available on every platform. The
+request handlers described here are implemented; the collection and
+scheduling mechanisms described above drive them.
.. ts:def:: AddBackupProviderRequest
@@ -1755,9 +1779,16 @@ wallet-core API. All requests below are available on every platform.
activate?: boolean;
}
-``addBackupProvider`` registers a sync server. Since a provider may charge an
-annual fee, the response either confirms the provider is ready or hands back
-a ``taler://pay/...`` URI for the account payment:
+``addBackupProvider`` registers a sync server: it fetches the provider's
+``/config`` for its terms (protocol version, annual fee, storage limit),
+stores a provider record, and -- when ``activate`` is set -- makes it the
+active sync target and runs the first backup cycle. The cycle is what
+settles the account payment: a sync account only exists once it has been
+paid for, and the server rejects every upload (even at a zero annual fee)
+until then. A zero-fee account is paid automatically; any other account
+produces a payment transaction that the user confirms from the wallet.
+The response either confirms the provider is ready or hands back a
+``taler://pay/...`` URI for the account payment:
.. ts:def:: AddBackupProviderResponse
@@ -1782,10 +1813,50 @@ last backup attempt.
interface BackupInfo {
walletRootPub: string;
- deviceId: string;
providers: ProviderInfo[];
}
+``ProviderInfo`` describes one known provider and the state of the wallet's
+account on it:
+
+.. ts:def:: ProviderInfo
+
+ interface ProviderInfo {
+ active: boolean;
+ backupProviderBaseUrl: string;
+ name: string;
+ terms?: BackupProviderTerms;
+ lastError?: TalerErrorDetail;
+ lastSuccessfulBackupTimestamp?: TalerPreciseTimestamp;
+ lastAttemptedBackupTimestamp?: TalerPreciseTimestamp;
+ paymentProposalIds: string[];
+ backupProblem?: BackupProblem;
+ paymentStatus: ProviderPaymentStatus;
+ }
+
+.. ts:def:: BackupProviderTerms
+
+ interface BackupProviderTerms {
+ supportedProtocolVersion: string;
+ annualFee: AmountString;
+ storageLimitInMegabytes: number;
+ }
+
+The provider's ``paymentStatus`` reflects how far the account payment has
+gotten, based on the payment transaction the wallet opened for it:
+
+.. ts:def:: ProviderPaymentStatus
+
+ type ProviderPaymentStatus =
+ | { type: "unpaid" }
+ | { type: "pending"; talerUri?: string }
+ | { type: "insufficient-balance"; amount: AmountString }
+ | { type: "paid"; paidUntil: AbsoluteTime }
+ | { type: "terms-changed";
+ paidUntil: AbsoluteTime;
+ oldTerms: BackupProviderTerms;
+ newTerms: BackupProviderTerms };
+
``getBackupRecovery`` returns the secret needed to restore the wallet on
another device, along with the providers to fetch the blocks from. It is
what the user backs up out of band, and what a restoring wallet is fed.
@@ -1800,6 +1871,30 @@ what the user backs up out of band, and what a restoring wallet is fed.
}[];
}
+``loadBackupRecovery`` feeds such a recovery document into a wallet, which
+is how a second (or replacing) device joins the sync group. With the
+default "theirs" strategy the wallet adopts the recovery's root key -- the
+key all per-provider account keys are derived from -- adds the recovery's
+providers, and drops its own sync pointers so that the next backup cycle
+re-pulls the whole linked list. With "ours" it keeps its own root key and only
+takes over the providers.
+
+.. ts:def:: RecoveryLoadRequest
+
+ interface RecoveryLoadRequest {
+ recovery: BackupRecovery;
+ strategy?: RecoveryMergeStrategy;
+ }
+
+.. ts:def:: RecoveryMergeStrategy
+
+ enum RecoveryMergeStrategy {
+ // Keep the local wallet root key, import and take over providers.
+ Ours = "ours";
+ // Migrate to the wallet root key from the recovery information.
+ Theirs = "theirs";
+ }
+
Account keys are not part of any of these payloads: they are derived from the
wallet root key and the provider's base URL, so each provider sees an
unlinkable account public key and only the root key has to be preserved.
@@ -1814,14 +1909,13 @@ Definition of done
* [x] Design backup schema.
* [ ] Design incremental sync.
-* [ ] Design backup/restore schedules (partial: backup process and schedule
- done; restore still missing).
+* [x] Design backup/restore schedules.
* [x] Design wallet-core API.
-* [ ] Wallet-core implementation (partial: block and blob encoding, CRDT
- merge, sync protocol client and signatures, increment collection and the
- scheduled backup cycle done; the API request handlers, blob upload,
- fetching and merging remote blocks, item deletion and the remaining
- increment types still missing).
+* [ ] Wallet-core implementation (block and blob encoding, CRDT merge, sync
+ protocol client and signatures, increment collection, the scheduled backup
+ cycle with its pull/merge/apply half, the API request handlers and the
+ account payment flow are done; item deletion and the remaining increment
+ types still missing).
* [x] Design sync API (+ auth).
* [ ] Server-side implementation (partial: block GET/POST/PUT/DELETE, object
store GET/POST with reference counting, /config and payments done;