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 | ;; The wallet layer's constants: the salt, the wire's size and shape limits,
;; the currency-id grammar and the reserved neutral unit of account.
;;
;; Everything a second implementation (banca, a bot, a phone app) has to agree
;; with byte-for-byte is HERE, in one file, and every one of these values is
;; pinned by a golden vector. Nothing below is policy that an app may tune —
;; the amount ceiling in particular is an arithmetic fact, not a limit anyone
;; chose (see AMT-MAX).
(ns ardegazu.wallet.consts)
;; NO WALLET SALT. social-kit has SOCIAL-SALT because it derives things — pairwise
;; channel ids, topic names. This layer derives NOTHING: every value it hashes is
;; already domain-separated by the "<domain>|v1|" prefix below, and the suite
;; X25519 keys an order is addressed with come from id-kit. A published constant
;; that nothing consumes and no golden vector pins is worse than an absent one —
;; it invites a second implementation to key something off a value this kit has
;; never actually agreed to. If a layer above ever needs one, it adds it then,
;; and a vector pins it in the same commit.
;;
;; ---- signature domains --------------------------------------------------------
;;
;; Every signature in this kit is Ed25519 over
;; "<domain>|v1|" + canon(<the artifact minus its signature field>)
;; with `canon` social-kit's canonical JSON (sorted keys, no whitespace, throws
;; on undefined). Distinct domains mean a signature taken for one artifact can
;; never be replayed as another.
;;
;; wrc and wrj SHARE a domain deliberately: they are the same act by the same
;; signer (the bank answering one order) and `t` is inside canon(), so the
;; preimages still differ. A decline can therefore never be re-read as a
;; receipt, and a bank needs one signing path, not two.
(def DOM-PAY-REQ "wpay-req")
(def DOM-PAY-ORD "wpay-ord")
(def DOM-PAY-RCP "wpay-rcp")
;; The issuance receipt gets its OWN domain, unlike wrj (which shares wrc's
;; because both are the same act — a bank answering one order). Issuance is a
;; different act with a different preimage shape: there is no embedded order,
;; so nothing inside canon() would keep a wri from colliding with some future
;; artifact that happens to share its field names. A separate domain makes the
;; separation structural rather than an accident of key sets.
(def DOM-PAY-ISS "wpay-iss")
;; ---- the escrow domains (BANK/2) ----------------------------------------------
;;
;; An escrow LOCK gets its own domain, on the `wri` reasoning rather than the
;; `wrj` one: it is a different act by the payer (money held against a condition,
;; not money sent) with a different preimage shape (a hashlock the payment order
;; has no field for). A separate domain makes that structural instead of an
;; accident of key sets.
(def DOM-PAY-LOCK "wpay-lock")
;; A beneficiary's RELEASE gets its own domain because it has a signer no other
;; artifact in this kit has. wpr is signed by a payee, wpo/wlk by a payer, and
;; wrc/wrj/wri/wlr by a bank; a wrl is signed by the party a lock is payable TO,
;; giving that money back. One signer, one act, one domain.
(def DOM-PAY-REL "wpay-rel")
;; The LOCK RECEIPT (`wlr`) shares DOM-PAY-RCP with wrc and wrj, and the sharing
;; rule above is why: it is the same act by the same signer — a bank answering
;; one instruction — with the instruction embedded and `t` inside canon(). The
;; reason wri needed its own domain does not apply, because a wri embeds NO
;; artifact and so has nothing inside canon() pinning it against a future
;; field-name collision. A wlr embeds a whole signed `lk`, exactly as a wrc
;; embeds a whole signed `po`.
;; ---- amounts ------------------------------------------------------------------
;;
;; Amounts are INTEGER MINOR UNITS. There are no floats anywhere in this kit and
;; no divisions on the wire — a currency's minor-unit exponent is the bank's
;; business, displayed by a UI, never carried in an order.
(def AMT-MIN 1)
;; 2^50. NOT a policy ceiling — a precision one, and it bounds ONE amount, not a
;; fold.
;;
;; Doubles are exact on integers up to 2^53, so three binary digits of headroom
;; is room for exactly EIGHT maximal amounts (2^53 / 2^50 = 8), not the thousand
;; an earlier draft of this comment claimed — the error was 128x. Eight is far
;; too few to fold a history over, so the ledger does NOT fold in doubles: it
;; sums in BigInt and reports the exact total alongside a Number for the UI (see
;; `balances` in ledger.cljs). This ceiling therefore only has to keep a single
;; amount exactly representable as it crosses the wire and through canon(),
;; which 2^50 does with room to spare.
;;
;; The ceiling stays where it is: lowering it would not fix a float fold, and
;; raising it past 2^53 would break the one thing it is actually for.
(def AMT-MAX 1125899906842624)
;; ---- strings ------------------------------------------------------------------
;;
;; `ctx` binds an order to an external cause (a game stake id, an invoice, a
;; room). It is carried VERBATIM into the receipt, so it is the one field a
;; third party is expected to match on — bounded, and free of control
;; characters so it can never smuggle a line break into a log or a display.
(def CTX-MAX 128)
;; `memo` is a note from the payer to the payee. Bounded in CODE POINTS, not
;; UTF-16 units, so an emoji costs one.
(def MEMO-MAX 140)
;; ---- shapes -------------------------------------------------------------------
;;
;; CANONICAL base64url, and this is not pedantry — it is the fix for a real
;; malleability that a tamper vector caught. Unpadded base64url of 64 bytes is
;; 86 characters, and the last character carries only 4 meaningful bits: its
;; low 4 bits are padding that every decoder ignores. So "…DA" and "…DB" decode
;; to the SAME signature, and a plain `{86}` pattern would let anyone mint
;; sixteen distinct-looking strings for one signature — enough to defeat any
;; dedup or cache that keys on the string, and enough to make "the same
;; artifact" a question with two answers.
;;
;; Every regex below therefore pins the padding bits to zero, by restricting the
;; final character to the ones whose spare bits are already 0:
;; 64 bytes / 86 chars -> 4 spare bits -> [AQgw]
;; 32 bytes / 43 chars -> 2 spare bits -> [AEIMQUYcgkosw048]
;; 16 bytes / 22 chars -> 4 spare bits -> [AQgw]
;; Any conforming encoder (id-kit's toB64url, btoa, Buffer, Python's
;; base64.urlsafe_b64encode) already produces exactly these, so nothing honest
;; is excluded. A second implementation MUST reject the non-canonical forms too.
;;
;; NUMERIC MALLEABILITY IS A DIFFERENT ANIMAL, and deliberately not fought here.
;; `{"amt":1e2}` and `{"amt":100}` are distinct bytes that both verify, and so
;; are `-0` and `0` in a `ts`. That is not the base64 problem wearing a hat: a
;; base64 string is COMPARED AS A STRING and never normalized, so sixteen
;; spellings really are sixteen distinct dedup keys for one signature. A JSON
;; number is normalized by the parser before this kit ever sees it — `1e2` IS
;; the double 100 by the time any predicate runs — and canon() re-serializes
;; from that double, so the preimage, the signature and the orderId are already
;; over the normalized form. Two spellings therefore produce the SAME artifact,
;; the SAME digest and the SAME verdict, which is exactly what a canonical
;; format is for. Rejecting them would mean re-reading the transmitted bytes
;; that JSON.parse has already thrown away, for no property that is not already
;; held. Every field this kit compares as a string is regex-pinned above.
;; A suite identity public key: 32 bytes, base64url, unpadded, canonical.
(def PUB-RE #"^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$")
;; A raw Ed25519 signature: 64 bytes, base64url, unpadded, canonical. Exact, not
;; a range — every signature in these artifacts is a raw Ed25519 one.
(def SIG-RE #"^[A-Za-z0-9_-]{85}[AQgw]$")
;; An order id: 16 random bytes, base64url, unpadded, canonical.
(def ORDER-ID-RE #"^[A-Za-z0-9_-]{21}[AQgw]$")
;; A SHA-256 digest, base64url, unpadded, canonical — the shape of
;; orderId/receiptKey, and of the keys the ledger stores beside its records.
;; Also the shape of BOTH halves of an escrow hashlock: the 32-byte preimage
;; and the digest of it. See HASHLOCK below.
(def DIGEST-RE #"^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$")
;; ---- the hashlock -------------------------------------------------------------
;;
;; An escrow lock is claimable by whoever can produce a preimage of `wlk.hash`.
;; The rule, and a second implementation must copy it exactly:
;;
;; preimage 32 random bytes, canonical base64url (DIGEST-RE, 43 chars)
;; hash sha256B64url(<the preimage's base64url TEXT>)
;;
;; THE DIGEST IS OVER THE TEXT, NOT THE BYTES, and that is deliberate. Every
;; other digest in this kit is taken over a canonical JSON string, so hashing a
;; string keeps one rule instead of two, and it removes the one place a second
;; implementation could silently disagree — whether to hash the 32 raw bytes or
;; the 43 characters that spell them. Base64url here is canonical (the final
;; character is restricted so the spare bits are zero), so the text has exactly
;; one spelling and hashing it loses nothing.
;;
;; A preimage is a SECRET until the moment it is claimed with, and then it is
;; public forever. Never reuse one across two locks: the first claim publishes
;; it, and every other lock sharing the digest becomes claimable by anyone
;; watching. One preimage per lock, always.
;; A currency code. Uppercase Latin only: a code is compared, printed and typed
;; by humans, and confusable scripts have no business in an equality test.
(def CODE-RE #"^[A-Z]{3,8}$")
;; A currency id is "<bankerIdPub>.<CODE>" — the issuing identity IS the
;; namespace, so two banks may both mint "LEI" and the ids never collide, and
;; nobody needs a registry.
(def CUR-RE #"^([A-Za-z0-9_-]{42}[AEIMQUYcgkosw048])\.([A-Z]{3,8})$")
;; ---- the neutral unit ---------------------------------------------------------
;;
;; "~.GAZ" is reserved. `~` 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 this package treats it as
;; quote-only:
;;
;; * `parseCurrency` accepts it and reports banker "~" (a pricing layer needs
;; that primitive);
;; * `isPayableCurrency` rejects it;
;; * every build* and every verify* in shapes.cljs rejects it, in all four
;; artifacts, because all four are instruments of payment. Nobody can mint
;; it, so nobody can settle it.
(def NEUTRAL-BANKER "~")
(def NEUTRAL-CODE "GAZ")
(def NEUTRAL-UNIT "~.GAZ")
;; ---- time ---------------------------------------------------------------------
;;
;; A VERDICT HERE DEPENDS ON THE READER'S WALL CLOCK, and a second implementation
;; has to plan for it. Two peers can disagree about one signature-valid artifact:
;;
;; * `ts?` rejects a timestamp more than SKEW-MS in the reader's future, so a
;; wallet whose clock is 10 minutes slow rejects an order a correctly-clocked
;; bank accepts — and it rejects it as MALFORMED, indistinguishable from a
;; forgery, not as "too early, try again";
;; * `pay-request-shape` bounds `exp` by the reader's now + MAX-TTL-MS + SKEW-MS,
;; so a far-future request can be read by a fast clock and refused by a slow one;
;; * `expired` is pure liveness and every caller may pin it — `expired(a, nowMs)`
;; and `balances(nowMs)` both take an explicit clock precisely so a fold can be
;; made reproducible.
;;
;; The signature itself never depends on the clock: preimages carry no `now`, so
;; an artifact that verifies for one reader verifies for every reader whose clock
;; admits it. Only ADMISSION is clock-dependent, only through `ts` and `exp`, and
;; only inside these two windows. A wallet showing "rejected" for an artifact a
;; peer accepted should suspect its own clock before it suspects the peer.
;; Clock skew we tolerate on a timestamp we did not generate. Same value the
;; social layer uses.
(def SKEW-MS 120000)
;; A payment request is a quote: perishable by default.
(def REQ-TTL-MS 3600000)
;; An order is a signed instruction in flight. Short by default — a payer who
;; walks away should not leave a live instruction behind them.
(def ORDER-TTL-MS 600000)
;; The furthest `exp` may sit beyond `ts`. A sanity bound, not a liveness one:
;; verify* rejects an artifact whose expiry is absurd, but does NOT reject one
;; that has merely expired — a bank has to be able to read an expired order in
;; order to decline it with why "expired". Use `expired` for liveness.
(def MAX-TTL-MS (* 90 86400000))
;; ---- sizes --------------------------------------------------------------------
;;
;; Serialized JSON ceilings, checked before anything is parsed field by field.
(def MAX-ARTIFACT-BYTES 4096)
;; A settlement embeds a whole order verbatim, so it gets its own headroom.
(def MAX-SETTLEMENT-BYTES 8192)
;; ---- decline reasons ----------------------------------------------------------
;;
;; A CLOSED set. A bank that needs to say more says it out of band; `why` is
;; machine-read by wallets to decide whether to retry, re-sequence or give up.
(def DECLINE-REASONS #js ["insufficient" "unknown-payer" "expired" "cur" "seq"])
;; ---- storage + links ----------------------------------------------------------
;; The ledger's localStorage prefix. Namespaced by IDENTITY, never by app: a
;; person's money follows them across every app in the suite, unlike the
;; per-app `ns:` keys the social layer uses.
(def LEDGER-KEY-PREFIX "wal:")
;; Where a `#pay=` link points. The banking app renders the request and hands
;; it to whichever wallet the visitor has.
(def PAY-BASE-URL "https://banca.ardegazu.ro/")
(def PAY-FRAGMENT-KEY "pay")
|