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 | // The wallet-kit seam (client/src/banca/lib/wallet.cljs), pinned.
//
// wallet-kit is released and sha-pinned, so lib/wallet.cljs no longer
// transcribes its preimage assembly or its shape grammar — it calls the kit.
// A PIN is only as good as what checks it (a bump can move a byte on the wire
// as easily as a transcription could drift), so this file checks it two ways
// that do not involve banca's code OR the kit's:
//
// · against wallet-kit's OWN published golden vector, copied into
// test/vectors/order.json — the same wpo wire bytes, the same 86-character
// signature and the same preimage string that repo derived with
// @noble/curves;
// · against a by-hand canonical-JSON template in test/vectors/independent.mjs,
// so a reader can check the key order character by character.
//
// Everything runs against test-dist/testlib.js — the BUILT output — never src/.
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import * as L from "../test-dist/testlib.js";
import { canonIndependent, unsignedOrderByHand, orderPreimageByHand } from "./vectors/independent.mjs";
const V = JSON.parse(readFileSync(new URL("./vectors/order.json", import.meta.url), "utf8"));
const W = V.wpo.wire;
const withField = (field, s) => ({ ...W, [field]: s });
const repeatCp = (cp, n) => String.fromCodePoint(cp).repeat(n);
test("the seam's constants equal wallet-kit's published ones", () => {
assert.equal(L.DOM_PAY_ORD, V.consts.DOM_PAY_ORD);
assert.equal(L.DOM_PAY_RCP, V.consts.DOM_PAY_RCP);
assert.equal(L.AMT_MIN, V.consts.AMT_MIN);
assert.equal(L.AMT_MAX, V.consts.AMT_MAX);
assert.equal(L.CTX_MAX, V.consts.CTX_MAX);
assert.equal(L.MEMO_MAX, V.consts.MEMO_MAX);
assert.equal(L.NEUTRAL_CODE, V.consts.NEUTRAL_CODE);
assert.equal(L.NEUTRAL_UNIT, V.consts.NEUTRAL_UNIT);
assert.deepEqual([...L.DECLINE_REASONS], V.consts.DECLINE_REASONS);
// 2^50 exactly — a precision fact, not a policy ceiling
assert.equal(L.AMT_MAX, 2 ** 50);
});
test("the order preimage is byte-identical to wallet-kit's published one", () => {
assert.equal(L.orderPreimage(W), V.wpo.preimage);
// …and to a template written out by hand, which is what makes the fixture
// checkable rather than merely consistent
assert.equal(orderPreimageByHand(W), V.wpo.preimage);
// …and to the domain prefix plus an independently re-implemented canon()
assert.equal(`wpay-ord|v1|${canonIndependent(unsignedOrderByHand(W))}`, V.wpo.preimage);
});
test("canon() and JSON.stringify are DIFFERENT orders, and both matter", () => {
// canon sorts (so the signature is order-independent); the wire is insertion
// order (so the bytes a peer holds are fixed). Conflating them is the classic
// interop failure and the vector pins both halves.
assert.notEqual(L.canon(L.unsignedOrder(W)), JSON.stringify(L.unsignedOrder(W)));
assert.equal(L.canon(L.unsignedOrder(W)), canonIndependent(unsignedOrderByHand(W)));
});
test("orderShape rebuilds the order in wpo's exact thirteen-key wire order", () => {
const o = L.orderShape(W);
assert.ok(o);
assert.equal(JSON.stringify(o), V.wpo.wire_json);
assert.deepEqual(Object.keys(o), [
"v", "t", "id", "cur", "amt", "seq", "from", "to", "ctx", "memo", "ts", "exp", "sig",
]);
// the rebuild is what the preimage may be taken over
assert.equal(L.orderPreimage(o), V.wpo.preimage);
});
test("a scrambled input rebuilds to the SAME wire bytes", () => {
// The point of rebuilding rather than returning `raw`: a peer that sent the
// keys in a different order gets normalised, and the signature still checks,
// because the signature is over canon().
const scrambled = {};
for (const k of Object.keys(W).sort()) scrambled[k] = W[k];
assert.notEqual(JSON.stringify(scrambled), V.wpo.wire_json);
assert.equal(JSON.stringify(L.orderShape(scrambled)), V.wpo.wire_json);
});
test("verifyOrder accepts the fixture and rejects a forgery", async () => {
assert.ok(await L.verifyOrder(W));
// flip a meaningful bit of the last signature character: 'Q' (16) -> 'A' (0).
// Both are canonical, so this survives SIG_RE and dies at Ed25519 — which is
// exactly the order the two checks must happen in.
const forged = { ...W, sig: W.sig.slice(0, 85) + (W.sig[85] === "A" ? "Q" : "A") };
assert.ok(L.orderShape(forged), "the forgery must still be well-SHAPED");
assert.equal(await L.verifyOrder(forged), null);
});
test("orderId is sha256B64url(canon(order minus sig))", async () => {
assert.equal(await L.orderId(W), V.order_id);
assert.match(await L.orderId(W), /^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/);
// over what the order SAYS: two orders differing only in `sig` share an id,
// and `po.id` is the payer's randomness, not a substitute
const resigned = { ...W, sig: W.sig.slice(0, 85) + "A" };
assert.equal(await L.orderId(resigned), V.order_id);
});
test("every shape_rejects row returns null", () => {
const survivors = V.shape_rejects.filter((r) => L.orderShape(r.order) !== null).map((r) => r.why);
assert.deepEqual(survivors, []);
assert.ok(V.shape_rejects.length >= 18, "the reject table must not shrink silently");
});
test("the shape gate is not vacuous — the unmutated fixture shapes", () => {
// Every row above is the fixture plus one change. If orderShape rejected
// everything, all 18 would pass and the table would prove nothing.
assert.ok(L.orderShape(W));
});
test("ctx and memo reject line terminators, C0, DEL and C1", () => {
for (const r of V.control_chars.rejected) {
const bad = withField(r.field, "a" + String.fromCodePoint(r.cp) + "b");
assert.equal(L.orderShape(bad), null, `${r.field} U+${r.cp.toString(16)} (${r.why}) must be rejected`);
}
for (const r of V.control_chars.accepted) {
const ok = withField(r.field, "a" + String.fromCodePoint(r.cp) + "b");
assert.ok(L.orderShape(ok), `${r.field} U+${r.cp.toString(16)} (${r.why}) must be accepted`);
}
assert.ok(V.control_chars.rejected.some((r) => r.cp === 8232), "U+2028 must be in the class");
assert.ok(V.control_chars.rejected.some((r) => r.cp === 8233), "U+2029 must be in the class");
});
test("ctx and memo are bounded in CODE POINTS, not UTF-16 units", () => {
for (const c of V.length_bounds.cases) {
const s = repeatCp(c.cp, c.len);
const got = L.orderShape(withField(c.field, s)) !== null;
assert.equal(got, c.shapes, `${c.field} of ${c.len} code points (U+${c.cp.toString(16)})`);
}
// the case that separates the two counts: 140 non-BMP emoji are 280 UTF-16
// units and must still fit a 140-code-point memo
assert.equal(repeatCp(128512, 140).length, 280);
assert.ok(L.orderShape(withField("memo", repeatCp(128512, 140))));
});
test("currency ids: the banker IS the namespace, and GAZ is reserved", () => {
for (const [pub, code, id] of V.currency.ok) assert.equal(L.currencyId(pub, code), id);
for (const [pub, code, why] of V.currency.rejected) {
assert.equal(L.currencyId(pub, code), null, why);
}
for (const [cur, want] of V.currency.payable) assert.equal(L.isPayableCurrency(cur), want);
// two banks may both mint "LEI" and the ids never collide — no registry needed
assert.notEqual(L.currencyId(V.actors.bank, "LEI"), L.currencyId(V.actors.bank2, "LEI"));
assert.equal(L.bankerOf(`${V.actors.bank}.LEI`), V.actors.bank);
// the neutral unit parses (a pricing layer needs that) but is never payable
assert.equal(L.parseCurrency("~.GAZ").banker, "~");
assert.equal(L.isPayableCurrency("~.GAZ"), false);
});
test("THE DELIBERATE DIVERGENCE: banca's orderShape is clock-free", () => {
// An order timestamped ten years in the reader's future must still SHAPE.
// wallet-kit's own orderShape rejects it — correctly, for a wallet deciding
// whether to sign or settle — but a settlement fold that consulted a wall
// clock would let two replicas whose clocks differ disagree about one entry,
// and a bank whose replicas disagree is not a bank. See lib/wallet.cljs's
// header and docs/PROTOCOL.md §6.
const far = V.clock_free_order;
assert.ok(far.ts > Date.now() + 120000, "the fixture must actually be in the future");
assert.ok(L.orderShape(far), "a far-future order must shape");
// the ts-RELATIVE bounds are kept, because they are clock-free
assert.equal(L.orderShape({ ...far, exp: far.ts }), null);
assert.equal(L.orderShape({ ...far, exp: far.ts + 90 * 86400000 + 1 }), null);
});
|