wallet-kit / test / escrow.test.mjs
  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
/**
 * BANK/2 escrow, against the committed dist/ and the independently-derived
 * fixtures in vectors/escrow.json.
 *
 * The fixtures are built by build-vectors.mjs from vectors/independent.mjs —
 * @noble/curves, node:crypto, and the preimages written out as literal
 * templates — so nothing here shares a line of code with the kit it checks. If
 * the kit and the fixture agree, two independent implementations agree.
 *
 * What this file is for, beyond "does it verify": escrow is the first place in
 * the suite where money is held rather than moved, and the failure that matters
 * is not a bad signature — it is an artifact that verifies while meaning
 * something other than what the holder thinks. So the cross-artifact and
 * wrong-signer rows below carry as much weight as the tamper table.
 */

import test from "node:test";
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";

import {
  DIGEST_RE,
  DOM_PAY_LOCK,
  DOM_PAY_RCP,
  DOM_PAY_REL,
  hashlockMatches,
  issuanceShape,
  lockId,
  lockPreimage,
  lockReceiptKey,
  lockReceiptPreimage,
  lockReceiptShape,
  lockShape,
  newHashlock,
  orderShape,
  releasePreimage,
  releaseShape,
  settlementShape,
  verifyLock,
  verifyLockReceipt,
  verifyRelease,
} from "../dist/index.js";

const esc = JSON.parse(
  await readFile(new URL("./vectors/escrow.json", import.meta.url), "utf8"),
);
const A = esc.artifacts;
const { bankPub, bank2Pub, payerPub, payeePub } = esc.actors;

/** Every verdict here is pinned clock-free — the form a bank's fold must use. */
const NO_CLOCK = null;

// ---- the wire ---------------------------------------------------------------

test("the three artifacts verify against independently-produced signatures", async () => {
  assert.ok(await verifyLock(A.wlk.wire, NO_CLOCK), "wlk");
  assert.ok(await verifyLockReceipt(A.wlr.wire, bankPub, NO_CLOCK), "wlr");
  assert.ok(await verifyRelease(A.wrl.wire, payeePub, NO_CLOCK), "wrl");
});

test("the kit's preimages are byte-identical to the hand-written templates", () => {
  assert.equal(lockPreimage(A.wlk.wire), A.wlk.preimage);
  assert.equal(lockReceiptPreimage(A.wlr.wire), A.wlr.preimage);
  assert.equal(releasePreimage(A.wrl.wire), A.wrl.preimage);
});

test("each preimage carries its own domain", () => {
  assert.ok(A.wlk.preimage.startsWith(`${DOM_PAY_LOCK}|v1|`));
  assert.ok(A.wrl.preimage.startsWith(`${DOM_PAY_REL}|v1|`));
  // wlr SHARES wrc's domain deliberately: same act, same signer, instruction
  // embedded, `t` inside canon(). See escrow.json's `domains` note.
  assert.ok(A.wlr.preimage.startsWith(`${DOM_PAY_RCP}|v1|`));
});

test("the transmitted key order is the documented one", () => {
  assert.deepEqual(Object.keys(A.wlk.wire), esc.key_order.wlk);
  assert.deepEqual(Object.keys(A.wlr.wire), esc.key_order.wlr);
  assert.deepEqual(Object.keys(A.wrl.wire), esc.key_order.wrl);
  // fourteen is past the nine-pair cliff where a #js{} literal silently
  // switches to hash order — the reason this kit has the `obj` macro at all
  assert.equal(esc.key_order.wlk.length, 14);
});

test("the wire order and the SIGNED order are different, and both are pinned", () => {
  // `hash` sits between `to` and `ctx` on the wire, but ASCII sorting inside
  // canon() puts it between `from` and `id`. A reader who assumed one order was
  // both would produce an artifact that transmits right and verifies nowhere.
  const wire = esc.key_order.wlk;
  assert.equal(wire[wire.indexOf("hash") - 1], "to");
  assert.equal(wire[wire.indexOf("hash") + 1], "ctx");
  assert.ok(A.wlk.preimage.includes('"from":'));
  const signed = A.wlk.preimage.slice(`${DOM_PAY_LOCK}|v1|`.length);
  assert.ok(signed.indexOf('"hash":') > signed.indexOf('"from":'));
  assert.ok(signed.indexOf('"hash":') < signed.indexOf('"id":'));
});

test("key order is the wire but not the signature — reversed forms still verify", async () => {
  const revLk = JSON.parse(esc.order_independence.wlk_reversed_json);
  const revLr = JSON.parse(esc.order_independence.wlr_reversed_json);
  assert.notEqual(JSON.stringify(revLk), A.wlk.wire_json, "byte-different JSON");
  assert.equal(lockPreimage(revLk), esc.order_independence.expect_same_preimage.wlk);
  assert.equal(lockReceiptPreimage(revLr), esc.order_independence.expect_same_preimage.wlr);
  assert.ok(await verifyLock(revLk, NO_CLOCK));
  assert.ok(await verifyLockReceipt(revLr, bankPub, NO_CLOCK));
});

// ---- the hashlock -----------------------------------------------------------

test("the fixture hashlock opens, and the digest is over the TEXT", async () => {
  const { pre, hash } = esc.hashlock;
  assert.ok(DIGEST_RE.test(pre) && DIGEST_RE.test(hash));
  assert.ok(await hashlockMatches(pre, hash));
  assert.equal(A.wlk.wire.hash, hash, "the fixture lock is locked to it");
});

