banca / docs / PROTOCOL.md
   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.

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/banca.git