1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
//! # Output locking
//!
//! This module is the single home for the wallet's advisory output-locking
//! vocabulary: the identity of a lock holder ([`LockOwner`]), the error surface
//! of lock acquisition ([`LockError`]), the storage contract through which
//! wallet backends persist lock state ([`OutputLockStore`]), the request
//! through which proposal-creation functions acquire locks ([`LockRequest`]
//! and [`unlock_proposal_inputs`]), and the policy types through which input
//! selection interacts with lock state ([`LockedInputPolicy`] and
//! [`LockFilter`]).
//!
//! ## Semantics
//!
//! All lock state is a pair of columns on each received output: a
//! `lock_expiry_height` and a `lock_owner`. Every behavior is derived from the
//! following invariants, stated here once:
//!
//! * **Locked**: an output is *locked* while `lock_expiry_height >=
//! target_height`, where the target height is the height at which a new
//! transaction would be mined (chain tip + 1). Balance computations tally an
//! output as locked under exactly this condition.
//! * **Eligible for selection**: input selection eligibility is the exact
//! complement of the locked condition, owner-scoped under a
//! [`LockedInputPolicy`]: an output is eligible when it carries no lock, when
//! its lock has expired (`lock_expiry_height < target_height`), or when its
//! `lock_owner` is one of the owners the policy admits. An output locked by
//! any other owner is never selected.
//! * **Acquisition**: a lock may be acquired ([`OutputLockStore::lock_outputs`])
//! when the output is unlocked, when its existing lock has expired as of the
//! chain tip, or when the existing lock is held by the *same* owner (an
//! idempotent re-acquire/extend, so a flow that crashed after locking may
//! safely retry under its original owner token). Acquisition fails only on
//! an active foreign lock. There is no stealing: because "expired as of the
//! chain tip" (`h <= chain_tip`) is exactly "not locked for selection"
//! (`h < target_height`), a foreign lock is replaceable exactly when the
//! output has already become selectable again.
//! * **Advisory**: locks are advisory in selection. An owner-scoped policy
//! override *spends through* a lock during selection; it never releases the
//! lock.
//! * **Release**: there are exactly four release paths: owner-scoped unlock
//! ([`OutputLockStore::unlock_output`], [`unlock_proposal_inputs`]),
//! owner-agnostic clearing ([`OutputLockStore::clear_locked_outputs`]),
//! unlock-on-store (implementations of
//! [`WalletWrite::store_transactions_to_be_sent`] unlock outputs recorded as
//! spent, the spend records having taken over double-selection protection),
//! and expiry (the passage of the chain tip beyond `lock_expiry_height`).
//! * **Expiry re-opens the race**: if an operation outlasts its lock window,
//! the lock expires and a concurrent proposal may select and spend the same
//! outputs. Lock windows must be chosen conservatively with respect to the
//! worst-case time between proposal creation and transaction storage.
//!
//! ## Integration map
//!
//! The locking feature touches the codebase at the following points, by role:
//!
//! * **Schema**: the `note_locking` migration in `zcash_client_sqlite`
//! (`wallet::init::migrations::note_locking`) adds the `lock_expiry_height`
//! and `lock_owner` columns to the four received-output tables; the table
//! definitions in `zcash_client_sqlite::wallet::db` carry them forward.
//! * **Acquisition**: the five proposal-creation functions
//! ([`propose_transfer`], [`propose_standard_transfer_to_address`],
//! [`propose_send_max_transfer`], [`propose_shielding`], and
//! [`propose_shielding_coinbase`]) accept an optional [`LockRequest`] and
//! lock every selected input via [`OutputLockStore::lock_outputs`].
//! * **Selection**: the seven `zcash_client_sqlite` leaf queries that select
//! spendable outputs (`get_spendable_note`, `select_unspent_notes`,
//! `select_spendable_notes_matching_value`, and `unspent_notes_meta` in
//! `wallet::common`; `get_spendable_transparent_outputs`,
//! `get_spendable_transparent_outputs_for_addresses`, and
//! `select_spendable_transparent_outputs` in `wallet::transparent`) embed
//! the eligibility fragment parameterized by a [`LockFilter`].
//! * **Balance**: four classification sites tally locked value separately:
//! the shielded-note branch of `get_wallet_summary` and the three
//! transparent tallies (`get_transparent_balances` and the two branches of
//! `add_transparent_account_balances`, including the coinbase arm) in
//! `zcash_client_sqlite`.
//! * **Release**: the four release paths listed under Semantics above.
//! * **Proposal decoding**: proposal decoding in [`crate::proto`] reads wallet
//! contents with [`LockFilter::Unfiltered`], since a persisted proposal must
//! be reconstructible regardless of current lock state.
//! * **Consumers**: `zcash_client_sqlite`'s
//! `PoolMigrations::migration_lock_owners` exposes the owners of a pool
//! migration's in-flight locks so callers can construct owner-scoped
//! policies; Zallet consumes the locking API for its transaction flows.
//!
//! [`OutputLockStore::lock_outputs`]: crate::data_api::OutputLockStore::lock_outputs
//! [`OutputLockStore::unlock_output`]: crate::data_api::OutputLockStore::unlock_output
//! [`OutputLockStore::clear_locked_outputs`]: crate::data_api::OutputLockStore::clear_locked_outputs
//! [`WalletWrite::store_transactions_to_be_sent`]: crate::data_api::WalletWrite::store_transactions_to_be_sent
//! [`propose_transfer`]: crate::data_api::wallet::propose_transfer
//! [`propose_standard_transfer_to_address`]: crate::data_api::wallet::propose_standard_transfer_to_address
//! [`propose_send_max_transfer`]: crate::data_api::wallet::propose_send_max_transfer
//! [`propose_shielding`]: crate::data_api::wallet::propose_shielding
//! [`propose_shielding_coinbase`]: crate::data_api::wallet::propose_shielding_coinbase
use BTreeSet;
use error;
use ;
use Hash;
use delegatable_trait;
use TxId;
use ;
use crate::;
/// An opaque token identifying the holder of an output lock.
///
/// A caller that locks outputs (directly via [`OutputLockStore::lock_outputs`], or through a
/// proposal-creation function's lock request) supplies an owner token and must retain it: the
/// token is what authorizes releasing the locks ([`OutputLockStore::unlock_output`],
/// [`unlock_proposal_inputs`]) and what makes re-locking idempotent (an owner may re-acquire or
/// extend its own active lock, for example when retrying a flow after a crash, while a different
/// owner's lock attempt fails until the lock expires).
///
/// The token is not a cryptographic secret: everything that can reach the wallet database can
/// read it. It exists to prevent *accidental* cross-flow interference between concurrent
/// in-process operations, not to protect against an adversary with database access.
///
/// [`OutputLockStore::lock_outputs`]: crate::data_api::OutputLockStore::lock_outputs
/// [`OutputLockStore::unlock_output`]: crate::data_api::OutputLockStore::unlock_output
/// [`unlock_proposal_inputs`]: crate::data_api::wallet::unlock_proposal_inputs
;
/// A transaction id may serve as a lock owner.
///
/// This is the right owner choice for flows that hold a durable transaction identity while
/// their locks are alive; most notably a persisted PCZT, whose v5 txid is fixed once its
/// effecting data is final. Deriving the owner from the txid lets such a flow re-derive its
/// token after a restart and release (or re-acquire) exactly its own locks.
///
/// It is NOT a suitable owner for proposal-time locking in general: at proposal creation no
/// transaction exists yet, a multi-step proposal builds several transactions, and a
/// transaction rebuilt after a crash generally has a different txid, which would defeat the
/// idempotent same-owner re-lock. Flows without a durable transaction identity should use
/// [`LockOwner::random`] and retain the token.
/// Errors that occur when attempting to lock an output.
/// The subset of wallet-storage operations that manage output locks.
///
/// [`WalletWrite`] requires this trait with its [`Self::AccountId`] and [`Self::Error`] types
/// tied to the corresponding [`WalletRead`] types, so every writable wallet backend is an
/// `OutputLockStore`. The separate trait exists so that lock storage can be implemented,
/// delegated, and reasoned about independently of the full wallet-mutation surface.
///
/// [`WalletRead`]: crate::data_api::WalletRead
/// Returns the [`OutputRef`] identifying each output that the given proposal consumes as an
/// input.
///
/// Each note or UTXO selected for spending is an *input* to the proposal's transaction, but is at
/// the same time an *output* of the earlier transaction that created it; an [`OutputRef`] names it
/// by that creating transaction's id, which is the stable identity the lock tables are keyed on.
/// A request to lock the inputs selected by a proposal, made when calling one of the
/// proposal-creation functions ([`propose_transfer`] and friends).
///
/// The caller supplies the [`LockOwner`] under which the locks are taken and must retain it: the
/// owner token is what authorizes releasing the locks with [`unlock_proposal_inputs`], and what
/// allows the same flow to re-lock its own inputs when retrying after a crash.
///
/// [`propose_transfer`]: crate::data_api::wallet::propose_transfer
/// Locks all inputs selected by the given proposal, preventing them from being
/// selected by subsequent proposals. The lock expires at the given height.
pub
/// Unlocks all inputs selected by the given proposal, reversing the locks acquired when the
/// proposal was created with a [`LockRequest`] under the same `owner`.
///
/// This is useful when a proposal is rejected or abandoned after its inputs were locked, so that
/// the outputs become available for selection and balance computation once again. Because
/// unlocking is scoped to `owner`, inputs that are not locked, or whose locks are held by a
/// different owner (for example a concurrently-created proposal), are left unchanged.
/// Governs whether input selection may draw on locked outputs, and with what preference.
///
/// Locks are advisory. The default, [`Self::Exclude`], never selects a locked output. The
/// overriding variants each carry the set of lock owners whose locks may be drawn upon; a locked
/// output whose owner is not in that set is never selected, regardless of variant. This keeps an
/// override scoped to a known reason (e.g. the wallet's own pool-migration PCZTs) and leaves every
/// other flow's locks intact. Overriding here only *spends through* a lock during selection; it
/// never releases the lock.
/// How a query filters candidate outputs by lock state.
///
/// Input selection for a proposal passes [`Self::Policy`], carrying the caller's owner-scoped
/// [`LockedInputPolicy`]. Retrieval/decoding paths that must expose wallet contents regardless of
/// locks (proposal decoding, low-level and test accessors) pass [`Self::Unfiltered`]. Keeping the
/// two separate means a `SpendPolicy` can only ever request an owner-scoped override, never an
/// unscoped "ignore all locks".