test("hashlockMatches is total — malformed input is false, never a throw", async () => {
  const { pre, hash } = esc.hashlock;
  for (const bad of [undefined, null, "", "x", 42, {}, [], pre.slice(0, -1), hash]) {
    assert.equal(await hashlockMatches(bad, hash), false, `pre=${JSON.stringify(bad)}`);
  }
  for (const bad of [undefined, null, "", "x", 42, {}, []]) {
    assert.equal(await hashlockMatches(pre, bad), false, `hash=${JSON.stringify(bad)}`);
  }
});

test("newHashlock produces a fresh, well-formed, self-consistent pair", async () => {
  const a = await newHashlock();
  const b = await newHashlock();
  assert.ok(DIGEST_RE.test(a.pre) && DIGEST_RE.test(a.hash));
  assert.notEqual(a.pre, b.pre, "one preimage per lock — these must never repeat");
  assert.ok(await hashlockMatches(a.pre, a.hash));
  assert.equal(await hashlockMatches(a.pre, b.hash), false);
});

// ---- the tamper table -------------------------------------------------------

const verifyFor = {
  wlk: (a) => verifyLock(a, NO_CLOCK),
  wlr: (a) => verifyLockReceipt(a, bankPub, NO_CLOCK),
  wrl: (a) => verifyRelease(a, payeePub, NO_CLOCK),
};

for (const row of esc.tamper) {
  test(`escrow rejected — ${row.name}`, async () => {
    assert.equal(await verifyFor[row.kind](row.artifact), null);
  });
}

// ---- meaning something other than what the holder thinks ---------------------

test("no escrow artifact is readable as any other artifact", () => {
  // A shape check that answered yes here would let a lock be folded as a
  // payment: the money moves instead of being held, and the hashlock — the
  // whole point — is dropped on the floor.
  assert.equal(orderShape(A.wlk.wire, NO_CLOCK), null, "a wlk is not a wpo");
  assert.equal(settlementShape(A.wlr.wire, NO_CLOCK), null, "a wlr is not a wrc");
  assert.equal(issuanceShape(A.wrl.wire, NO_CLOCK), null, "a wrl is not a wri");
  assert.equal(lockShape(A.wlr.wire, NO_CLOCK), null, "a wlr is not a wlk");
  assert.equal(lockReceiptShape(A.wlk.wire, NO_CLOCK), null, "a wlk is not a wlr");
  assert.equal(releaseShape(A.wlk.wire, NO_CLOCK), null, "a wlk is not a wrl");
});

test("a lock receipt by the wrong bank is refused even though it is internally valid", async () => {
  // The row in the tamper table is signed by BANK2 naming ITSELF as the bank —
  // every internal check passes and only banker(lk.cur) !== bank catches it.
  const row = esc.tamper.find((r) => r.name.includes("does not issue lk.cur"));
  assert.ok(row, "the fixture row exists");
  assert.equal(await verifyLockReceipt(row.artifact, bank2Pub, NO_CLOCK), null);
  assert.equal(lockReceiptShape(row.artifact, NO_CLOCK), null);
});

test("expectedBankPub is the caller's expectation, and skipping it is the caller's risk", async () => {
  assert.ok(await verifyLockReceipt(A.wlr.wire, bankPub, NO_CLOCK), "the bank you waited for");
  assert.equal(await verifyLockReceipt(A.wlr.wire, bank2Pub, NO_CLOCK), null, "some other bank");
  assert.ok(await verifyLockReceipt(A.wlr.wire, undefined, NO_CLOCK), "omitted skips the comparison");
});

test("a release proves only that SOMEBODY signed away SOME lock", async () => {
  // The kit never sees the lock, so this is the whole of what it can promise —
  // and it is why a bank MUST pass expectedTo and compare lh and cur itself.
  assert.ok(await verifyRelease(A.wrl.wire, payeePub, NO_CLOCK));
  assert.equal(await verifyRelease(A.wrl.wire, payerPub, NO_CLOCK), null);
  const r = releaseShape(A.wrl.wire, NO_CLOCK);
  assert.equal(r.to, payeePub, "the beneficiary, who is the signer");
  assert.equal(r.lh, esc.artifacts.wrl.wire.lh, "and it names exactly one lock ENTRY");
});

// ---- dedup keys -------------------------------------------------------------

test("the dedup keys match the independently-hashed fixtures", async () => {
  assert.equal(await lockId(A.wlk.wire), esc.dedup_keys.lock_id.sha256_b64url);
  assert.equal(await lockReceiptKey(A.wlr.wire), esc.dedup_keys.lock_receipt_key.sha256_b64url);
});

test("lockReceiptKey is blind to ts and the log ref, and is not a receiptKey", async () => {
  // A bank re-issuing one answer with a fresh stamp has answered ONCE; folding
  // it twice would double a balance.
  const restamped = { ...A.wlr.wire, ts: A.wlr.wire.ts + 999999, seq: "L-elsewhere" };
  assert.equal(await lockReceiptKey(restamped), await lockReceiptKey(A.wlr.wire));
  const other = { ...A.wlr.wire, bank: bank2Pub };
  assert.notEqual(await lockReceiptKey(other), await lockReceiptKey(A.wlr.wire),
    "two banks answering one lock stay distinct");
});

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/wallet-kit.git