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
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109 | # BANK/1 and BANK/2 — the banca protocol
The contract between a banca client and the network it runs on.
**The base contract is sueta protocol v2, unchanged.** Transport, sealing, the
OrbitDB log, presence, the offline mailbox — all of it comes from the shared
rooms core (`ardegazu.rooms.*`, compiled off the classpath out of
`ardegazu-rooms-kit`) and is specified in the chat repo:
```
git clone https://git.ardegazu.ro/chat.git # docs/PROTOCOL.md
```
This document covers only what banca adds on top: the op union, the settlement
fold, and the honest limits of both.
**App salt:** `banca.ardegazu.ro/v<n>`, where `<n>` is the BANK VERSION OF THAT
BANK — see §13. The salt forks every cryptographic derivation and every storage
namespace away from chat, board and each other app on the stack, and now away
from banca's own other protocol version: a banca room and a chat room with the
same secret are different rooms, and so are a BANK/1 bank and a BANK/2 one,
whatever the op shapes are.
**TWO PROTOCOL VERSIONS RUN SIDE BY SIDE, in one app, for ever.** BANK/1 is
everything in §1–§12 and is unchanged. BANK/2 adds escrow — money the bank
HOLDS against a hashlock, so two banks can settle one trade atomically without
trusting each other — and §13 is the whole of it. A bank declares its version in
its link, and banks founded under BANK/1 keep working exactly as they did.
**Class 1, per `dev/docs/HOST-ELECTION.md` §9.** A bank is a mergeable log with a
deterministic tie-break, so it carries **no host election, no `ro`, no `cl` and
no host id — ever.** Adding one would introduce a single point of failure into a
design that has none.
---
## 1. What a bank is
Any suite identity founds a bank by chartering one and issuing its own currency.
Other identities become its customers. **A bank is one encrypted OrbitDB log**,
addressed by a link, replicated by everyone who holds it.
```
#<secret>.<bankerPub> BANK/1
#<secret>.<bankerPub>.<v> BANK/2 and later — see §13
```
board's strict form, and for the same reason: **authority is a pure function of
the link.** `bankerPub` is the raw Ed25519 identity public key of the founder,
appended at creation before the link is ever shared. Banker ops fold only when
the hardened authorship binding attributes the entry to that key. There is no
genesis-op race, nothing to TOFU, and no way to seize a bank by getting an entry
in first.
A link **without** the `.<bankerPub>` suffix is not a bank. `BankFold` drops a
malformed or absent banker key and folds to nothing: no charter, no accounts, no
balances.
An absent VERSION suffix reads as 1, and `.1` is refused rather than accepted as
a synonym — one bank, one spelling, the rule canonical base64url follows
everywhere here. A present suffix is a small integer with no leading zero,
`^[2-9][0-9]{0,2}$` exactly (`link.cljs` `version-part?`). A link naming a
version this build cannot fold **parses**, so
the app can say *update* rather than *broken link*; folding a bank whose op
union you do not know is how a replica ends up quietly disagreeing about who
owns what.
**A suite identity is required to transact.** Entry authorship comes from the
log's verified author id — the suite Ed25519 idPub for a member with an
identity, and the per-device OrbitDB fallback key for anyone without one. The
fold gates every account key on the canonical 43-character base64url shape, so
ops authored by the fallback identity never fold: **an identity-less visitor is
read-only.** Money binds to the key, and banca must never auto-adopt on an
identity conflict.
The one exception, and it is deliberate: an identity-less client may still
**relay** a `pay` (§4).
---
## 2. Currencies
A currency id is wallet-kit's, verbatim:
```
<43-char canonical base64url bankerPub>.<CODE> CODE matching ^[A-Z]{3,8}$
```
The issuing identity **is** the namespace, so two banks may both mint `LEI` and
the ids never collide — no registry, no authority, nobody to ask.
`~.GAZ` is the suite's **reserved neutral unit of account**. `~` is not a
base64url character, so no identity can ever hold it: the id is
unforgeable-by-construction and permanently unissued. It exists as a
*denomination* — a way to price something before agreeing which bank's paper
settles it — and it is **quote-only**: nothing can be minted in it and nothing
can be settled in it.
**A charter claiming code `GAZ` is refused, for every banker.** `<banker>.GAZ`
would read as the neutral unit in any interface that shows a code without its
issuer, so it cannot exist. `valid-charter?` rejects it and the op is dropped at
ingest; a log whose only charter is a `GAZ` one has no bank at all.
Amounts are **integer minor units**, `1 ≤ amt ≤ 2^50`. There are no floats
anywhere on this wire and no division ever crosses it: a currency's minor-unit
exponent (`charter.dec`) is display metadata, nothing more. The `2^50` ceiling is
an arithmetic fact rather than a policy — see §7.
---
## 3. The op union
Every op is `JSON.stringify`'d before the rooms core seals it, so **key order is
part of the bytes a peer verifies**. Builders use `ardegazu.rooms.js/ordered`
(sequential `unchecked-set`), never `#js {}` or `js-obj`, which silently switch
to hash order at nine pairs at every optimization level.
```ts
{ t:"charter", ts, name, code, sym, dec, join } // banker-only; first-from-banker wins
{ t:"open", ts, name } // self-open (join:"open") or knock
{ t:"acct", ts, to, op } // banker-only; op: "approve" | "close"
{ t:"mint", ts, seq, to, amt, memo? } // banker-only; credits `to`; supply += amt
{ t:"burn", ts, seq, amt } // banker-only; debits the BANKER's own account
{ t:"pay", ts, po } // po = a wallet-kit payment order (wpo)
{ t:"ack", ts, h, bsig } // banker-only; h = the slot entry's hash (pay or mint)
{ t:"rate", ts, num, den } // banker-only; 1 unit = num/den GAZ
{ t:"req", ts, to, amt, memo? } // in-log payment request
```
and, **in a BANK/2 bank only** (§13):
```ts
{ t:"lock", ts, lk } // lk = a wallet-kit escrow lock order (wlk)
{ t:"claim", ts, h, pre } // h = the LOCK ENTRY's hash; pre opens its hashlock
{ t:"unlock", ts, h } // banker-only refund
{ t:"unlock", ts, h, rel } // refund released by the beneficiary
```
**`valid-op?` takes the room's version**, and an escrow op is refused outright
under BANK/1. That is the compatibility gate of the whole design and it guards
the direction most easily got wrong: a BANK/1 client drops an unknown `t`
silently, so a BANK/2 client that appended one into a v1 bank would leave the
two folding different balances — a partition arriving from the build that knew
better. A version argument that is not a positive safe integer reads as 1: a
garbled version must never select the permissive mode.
`t` is always first and `ts` always second, in every op.
**Exact key sets.** Every validator checks `Object.keys(op).length` against the
op's key count. v1 has **no extension slot**: a field that does not exist above
cannot be added by a peer and quietly ride along into someone's fold. A new field
means a new `t`, or a protocol bump. An absent `memo` is an **absent key**, never
an empty string and never a null.
**A validator is the contract.** An op that does not validate is **dropped, never
repaired** — it never enters the entry set, so it cannot influence any pass. An
op whose `t` this version does not know is dropped too, silently: an op the fold
cannot judge is an op the fold must not fold.
### Bounds
| field | bound | why |
|---|---|---|
| `ts` | non-negative integer | see §6 — never compared to a wall clock |
| `name` | ≤ 40 code points, no control chars | display |
| `sym` | ≤ 8 code points, no control chars | display |
| `code` | `^[A-Z]{3,8}$`, never `GAZ` | compared, printed and typed by humans; confusable scripts have no business in an equality test |
| `dec` | integer 0..8 | display exponent only |
| `join` | `"open"` \| `"approve"` | — |
| `amt` | integer 1..2^50 | wallet-kit `AMT_MIN`..`AMT_MAX` |
| `seq` | integer 1..2^50 | the payer's slot; 1-based |
| `memo` | ≤ 140 code points, no control chars | wallet-kit `MEMO_MAX` |
| `h` | non-empty string ≤ 128, no control chars | an entry hash, opaque to banca |
| `bsig` | canonical base64url Ed25519, exactly 86 chars | wallet-kit `SIG_RE` |
| `num`, `den` | integers ≥ 1 | — |
| `po` | a wallet-kit `wpo`, 13 keys | §4 |
The control-character class is C0, DEL, C1 **and U+2028 / U+2029** — the last two
are not control characters by Unicode category but are line terminators in
ECMAScript and in every log and console renderer. Text fields are bounded in
**code points**, not UTF-16 units, so an emoji costs one.
**There is no retention bound anywhere.** No entry cap, no eviction, no
drop-oldest, no ring buffer on ledger data. A balance is a function of *all*
history, so a fold that forgot would be a fold that lied. The scaffold's
newest-N replay window is deliberately lifted — **including at boot**: the room
replays with `loadTail(-1)`, @orbitdb/core's oplog iterator for *every entry*,
rather than the scaffold's newest-500. A window there is the worst of the three
places to have one, because the number it produces is wrong offline and
silently. Correctness bounds stay; retention is the user's choice and only ever
an explicit, user-driven action.
---
## 4. Authorization — the asymmetry that matters
| op | authorized by |
|---|---|
| `charter` `acct` `mint` `burn` `ack` `rate` | **entry authorship** attributed to `bankerPub` |
| `open` `req` | **entry authorship**, any suite identity |
| `pay` | **`po.sig`** — the payer's signature *inside the order* |
| `lock` | **`lk.sig`** — `pay`'s rule, for `pay`'s payoff (§13) |
| `claim` | **the preimage** — `sha256B64url(pre)` = the lock's `hash` (§13) |
| `unlock` | the banker's authorship, **or** a `rel` the beneficiary signed (§13) |
`pay` is the load-bearing one. It is **not** authorized by who appended the
entry. The payer's own replica, any relaying member, or the banker may append an
order pulled out of the mailbox, and it settles identically wherever it lands.
That is what makes offline payment work at all, and it is why a payer's slot is
`(po.from, po.seq)` rather than anything derived from the appender.
A consequence worth stating: an **identity-less** client — one on the per-device
OrbitDB fallback key, whose own `open` and `req` are ignored — can still relay
someone else's signed order, and it settles. Relaying is not transacting.
`po.sig` is Ed25519 and therefore asynchronous. The fold verifies it out of band,
**pessimistically**: an order whose signature has not (yet) verified is not a
slot contender and is not recorded at all. An unverified order is not an order.
When a verdict lands the fold re-runs from scratch and fires `onChange`.
### The payment order
`po` is a wallet-kit `wpo`, carried **verbatim** — banca never rebuilds,
re-orders or re-signs it, because the payer's signature is over exactly those
bytes. Thirteen keys, in order:
```
v t id cur amt seq from to ctx memo ts exp sig
```
signed as `"wpay-ord|v1|" + canon(order minus sig)` with `canon` = social-kit's
canonical JSON (sorted keys, no whitespace). Both facts matter and both are
golden-vectored: **`canon` sorts** (so the signature is order-independent) and
**the wire is insertion order** (so the bytes a peer holds are fixed).
`wpo.seq` is a **NUMBER** — the payer's per-`(payer, bank)` slot, and the
double-spend defense. `wrc.seq` (§5) is a **STRING** — the bank's own log
reference. They are both called `seq` because that is what each is in its own
log; a second implementation that conflates them will double-spend or
double-fold.
---
## 5. The settlement fold
> **The log is the settlement authority. The banker is a notary, not a
> gatekeeper.**
Any member's replica may append a payment, and a deterministic causal fold
settles it **provisionally** the moment it replicates. Two customers converge on
the same balances with the banker asleep. An offline payment rides the mailbox
and settles when it lands. Nothing waits on anyone.
The banker's `ack` provides **finality**, and only finality.
### The algorithm
```
ingest validate the WHOLE entry — `op`, `hash` AND `clock` — and drop it
if any of the three does not hold. Never repair.
sort entries by (lamport clock, entry hash) — board's AccessFold
order, tie-break included, written total by construction
pre-pass 0 the CHARTER: the first valid `charter` from bankerPub, in sort
order. No charter ⇒ no bank ⇒ nothing else folds at all.
pre-pass A the ACKED set: every `h` named by a valid `ack` from bankerPub.
pre-pass B the SLOT WINNERS: group every slot-claiming entry (`pay`, `mint`,
`burn`) that passes STRUCTURE + AUTHORIZATION by its slot, and
pick one winner per slot —
· an ACKED entry beats an unacked one;
· otherwise (clock, hash)-first wins.
main fold walk the sorted entries oldest-first, judging each against the
state folded SO FAR. A slot-claiming entry folds only if it is its
slot's winner; the losers fold to `failed` with why `"seq"`.
```
### The envelope is as untrusted as the op
A log entry is `{hash, from, clock, op}`, and **the sort — therefore every pass
below it — is built out of `hash` and `clock`.** Neither is a value this app
produces. rooms-kit reads an entry's clock as `entry.clock?.time ?? 0`, a
*nullish* coalesce that passes any non-null value straight through, and
`@orbitdb/core` signs `clock` verbatim while only checking that it is not
`undefined`. An ordinary member can therefore **sign** an entry whose
`clock.time` is the string `"abc"`, and it verifies.
So `ingest` admits an entry only when
| field | rule |
|---|---|
| `hash` | a non-empty **string** |
| `clock` | a **non-negative safe integer** (`Number.isSafeInteger`) |
| `op` | valid per §3 |
and drops it otherwise — the same house rule the op validator follows, for the
same reason. Without the clock rule, `(a.clock - b.clock)` is `NaN` for a
non-numeric clock and `Array.prototype.sort` with a NaN comparator orders by
**input permutation**, i.e. by arrival order: one crafted entry carrying a
perfectly ordinary valid `open` produced seven distinct folded states over 24
arrival orders of one entry set — including states where a payment never
happened and states where the *mint* failed, so supply itself diverged. Money
conservation held inside each state; the states disagreed. That is a permanent
partition of a bank, and it needed no forgery.
Authorization is a pure function of the entry — a banker's key, or `po.sig` —
never of state-so-far, so pre-pass B is well defined and non-circular.
Membership and balance are state-dependent and belong in the main fold.
**The whole fold is a pure function of the entry set.** Not of arrival order,
not of the wall clock, not of who is online. That is what makes replicas
unpartitionable, and it is why the ack rule is a pre-pass over the whole set
rather than a state transition inside the fold.
### Slots
| op | slot key | space |
|---|---|---|
| `pay` | `"p|" + po.from + "|" + po.seq` | the payer's **payment** sequence |
| `mint`, `burn` | `"i|" + bankerPub + "|" + op.seq` | the banker's **issuance** sequence |
**Two spaces, namespaced by class, and they are not one space because they
cannot be kept in step.** A `pay`'s `seq` comes out of the *payer's wallet* —
wallet-kit's `LedgerStore` owns that counter and derives it from that wallet's
own receipts alone — while a `mint`'s or `burn`'s comes out of *this fold*. The
banker holds an account and may pay like anyone else, so a shared space collides
by construction: a banker's wallet holds no receipts, so it issues `seq: 1` for
the banker's first order, which is the slot the banker's first mint already took.
That is not a retryable race. A consumed slot is permanent (rule 1 below), so
whichever entry sorts first wins and the other is `failed`/`"seq"` **forever** —
the payment if the mint sorted first, the *issuance* if the order did, in which
case supply diverges from what every member was told had been minted. And the
loser cannot back off: a banca `failed` is not a `wrj`, so the payer's wallet
never learns to advance its counter. Either the banker can never pay out of
their own bank, or a mint silently does not exist.
Within one space the rule is unchanged: **one monotonic sequence per account,
shared by every act that moves that account's money** — a payer's payments are
one sequence, the banker's mints and burns are another.
### The two rules that make it airtight
**1. A seq slot is consumed by its winner even when that winner fails.** An
overdraft folds to `failed` **permanently** and the slot stays spent. If a
failure released its slot, a later `mint` topping the payer up would
retroactively resurrect a payment everyone had already read as failed — the same
money spent twice, months apart, with no equivocation anywhere in the log. The
slot is the double-spend defense; a defense that unwinds is not one.
**2. The banker never gates validity, only irreversibility.** A dead banker key
leaves the bank **limping** — provisional settlement continues, payments keep
converging, nothing finalizes and nothing new is minted — rather than bricked.
That is a deliberate trade, and §9 states it as a limitation.
### Per-op fold rules
| op | folds when | effect |
|---|---|---|
| `charter` | author = `bankerPub`; first in sort order | sets `cur = <banker>.<code>`, the join policy and the display metadata. A second charter from the banker is **inert** and stays visible in the log forever — a banker cannot redenominate a bank out from under its customers. The banker holds an open account from this moment. |
| `open` | author is a suite identity | creates the account (state `open` under `join:"open"`, `pending` under `join:"approve"`) or renames an existing one. **Never reopens a `closed` account** — only `acct approve` does, so a closed customer cannot readmit itself. |
| `acct` | author = `bankerPub` | `approve` → state `open` (also readmits); `close` → state `closed`. |
| `mint` | author = `bankerPub`; **issuance** slot winner | `to` must be an **open** account — never `pending` (an unapproved knock) and never `closed` — else `failed`/`unknown-payer`, with the issuance slot spent anyway. Otherwise credits `to` and raises `supply`. It does **not** touch the banker's own balance, and it does not move the banker's *payment* counter either. |
| `burn` | author = `bankerPub`; **issuance** slot winner | debits the **banker's own** account and lowers `supply`; `failed`/`insufficient` if short. `burn` names no account: a banker cannot reach into a customer's balance. |
| `pay` | `po.sig` verifies; **payment** slot winner | `po.cur` must be this bank's, else `failed`/`cur` **and no slot is consumed** — one bank's sequence space is not another's. Payer and payee must both be **open** accounts, else `failed`/`unknown-payer`. Payer balance must cover `amt`, else `failed`/`insufficient`. Otherwise debits payer, credits payee. |
| `ack` | author = `bankerPub` | consumed by pre-pass A. An ack naming an unknown hash is inert — which is how a banker acking a payment it has not replicated yet behaves. |
| `rate` | author = `bankerPub` | sets `rate = {num, den}`; last writer in sort order wins. Informational (§9). |
| `req` | author is an **open** account | appended to the request list. Nothing consumes a `req`: no payment references one and a settled payment does not close one. It is a message with a number attached. |
### Payment status
| status | meaning |
|---|---|
| `held` | a BANK/2 lock folded: the money left the payer's balance and is held against a hashlock, belonging to neither party (§13) |
| `returned` | a lock was released: the money went back to its payer, and the trade it was a leg of did not happen (§13) |
| `settled` | money moved and no ack has pinned it. What an interface may call that depends on `kind` — see below, and §12 |
| `final` | money moved; the banker acked it — **irreversible** |
| `failed` | money did not move, and the slot is spent anyway |
`acked` is carried as a separate boolean, so *failed and acked* — a permanently
failed payment — is expressible without a fourth status word. It is a real case,
not a theoretical one: a banker who acks **both** contenders for one slot leaves
the `(clock, hash)` loser `failed` with an ack naming it, and a UI asking "did
the banker answer my payment?" has to be told yes. `acked` is therefore the
ack's existence on every path, including every failure path.
`why` uses wallet-kit's closed `DECLINE_REASONS` vocabulary (`insufficient`,
`unknown-payer`, `expired`, `cur`, `seq`), so a wallet reading a bank's fold and
a wallet reading a `wrj` speak one language.
#### `settled` covers two different pasts — and a mint's ack signs a `wri` now
The **fold's** ack machinery is blind to `kind`. Pre-pass A collects every `h` a
banker's `ack` names, pre-pass B ranks acked above unacked whatever the entry is,
and the `mint`/`burn` branches read the acked set exactly as `pay` does — so an
acked issuance is `final`, its issuance slot is pinned, and no withheld
back-dated mint can displace it. The mechanism always existed.
**What used to be missing was anything for that ack to sign.** A `mint` carries
no `po`, so there was no settlement to build, the `bsig` on an ack naming one
would have been a signature over nothing, and banca refused to append one — an
issuance in a bank run by this implementation was **never pinned**. The
consequence was a real protocol hole:
> A banker can withhold a second `mint` claiming the same issuance slot and
> release it later with a back-dated Lamport clock. `(clock, hash)`-first is
> blind to arrival time, so the withheld issuance takes the slot and the mint
> everyone acted on flips to `failed`/`"seq"` — **removing money from a
> customer's balance**. It is the only way a banker can do that: `burn` debits
> the banker's own account and `acct close` freezes a balance rather than
> emptying it. Golden-vectored as
> `withheld-backdated-mint-displaces-an-issuance`.
**wallet-kit's issuance receipt (`wri`) closes it for mints** (§8). Acking a
mint now means building a `wri` — the bank's signature over the mint's currency,
issuance slot, recipient, amount and entry hash — and putting *its* `bsig` and
`ts` into the four-key ack. `view/ackable` offers settled mints beside settled
payments, `receiptFor` rebuilds the wri for a final mint, and the recipient
holds a receipt no back-dated log entry can displace. Golden-vectored as
`a-wri-ack-closes-the-confiscation-window`: the same withheld back-dated mint,
plus one wri-carrying ack, and the customer's balance survives.
**A `burn` stays unpinnable, by design and not by omission.** A burn debits the
banker's own account and names no recipient, so displacing one moves only the
displacer's money — and a burn-wri would be signed content with no natural
meaning (a `to` that is really a `from`). The kit's `buildIssuanceReceipt` would
sign one without complaint; the MINT-ONLY rule lives in banca (`view/ackable`
never offers a burn, `receiptFor` answers null for one, the store refuses a
direct ack on one). A future artifact that pins burns gets its own `t` and
domain.
Everything a banker does here remains attributable, not prevented (§9). §11
keeps the burn limitation; §12 says what an interface must call a settled
issuance — still not the word it calls a settled payment, and with a sentence
that now differs between the mint (whose ack is coming) and the burn (whose
never is).
`nextSeq(id)` is the highest **payment** slot the account has consumed, plus one;
`nextIssueSeq()` is the same for the banker's issuance space. Both only ever
rise, and a slot that was never claimed is skipped forever — the safe direction
for a counter that must never hand out a spent one. Issuance never moves
`nextSeq`: that number is what a wallet puts in `wpo.seq`, and a mint has no
business changing it.
Both return **null when the space is spent**. `wpo.seq` is bounded by
wallet-kit's `seq-no?` at `AMT_MAX` (2^50) and a slot is consumed by a monotone
max, so one order at the ceiling pins the account: `AMT_MAX + 1` is a number no
order carrying it can ever shape, and `AMT_MAX` is a slot already spent. Neither
is an answer, so there is none — the same stance `LedgerStore.reserveSeq` takes
when it rejects on an exhausted bank. Self-inflicted (§9, slot exhaustion) and
unreachable in practice, which is exactly why it is written down.
---
## 6. Expiry is not foldable
`po.exp` exists and is sanity-checked relative to `po.ts` (`exp > ts`,
`exp ≤ ts + 90 days`) — both clock-free comparisons between two values that
travel together on the wire.
**The fold never reads it for liveness, and never consults a wall clock at all.**
A verdict that depended on the reader's clock would let two replicas three
minutes apart disagree about one entry, and a bank whose replicas disagree is not
a bank.
This costs nothing. The seq slot is a strictly stronger replay defense than
expiry: once slot `(payer, n)` is consumed, no re-append of that order can ever
move money again, at any age. A banker who wants to refuse a stale order does it
out of band, with a wallet-kit `wrj` carrying why `"expired"`.
**Consequence for implementers:** banca's admission check for a `wpo` is
wallet-kit's own `orderShape`, called with the kit's explicit admission clock set
to `null` — **no clock**. That form drops exactly one bound (`ts` no further
ahead than `SKEW_MS`) and keeps every other, the ts-relative `exp` bounds
included; it is *deterministic, not weaker*. The kit's default (an omitted
`nowMs`) is the wall clock, which is right for a wallet deciding whether to sign
or settle something in flight and wrong for a fold, and anything that is neither
a finite number nor `null` — a string, `NaN`, an object — deliberately reads as
*omitted*, so a typo cannot select the permissive mode. `verifyOrder` and
`verifySettlement` take the same argument and banca passes `null` to all three.
See `client/src/banca/lib/wallet.cljs`.
---
## 7. Arithmetic
Balances and supply fold in **BigInt**, and are reported both exactly (a decimal
string, `balExact` / `supplyExact`) and as a `Number` for display.
A double is exact on integers to 2^53 and one amount may be 2^50, so a `Number`
fold holds exactly **eight** maximal amounts before it starts losing units while
reporting a clean-looking total. Eight is far too few to fold a history over. The
`2^50` ceiling is therefore not a policy limit — it is what keeps a *single*
amount exactly representable as it crosses the wire and through `canon()`.
Lowering it would not fix a double fold; raising it past 2^53 would break the one
thing it is for.
---
## 8. Receipts
A **portable double-signed receipt** (`wrc`) is not a new artifact — both halves
already exist in the log, so it is a projection every member can compute:
```
{ v:1, t:"wrc", po, seq, ts, bank, bsig } // seven keys, in this order
po the order out of the `pay` entry, verbatim, sig included
seq the bank's log reference — banca uses THE PAY ENTRY'S HASH, which is
exactly what the ack names in `h`
ts the ack op's timestamp
bank bankerPub
bsig the ack op's signature
```
signed as `"wpay-rcp|v1|" + canon(receipt minus bsig)`. `t` is **inside** the
preimage, which is what keeps a decline (`wrj`) from ever being re-read as a
receipt even though both share the `wpay-rcp` domain.
**`ack.ts` is part of what the banker signed, so it is not a fresh timestamp.**
A banker acknowledges by building the artifact — wallet-kit's `buildSettlement`
for a payment, `buildIssuanceReceipt` for a mint; both stamp the artifact's own
`ts` and sign over it — and then appending an `ack` carrying **that** `ts` and
its `bsig`. Stamping `Date.now()` on the op instead would rebuild a receipt over
a preimage nobody ever signed: well-formed, final, and worthless to a third
party, with nothing in the fold able to tell. `mk-ack` therefore takes the
timestamp as an argument; its shorter arity stamps the clock and is for a caller
that is not rebuilding a receipt.
`po.sig` is inside the preimage too. A receipt whose preimage omitted it would
let a bank re-issue the same settlement over a re-signed order.
### The issuance receipt (`wri`)
A **bank-signed issuance receipt** gives a **mint** the same portability — both
halves already in the log, a projection every member can compute:
```
{ v:1, t:"wri", cur, seq, to, amt, h, ts, bank, bsig } // ten keys, in this order
cur the charter's currency id
seq the BANK's own issuance slot NUMBER — a third meaning of the word:
wpo.seq is the payer's number, wrc.seq is the bank's log-ref STRING
to the mint's recipient — the ONLY identity a wri can ever credit
amt the mint's amount
h THE MINT ENTRY'S HASH — exactly what the ack names in `h`
ts the ack op's timestamp (the wri's own; see above)
bank bankerPub
bsig the ack op's signature
```
signed as `"wpay-iss|v1|" + canon(receipt minus bsig)` — its own domain, so a
settlement signature can never dress up as an issuance receipt or vice versa.
The kit enforces `banker(cur) === bank` (no bank can finalize another bank's
issuance), and its dedup key `issuanceKey = sha256B64url(canon({v,t:"wri",bank,h}))`
is deliberately blind to `ts`/`seq`/`to`/`amt`/`cur`: `(bank, h)` IS the fact
being made final, a re-issue with a fresh `ts` is the same fact said twice, and
two amounts under one hash are a signed contradiction the collision exposes.
The flip side is banca's to keep: **never emit two wri with different content
for one entry hash** — `view/ackable` excludes acked entries, so the banker's
screen never builds a second one for an answered `h`.
**MINT ONLY.** A burn names no recipient, so a burn-wri would be signed content
with no natural meaning; the kit's builder does not refuse it at runtime, and
banca is where the rule lives (§5). Burns stay unpinnable and receipt-less.
**Delivery is manual in v1.** The recipient of a final mint copies or downloads
their own wri from the ledger row, exactly as a payee exports a `wrc` — every
member can rebuild it, so nothing has to be sent. Automatic delivery into the
recipient's wallet inbox (`LedgerStore.applyIssuance` folds only receipts
addressed to the local identity, so a wri must reach *its recipient*, not a
broadcast) is future work.
`receiptFor(hash)` returns a receipt only for a record that is **`final`**: the
`wrc` for a payment, the `wri` for a mint, and `null` for a burn always. A
provisional settlement has no receipt to show, and pretending otherwise is the
exact confusion this protocol exists to avoid. The case that rule is really for
is *failed **and** acked*: every ingredient of a receipt is present — the signed
order, the banker's `bsig`, the log ref — and the only thing saying the money
never moved is the status.
**Verify against a NAMED bank.** `verifySettlement(raw, expectedBankPub, nowMs)`
and `verifyIssuanceReceipt(raw, expectedBankPub, nowMs)` take the bank you were
waiting for, and banca's seam makes it *required* for both: a caller that cannot
name one gets `null` rather than a permissive check. Without it, a well-formed
artifact by some **other** bank — over that bank's own paper, with that bank's
own valid signature, every internal check passing — is indistinguishable from
yours, and nothing else in the artifact tells them apart.
**An honest asymmetry, stated plainly — for both receipts.** A banker *can*
append an `ack` whose `bsig` is well-formed and simply wrong. The fold still
treats that record as final — finality is authorship, and a banker's act is
attributable rather than preventable — but the exported receipt will not verify
for a third party. Verify before you rely: run `verify-settlement` (a wrc) or
`verify-issuance` (a wri) on anything you hand to someone else; banca's export
path does, and says when it cannot produce one.
---
## 9. Injection surfaces, named
**Withheld, back-dated clocks.** A member can hold an entry and release it weeks
later with a Lamport clock that sorts it before payments everyone has already
acted on. `(clock, hash)`-first is blind to arrival time, so with no ack the
withheld order takes the slot and displaces a settled payment. **This is the hole
the ack closes**, and it is the only thing the ack does. Bounded by: get an ack.
Golden-vectored both ways (`withheld-backdated-no-ack` /
`withheld-backdated-after-ack`).
**Withheld, back-dated *issuance*.** The same hole, on the banker's own sequence
space. **Bounded by: an ack, since the `wri`** — the issuance receipt gives a
mint's ack a preimage to sign (§5, §8), so the banker's queue offers settled
mints and an acked mint can no longer be displaced. (First shipped in banca app
version 2; earlier clients had nothing to sign and never appended one, so a mint
acked by an old log stays whatever that log says.) A mint the banker has *not*
acked yet is still displaceable, and a **burn** always is — a burn has no wri by
design, and displacing one moves only the banker's own money. Both mints stay
signed by the banker and every member holds both, forever. Golden-vectored both
ways (`withheld-backdated-mint-displaces-an-issuance` /
`a-wri-ack-closes-the-confiscation-window`), and §12 requires an interface to
say which future each unpinned issuance has.
**Banker equivocation.** Two mints at one seq, or an ack for the loser of a
`(clock, hash)` race, or a second charter. The fold resolves each
deterministically — the whole point is that it *has* an answer — and every one of
those acts is a signed, replicated entry every member holds and can show to
anyone. Not prevented. Attributable.
**`req` spam.** Any open account may append unlimited requests, and the retention
rule forbids capping the list. Mitigations are social and interface-level (a
per-author view, a mute, closing the account), never a silent drop, because a
drop would make two replicas disagree.
**Memo and name rendering.** `memo`, `ctx`, `name` and `sym` arrive from members
and are bounded, control-character-free and line-terminator-free by the
validators — but they are **not sanitized for markup**. Any interface built on
this fold must render them as text nodes, never as HTML. The repo's
`source-hygiene.test.mjs` forbids raw-HTML sinks in new code for exactly this
reason.
**Hostile envelope fields.** `hash` and `clock` are what the whole sort is built
out of and neither is a value this app produces — an ordinary member can sign an
entry whose `clock.time` is a string, and it verifies. `ingest` validates both
and drops what fails; see §5, "The envelope is as untrusted as the op". Bounded
by: the ingest gate, which is the defense. The comparator being total by
construction is a second layer and not one anything can reach while the gate
holds.
**Log flooding.** Anyone with the link can append. Sealing is the access control
(the base protocol), so a URL holder can add entries a member will fold or drop;
invalid ops cost a validator call. There is no rate limit in the protocol.
**Replay cost.** The retention rule (§5) forbids a window, so every replica
folds the whole history forever and a member can lengthen it for free. What must
not also be true is that the *fold* costs more than linear in it: appending used
to refold from scratch per entry (and again per order-signature verdict), with
`orderShape` — a `JSON.stringify` plus regexes — recomputed up to three times per
`pay` per refold, so 800 payments cost 5.0 s to replay and 50 cost 34 ms: 0.69 →
6.29 ms/entry, quadratic and cheap to inflate. An append now marks the fold stale
and readers fold once, `orderShape` is memoised by entry hash, and an unverified
signature no longer buys the rebuild at all. The same 800 payments cost 21 ms,
flat at 0.03 ms/entry. **The work is bounded, the history is not** — nothing is
evicted, no window is introduced, and the fold is still computed only by
`refold`, from scratch, over everything.
**Slot exhaustion.** A payer can burn their own sequence space with failed
payments. It costs them, not the bank, and `nextSeq` keeps rising.
---
## 10. What each party can and cannot see
| party | the bank's log | balances | receipts |
|---|---|---|---|
| stranger without the link | nothing — cannot even compute the address | nothing | nothing |
| stranger **with** the link | everything: every op, every amount, every account key, forever | can compute all of them | can rebuild any |
| customer | everything | everything | everything |
| ex-customer (`acct close`) who kept the link | **everything, forever** — including every payment made after they left | yes | yes |
| the banker | everything, plus the power to mint, ack and close | everything | everything |
| relay / mailbox node | ciphertext, sizes, timing, IP addresses | nothing | nothing |
**The ledger is member-visible by construction.** A bank has one shared log and
one shared key; every member sees every payment between every other pair of
members, including the amounts and the memos. There is no per-pair privacy inside
a bank and this design does not offer one.
---
## 11. Limitations — read these before you keep money here
- **You trust each banker with that bank's money.** A banker can mint without
limit, refuse to ack forever, and close your account. None of it is
cryptographically prevented. What you get is a signed, replicated record of
every act, which you can show to anyone. That is *attribution*, not *security*,
and the two should never be confused.
- **Provisional is not final.** A `settled` payment is what the entry set implies
right now. It can still be displaced by an entry that has not reached you yet
(§9). Only `final` — acked — is irreversible. An interface that renders the two
the same way is lying to its user.
- **A mint is final only once acked; a burn never is.** A `mint`'s ack signs a
wallet-kit issuance receipt (`wri`, §8) now, so an acked mint is pinned and
its recipient holds a receipt no back-dated log entry can displace. Until the
banker acks it, money you were issued is still displaceable: the banker, and
only the banker, can release a withheld back-dated `mint` for the same
issuance slot and take it back — signed, visible to every link holder, and not
prevented, only bounded by the ack (§9). This is the one power that reaches
*into* a customer's balance; a `burn` cannot, and a burn also cannot be made
final — it has no signable preimage by design, so it stays unpinnable for as
long as the bank exists.
- **`wri` delivery is manual in v1.** A final mint's receipt is rebuilt from the
log by any member, and the recipient exports their own (§8). Nothing pushes it
into the recipient's wallet; that auto-delivery is future work.
- **The ledger is public to link holders, permanently.** Removal is not
retraction: an ex-member with the URL keeps reading, and could have copied
everything anyway. Closing an account stops it transacting; it does not blind
it.
- **The bank dies with the banker key.** No recovery, no succession, no
multi-signature. A lost banker seed means a bank that can still settle
provisionally and can never mint or finalize again — limping, permanently.
- **Sybils are free.** An identity is a keypair anyone can generate in a
millisecond. `join:"approve"` is the only gate, and it is one person's
judgement, not a proof of anything.
- **`rate` is an assertion, not an oracle.** The banker says one unit is
`num/den` GAZ. Nothing checks it, nothing enforces it, nobody has to honour it,
and there is no market anywhere in this protocol. It is a number a banker
published, no more binding than a sign in a window.
- **A BANK/2 lock can be stuck.** If no preimage is ever revealed, the banker
never attests the timeout and the beneficiary never releases, the money stays
locked for ever. §13.6.
- **Nothing here is insured, redeemable or legal tender.** A balance in a banca
bank is a record of what a log says, agreed to by the people reading it.
---
## 12. What an interface built on this must say
The fold is honest by construction; an interface is not. These are the rules
banca's own screens are held to, and any second implementation that wants to be
trusted with someone's money should hold itself to them too.
**The four words, and only these four.** The fold's `status` vocabulary is
`settled | final | failed`, and `settled` **must not reach a screen**. In every
other financial interface ever built that word means *done*; here it means the
opposite of pinned. banca translates it in one place
(`client/src/banca/lib/view.cljs`, `payment-state`), and no catalog in any
language contains the string `settled` — asserted, in `test/i18n.test.mjs`.
It translates to **two** words, not one, because the fold's one status covers two
different futures (§5):
| the fold says | on a | the user reads | and beside it |
|---|---|---|---|
| `settled` | `pay` | **provisional** | *not final: the banker has not acknowledged it yet* |
| `settled` | `mint` | **unpinned** | *not final: the bank has not pinned this issuance yet — its acknowledgement signs a receipt you keep* |
| `settled` | `burn` | **unpinned** | *a burn cannot be made final — only the bank could displace it, and that act would be signed* |
| `final` | any | **final** | *the banker acknowledged this — it cannot be undone* |
| `failed` | any | **failed** | the `why`, in words |
`final` and `failed` do not split, and that is not an oversight: the fold's ack
pre-pass is blind to `kind`, so an acked issuance really is pinned and **final**
is the true word for it. The `unpinned` WORD does not split either — a mint and
a burn are both the bank's act, not a member's payment — but its SENTENCE does,
because the two have different futures now: a mint's ack is producible (it signs
a `wri`, §8) and a burn's never will be.
The sentence is not a tooltip. A chip reading `provisional` with its meaning
parked behind a hover is the same lie with an extra step.
**A word (or sentence) that promises must be kept by some screen.**
`provisional` says *yet*, and since the `wri` the mint's sentence says *yet*
too: an acknowledgement is coming. So the set of records whose wording promises
one must be **exactly** the set the banker screen is offering to acknowledge —
settled payments and settled mints — and the burn, whose sentence promises
nothing, must never be offered. The invariant is asserted as an iff over every
scenario in `test/view.test.mjs` (a hash is in `ackable` exactly when the record
reads `provisional`, or reads `unpinned` and is a mint) rather than as rules
that happen to line up — because for a while they did not, and the result was a
wallet that read *"the banker has not acknowledged it yet"* beside a banker's
tab that read *"nothing is waiting"*, on the same mint, forever.
**A balance is not a claim about the future — and it carries two numbers, not
one.** Beside the total, the wallet says how many of the viewer's own payments
are provisional and what they come to (`provisional-count` / `provisional-net`),
and *separately* how many issuances are unpinned — the UNACKED mints plus every
settled burn; an acked mint has moved to `final` — and what those come to
(`unpinned-count` / `unpinned-net`), all four folded in BigInt like the balance
itself. Collapsing them into one number blurred two different claims; dropping
the second would be worse still, because "all of it is final" over money a
withheld issuance can still take back is the very lie §11 forbids. Each total
carries its own sentence, neither sentence is shown for a category with nothing
in it, and *"all of it is final" requires **both** counts to be zero*.
**"Acknowledge everything" acknowledges only what may be acknowledged.** The
banker's one-tap action walks `view/ackable`, which is `pay` **and `mint`**
records with status `settled` and no ack — **never a failed one**. An ack is not
a comment: pre-pass B ranks an acked entry above an unacked one, so acking the
loser of a contested slot *displaces what its winner already did*
(`ack-pins-the-clock-loser` is that exact entry set, and the rule keeps failed
mints out for the same reason). A rule written as "everything not final" would
reverse a settlement on every tap. **Burns** are excluded for a second,
independent reason: a burn has no signable preimage (§5, §8), and signing
something arbitrary and calling it finality would be worse than the gap it
papered over. One sequential pass acks both kinds — a payment's ack signs its
settlement, a mint's signs its `wri` — and the banker's screen says all of this
in words. Excluding acked entries doubles as the equivocation guard: no second
`wri` is ever built for a hash the bank already answered.
**Offer a receipt only where one can be rebuilt.** `receiptFor` answers a `wrc`
for a final payment and a `wri` for a final mint, and `null` for a burn however
`final` a foreign log made it (§8) — so a button guarded on `final` alone would
appear on that burn and report *"the banker's signature does not verify"*,
naming the one failure that did not occur. banca guards on `view/receiptable?`
(`final` **and** not a burn), and `test/view.test.mjs` holds it to `receiptFor`'s
own answer over every scenario. Exporting is also delivery in v1: the recipient
of a mint copies their own `wri` from the row, and that receipt is what closes
the confiscation window for them (§9).
**Nothing is signed that cannot settle.** Every action checks its preconditions
before it builds an artifact — `nextSeq`/`nextIssueSeq` returning **null** (§5)
included, which is the one refusal a user could never discover by trying. A
signed order carrying a null `seq` is paper no fold can shape.
**Say the reserved code before the button.** A charter claiming `GAZ` is not
refused by the founder's client and accepted by everyone else: it is *dropped at
ingest, on every replica*, leaving a link, a log, and a bank that silently does
not exist. The founding form names the rule as it is typed.
**Every untrusted string is a text node.** Names, symbols and memos are bounded
and control-character-free (§9) and are *not* markup-safe. banca builds nodes
(`client/src/banca/app/dom.cljs`); `test/source-hygiene.test.mjs` forbids
`innerHTML`, `outerHTML`, `insertAdjacentHTML` and any `esc` function outright,
with an empty allowlist.
**The link is the capability, and the sheet that shows it says so.** Anyone
holding it replicates the whole ledger, forever, and closing an account does not
take that back (§10).
---
## 13. BANK/2 — escrow
Money the bank **holds** against a hashlock instead of moving. Everything above
is unchanged; this section is the whole of what BANK/2 adds.
**Why it exists.** A trade of one bank's paper for another's is two payments in
two independent logs with no shared ordering, so it cannot be made atomic at the
artifact layer — a `wpo` binds to exactly one bank, and there is no two-phase
commit anywhere in this design. Lock both legs to ONE hashlock and it becomes
atomic: the secret-holder claims first and thereby publishes the secret, and the
counterparty claims the other leg with it. Either both complete or neither does.
**The secret-holder must lock FIRST.** Otherwise they could claim the other leg
while never having locked anything of their own. This is not a convention; the
construction does not hold without it.
### 13.1 The rule, and the invariant it looked like it would break
§6 is categorical: the fold never consults a wall clock. A hashlock refund is a
*timeout*, which is exactly a clock condition. The answer is one fold rule:
> **A `claim` beats an `unlock` for the same lock, unconditionally** — decided in
> a pre-pass over the whole entry set, exactly as the ack rule already is.
A textbook hashlocked swap needs the second leg's timeout strictly before the
first's, so nobody can still be claiming after their own lock expired. Across two
**independent bankers** that ordering is unenforceable — banker M could attest a
timeout early while banker D attests one late, and no rule in either log can
constrain the other. With claim-beats-unlock it is also **unnecessary**: once the
preimage is public the counterparty's claim wins whenever it lands, next minute
or next month, against a return that folded weeks earlier, in a log they had not
replicated at the time. There is no window and no deadline to get wrong.
The property that makes this work is the one that looked like the obstacle. The
fold has **no clock**, so *too late* is not something it can think. A return is
not a deadline that closes; it is a claim on the money that a preimage outranks.
### 13.2 What authorises a return, since it is not a clock
| an `unlock` folds when | meaning | works when |
|---|---|---|
| its **author is the banker** | the banker attests the lock's advisory `exp` has passed | the banker is alive |
| it carries a valid **`rel`** | the beneficiary hands the money back voluntarily | always, banker or no banker |
The banker's attestation is a new trust assumption and should be measured
against the ones already made rather than in the abstract. §11 already says you
trust a banker not to mint without limit, not to withhold acks for ever, and not
to close your account. **Trusting the same key to read a clock honestly is
strictly less than any of those** — and unlike them it is bounded: an early
refund is overruled by any later claim, so the worst it can do is return money
to its own payer.
The beneficiary release needs no clock and no bank, and it is what keeps a lost
banker seed from being a lost-funds event.
**The kit cannot check what a release is ABOUT.** `verifyRelease` never sees the
lock, so it proves only that somebody signed away *some* lock. That the release
names THIS lock — its entry hash, its currency, its beneficiary — is the FOLD's
job, and the fold is the only party holding all three.
### 13.3 The fold
State gains a thirteenth key, `escrows` (lock entry hash → record), and each
account gains `locked` — what it currently has out in escrow.
**Conservation is the property to test:** a `lock` debits `bal[from]` and credits
`locked[from]`; a `claim` debits `locked[from]` and credits `bal[to]`; an
`unlock` debits `locked[from]` and credits `bal[from]`. `supply` is untouched by
all three, and at every point
```
Σ bal + Σ locked === supply
```
**A lock claims the payer's PAYMENT slot** — `"p|" + lk.from + "|" + lk.seq`, the
same space a `pay` claims, from the same counter in the payer's wallet. Not a
second space: a lock and a payment are both this account moving its own money,
and the fold consumes one slot per act. Rule 1 holds unchanged — a failed lock
spends its slot permanently.
`claim` and `unlock` claim no slot. They are resolved by a pre-pass instead:
```
pre-pass C the ESCROW WINNERS: group every `claim` and `unlock` by the lock it
names, and pick one winner per LOCK —
· a VERIFIED claim beats any unlock, unconditionally;
· among claims, (clock, hash)-first;
· among unlocks, (clock, hash)-first.
```
A claim or a return naming something that is not a folded lock is **inert** — the
same stance an `ack` naming an unknown hash takes, for the same reason.
**A resolution is applied at whichever of {the lock, the winning resolution}
comes LATER** in `(clock, hash)` order. A claim CAN sort before its own lock:
causally impossible in an honest log, and trivially produced by a crafted clock.
Applying it only where the fold meets the resolution would lose it for ever;
applying it only at the lock would move money before the lock existed.
**Preimages and release signatures are verified asynchronously and
pessimistically**, exactly as `po.sig` is: an unverified claim is not a claim.
Neither can be checked at ingest, because both name their lock by ENTRY HASH and
that lock may not have replicated yet — so the pre-pass starts them once the lock
is in hand, and a verdict landing marks the fold stale.
**The hashlock digest is over the preimage's base64url TEXT, not its 32 bytes**
(wallet-kit `consts.cljs`). One hashing rule for the whole suite, and no place
for a second implementation to silently pick the other reading. **Never reuse a
preimage across two locks:** the first claim publishes it, and every other lock
sharing the digest becomes claimable by anyone watching.
### 13.4 Per-op fold rules
| op | folds when | effect |
|---|---|---|
| `lock` | `lk.sig` verifies; **payment** slot winner | `lk.cur` must be this bank's, else `failed`/`cur` **and no slot is consumed**. Payer and payee both **open**, else `failed`/`unknown-payer`. Payer's balance must cover `amt`, else `failed`/`insufficient`. Otherwise debits `bal`, credits `locked`, opens an escrow record at `held` |
| `claim` | preimage verified; escrow winner | debits `locked[from]`, credits `bal[to]`; the lock reads `settled`. `to` must still be an **open** account, else the claim is inert and the lock stays `held` |
| `unlock` | authorised per §13.2; escrow winner | debits `locked[from]`, credits `bal[from]`; the lock reads `returned`. The payer is credited whatever state their account is in — `acct close` freezes a balance rather than emptying it, and this money was never anyone else's |
| `ack` | author = `bankerPub` | an ack naming a **claimed** lock makes it `final` and signs a `wlr` (§8) |
**The lock receipt (`wlr`)** is the escrow analogue of §8's two — both halves
already in the log, a projection every member can rebuild: the `lk` out of the
`lock` entry verbatim, plus the ack's `ts` and `bsig`, key order from the kit's
`unsignedLockReceipt`. **Its `seq` is the LOCK ENTRY's hash** — the same
identifier the ack names in `h`, and the same choice `wrc.seq` makes for a
payment: `ack.h` and a receipt's log reference are one number everywhere in
this protocol, and a second convention here (the claim entry's hash, say) would
be one more thing for a second implementation to get wrong. §8's verify rule
applies unchanged: `verify-lock-receipt` takes the bank you were waiting for,
required.
`DECLINE_REASONS` does not change. A claim with a wrong preimage is not a
declined payment — it is an op the fold cannot act on, and it folds to nothing at
all, so it never needs a `why`. A failed lock fails for reasons the closed set
already names.
### 13.5 The version rides the link
New op types are not additive: §3 drops an unknown `t` **silently**, so a BANK/1
client reading a bank that has ever used escrow computes different balances — it
sees the lock's debit and never the claim's credit. That is a partition, not a
degraded view.
So the version belongs to the **bank**, and it is in the link:
```
#<secret>.<bankerPub> BANK/1, salt banca.ardegazu.ro/v1
#<secret>.<bankerPub>.2 BANK/2, salt banca.ardegazu.ro/v2
```
**A deployed BANK/1 client rejects a v2 link all by itself**, with no release and
no cooperation. Its `parse` splits on the FIRST dot and requires both halves to
be exactly 43 base64url characters, so it reads the banker half as
`<bankerPub>.2` — one character too long, carrying a `.` the alphabet does not
contain — and answers *this is not a bank link*. A visible refusal, never a wrong
room.
**The salt must move too, even though the link already separates.** If both
versions shared one salt, one secret would derive ONE room, and a banker who
appended `.2` to their own existing link would open their EXISTING bank under v2
rules and start appending escrow ops into a log BANK/1 clients are still reading
and silently dropping. Self-inflicted, one keystroke away, and completely
invisible. Different salts make it unreachable.
**Storage namespacing does NOT move.** The identity mirror, the saved-bank list,
the language and the block store belong to the person, not to the bank, and
splitting them by protocol version would hand someone two identities in one tab.
OrbitDB's per-room directory embeds the roomId, which is already salt-derived, so
v1 and v2 rooms never collide inside the one block store.
**There is no migration and none is offered.** A v1→v2 bridge would have to fold
both op unions and produce one answer, and there is no such answer. A banker who
wants escrow founds a v2 bank; moving balances across is ordinary banca usage —
mint in the new, burn in the old — banker-driven, attributable, and visible to
every member of both.
### 13.6 What escrow does NOT do
- **It does not make a banker trustworthy.** Escrow constrains a *counterparty*,
not the bank. Both bankers can still mint without limit and refuse to ack. §11
is unchanged and is still the thing to read before keeping money here.
- **A locked leg can be stuck.** If no preimage is ever revealed **and** the
banker never attests **and** the beneficiary never releases, the money stays
locked. It is the escrow analogue of "the bank dies with the banker key", and
it is bounded the same way: it costs the payer their own money, it needs the
banker gone *and* the counterparty gone or hostile, and every step is signed
and replicated.
- **No multi-hop, no partial claims, no beneficiary-less locks.** One lock pair,
two banks, one preimage; a lock is claimed whole or returned whole. A *firm*
quote — a resting offer backed by a live lock — needs a lock with no
beneficiary, which this version cannot express.
### 13.7 What an interface must say about it
§12's rules apply unchanged, and escrow adds two words to the four:
| the fold says | the user reads | and beside it |
|---|---|---|
| `held` | **locked** | *held by the bank against a condition — it is not theirs yet, and it comes back to you if this trade does not complete* |
| `returned` | **returned** | *the trade did not complete, and your money came back* |
A **claimed** lock is not a third word: it reads `provisional`, because that is
what it is — a member's money reached another member and the banker has not
pinned it yet.
**A held lock is never offered for acknowledgement.** Nothing has happened that a
bank could sign a receipt for, and offering one would promise a finality the
protocol cannot deliver. `view/ackable` gets this from `status` alone — a claimed
lock IS `settled` — and the iff of §12 covers it.
**A balance carries THREE outstanding totals, not two.** Locked money is neither
provisional (nothing moved to anyone) nor unpinned (the bank issued nothing): it
has already left the balance shown and reached nobody. *"All of it is final"*
requires all three to be zero, and **both** parties to a held lock count it —
the payer cannot spend it and the beneficiary does not have it, so neither may be
told everything is settled.
## 14. Implementation notes
**The wallet-kit seam.** `ardegazu-wallet-kit` owns the eight artifacts of the
suite economy (`wpr`, `wpo`, `wrc`, `wrj`, `wri`, and BANK/2's `wlk`, `wlr`,
`wrl`), their preimages and their shape grammar. It is
released and **sha-pinned** in `client/package.json`, so
`client/src/banca/lib/wallet.cljs` is no longer a transcription: every preimage,
shape check and currency rule there is the kit's own function. The golden vectors
in `client/test/vectors/{order,receipts}.json` still assert them against
wallet-kit's **own published** fixture and against by-hand canonical-JSON
templates — the pin is checked, not trusted, so a kit bump that moved a byte on
the wire goes red here rather than silently in a bank.
What remains local, and why:
| kept | reason |
|---|---|
| `canon` | social-kit's canonical JSON; wallet-kit signs over it but does not re-export it |
| `pub?` `sig?` `amount?` `seq-no?` | one-line applications of the kit's **own** exported `PUB_RE` / `SIG_RE` / `AMT_MIN` / `AMT_MAX`. The kit exports the constants, not the predicates; nothing here restates a rule |
| `text?` `wire-ts?` | receive-side rules for **banca's own** op fields (`charter.name`, `charter.sym`, `mint.memo`, every op's `ts`) — not wallet-kit artifact fields, so there is nothing to defer to. The kit's `sanitizeCtx`/`sanitizeMemo` *clamp*, which is the wrong shape for a validator that must drop, never repair |
| `currency-id` | the **banker's** side of a currency id. The kit parses ids and never mints one, because only a bank does that; the reserved `GAZ` refusal lives with it |
| `receipt-of` | banca **rebuilds** a `wrc` from two log entries that already carry both halves. The kit's `buildSettlement` **signs** one, which needs the banker's key. The key order still comes from the kit's `unsignedSettlement` |
| `issuance-of` | the same rebuild for a `wri` — mint entry + ack, key order from the kit's `unsignedIssuance` |
| `lock-receipt-of` | the same rebuild for a `wlr` — lock entry + ack, key order from the kit's `unsignedLockReceipt` (§13.4) |
| `verify-settlement` / `verify-issuance` / `verify-lock-receipt` | two-argument wrappers that make `expectedBankPub` **required** (§8) rather than optional |
| the MINT-ONLY rule | `buildIssuanceReceipt` signs anything of its own currency at runtime; that a **burn** never gets a `wri` is banca's rule, kept in `view/ackable`, `receiptFor` and the store's ack action (§5) |
The one divergence that used to be an open question is closed upstream: the kit
ships an explicit admission clock and `null` means *no clock*, which is what a
replicated fold needs. banca passes `null`; §6 says what that changes.
**Golden vectors.** `client/test/vectors/{order,receipts,fold}.json`, generated
by `build-vectors.mjs` from `independent.mjs` — @noble/curves, a canonical-JSON
re-implementation, and the preimages written out as literal templates. Every fold
expectation is hand-computed beside an `input_desc` sentence a reader can check.
Vectors run against `test-dist/testlib.js` (the built output), never `src/`.
**Order independence** is asserted for every scenario, over three fixed insertion
permutations, by byte-comparing `JSON.stringify(snapshot())`. It is the property
that keeps two replicas from ever partitioning, and it is checked as a property
rather than pinned as a value.
**`onEntry` must be passed INTO the log open call, never assigned after.**
Entries can arrive the moment `sync.start()` runs and `emit` marks hashes as
seen, so a late-attached handler loses them permanently. `client/src/banca/
room.cljs` shows the pattern, buffering into a `pending` array.
|